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 some example programs: https://github.com/adamdruppe/minigui-samples
333 
334 	When you can't build your application out of existing widgets, you'll want to make your own. The general pattern is to subclass [Widget], write a constructor that takes a `Widget` parent argument you pass to `super`, then set some values, override methods you want to customize, and maybe add child widgets and events as appropriate. You might also be able to subclass an existing other Widget and customize that way.
335 
336 	To get more specific, let's consider a few illustrative examples, then we'll come back to some principles.
337 
338 	$(H5 Custom Widget Examples)
339 
340 	$(H5 More notes)
341 
342 	See [Widget].
343 
344 	If you override [Widget.recomputeChildLayout], don't forget to call `registerMovement()` at the top of it, then call recomputeChildLayout of all its children too!
345 
346 		If you need a nested OS level window, see [NestedChildWindowWidget]. Use [Widget.scaleWithDpi] to convert logical pixels to physical pixels, as required.
347 
348 		See [Widget.OverrideStyle], [Widget.paintContent], [Widget.dynamicState] for some useful starting points.
349 
350 		You may also want to provide layout and style hints by overriding things like [Widget.flexBasisWidth], [Widget.flexBasisHeight], [Widget.minHeight], yada, yada, yada.
351 
352 		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!)
353 
354 		$(TIP
355 			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.
356 		)
357 
358 	$(H5 Timers and animations)
359 
360 	The [Timer] class is available and you can call `widget.redraw();` to trigger a redraw from a timer handler.
361 
362 	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.
363 
364 	$(H5 Clipboard integrations, drag and drop)
365 
366 	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.
367 
368 	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.
369 
370 	See: [draggable], [DropHandler], [setClipboardText], [setClipboardImage], [getClipboardText], [getClipboardImage], [setPrimarySelection], and others from simpledisplay.
371 
372 	$(H5 Context menus)
373 
374 	Override [Widget.contextMenu] in your subclass.
375 
376 	$(H4 Coming later)
377 
378 	Among the unfinished features: unified selections, translateable strings, external integrations.
379 
380 	$(H2 Running minigui programs)
381 
382 	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.
383 
384 	$(H2 Building minigui programs)
385 
386 	minigui's only required dependencies are [arsd.simpledisplay], [arsd.color], and
387 	[arsd.textlayouter], on which it is built. simpledisplay provides the low-level
388 	interfaces and minigui builds the concept of widgets inside the windows on top of it.
389 
390 	Its #1 goal is to be useful without being large and complicated like GTK and Qt.
391 	It isn't hugely concerned with appearance - on Windows, it just uses the native
392 	controls and native theme, and on Linux, it keeps it simple and I may change that
393 	at any time, though after May 2021, you can customize some things with css-inspired
394 	[Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`,
395 	you can use the custom implementation there too, but... you shouldn't.)
396 
397 	The event model is similar to what you use in the browser with Javascript and the
398 	layout engine tries to automatically fit things in, similar to a css flexbox.
399 
400 	FOR BEST RESULTS: be sure to link with the appropriate subsystem command
401 	`-L/SUBSYSTEM:WINDOWS` and -L/entry:mainCRTStartup`. If using ldc instead
402 	of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w".
403 
404 	Otherwise you'll get a console and possibly other visual bugs. But if you do use
405 	the subsystem:windows, note that Phobos' writeln will crash the program!
406 
407 	HTML_To_Classes:
408 	$(SMALL_TABLE
409 		HTML Code | Minigui Class
410 
411 		`<input type="text">` | [LineEdit]
412 		`<input type="password">` | [PasswordEdit]
413 		`<textarea>` | [TextEdit]
414 		`<select>` | [DropDownSelection]
415 		`<input type="checkbox">` | [Checkbox]
416 		`<input type="radio">` | [Radiobox]
417 		`<button>` | [Button]
418 	)
419 
420 
421 	Stretchiness:
422 		The default is 4. You can use larger numbers for things that should
423 		consume a lot of space, and lower numbers for ones that are better at
424 		smaller sizes.
425 
426 	Overlapped_input:
427 		COMING EVENTUALLY:
428 		minigui will include a little bit of I/O functionality that just works
429 		with the event loop. If you want to get fancy, I suggest spinning up
430 		another thread and posting events back and forth.
431 
432 	$(H2 Add ons)
433 		See the `minigui_addons` directory in the arsd repo for some add on widgets
434 		you can import separately too.
435 
436 	$(H3 XML definitions)
437 		If you use [arsd.minigui_xml], you can create widget trees from XML at runtime.
438 
439 	$(H3 Scriptability)
440 		minigui is compatible with [arsd.script]. If you see `@scriptable` on a method
441 		in this documentation, it means you can call it from the script language.
442 
443 		Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml]
444 		and make [arsd.minigui_xml.makeWidgetFromString] available to your script:
445 
446 		---
447 		import arsd.minigui_xml;
448 		import arsd.script;
449 
450 		var globals = var.emptyObject;
451 		globals.makeWidgetFromString = &makeWidgetFromString;
452 
453 		// this now works
454 		interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals);
455 		---
456 
457 		More to come.
458 
459 	Widget_tree_notes:
460 		minigui doesn't really formalize these distinctions, but in practice, there are multiple types of widgets:
461 
462 		$(LIST
463 			* Containers - a widget that holds other widgets directly, generally [Layout]s. [WidgetContainer] is an attempt to formalize this but is nothing really special.
464 
465 			* Reparenting containers - a widget that holds other widgets inside a different one of their parents. [MainWindow] is an example - any time you try to add a child to the main window, it actually goes to a special container one layer deeper. [ScrollMessageWidget] also works this way.
466 
467 			---
468 			auto child = new Widget(mainWindow);
469 			assert(child.parent is mainWindow); // fails, its actual parent is mainWindow's inner container instead.
470 			---
471 
472 			* Limiting containers - a widget that can only hold children of a particular type. See [TabWidget], which can only hold [TabWidgetPage]s.
473 
474 			* Simple controls - a widget that cannot have children, but instead does a specific job.
475 
476 			* Compound controls - a widget that is comprised of children internally to help it do a specific job, but externally acts like a simple control that does not allow any more children. Ideally, this is encapsulated, but in practice, it leaks right now.
477 		)
478 
479 		In practice, all of these are [Widget]s right now, but this violates the OOP principles of substitutability since some operations are not actually valid on all subclasses.
480 
481 		Future breaking changes might be related to making this more structured but im not sure it is that important to actually break stuff over.
482 
483 	My_UI_Guidelines:
484 		Note that the Linux custom widgets generally aim to be efficient on remote X network connections.
485 
486 		In a perfect world, you'd achieve all the following goals:
487 
488 		$(LIST
489 			* All operations are present in the menu
490 			* The operations the user wants at the moment are right where they want them
491 			* All operations can be scripted
492 			* The UI does not move any elements without explicit user action
493 			* All numbers can be seen and typed in if wanted, even if the ui usually hides them
494 		)
495 
496 	$(H2 Future Directions)
497 
498 	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.
499 
500 	History:
501 		In January 2025 (dub v12.0), minigui got a few more breaking changes:
502 
503 		$(LIST
504 			* `defaultEventHandler_*` functions take more specific objects. So if you see errors like:
505 
506 			---
507 			Error: function `void arsd.minigui.EditableTextWidget.defaultEventHandler_focusin(Event foe)` does not override any function, did you mean to override `void arsd.minigui.Widget.defaultEventHandler_focusin(arsd.minigui.FocusInEvent event)`?
508 			---
509 
510 			Go to the file+line number from the error message and change `Event` to `FocusInEvent` (or whatever one it tells you in the "did you mean" part of the error) and recompile. No other changes should be necessary to be compatible with this change.
511 
512 			* Most event classes, except those explicitly used as a base class, are now marked `final`. If you depended on this subclassing, let me know and I'll see what I can do, but I expect there's little use of it. I now recommend all event classes the `final` unless you are specifically planning on extending it.
513 		)
514 
515 		Minigui had mostly additive changes or bug fixes since its inception until May 2021.
516 
517 		In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd
518 		tag this as version 2.0.
519 
520 		Among the changes:
521 		$(LIST
522 			* 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.
523 
524 			See [Event] for details.
525 
526 			* 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.
527 
528 			See [DoubleClickEvent] for details.
529 
530 			* 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.
531 
532 			See [Widget.Style] for details.
533 
534 			* 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.
535 
536 			* 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.
537 
538 			* [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor.
539 
540 			* 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.
541 
542 			* Various non-breaking additions.
543 		)
544 +/
545 module arsd.minigui;
546 			// * A widget must now opt in to receiving keyboard focus, rather than opting out.
547 
548 /++
549 	This hello world sample will have an oversized button, but that's ok, you see your first window!
550 +/
551 version(Demo)
552 unittest {
553 	import arsd.minigui;
554 
555 	void main() {
556 		auto window = new MainWindow();
557 
558 		// note the parent widget is almost always passed as the last argument to a constructor
559 		auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window);
560 		auto button = new Button("Close", window);
561 		button.addWhenTriggered({
562 			window.close();
563 		});
564 
565 		window.loop();
566 	}
567 
568 	main(); // exclude from docs
569 }
570 
571 /++
572 	$(ID layout-example)
573 
574 	This example shows one way you can partition your window into a header
575 	and sidebar. Here, the header and sidebar have a fixed width, while the
576 	rest of the content sizes with the window.
577 
578 	It might be a new way of thinking about window layout to do things this
579 	way - perhaps [GridLayout] more matches your style of thought - but the
580 	concept here is to partition the window into sub-boxes with a particular
581 	size, then partition those boxes into further boxes.
582 
583 	$(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.)
584 
585 	So to make the header, start with a child layout that has a max height.
586 	It will use that space from the top, then the remaining children will
587 	split the remaining area, meaning you can think of is as just being another
588 	box you can split again. Keep splitting until you have the look you desire.
589 +/
590 // https://github.com/adamdruppe/arsd/issues/310
591 version(minigui_screenshots)
592 @Screenshot("layout")
593 unittest {
594 	import arsd.minigui;
595 
596 	// This helper class is just to help make the layout boxes visible.
597 	// think of it like a <div style="background-color: whatever;"></div> in HTML.
598 	class ColorWidget : Widget {
599 		this(Color color, Widget parent) {
600 			this.color = color;
601 			super(parent);
602 		}
603 		Color color;
604 		class Style : Widget.Style {
605 			override WidgetBackground background() { return WidgetBackground(color); }
606 		}
607 		mixin OverrideStyle!Style;
608 	}
609 
610 	void main() {
611 		auto window = new Window;
612 
613 		// the key is to give it a max height. This is one way to do it:
614 		auto header = new class HorizontalLayout {
615 			this() { super(window); }
616 			override int maxHeight() { return 50; }
617 		};
618 		// this next line is a shortcut way of doing it too, but it only works
619 		// for HorizontalLayout and VerticalLayout, and is less explicit, so it
620 		// is good to know how to make a new class like above anyway.
621 		// auto header = new HorizontalLayout(50, window);
622 
623 		auto bar = new HorizontalLayout(window);
624 
625 		// or since this is so common, VerticalLayout and HorizontalLayout both
626 		// can just take an argument in their constructor for max width/height respectively
627 
628 		// (could have tone this above too, but I wanted to demo both techniques)
629 		auto left = new VerticalLayout(100, bar);
630 
631 		// and this is the main section's container. A plain Widget instance is good enough here.
632 		auto container = new Widget(bar);
633 
634 		// and these just add color to the containers we made above for the screenshot.
635 		// in a real application, you can just add your actual controls instead of these.
636 		auto headerColorBox = new ColorWidget(Color.teal, header);
637 		auto leftColorBox = new ColorWidget(Color.green, left);
638 		auto rightColorBox = new ColorWidget(Color.purple, container);
639 
640 		window.loop();
641 	}
642 
643 	main(); // exclude from docs
644 }
645 
646 
647 import arsd.core;
648 import arsd.textlayouter;
649 
650 alias Timer = arsd.simpledisplay.Timer;
651 public import arsd.simpledisplay;
652 /++
653 	Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports)
654 
655 	History:
656 		Was private until May 15, 2021.
657 +/
658 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle()
659 
660 version(Windows) {
661 	import core.sys.windows.winnls;
662 	import core.sys.windows.windef;
663 	import core.sys.windows.basetyps;
664 	import core.sys.windows.winbase;
665 	import core.sys.windows.winuser;
666 	import core.sys.windows.wingdi;
667 	static import gdi = core.sys.windows.wingdi;
668 }
669 
670 version(Windows) {
671 	version(minigui_manifest) {} else version=minigui_no_manifest;
672 
673 	version(minigui_no_manifest) {} else
674 	static if(__VERSION__ >= 2_083)
675 	version(CRuntime_Microsoft) { // FIXME: mingw?
676 		// assume we want commctrl6 whenever possible since there's really no reason not to
677 		// and this avoids some of the manifest hassle
678 		pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"");
679 	}
680 }
681 
682 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default.
683 private bool lastDefaultPrevented;
684 
685 /// Methods marked with this are available from scripts if added to the [arsd.script] engine.
686 alias scriptable = arsd_jsvar_compatible;
687 
688 version(Windows) {
689 	// use native widgets when available unless specifically asked otherwise
690 	version(custom_widgets) {
691 		enum bool UsingCustomWidgets = true;
692 		enum bool UsingWin32Widgets = false;
693 	} else {
694 		version = win32_widgets;
695 		enum bool UsingCustomWidgets = false;
696 		enum bool UsingWin32Widgets = true;
697 	}
698 	// and native theming when needed
699 	//version = win32_theming;
700 } else {
701 	enum bool UsingCustomWidgets = true;
702 	enum bool UsingWin32Widgets = false;
703 	version=custom_widgets;
704 }
705 
706 
707 
708 /*
709 
710 	The main goals of minigui.d are to:
711 		1) Provide basic widgets that just work in a lightweight lib.
712 		   I basically want things comparable to a plain HTML form,
713 		   plus the easy and obvious things you expect from Windows
714 		   apps like a menu.
715 		2) Use native things when possible for best functionality with
716 		   least library weight.
717 		3) Give building blocks to provide easy extension for your
718 		   custom widgets, or hooking into additional native widgets
719 		   I didn't wrap.
720 		4) Provide interfaces for easy interaction between third
721 		   party minigui extensions. (event model, perhaps
722 		   signals/slots, drop-in ease of use bits.)
723 		5) Zero non-system dependencies, including Phobos as much as
724 		   I reasonably can. It must only import arsd.color and
725 		   my simpledisplay.d. If you need more, it will have to be
726 		   an extension module.
727 		6) An easy layout system that generally works.
728 
729 	A stretch goal is to make it easy to make gui forms with code,
730 	some kind of resource file (xml?) and even a wysiwyg designer.
731 
732 	Another stretch goal is to make it easy to hook data into the gui,
733 	including from reflection. So like auto-generate a form from a
734 	function signature or struct definition, or show a list from an
735 	array that automatically updates as the array is changed. Then,
736 	your program focuses on the data more than the gui interaction.
737 
738 
739 
740 	STILL NEEDED:
741 		* combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect)
742 		* slider
743 		* listbox
744 		* spinner
745 		* label?
746 		* rich text
747 */
748 
749 
750 /+
751 	enum LayoutMethods {
752 		 verticalFlex,
753 		 horizontalFlex,
754 		 inlineBlock, // left to right, no stretch, goes to next line as needed
755 		 static, // just set to x, y
756 		 verticalNoStretch, // browser style default
757 
758 		 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line
759 
760 		 grid, // magic
761 	}
762 +/
763 
764 /++
765 	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.
766 
767 
768 	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.
769 
770 	---
771 	class MinimalWidget : Widget {
772 		this(Widget parent) {
773 			super(parent);
774 		}
775 	}
776 	---
777 
778 	$(SIDEBAR
779 		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.
780 	)
781 
782 	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.
783 
784 	Among the things you'll most likely want to change in your custom widget:
785 
786 	$(LIST
787 		* 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.)
788 
789 		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.
790 
791 		Do this $(I after) calling the `super` constructor.
792 
793 		* 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.
794 
795 		Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision.
796 
797 		* 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.
798 
799 		* 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.
800 	)
801 
802 	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.
803 
804 	It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example.
805 
806 	Your own custom-drawn and native system controls can exist side-by-side.
807 
808 	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.
809 +/
810 class Widget : ReflectableProperties {
811 
812 	private int toolbarIconSize() {
813 		return scaleWithDpi(24);
814 	}
815 
816 
817 	/++
818 		Returns the current size of the widget.
819 
820 		History:
821 			Added January 3, 2025
822 	+/
823 	final Size size() const {
824 		return Size(width, height);
825 	}
826 
827 	private bool willDraw() {
828 		return true;
829 	}
830 
831 	/+
832 	/++
833 		Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need.
834 
835 		History:
836 			Added September 15, 2021
837 			implemented.... ???
838 	+/
839 	void prepareReflection(this This)() {
840 
841 	}
842 	+/
843 
844 	private bool _enabled = true;
845 
846 	/++
847 		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.
848 
849 		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.
850 
851 		History:
852 			Added November 23, 2021 (dub v10.4)
853 
854 			Warning: the specific behavior of disabling with parents may change in the future.
855 		Bugs:
856 			Currently only implemented for widgets backed by native Windows controls.
857 
858 		See_Also: [disabledReason], [disabledBy]
859 	+/
860 	@property bool enabled() {
861 		return disabledBy() is null;
862 	}
863 
864 	/// ditto
865 	@property void enabled(bool yes) {
866 		_enabled = yes;
867 		version(win32_widgets) {
868 			if(hwnd)
869 				EnableWindow(hwnd, yes);
870 		}
871 		setDynamicState(DynamicState.disabled, yes);
872 	}
873 
874 	private string disabledReason_;
875 
876 	/++
877 		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.
878 
879 		Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though.
880 
881 		History:
882 			Added November 23, 2021 (dub v10.4)
883 		See_Also: [enabled], [disabledBy]
884 	+/
885 	@property string disabledReason() {
886 		auto w = disabledBy();
887 		return (w is null) ? null : w.disabledReason_;
888 	}
889 
890 	/// ditto
891 	@property void disabledReason(string reason) {
892 		disabledReason_ = reason;
893 	}
894 
895 	/++
896 		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.
897 
898 		History:
899 			Added November 25, 2021 (dub v10.4)
900 		See_Also: [enabled], [disabledReason]
901 	+/
902 	Widget disabledBy() {
903 		Widget p = this;
904 		while(p) {
905 			if(!p._enabled)
906 				return p;
907 			p = p.parent;
908 		}
909 		return null;
910 	}
911 
912 	/// Implementations of [ReflectableProperties] interface. See the interface for details.
913 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
914 		if(valueIsJson)
915 			return SetPropertyResult.wrongFormat;
916 		switch(name) {
917 			case "name":
918 				this.name = value.idup;
919 				return SetPropertyResult.success;
920 			case "statusTip":
921 				this.statusTip = value.idup;
922 				return SetPropertyResult.success;
923 			default:
924 				return SetPropertyResult.noSuchProperty;
925 		}
926 	}
927 	/// ditto
928 	void getPropertiesList(scope void delegate(string name) sink) const {
929 		sink("name");
930 		sink("statusTip");
931 	}
932 	/// ditto
933 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
934 		switch(name) {
935 			case "name":
936 				sink(name, this.name, false);
937 				return;
938 			case "statusTip":
939 				sink(name, this.statusTip, false);
940 				return;
941 			default:
942 				sink(name, null, true);
943 		}
944 	}
945 
946 	/++
947 		Scales the given value to the system-reported DPI for the monitor on which the widget resides.
948 
949 		History:
950 			Added November 25, 2021 (dub v10.5)
951 			`Point` overload added January 12, 2022 (dub v10.6)
952 	+/
953 	int scaleWithDpi(int value, int assumedDpi = 96) {
954 		// avoid potential overflow with common special values
955 		if(value == int.max)
956 			return int.max;
957 		if(value == int.min)
958 			return int.min;
959 		if(value == 0)
960 			return 0;
961 		return value * currentDpi(assumedDpi) / assumedDpi;
962 	}
963 
964 	/// ditto
965 	Point scaleWithDpi(Point value, int assumedDpi = 96) {
966 		return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi));
967 	}
968 
969 	/++
970 		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.
971 
972 		Not entirely stable.
973 
974 		History:
975 			Added August 25, 2023 (dub v11.1)
976 	+/
977 	final int currentDpi(int assumedDpi = 96) {
978 		// assert(parentWindow !is null);
979 		// assert(parentWindow.win !is null);
980 		auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi;
981 		//divide = 138; // to test 1.5x
982 		// 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.
983 		// this also covers the case when actualDpi returns 0.
984 		if(divide < 96)
985 			divide = 96;
986 		return divide;
987 	}
988 
989 	// avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable
990 	// I'll think up something better eventually
991 
992 	// FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight.
993 	protected final int defaultLineHeight() {
994 		auto cs = getComputedStyle();
995 		if(cs.font && !cs.font.isNull)
996 			return cs.font.height() * 5 / 4;
997 		else
998 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4);
999 	}
1000 
1001 	/++
1002 
1003 		History:
1004 			Added August 25, 2023 (dub v11.1)
1005 	+/
1006 	protected final int defaultTextHeight(int numberOfLines = 1) {
1007 		auto cs = getComputedStyle();
1008 		if(cs.font && !cs.font.isNull)
1009 			return cs.font.height() * numberOfLines;
1010 		else
1011 			return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines;
1012 	}
1013 
1014 	protected final int defaultTextWidth(const(char)[] text) {
1015 		auto cs = getComputedStyle();
1016 		if(cs.font && !cs.font.isNull)
1017 			return cs.font.stringWidth(text);
1018 		else
1019 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2);
1020 	}
1021 
1022 	/++
1023 		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.
1024 
1025 		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.
1026 
1027 		History:
1028 			Added May 22, 2021
1029 	+/
1030 	protected bool encapsulatedChildren() {
1031 		return false;
1032 	}
1033 
1034 	private void privateDpiChanged() {
1035 		dpiChanged();
1036 		foreach(child; children)
1037 			child.privateDpiChanged();
1038 	}
1039 
1040 	/++
1041 		Virtual hook to update any caches or fonts you need on the event of a dpi scaling change.
1042 
1043 		History:
1044 			Added January 12, 2022 (dub v10.6)
1045 	+/
1046 	protected void dpiChanged() {
1047 
1048 	}
1049 
1050 	// Default layout properties {
1051 
1052 		int minWidth() { return 0; }
1053 		int minHeight() {
1054 			// default widgets have a vertical layout, therefore the minimum height is the sum of the contents
1055 			int sum = this.paddingTop + this.paddingBottom;
1056 			foreach(child; children) {
1057 				if(child.hidden)
1058 					continue;
1059 				sum += child.minHeight();
1060 				sum += child.marginTop();
1061 				sum += child.marginBottom();
1062 			}
1063 
1064 			return sum;
1065 		}
1066 		int maxWidth() { return int.max; }
1067 		int maxHeight() { return int.max; }
1068 		int widthStretchiness() { return 4; }
1069 		int heightStretchiness() { return 4; }
1070 
1071 		/++
1072 			Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items.
1073 
1074 			History:
1075 				Added June 15, 2021 (dub v10.1)
1076 		+/
1077 		int widthShrinkiness() { return 0; }
1078 		/// ditto
1079 		int heightShrinkiness() { return 0; }
1080 
1081 		/++
1082 			The initial size of the widget for layout calculations. Default is 0.
1083 
1084 			See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis]
1085 
1086 			History:
1087 				Added June 15, 2021 (dub v10.1)
1088 		+/
1089 		int flexBasisWidth() { return 0; }
1090 		/// ditto
1091 		int flexBasisHeight() { return 0; }
1092 
1093 		/++
1094 			Not stable.
1095 
1096 			Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored.
1097 
1098 			So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6.
1099 
1100 			History:
1101 				Added January 5, 2023
1102 		+/
1103 		Rectangle defaultMargin;
1104 		/// ditto
1105 		Rectangle defaultPadding;
1106 
1107 		int marginLeft() { return scaleWithDpi(defaultMargin.left); }
1108 		int marginRight() { return scaleWithDpi(defaultMargin.right); }
1109 		int marginTop() { return scaleWithDpi(defaultMargin.top); }
1110 		int marginBottom() { return scaleWithDpi(defaultMargin.bottom); }
1111 		int paddingLeft() { return scaleWithDpi(defaultPadding.left); }
1112 		int paddingRight() { return scaleWithDpi(defaultPadding.right); }
1113 		int paddingTop() { return scaleWithDpi(defaultPadding.top); }
1114 		int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); }
1115 		//LinePreference linePreference() { return LinePreference.PreferOwnLine; }
1116 
1117 		private bool recomputeChildLayoutRequired = true;
1118 		private static class RecomputeEvent {}
1119 		private __gshared rce = new RecomputeEvent();
1120 		protected final void queueRecomputeChildLayout() {
1121 			recomputeChildLayoutRequired = true;
1122 
1123 			if(this.parentWindow) {
1124 				auto sw = this.parentWindow.win;
1125 				assert(sw !is null);
1126 				if(!sw.eventQueued!RecomputeEvent) {
1127 					sw.postEvent(rce);
1128 					// writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
1129 				}
1130 			}
1131 
1132 		}
1133 
1134 		protected final void recomputeChildLayoutEntry() {
1135 			if(recomputeChildLayoutRequired) {
1136 				recomputeChildLayout();
1137 				recomputeChildLayoutRequired = false;
1138 				redraw();
1139 			} else {
1140 				// I still need to check the tree just in case one of them was queued up
1141 				// and the event came up here instead of there.
1142 				foreach(child; children)
1143 					child.recomputeChildLayoutEntry();
1144 			}
1145 		}
1146 
1147 		// this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon
1148 		void recomputeChildLayout() {
1149 			.recomputeChildLayout!"height"(this);
1150 		}
1151 
1152 	// }
1153 
1154 
1155 	/++
1156 		Returns the style's tag name string this object uses.
1157 
1158 		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.
1159 
1160 		This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS.
1161 
1162 		History:
1163 			Added May 10, 2021
1164 	+/
1165 	string styleTagName() const {
1166 		string n = typeid(this).name;
1167 		foreach_reverse(idx, ch; n)
1168 			if(ch == '.') {
1169 				n = n[idx + 1 .. $];
1170 				break;
1171 			}
1172 		return n;
1173 	}
1174 
1175 	/// API for the [styleClassList]
1176 	static struct ClassList {
1177 		private Widget widget;
1178 
1179 		///
1180 		void add(string s) {
1181 			widget.styleClassList_ ~= s;
1182 		}
1183 
1184 		///
1185 		void remove(string s) {
1186 			foreach(idx, s1; widget.styleClassList_)
1187 				if(s1 == s) {
1188 					widget.styleClassList_[idx] = widget.styleClassList_[$-1];
1189 					widget.styleClassList_ = widget.styleClassList_[0 .. $-1];
1190 					widget.styleClassList_.assumeSafeAppend();
1191 					return;
1192 				}
1193 		}
1194 
1195 		/// Returns true if it was added, false if it was removed.
1196 		bool toggle(string s) {
1197 			if(contains(s)) {
1198 				remove(s);
1199 				return false;
1200 			} else {
1201 				add(s);
1202 				return true;
1203 			}
1204 		}
1205 
1206 		///
1207 		bool contains(string s) const {
1208 			foreach(s1; widget.styleClassList_)
1209 				if(s1 == s)
1210 					return true;
1211 			return false;
1212 
1213 		}
1214 	}
1215 
1216 	private string[] styleClassList_;
1217 
1218 	/++
1219 		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.
1220 
1221 		It has no inherent meaning, it is really just a place to put some metadata tags on individual objects.
1222 
1223 		History:
1224 			Added May 10, 2021
1225 	+/
1226 	inout(ClassList) styleClassList() inout {
1227 		return cast(inout(ClassList)) ClassList(cast() this);
1228 	}
1229 
1230 	/++
1231 		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.
1232 
1233 		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.
1234 
1235 		The upper 32 bits are available for your own extensions.
1236 
1237 		History:
1238 			Added May 10, 2021
1239 
1240 		Examples:
1241 
1242 		---
1243 		addEventListener((MouseUpEvent ev) {
1244 			if(ev.button == MouseButton.left) {
1245 				// the first arg is the state to modify, the second arg is what to set it to
1246 				setDynamicState(DynamicState.depressed, false);
1247 			}
1248 		});
1249 		---
1250 
1251 	+/
1252 	enum DynamicState : ulong {
1253 		focus = (1 << 0), /// the widget currently has the keyboard focus
1254 		hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated)
1255 		valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if no validation has been performed!)
1256 		invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if no validation has been performed!)
1257 		checked = (1 << 4), /// the widget is toggleable and currently toggled on
1258 		selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked.
1259 		disabled = (1 << 6), /// the widget is currently unable to perform its designated task
1260 		indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked
1261 		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.
1262 
1263 		USER_BEGIN = (1UL << 32),
1264 	}
1265 
1266 	// I want to add the primary and cancel styles to buttons at least at some point somehow.
1267 
1268 	/// ditto
1269 	@property ulong dynamicState() { return dynamicState_; }
1270 	/// ditto
1271 	@property ulong dynamicState(ulong newValue) {
1272 		if(dynamicState != newValue) {
1273 			auto old = dynamicState_;
1274 			dynamicState_ = newValue;
1275 
1276 			useStyleProperties((scope Widget.Style s) {
1277 				if(s.variesWithState(old ^ newValue))
1278 					redraw();
1279 			});
1280 		}
1281 		return dynamicState_;
1282 	}
1283 
1284 	/// ditto
1285 	void setDynamicState(ulong flags, bool state) {
1286 		auto ds = dynamicState_;
1287 		if(state)
1288 			ds |= flags;
1289 		else
1290 			ds &= ~flags;
1291 
1292 		dynamicState = ds;
1293 	}
1294 
1295 	private ulong dynamicState_;
1296 
1297 	deprecated("Use dynamic styles instead now") {
1298 		Color backgroundColor() { return backgroundColor_; }
1299 		void backgroundColor(Color c){ this.backgroundColor_ = c; }
1300 
1301 		MouseCursor cursor() { return GenericCursor.Default; }
1302 	} private Color backgroundColor_ = Color.transparent;
1303 
1304 
1305 	/++
1306 		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).
1307 
1308 		It is here so there can be a specificity switch.
1309 
1310 		See [OverrideStyle] for a helper function to use your own.
1311 
1312 		History:
1313 			Added May 11, 2021
1314 	+/
1315 	static class Style/* : StyleProperties*/ {
1316 		public Widget widget; // public because the mixin template needs access to it
1317 
1318 		/++
1319 			You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style.
1320 
1321 			History:
1322 				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.
1323 		+/
1324 		bool variesWithState(ulong dynamicStateFlags) {
1325 			version(win32_widgets) {
1326 				if(widget.hwnd)
1327 					return false;
1328 			}
1329 			return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false);
1330 		}
1331 
1332 		///
1333 		Color foregroundColor() {
1334 			return WidgetPainter.visualTheme.foregroundColor;
1335 		}
1336 
1337 		///
1338 		WidgetBackground background() {
1339 			// the default is a "transparent" background, which means
1340 			// it goes as far up as it can to get the color
1341 			if (widget.backgroundColor_ != Color.transparent)
1342 				return WidgetBackground(widget.backgroundColor_);
1343 			if (widget.parent)
1344 				return widget.parent.getComputedStyle.background;
1345 			return WidgetBackground(widget.backgroundColor_);
1346 		}
1347 
1348 		private static OperatingSystemFont fontCached_;
1349 		private OperatingSystemFont fontCached() {
1350 			if(fontCached_ is null)
1351 				fontCached_ = font();
1352 			return fontCached_;
1353 		}
1354 
1355 		/++
1356 			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.
1357 		+/
1358 		OperatingSystemFont font() {
1359 			return null;
1360 		}
1361 
1362 		/++
1363 			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.
1364 
1365 			You can return a member of [GenericCursor] or your own [MouseCursor] instance.
1366 
1367 			History:
1368 				Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021
1369 		+/
1370 		MouseCursor cursor() {
1371 			return GenericCursor.Default;
1372 		}
1373 
1374 		FrameStyle borderStyle() {
1375 			return FrameStyle.none;
1376 		}
1377 
1378 		/++
1379 		+/
1380 		Color borderColor() {
1381 			return Color.transparent;
1382 		}
1383 
1384 		FrameStyle outlineStyle() {
1385 			if(widget.dynamicState & DynamicState.focus)
1386 				return FrameStyle.dotted;
1387 			else
1388 				return FrameStyle.none;
1389 		}
1390 
1391 		Color outlineColor() {
1392 			return foregroundColor;
1393 		}
1394 	}
1395 
1396 	/++
1397 		This mixin overrides the [useStyleProperties] method to direct it toward your own style class.
1398 		The basic usage is simple:
1399 
1400 		---
1401 		static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */
1402 			// override style hints as-needed here
1403 		}
1404 		OverrideStyle!Style; // add the method
1405 		---
1406 
1407 		$(TIP
1408 			While the class is not forced to be `static`, for best results, it should be. A non-static class
1409 			can not be inherited by other objects whereas the static one can. A property on the base class,
1410 			called [Widget.Style.widget|widget], is available for you to access its properties.
1411 		)
1412 
1413 		This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must
1414 		repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches
1415 		and that's a little tedious to repeat in your child classes too when you only care about changing the type.
1416 
1417 
1418 		It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget.
1419 		You may also just override `variesWithState` when you use this flag.
1420 
1421 		---
1422 		mixin OverrideStyle!(
1423 			DynamicState.focus, YourFocusedStyle,
1424 			DynamicState.hover, YourHoverStyle,
1425 			YourDefaultStyle
1426 		)
1427 		---
1428 
1429 		It checks if `dynamicState` matches the state and if so, returns the object given.
1430 
1431 		If there is no state mask given, the next one matches everything. The first match given is used.
1432 
1433 		However, since in most cases you'll want check state inside your individual methods, you probably won't
1434 		find much use for this whole-class swap out.
1435 
1436 		History:
1437 			Added May 16, 2021
1438 	+/
1439 	static protected mixin template OverrideStyle(S...) {
1440 		static import amg = arsd.minigui;
1441 		override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) {
1442 			ulong mask = 0;
1443 			foreach(idx, thing; S) {
1444 				static if(is(typeof(thing) : ulong)) {
1445 					mask = thing;
1446 				} else {
1447 					if(!(idx & 1) || (this.dynamicState & mask) == mask) {
1448 						//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.");
1449 						scope amg.Widget.Style s = new thing();
1450 						s.widget = this;
1451 						dg(s);
1452 						return;
1453 					}
1454 				}
1455 			}
1456 		}
1457 	}
1458 	/++
1459 		You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose.
1460 	+/
1461 	void useStyleProperties(scope void delegate(scope Style props) dg) {
1462 		scope Style s = new Style();
1463 		s.widget = this;
1464 		dg(s);
1465 	}
1466 
1467 
1468 	protected void sendResizeEvent() {
1469 		this.emit!ResizeEvent();
1470 	}
1471 
1472 	/++
1473 		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.
1474 
1475 		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);`
1476 
1477 		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.
1478 
1479 		See_Also:
1480 			[createContextMenuFromAnnotatedCode]
1481 	+/
1482 	Menu contextMenu(int x, int y) { return null; }
1483 
1484 	/++
1485 		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.
1486 	+/
1487 	final bool showContextMenu(int x, int y) {
1488 		return showContextMenu(x, y, -2, -2);
1489 	}
1490 
1491 	private final bool showContextMenu(int x, int y, int screenX, int screenY) {
1492 		if(parentWindow is null || parentWindow.win is null) return false;
1493 
1494 		auto menu = this.contextMenu(x, y);
1495 		if(menu is null)
1496 			return false;
1497 
1498 		version(win32_widgets) {
1499 			// FIXME: if it is -1, -1, do it at the current selection location instead
1500 			// tho the corner of the window, which it does now, isn't the literal worst.
1501 
1502 			// i see notepad just seems to put it in the center of the window so idk
1503 
1504 			if(screenX < 0 && screenY < 0) {
1505 				auto p = this.globalCoordinates();
1506 				if(screenX == -2)
1507 					p.x += x;
1508 				if(screenY == -2)
1509 					p.y += y;
1510 
1511 				screenX = p.x;
1512 				screenY = p.y;
1513 			}
1514 
1515 			if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null))
1516 				throw new Exception("TrackContextMenuEx");
1517 		} else version(custom_widgets) {
1518 			menu.popup(this, x, y);
1519 		}
1520 
1521 		return true;
1522 	}
1523 
1524 	/++
1525 		Removes this widget from its parent.
1526 
1527 		History:
1528 			`removeWidget` was made `final` on May 11, 2021.
1529 	+/
1530 	@scriptable
1531 	final void removeWidget() {
1532 		auto p = this.parent;
1533 		if(p) {
1534 			int item;
1535 			for(item = 0; item < p._children.length; item++)
1536 				if(p._children[item] is this)
1537 					break;
1538 			auto idx = item;
1539 			for(; item < p._children.length - 1; item++)
1540 				p._children[item] = p._children[item + 1];
1541 			p._children = p._children[0 .. $-1];
1542 
1543 			this.parent.widgetRemoved(idx, this);
1544 			//this.parent = null;
1545 
1546 			p.queueRecomputeChildLayout();
1547 		}
1548 		version(win32_widgets) {
1549 			removeAllChildren();
1550 			if(hwnd) {
1551 				DestroyWindow(hwnd);
1552 				hwnd = null;
1553 			}
1554 		}
1555 	}
1556 
1557 	/++
1558 		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.
1559 
1560 		History:
1561 			Added September 19, 2021
1562 	+/
1563 	protected void widgetRemoved(size_t oldIndex, Widget oldReference) { }
1564 
1565 	/++
1566 		Removes all child widgets from `this`. You should not use the removed widgets again.
1567 
1568 		Note that on Windows, it also destroys the native handles for the removed children recursively.
1569 
1570 		History:
1571 			Added July 1, 2021 (dub v10.2)
1572 	+/
1573 	void removeAllChildren() {
1574 		version(win32_widgets)
1575 		foreach(child; _children) {
1576 			child.removeAllChildren();
1577 			if(child.hwnd) {
1578 				DestroyWindow(child.hwnd);
1579 				child.hwnd = null;
1580 			}
1581 		}
1582 		auto orig = this._children;
1583 		this._children = null;
1584 		foreach(idx, w; orig)
1585 			this.widgetRemoved(idx, w);
1586 
1587 		queueRecomputeChildLayout();
1588 	}
1589 
1590 	/++
1591 		Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible.
1592 	+/
1593 	@scriptable
1594 	Widget getChildByName(string name) {
1595 		return getByName(name);
1596 	}
1597 	/++
1598 		Finds the nearest descendant with the requested type and [name]. May return `this`.
1599 	+/
1600 	final WidgetClass getByName(WidgetClass = Widget)(string name) {
1601 		if(this.name == name)
1602 			if(auto c = cast(WidgetClass) this)
1603 				return c;
1604 		foreach(child; children) {
1605 			auto w = child.getByName(name);
1606 			if(auto c = cast(WidgetClass) w)
1607 				return c;
1608 		}
1609 		return null;
1610 	}
1611 
1612 	/++
1613 		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.
1614 		Names should be unique in a window.
1615 
1616 		See_Also: [getByName], [getChildByName]
1617 	+/
1618 	@scriptable string name;
1619 
1620 	private EventHandler[][string] bubblingEventHandlers;
1621 	private EventHandler[][string] capturingEventHandlers;
1622 
1623 	/++
1624 		Default event handlers. These are called on the appropriate
1625 		event unless [Event.preventDefault] is called on the event at
1626 		some point through the bubbling process.
1627 
1628 
1629 		If you are implementing your own widget and want to add custom
1630 		events, you should follow the same pattern here: create a virtual
1631 		function named `defaultEventHandler_eventname` with the implementation,
1632 		then, override [setupDefaultEventHandlers] and add a wrapped caller to
1633 		`defaultEventHandlers["eventname"]`. It should be wrapped like so:
1634 		`defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`.
1635 		This ensures virtual dispatch based on the correct subclass.
1636 
1637 		Also, don't forget to call `super.setupDefaultEventHandlers();` too in your
1638 		overridden version.
1639 
1640 		You only need to do that on parent classes adding NEW event types. If you
1641 		just want to change the default behavior of an existing event type in a subclass,
1642 		you override the function (and optionally call `super.method_name`) like normal.
1643 
1644 		History:
1645 			Some of the events changed to take specific subclasses instead of generic `Event`
1646 			on January 3, 2025.
1647 
1648 	+/
1649 	protected EventHandler[string] defaultEventHandlers;
1650 
1651 	/// ditto
1652 	void setupDefaultEventHandlers() {
1653 		defaultEventHandlers["click"] = (Widget t, Event event)      { if(auto e = cast(ClickEvent) event) t.defaultEventHandler_click(e); };
1654 		defaultEventHandlers["dblclick"] = (Widget t, Event event)   { if(auto e = cast(DoubleClickEvent) event) t.defaultEventHandler_dblclick(e); };
1655 		defaultEventHandlers["keydown"] = (Widget t, Event event)    { if(auto e = cast(KeyDownEvent) event) t.defaultEventHandler_keydown(e); };
1656 		defaultEventHandlers["keyup"] = (Widget t, Event event)      { if(auto e = cast(KeyUpEvent) event) t.defaultEventHandler_keyup(e); };
1657 		defaultEventHandlers["mouseover"] = (Widget t, Event event)  { if(auto e = cast(MouseOverEvent) event) t.defaultEventHandler_mouseover(e); };
1658 		defaultEventHandlers["mouseout"] = (Widget t, Event event)   { if(auto e = cast(MouseOutEvent) event) t.defaultEventHandler_mouseout(e); };
1659 		defaultEventHandlers["mousedown"] = (Widget t, Event event)  { if(auto e = cast(MouseDownEvent) event) t.defaultEventHandler_mousedown(e); };
1660 		defaultEventHandlers["mouseup"] = (Widget t, Event event)    { if(auto e = cast(MouseUpEvent) event) t.defaultEventHandler_mouseup(e); };
1661 		defaultEventHandlers["mouseenter"] = (Widget t, Event event) { if(auto e = cast(MouseEnterEvent) event) t.defaultEventHandler_mouseenter(e); };
1662 		defaultEventHandlers["mouseleave"] = (Widget t, Event event) { if(auto e = cast(MouseLeaveEvent) event) t.defaultEventHandler_mouseleave(e); };
1663 		defaultEventHandlers["mousemove"] = (Widget t, Event event)  { if(auto e = cast(MouseMoveEvent) event) t.defaultEventHandler_mousemove(e); };
1664 		defaultEventHandlers["char"] = (Widget t, Event event)       { if(auto e = cast(CharEvent) event) t.defaultEventHandler_char(e); };
1665 		defaultEventHandlers["triggered"] = (Widget t, Event event)  { if(auto e = cast(Event) event) t.defaultEventHandler_triggered(e); };
1666 		defaultEventHandlers["change"] = (Widget t, Event event)     { if(auto e = cast(ChangeEventBase) event) t.defaultEventHandler_change(e); };
1667 		defaultEventHandlers["focus"] = (Widget t, Event event)      { if(auto e = cast(FocusEvent) event) t.defaultEventHandler_focus(e); };
1668 		defaultEventHandlers["blur"] = (Widget t, Event event)       { if(auto e = cast(BlurEvent) event) t.defaultEventHandler_blur(e); };
1669 		defaultEventHandlers["focusin"] = (Widget t, Event event)    { if(auto e = cast(FocusInEvent) event) t.defaultEventHandler_focusin(e); };
1670 		defaultEventHandlers["focusout"] = (Widget t, Event event)   { if(auto e = cast(FocusOutEvent) event) t.defaultEventHandler_focusout(e); };
1671 	}
1672 
1673 	/// ditto
1674 	void defaultEventHandler_click(ClickEvent event) {}
1675 	/// ditto
1676 	void defaultEventHandler_dblclick(DoubleClickEvent event) {}
1677 	/// ditto
1678 	void defaultEventHandler_keydown(KeyDownEvent event) {}
1679 	/// ditto
1680 	void defaultEventHandler_keyup(KeyUpEvent event) {}
1681 	/// ditto
1682 	void defaultEventHandler_mousedown(MouseDownEvent event) {
1683 		if(event.button == MouseButton.left) {
1684 			if(this.tabStop) {
1685 				this.focus();
1686 			}
1687 		} else if(event.button == MouseButton.right) {
1688 			showContextMenu(event.clientX, event.clientY);
1689 		}
1690 	}
1691 	/// ditto
1692 	void defaultEventHandler_mouseover(MouseOverEvent event) {}
1693 	/// ditto
1694 	void defaultEventHandler_mouseout(MouseOutEvent event) {}
1695 	/// ditto
1696 	void defaultEventHandler_mouseup(MouseUpEvent event) {}
1697 	/// ditto
1698 	void defaultEventHandler_mousemove(MouseMoveEvent event) {}
1699 	/// ditto
1700 	void defaultEventHandler_mouseenter(MouseEnterEvent event) {}
1701 	/// ditto
1702 	void defaultEventHandler_mouseleave(MouseLeaveEvent event) {}
1703 	/// ditto
1704 	void defaultEventHandler_char(CharEvent event) {}
1705 	/// ditto
1706 	void defaultEventHandler_triggered(Event event) {}
1707 	/// ditto
1708 	void defaultEventHandler_change(ChangeEventBase event) {}
1709 	/// ditto
1710 	void defaultEventHandler_focus(FocusEvent event) {}
1711 	/// ditto
1712 	void defaultEventHandler_blur(BlurEvent event) {}
1713 	/// ditto
1714 	void defaultEventHandler_focusin(FocusInEvent event) {}
1715 	/// ditto
1716 	void defaultEventHandler_focusout(FocusOutEvent event) {}
1717 
1718 	/++
1719 		[Event]s use a Javascript-esque model. See more details on the [Event] page.
1720 
1721 		[addEventListener] returns an opaque handle that you can later pass to [removeEventListener].
1722 
1723 		addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out
1724 		of participating in handler delegation.
1725 
1726 		$(TIP
1727 			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.
1728 		)
1729 	+/
1730 	EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) {
1731 		return addEventListener(event, (Widget, scope Event e) {
1732 			if(e.srcElement is this)
1733 				handler();
1734 		}, useCapture);
1735 	}
1736 
1737 	/// ditto
1738 	EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1739 		return addEventListener(event, (Widget, Event e) {
1740 			if(e.srcElement is this)
1741 				handler(e);
1742 		}, useCapture);
1743 	}
1744 
1745 	/// ditto
1746 	EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) {
1747 		static if(is(Handler Fn == delegate)) {
1748 		static if(is(Fn Params == __parameters)) {
1749 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1750 				if(e.srcElement !is this)
1751 					return;
1752 				auto ty = cast(Params[0]) e;
1753 				if(ty !is null)
1754 					handler(ty);
1755 			}, useCapture);
1756 		} else static assert(0);
1757 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1758 	}
1759 
1760 	/// ditto
1761 	@scriptable
1762 	EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) {
1763 		return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture);
1764 	}
1765 
1766 	/// ditto
1767 	EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) {
1768 		static if(is(Handler Fn == delegate)) {
1769 		static if(is(Fn Params == __parameters)) {
1770 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1771 				auto ty = cast(Params[0]) e;
1772 				if(ty !is null)
1773 					handler(ty);
1774 			}, useCapture);
1775 		} else static assert(0);
1776 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1777 	}
1778 
1779 	/// ditto
1780 	EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1781 		return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture);
1782 	}
1783 
1784 	/// ditto
1785 	EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) {
1786 		if(event.length > 2 && event[0..2] == "on")
1787 			event = event[2 .. $];
1788 
1789 		if(useCapture)
1790 			capturingEventHandlers[event] ~= handler;
1791 		else
1792 			bubblingEventHandlers[event] ~= handler;
1793 
1794 		return EventListener(this, event, handler, useCapture);
1795 	}
1796 
1797 	/// ditto
1798 	void removeEventListener(string event, EventHandler handler, bool useCapture = false) {
1799 		if(event.length > 2 && event[0..2] == "on")
1800 			event = event[2 .. $];
1801 
1802 		if(useCapture) {
1803 			if(event in capturingEventHandlers)
1804 			foreach(ref evt; capturingEventHandlers[event])
1805 				if(evt is handler) evt = null;
1806 		} else {
1807 			if(event in bubblingEventHandlers)
1808 			foreach(ref evt; bubblingEventHandlers[event])
1809 				if(evt is handler) evt = null;
1810 		}
1811 	}
1812 
1813 	/// ditto
1814 	void removeEventListener(EventListener listener) {
1815 		removeEventListener(listener.event, listener.handler, listener.useCapture);
1816 	}
1817 
1818 	static if(UsingSimpledisplayX11) {
1819 		void discardXConnectionState() {
1820 			foreach(child; children)
1821 				child.discardXConnectionState();
1822 		}
1823 
1824 		void recreateXConnectionState() {
1825 			foreach(child; children)
1826 				child.recreateXConnectionState();
1827 			redraw();
1828 		}
1829 	}
1830 
1831 	/++
1832 		Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen.
1833 
1834 		History:
1835 			`globalCoordinates` was made `final` on May 11, 2021.
1836 	+/
1837 	Point globalCoordinates() {
1838 		int x = this.x;
1839 		int y = this.y;
1840 		auto p = this.parent;
1841 		while(p) {
1842 			x += p.x;
1843 			y += p.y;
1844 			p = p.parent;
1845 		}
1846 
1847 		static if(UsingSimpledisplayX11) {
1848 			auto dpy = XDisplayConnection.get;
1849 			arsd.simpledisplay.Window dummyw;
1850 			XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw);
1851 		} else version(Windows) {
1852 			POINT pt;
1853 			pt.x = x;
1854 			pt.y = y;
1855 			MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1);
1856 			x = pt.x;
1857 			y = pt.y;
1858 		} else {
1859 			featureNotImplemented();
1860 		}
1861 
1862 		return Point(x, y);
1863 	}
1864 
1865 	version(win32_widgets)
1866 	int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; }
1867 
1868 	version(win32_widgets)
1869 	/// Called when a WM_COMMAND is sent to the associated hwnd.
1870 	void handleWmCommand(ushort cmd, ushort id) {}
1871 
1872 	version(win32_widgets)
1873 	/++
1874 		Called when a WM_NOTIFY is sent to the associated hwnd.
1875 
1876 		History:
1877 	+/
1878 	int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; }
1879 
1880 	version(win32_widgets)
1881 	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); }
1882 
1883 	/++
1884 		This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget.
1885 
1886 		Updates to this variable will only be made visible on the next mouse enter event.
1887 	+/
1888 	@scriptable string statusTip;
1889 	// string toolTip;
1890 	// string helpText;
1891 
1892 	/++
1893 		If true, this widget can be focused via keyboard control with the tab key.
1894 
1895 		If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to).
1896 	+/
1897 	bool tabStop = true;
1898 	/++
1899 		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.)
1900 	+/
1901 	int tabOrder;
1902 
1903 	version(win32_widgets) {
1904 		static Widget[HWND] nativeMapping;
1905 		/// The native handle, if there is one.
1906 		HWND hwnd;
1907 		WNDPROC originalWindowProcedure;
1908 
1909 		SimpleWindow simpleWindowWrappingHwnd;
1910 
1911 		// please note it IGNORES your return value and does NOT forward it to Windows!
1912 		int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) {
1913 			return 0;
1914 		}
1915 	}
1916 	private bool implicitlyCreated;
1917 
1918 	/// 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.
1919 	int x;
1920 	/// ditto
1921 	int y;
1922 	private int _width;
1923 	private int _height;
1924 	private Widget[] _children;
1925 	private Widget _parent;
1926 	private Window _parentWindow;
1927 
1928 	/++
1929 		Returns the window to which this widget is attached.
1930 
1931 		History:
1932 			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.
1933 	+/
1934 	final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; }
1935 	private @property void parentWindow(Window parent) {
1936 		auto old = _parentWindow;
1937 		_parentWindow = parent;
1938 		newParentWindow(old, _parentWindow);
1939 		foreach(child; children)
1940 			child.parentWindow = parent; // please note that this is recursive
1941 	}
1942 
1943 	/++
1944 		Called when the widget has been added to or remove from a parent window.
1945 
1946 		Note that either oldParent and/or newParent may be null any time this is called.
1947 
1948 		History:
1949 			Added September 13, 2024
1950 	+/
1951 	protected void newParentWindow(Window oldParent, Window newParent) {}
1952 
1953 	/++
1954 		Returns the list of the widget's children.
1955 
1956 		History:
1957 			Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private.
1958 
1959 			Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list.
1960 	+/
1961 	final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; }
1962 
1963 	/++
1964 		Returns the widget's parent.
1965 
1966 		History:
1967 			Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted.
1968 
1969 			The parent should only be managed by the [addChild] and [removeWidget] method.
1970 	+/
1971 	final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; }
1972 
1973 	/// The widget's current size.
1974 	final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; }
1975 	/// ditto
1976 	final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; }
1977 
1978 	/// Only the layout manager should be calling these.
1979 	final protected @property int width(int a) @safe { return _width = a; }
1980 	/// ditto
1981 	final protected @property int height(int a) @safe { return _height = a; }
1982 
1983 	/++
1984 		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.
1985 
1986 		It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size.
1987 	+/
1988 	protected void registerMovement() {
1989 		version(win32_widgets) {
1990 			if(hwnd) {
1991 				auto pos = getChildPositionRelativeToParentHwnd(this);
1992 				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
1993 				this.redraw();
1994 			}
1995 		}
1996 		sendResizeEvent();
1997 	}
1998 
1999 	/// Creates the widget and adds it to the parent.
2000 	this(Widget parent) {
2001 		if(parent !is null)
2002 			parent.addChild(this);
2003 		setupDefaultEventHandlers();
2004 	}
2005 
2006 	/// 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.
2007 	@scriptable
2008 	bool isFocused() {
2009 		return parentWindow && parentWindow.focusedWidget is this;
2010 	}
2011 
2012 	private bool showing_ = true;
2013 	///
2014 	bool showing() const { return showing_; }
2015 	///
2016 	bool hidden() const { return !showing_; }
2017 	/++
2018 		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.
2019 
2020 		Note that a widget only ever shows if all its parents are showing too.
2021 	+/
2022 	void showing(bool s, bool recalculate = true) {
2023 		if(s != showing_) {
2024 			showing_ = s;
2025 			// writeln(typeid(this).toString, " ", this.parent ? typeid(this.parent).toString : "null", " ", s);
2026 
2027 			showNativeWindowChildren(s);
2028 
2029 			if(parent && recalculate) {
2030 				parent.queueRecomputeChildLayout();
2031 				parent.redraw();
2032 			}
2033 
2034 			if(s) {
2035 				queueRecomputeChildLayout();
2036 				redraw();
2037 			}
2038 		}
2039 	}
2040 	/// Convenience method for `showing = true`
2041 	@scriptable
2042 	void show() {
2043 		showing = true;
2044 	}
2045 	/// Convenience method for `showing = false`
2046 	@scriptable
2047 	void hide() {
2048 		showing = false;
2049 	}
2050 
2051 	/++
2052 		If you are a native window, show/hide it based on shouldShow and return `true`.
2053 
2054 		Otherwise, do nothing and return false.
2055 	+/
2056 	protected bool showOrHideIfNativeWindow(bool shouldShow) {
2057 		version(win32_widgets) {
2058 			if(hwnd) {
2059 				ShowWindow(hwnd, shouldShow ? SW_SHOW : SW_HIDE);
2060 				return true;
2061 			} else {
2062 				return false;
2063 			}
2064 		} else {
2065 			return false;
2066 		}
2067 	}
2068 
2069 	private void showNativeWindowChildren(bool s) {
2070 		if(!showOrHideIfNativeWindow(s && showing))
2071 			foreach(child; children)
2072 				child.showNativeWindowChildren(s);
2073 	}
2074 
2075 	///
2076 	@scriptable
2077 	void focus() {
2078 		assert(parentWindow !is null);
2079 		if(isFocused())
2080 			return;
2081 
2082 		if(parentWindow.focusedWidget) {
2083 			// FIXME: more details here? like from and to
2084 			auto from = parentWindow.focusedWidget;
2085 			parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false);
2086 			parentWindow.focusedWidget = null;
2087 			from.emit!BlurEvent();
2088 			from.emit!FocusOutEvent();
2089 		}
2090 
2091 
2092 		version(win32_widgets) {
2093 			if(this.hwnd !is null)
2094 				SetFocus(this.hwnd);
2095 		}
2096 		//else static if(UsingSimpledisplayX11)
2097 			//this.parentWindow.win.focus();
2098 
2099 		parentWindow.focusedWidget = this;
2100 		parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true);
2101 		this.emit!FocusEvent();
2102 		this.emit!FocusInEvent();
2103 	}
2104 
2105 	/+
2106 	/++
2107 		Unfocuses the widget. This may reset
2108 	+/
2109 	@scriptable
2110 	void blur() {
2111 
2112 	}
2113 	+/
2114 
2115 
2116 	/++
2117 		This is called when the widget is added to a window. It gives you a chance to set up event hooks.
2118 
2119 		Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods.
2120 	+/
2121 	void attachedToWindow(Window w) {}
2122 	/++
2123 		Callback when the widget is added to another widget.
2124 
2125 		Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful.
2126 	+/
2127 	void addedTo(Widget w) {}
2128 
2129 	/++
2130 		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.
2131 
2132 		This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget.
2133 	+/
2134 	protected void addChild(Widget w, int position = int.max) {
2135 		assert(w._parent !is this, "Child cannot be added twice to the same parent");
2136 		assert(w !is this, "Child cannot be its own parent!");
2137 		w._parent = this;
2138 		if(position == int.max || position == children.length) {
2139 			_children ~= w;
2140 		} else {
2141 			assert(position < _children.length);
2142 			_children.length = _children.length + 1;
2143 			for(int i = cast(int) _children.length - 1; i > position; i--)
2144 				_children[i] = _children[i - 1];
2145 			_children[position] = w;
2146 		}
2147 
2148 		this.parentWindow = this._parentWindow;
2149 
2150 		w.addedTo(this);
2151 
2152 		bool parentIsNative;
2153 		version(win32_widgets) {
2154 			parentIsNative = hwnd !is null;
2155 		}
2156 		if(!parentIsNative && !showing)
2157 			w.showOrHideIfNativeWindow(false);
2158 
2159 		if(parentWindow !is null) {
2160 			w.attachedToWindow(parentWindow);
2161 			parentWindow.queueRecomputeChildLayout();
2162 			parentWindow.redraw();
2163 		}
2164 	}
2165 
2166 	/++
2167 		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.
2168 	+/
2169 	Widget getChildAtPosition(int x, int y) {
2170 		// it goes backward so the last one to show gets picked first
2171 		// might use z-index later
2172 		foreach_reverse(child; children) {
2173 			if(child.hidden)
2174 				continue;
2175 			if(child.x <= x && child.y <= y
2176 				&& ((x - child.x) < child.width)
2177 				&& ((y - child.y) < child.height))
2178 			{
2179 				return child;
2180 			}
2181 		}
2182 
2183 		return null;
2184 	}
2185 
2186 	/++
2187 		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.
2188 
2189 		History:
2190 			Added July 2, 2021 (v10.2)
2191 	+/
2192 	protected void addScrollPosition(ref int x, ref int y) {}
2193 
2194 	/++
2195 		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.
2196 
2197 		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.
2198 
2199 		[paint] is not called for system widgets as the OS library draws them instead.
2200 
2201 
2202 		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.
2203 
2204 		You should also look at [WidgetPainter.visualTheme] to be theme aware.
2205 
2206 		History:
2207 			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.
2208 	+/
2209 	void paint(WidgetPainter painter) {
2210 		version(win32_widgets)
2211 			if(hwnd) {
2212 				return;
2213 			}
2214 		painter.drawThemed(&paintContent); // note this refers to the following overload
2215 	}
2216 
2217 	/++
2218 		Responsible for drawing the content as the theme engine is responsible for other elements.
2219 
2220 		$(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.)
2221 
2222 		Params:
2223 			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.
2224 
2225 			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.
2226 
2227 			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.
2228 
2229 		Returns:
2230 			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.
2231 
2232 		History:
2233 			Added May 15, 2021
2234 	+/
2235 	Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
2236 		return bounds;
2237 	}
2238 
2239 	deprecated("Change ScreenPainter to WidgetPainter")
2240 	final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
2241 
2242 	/// I don't actually like the name of this
2243 	/// this draws a background on it
2244 	void erase(WidgetPainter painter) {
2245 		version(win32_widgets)
2246 			if(hwnd) return; // Windows will do it. I think.
2247 
2248 		auto c = getComputedStyle().background.color;
2249 		painter.fillColor = c;
2250 		painter.outlineColor = c;
2251 
2252 		version(win32_widgets) {
2253 			HANDLE b, p;
2254 			if(c.a == 0 && parent is parentWindow) {
2255 				// I don't remember why I had this really...
2256 				b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
2257 				p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
2258 			}
2259 		}
2260 		painter.drawRectangle(Point(0, 0), width, height);
2261 		version(win32_widgets) {
2262 			if(c.a == 0 && parent is parentWindow) {
2263 				SelectObject(painter.impl.hdc, p);
2264 				SelectObject(painter.impl.hdc, b);
2265 			}
2266 		}
2267 	}
2268 
2269 	///
2270 	WidgetPainter draw() {
2271 		int x = this.x, y = this.y;
2272 		auto parent = this.parent;
2273 		while(parent) {
2274 			x += parent.x;
2275 			y += parent.y;
2276 			parent = parent.parent;
2277 		}
2278 
2279 		auto painter = parentWindow.win.draw(true);
2280 		painter.originX = x;
2281 		painter.originY = y;
2282 		painter.setClipRectangle(Point(0, 0), width, height);
2283 		return WidgetPainter(painter, this);
2284 	}
2285 
2286 	/// 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.
2287 	protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
2288 		if(hidden)
2289 			return;
2290 
2291 		int paintX = x;
2292 		int paintY = y;
2293 		if(this.useNativeDrawing()) {
2294 			paintX = 0;
2295 			paintY = 0;
2296 			lox = 0;
2297 			loy = 0;
2298 			containment = Rectangle(0, 0, int.max, int.max);
2299 		}
2300 
2301 		painter.originX = lox + paintX;
2302 		painter.originY = loy + paintY;
2303 
2304 		bool actuallyPainted = false;
2305 
2306 		const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height)));
2307 		if(clip == Rectangle.init) {
2308 			// writeln(this, " clipped out");
2309 			return;
2310 		}
2311 
2312 		bool invalidateChildren = invalidate;
2313 
2314 		if(redrawRequested || force) {
2315 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
2316 
2317 			painter.drawingUpon = this;
2318 
2319 			erase(painter);
2320 			if(painter.visualTheme)
2321 				painter.visualTheme.doPaint(this, painter);
2322 			else
2323 				paint(painter);
2324 
2325 			if(invalidate) {
2326 				// sdpyPrintDebugString("invalidate " ~ typeid(this).name);
2327 				auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height));
2328 				painter.invalidateRect(region);
2329 				// children are contained inside this, so no need to do extra work
2330 				invalidateChildren = false;
2331 			}
2332 
2333 			redrawRequested = false;
2334 			actuallyPainted = true;
2335 		}
2336 
2337 		foreach(child; children) {
2338 			version(win32_widgets)
2339 				if(child.useNativeDrawing()) continue;
2340 			child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren);
2341 		}
2342 
2343 		version(win32_widgets)
2344 		foreach(child; children) {
2345 			if(child.useNativeDrawing) {
2346 				painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child);
2347 				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
2348 			}
2349 		}
2350 	}
2351 
2352 	protected bool useNativeDrawing() nothrow {
2353 		version(win32_widgets)
2354 			return hwnd !is null;
2355 		else
2356 			return false;
2357 	}
2358 
2359 	private static class RedrawEvent {}
2360 	private __gshared re = new RedrawEvent();
2361 
2362 	private bool redrawRequested;
2363 	///
2364 	final void redraw(string file = __FILE__, size_t line = __LINE__) {
2365 		redrawRequested = true;
2366 
2367 		if(this.parentWindow) {
2368 			auto sw = this.parentWindow.win;
2369 			assert(sw !is null);
2370 			if(!sw.eventQueued!RedrawEvent) {
2371 				sw.postEvent(re);
2372 				//  writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
2373 			}
2374 		}
2375 	}
2376 
2377 	private SimpleWindow drawableWindow;
2378 
2379 	/++
2380 		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.
2381 
2382 		Returns:
2383 			`true` if you should do your default behavior.
2384 
2385 		History:
2386 			Added May 5, 2021
2387 
2388 		Bugs:
2389 			It does not do the static checks on gdc right now.
2390 	+/
2391 	final protected bool emit(EventType, this This, Args...)(Args args) {
2392 		version(GNU) {} else
2393 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2394 		auto e = new EventType(this, args);
2395 		e.dispatch();
2396 		return !e.defaultPrevented;
2397 	}
2398 	/// ditto
2399 	final protected bool emit(string eventString, this This)() {
2400 		auto e = new Event(eventString, this);
2401 		e.dispatch();
2402 		return !e.defaultPrevented;
2403 	}
2404 
2405 	/++
2406 		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.
2407 
2408 		History:
2409 			Added May 5, 2021
2410 	+/
2411 	final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) {
2412 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2413 		return addEventListener(handler);
2414 	}
2415 
2416 	/++
2417 		Gets the computed style properties from the visual theme.
2418 
2419 		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].)
2420 
2421 		History:
2422 			Added May 8, 2021
2423 	+/
2424 	final StyleInformation getComputedStyle() {
2425 		return StyleInformation(this);
2426 	}
2427 
2428 	int focusableWidgets(scope int delegate(Widget) dg) {
2429 		foreach(widget; WidgetStream(this)) {
2430 			if(widget.tabStop && !widget.hidden) {
2431 				int result = dg(widget);
2432 				if (result)
2433 					return result;
2434 			}
2435 		}
2436 		return 0;
2437 	}
2438 
2439 	/++
2440 		Calculates the border box (that is, the full width/height of the widget, from border edge to border edge)
2441 		for the given content box (the area between the padding)
2442 
2443 		History:
2444 			Added January 4, 2023 (dub v11.0)
2445 	+/
2446 	Rectangle borderBoxForContentBox(Rectangle contentBox) {
2447 		auto cs = getComputedStyle();
2448 
2449 		auto borderWidth = getBorderWidth(cs.borderStyle);
2450 
2451 		auto rect = contentBox;
2452 
2453 		rect.left -= borderWidth;
2454 		rect.right += borderWidth;
2455 		rect.top -= borderWidth;
2456 		rect.bottom += borderWidth;
2457 
2458 		auto insideBorderRect = rect;
2459 
2460 		rect.left -= cs.paddingLeft;
2461 		rect.right += cs.paddingRight;
2462 		rect.top -= cs.paddingTop;
2463 		rect.bottom += cs.paddingBottom;
2464 
2465 		return rect;
2466 	}
2467 
2468 
2469 	// FIXME: I kinda want to hide events from implementation widgets
2470 	// so it just catches them all and stops propagation...
2471 	// i guess i can do it with a event listener on star.
2472 
2473 	mixin Emits!KeyDownEvent; ///
2474 	mixin Emits!KeyUpEvent; ///
2475 	mixin Emits!CharEvent; ///
2476 
2477 	mixin Emits!MouseDownEvent; ///
2478 	mixin Emits!MouseUpEvent; ///
2479 	mixin Emits!ClickEvent; ///
2480 	mixin Emits!DoubleClickEvent; ///
2481 	mixin Emits!MouseMoveEvent; ///
2482 	mixin Emits!MouseOverEvent; ///
2483 	mixin Emits!MouseOutEvent; ///
2484 	mixin Emits!MouseEnterEvent; ///
2485 	mixin Emits!MouseLeaveEvent; ///
2486 
2487 	mixin Emits!ResizeEvent; ///
2488 
2489 	mixin Emits!BlurEvent; ///
2490 	mixin Emits!FocusEvent; ///
2491 
2492 	mixin Emits!FocusInEvent; ///
2493 	mixin Emits!FocusOutEvent; ///
2494 }
2495 
2496 /+
2497 /++
2498 	Interface to indicate that the widget has a simple value property.
2499 
2500 	History:
2501 		Added August 26, 2021
2502 +/
2503 interface HasValue!T {
2504 	/// Getter
2505 	@property T value();
2506 	/// Setter
2507 	@property void value(T);
2508 }
2509 
2510 /++
2511 	Interface to indicate that the widget has a range of possible values for its simple value property.
2512 	This would be present on something like a slider or possibly a number picker.
2513 
2514 	History:
2515 		Added September 11, 2021
2516 +/
2517 interface HasRangeOfValues!T : HasValue!T {
2518 	/// The minimum and maximum values in the range, inclusive.
2519 	@property T minValue();
2520 	@property void minValue(T); /// ditto
2521 	@property T maxValue(); /// ditto
2522 	@property void maxValue(T); /// ditto
2523 
2524 	/// The smallest step the user interface allows. User may still type in values without this limitation.
2525 	@property void step(T);
2526 	@property T step(); /// ditto
2527 }
2528 
2529 /++
2530 	Interface to indicate that the widget has a list of possible values the user can choose from.
2531 	This would be present on something like a drop-down selector.
2532 
2533 	The value is NOT necessarily one of the items on the list. Consider the case of a free-entry
2534 	combobox.
2535 
2536 	History:
2537 		Added September 11, 2021
2538 +/
2539 interface HasListOfValues!T : HasValue!T {
2540 	@property T[] values;
2541 	@property void values(T[]);
2542 
2543 	@property int selectedIndex(); // note it may return -1!
2544 	@property void selectedIndex(int);
2545 }
2546 +/
2547 
2548 /++
2549 	History:
2550 		Added September 2021 (dub v10.4)
2551 +/
2552 class GridLayout : Layout {
2553 
2554 	// 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.
2555 
2556 	/++
2557 		If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to.
2558 	+/
2559 	enum Gravity {
2560 		Center    = 0,
2561 		NorthWest = North | West,
2562 		North     = 0b10_00,
2563 		NorthEast = North | East,
2564 		West      = 0b00_10,
2565 		East      = 0b00_01,
2566 		SouthWest = South | West,
2567 		South     = 0b01_00,
2568 		SouthEast = South | East,
2569 	}
2570 
2571 	/++
2572 		The width and height are in some proportional units and can often just be 12.
2573 	+/
2574 	this(int width, int height, Widget parent) {
2575 		this.gridWidth = width;
2576 		this.gridHeight = height;
2577 		super(parent);
2578 	}
2579 
2580 	/++
2581 		Sets the position of the given child.
2582 
2583 		The units of these arguments are in the proportional grid units you set in the constructor.
2584 	+/
2585 	Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) {
2586 		// ensure it is in bounds
2587 		// then ensure no overlaps
2588 
2589 		ChildPosition p = ChildPosition(child, x, y, width, height, gravity);
2590 
2591 		foreach(ref position; positions) {
2592 			if(position.widget is child) {
2593 				position = p;
2594 				goto set;
2595 			}
2596 		}
2597 
2598 		positions ~= p;
2599 
2600 		set:
2601 
2602 		// FIXME: should this batch?
2603 		queueRecomputeChildLayout();
2604 
2605 		return child;
2606 	}
2607 
2608 	override void addChild(Widget w, int position = int.max) {
2609 		super.addChild(w, position);
2610 		//positions ~= ChildPosition(w);
2611 		if(position != int.max) {
2612 			// FIXME: align it so they actually match.
2613 		}
2614 	}
2615 
2616 	override void widgetRemoved(size_t idx, Widget w) {
2617 		// FIXME: keep the positions array aligned
2618 		// positions[idx].widget = null;
2619 	}
2620 
2621 	override void recomputeChildLayout() {
2622 		registerMovement();
2623 		int onGrid = cast(int) positions.length;
2624 		c: foreach(child; children) {
2625 			// just snap it to the grid
2626 			if(onGrid)
2627 			foreach(position; positions)
2628 				if(position.widget is child) {
2629 					child.x = this.width * position.x / this.gridWidth;
2630 					child.y = this.height * position.y / this.gridHeight;
2631 					child.width = this.width * position.width / this.gridWidth;
2632 					child.height = this.height * position.height / this.gridHeight;
2633 
2634 					auto diff = child.width - child.maxWidth();
2635 					// FIXME: gravity?
2636 					if(diff > 0) {
2637 						child.width = child.width - diff;
2638 
2639 						if(position.gravity & Gravity.West) {
2640 							// nothing needed, already aligned
2641 						} else if(position.gravity & Gravity.East) {
2642 							child.x += diff;
2643 						} else {
2644 							child.x += diff / 2;
2645 						}
2646 					}
2647 
2648 					diff = child.height - child.maxHeight();
2649 					// FIXME: gravity?
2650 					if(diff > 0) {
2651 						child.height = child.height - diff;
2652 
2653 						if(position.gravity & Gravity.North) {
2654 							// nothing needed, already aligned
2655 						} else if(position.gravity & Gravity.South) {
2656 							child.y += diff;
2657 						} else {
2658 							child.y += diff / 2;
2659 						}
2660 					}
2661 					child.recomputeChildLayout();
2662 					onGrid--;
2663 					continue c;
2664 				}
2665 			// the position isn't given on the grid array, we'll just fill in from where the explicit ones left off.
2666 		}
2667 	}
2668 
2669 	private struct ChildPosition {
2670 		Widget widget;
2671 		int x;
2672 		int y;
2673 		int width;
2674 		int height;
2675 		Gravity gravity;
2676 	}
2677 	private ChildPosition[] positions;
2678 
2679 	int gridWidth = 12;
2680 	int gridHeight = 12;
2681 }
2682 
2683 ///
2684 abstract class ComboboxBase : Widget {
2685 	// if the user can enter arbitrary data, we want to use  2 == CBS_DROPDOWN
2686 	// or to always show the list, we want CBS_SIMPLE == 1
2687 	version(win32_widgets)
2688 		this(uint style, Widget parent) {
2689 			super(parent);
2690 			createWin32Window(this, "ComboBox"w, null, style);
2691 		}
2692 	else version(custom_widgets)
2693 		this(Widget parent) {
2694 			super(parent);
2695 
2696 			addEventListener((KeyDownEvent event) {
2697 				if(event.key == Key.Up) {
2698 					setSelection(selection_-1);
2699 					event.preventDefault();
2700 				}
2701 				if(event.key == Key.Down) {
2702 					setSelection(selection_+1);
2703 					event.preventDefault();
2704 				}
2705 
2706 			});
2707 
2708 		}
2709 	else static assert(false);
2710 
2711 	protected void scrollSelectionIntoView() {}
2712 
2713 	/++
2714 		Returns the current list of options in the selection.
2715 
2716 		History:
2717 			Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private.
2718 	+/
2719 	final @property string[] options() const {
2720 		return cast(string[]) options_;
2721 	}
2722 
2723 	/++
2724 		Replaces the list of options in the box. Note that calling this will also reset the selection.
2725 
2726 		History:
2727 			Added December, 29 2024
2728 	+/
2729 	final @property void options(string[] options) {
2730 		version(win32_widgets)
2731 			SendMessageW(hwnd, 331 /*CB_RESETCONTENT*/, 0, 0);
2732 		selection_ = -1;
2733 		options_ = null;
2734 		foreach(opt; options)
2735 			addOption(opt);
2736 
2737 		version(custom_widgets)
2738 			redraw();
2739 	}
2740 
2741 	private string[] options_;
2742 	private int selection_ = -1;
2743 
2744 	/++
2745 		Adds an option to the end of options array.
2746 	+/
2747 	void addOption(string s) {
2748 		options_ ~= s;
2749 		version(win32_widgets)
2750 		SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s));
2751 	}
2752 
2753 	/++
2754 		Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected.
2755 	+/
2756 	int getSelection() {
2757 		return selection_;
2758 	}
2759 
2760 	/++
2761 		Returns the current selection as a string.
2762 
2763 		History:
2764 			Added November 17, 2021
2765 	+/
2766 	string getSelectionString() {
2767 		return selection_ == -1 ? null : options[selection_];
2768 	}
2769 
2770 	/++
2771 		Sets the current selection to an index in the options array, or to the given option if present.
2772 		Please note that the string version may do a linear lookup.
2773 
2774 		Returns:
2775 			the index you passed in
2776 
2777 		History:
2778 			The `string` based overload was added on March 1, 2022 (dub v10.7).
2779 
2780 			The return value was `void` prior to March 1, 2022.
2781 	+/
2782 	int setSelection(int idx) {
2783 		if(idx < -1)
2784 			idx = -1;
2785 		if(idx + 1 > options.length)
2786 			idx = cast(int) options.length - 1;
2787 
2788 		selection_ = idx;
2789 
2790 		version(win32_widgets)
2791 		SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0);
2792 
2793 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2794 		t.dispatch();
2795 
2796 		scrollSelectionIntoView();
2797 
2798 		return idx;
2799 	}
2800 
2801 	/// ditto
2802 	int setSelection(string s) {
2803 		if(s !is null)
2804 		foreach(idx, item; options)
2805 			if(item == s) {
2806 				return setSelection(cast(int) idx);
2807 			}
2808 		return setSelection(-1);
2809 	}
2810 
2811 	/++
2812 		This event is fired when the selection changes. Both [Event.stringValue] and
2813 		[Event.intValue] are filled in - `stringValue` is the text in the selection
2814 		and `intValue` is the index of the selection. If the combo box allows multiple
2815 		selection, these values will include only one of the selected items - for those,
2816 		you should loop through the values and check their selected flag instead.
2817 
2818 		(I know that sucks, but it is how it is right now.)
2819 
2820 		History:
2821 			It originally inherited from `ChangeEvent!String`, but now does from [ChangeEventBase] as of January 3, 2025.
2822 			This shouldn't break anything if you used it through either its own name `SelectionChangedEvent` or through the
2823 			base `Event`, only if you specifically used `ChangeEvent!string` - those handlers may now get `null` or fail to
2824 			be called. If you did do this, just change it to generic `Event`, as `stringValue` and `intValue` are already there.
2825 	+/
2826 	static final class SelectionChangedEvent : ChangeEventBase {
2827 		this(Widget target, int iv, string sv) {
2828 			super(target);
2829 			this.iv = iv;
2830 			this.sv = sv;
2831 		}
2832 		immutable int iv;
2833 		immutable string sv;
2834 
2835 		deprecated("Use stringValue or intValue instead") @property string value() {
2836 			return sv;
2837 		}
2838 
2839 		override @property string stringValue() { return sv; }
2840 		override @property int intValue() { return iv; }
2841 	}
2842 
2843 	version(win32_widgets)
2844 	override void handleWmCommand(ushort cmd, ushort id) {
2845 		if(cmd == CBN_SELCHANGE) {
2846 			selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0);
2847 			fireChangeEvent();
2848 		}
2849 	}
2850 
2851 	private void fireChangeEvent() {
2852 		if(selection_ >= options.length)
2853 			selection_ = -1;
2854 
2855 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2856 		t.dispatch();
2857 	}
2858 
2859 	override int minWidth() { return scaleWithDpi(32); }
2860 
2861 	version(win32_widgets) {
2862 		override int minHeight() { return defaultLineHeight + 6; }
2863 		override int maxHeight() { return defaultLineHeight + 6; }
2864 	} else {
2865 		override int minHeight() { return defaultLineHeight + 4; }
2866 		override int maxHeight() { return defaultLineHeight + 4; }
2867 	}
2868 
2869 	version(custom_widgets)
2870 	void popup() {
2871 		CustomComboBoxPopup popup = new CustomComboBoxPopup(this);
2872 	}
2873 
2874 }
2875 
2876 private class CustomComboBoxPopup : Window {
2877 	private ComboboxBase associatedWidget;
2878 	private ListWidget lw;
2879 	private bool cancelled;
2880 
2881 	this(ComboboxBase associatedWidget) {
2882 		this.associatedWidget = associatedWidget;
2883 
2884 		// FIXME: this should scroll if there's too many elements to reasonably fit on screen
2885 
2886 		auto w = associatedWidget.width;
2887 		// FIXME: suggestedDropdownHeight see below
2888 		auto h = cast(int) associatedWidget.options.length * associatedWidget.defaultLineHeight + associatedWidget.scaleWithDpi(8);
2889 
2890 		// FIXME: this sux
2891 		if(h > associatedWidget.parentWindow.height)
2892 			h = associatedWidget.parentWindow.height;
2893 
2894 		auto mh = associatedWidget.scaleWithDpi(16 + 16 + 32); // to make the scrollbar look ok
2895 		if(h < mh)
2896 			h = mh;
2897 
2898 		auto coord = associatedWidget.globalCoordinates();
2899 		auto dropDown = new SimpleWindow(
2900 			w, h,
2901 			null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, associatedWidget.parentWindow ? associatedWidget.parentWindow.win : null);
2902 
2903 		super(dropDown);
2904 
2905 		dropDown.move(coord.x, coord.y + associatedWidget.height);
2906 
2907 		this.lw = new ListWidget(this);
2908 		version(custom_widgets)
2909 			lw.multiSelect = false;
2910 		foreach(option; associatedWidget.options)
2911 			lw.addOption(option);
2912 
2913 		auto originalSelection = associatedWidget.getSelection;
2914 		lw.setSelection(originalSelection);
2915 		lw.scrollSelectionIntoView();
2916 
2917 		/+
2918 		{
2919 			auto cs = getComputedStyle();
2920 			auto painter = dropDown.draw();
2921 			draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color);
2922 			auto p = Point(4, 4);
2923 			painter.outlineColor = cs.foregroundColor;
2924 			foreach(option; associatedWidget.options) {
2925 				painter.drawText(p, option);
2926 				p.y += defaultLineHeight;
2927 			}
2928 		}
2929 
2930 		dropDown.setEventHandlers(
2931 			(MouseEvent event) {
2932 				if(event.type == MouseEventType.buttonReleased) {
2933 					dropDown.close();
2934 					auto element = (event.y - 4) / defaultLineHeight;
2935 					if(element >= 0 && element <= associatedWidget.options.length) {
2936 						associatedWidget.selection_ = element;
2937 
2938 						associatedWidget.fireChangeEvent();
2939 					}
2940 				}
2941 			}
2942 		);
2943 		+/
2944 
2945 		Widget previouslyFocusedWidget;
2946 
2947 		dropDown.visibilityChanged = (bool visible) {
2948 			if(visible) {
2949 				this.redraw();
2950 				captureMouse(this);
2951 				//dropDown.grabInput();
2952 
2953 				if(previouslyFocusedWidget is null)
2954 					previouslyFocusedWidget = associatedWidget.parentWindow.focusedWidget;
2955 				associatedWidget.parentWindow.focusedWidget = lw;
2956 			} else {
2957 				//dropDown.releaseInputGrab();
2958 				releaseMouseCapture();
2959 
2960 				if(!cancelled)
2961 					associatedWidget.setSelection(lw.getSelection);
2962 
2963 				associatedWidget.parentWindow.focusedWidget = previouslyFocusedWidget;
2964 			}
2965 		};
2966 
2967 		dropDown.show();
2968 	}
2969 
2970 	private bool shouldCloseIfClicked(Widget w) {
2971 		if(w is this)
2972 			return true;
2973 		version(custom_widgets)
2974 		if(cast(TextListViewWidget.TextListViewItem) w)
2975 			return true;
2976 		return false;
2977 	}
2978 
2979 	override void defaultEventHandler_click(ClickEvent ce) {
2980 		if(ce.button == MouseButton.left && shouldCloseIfClicked(ce.target)) {
2981 			this.win.close();
2982 		}
2983 	}
2984 
2985 	override void defaultEventHandler_char(CharEvent ce) {
2986 		if(ce.character == '\n')
2987 			this.win.close();
2988 	}
2989 
2990 	override void defaultEventHandler_keydown(KeyDownEvent kde) {
2991 		if(kde.key == Key.Escape) {
2992 			cancelled = true;
2993 			this.win.close();
2994 		}/+ else if(kde.key == Key.Up || kde.key == Key.Down)
2995 			{} // intentionally blank, the list view handles these
2996 			// separately from the scroll message widget default handler
2997 		else if(lw && lw.glvw && lw.glvw.smw)
2998 			lw.glvw.smw.defaultKeyboardListener(kde);+/
2999 	}
3000 }
3001 
3002 /++
3003 	A drop-down list where the user must select one of the
3004 	given options. Like `<select>` in HTML.
3005 
3006 	The current selection is given as a string or an index.
3007 	It emits a SelectionChangedEvent when it changes.
3008 +/
3009 class DropDownSelection : ComboboxBase {
3010 	/++
3011 		Creates a drop down selection, optionally passing its initial list of options.
3012 
3013 		History:
3014 			The overload with the `options` parameter was added December 29, 2024.
3015 	+/
3016 	this(Widget parent) {
3017 		version(win32_widgets)
3018 			super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent);
3019 		else version(custom_widgets) {
3020 			super(parent);
3021 
3022 			addEventListener("focus", () { this.redraw; });
3023 			addEventListener("blur", () { this.redraw; });
3024 			addEventListener(EventType.change, () { this.redraw; });
3025 			addEventListener("mousedown", () { this.focus(); this.popup(); });
3026 			addEventListener((KeyDownEvent event) {
3027 				if(event.key == Key.Space)
3028 					popup();
3029 			});
3030 		} else static assert(false);
3031 	}
3032 
3033 	/// ditto
3034 	this(string[] options, Widget parent) {
3035 		this(parent);
3036 		this.options = options;
3037 	}
3038 
3039 	mixin Padding!q{2};
3040 	static class Style : Widget.Style {
3041 		override FrameStyle borderStyle() { return FrameStyle.risen; }
3042 	}
3043 	mixin OverrideStyle!Style;
3044 
3045 	version(custom_widgets)
3046 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
3047 		auto cs = getComputedStyle();
3048 
3049 		painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]);
3050 
3051 		painter.outlineColor = cs.foregroundColor;
3052 		painter.fillColor = cs.foregroundColor;
3053 
3054 		/+
3055 		Point[4] triangle;
3056 		enum padding = 6;
3057 		enum paddingV = 7;
3058 		enum triangleWidth = 10;
3059 		triangle[0] = Point(width - padding - triangleWidth, paddingV);
3060 		triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV);
3061 		triangle[2] = Point(width - padding - 0, paddingV);
3062 		triangle[3] = triangle[0];
3063 		painter.drawPolygon(triangle[]);
3064 		+/
3065 
3066 		auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2);
3067 
3068 		painter.drawPolygon(
3069 			scaleWithDpi(Point(2, 6) + offset),
3070 			scaleWithDpi(Point(7, 11) + offset),
3071 			scaleWithDpi(Point(12, 6) + offset),
3072 			scaleWithDpi(Point(2, 6) + offset)
3073 		);
3074 
3075 
3076 		return bounds;
3077 	}
3078 
3079 	version(win32_widgets)
3080 	override void registerMovement() {
3081 		version(win32_widgets) {
3082 			if(hwnd) {
3083 				auto pos = getChildPositionRelativeToParentHwnd(this);
3084 				// the height given to this from Windows' perspective is supposed
3085 				// to include the drop down's height. so I add to it to give some
3086 				// room for that.
3087 				// FIXME: maybe make the subclass provide a suggestedDropdownHeight thing
3088 				MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true);
3089 			}
3090 		}
3091 		sendResizeEvent();
3092 	}
3093 }
3094 
3095 /++
3096 	A text box with a drop down arrow listing selections.
3097 	The user can choose from the list, or type their own.
3098 +/
3099 class FreeEntrySelection : ComboboxBase {
3100 	this(Widget parent) {
3101 		this(null, parent);
3102 	}
3103 
3104 	this(string[] options, Widget parent) {
3105 		version(win32_widgets)
3106 			super(2 /* CBS_DROPDOWN */, parent);
3107 		else version(custom_widgets) {
3108 			super(parent);
3109 			auto hl = new HorizontalLayout(this);
3110 			lineEdit = new LineEdit(hl);
3111 
3112 			tabStop = false;
3113 
3114 			// lineEdit.addEventListener((FocusEvent fe) {  lineEdit.selectAll(); } );
3115 
3116 			auto btn = new class ArrowButton {
3117 				this() {
3118 					super(ArrowDirection.down, hl);
3119 				}
3120 				override int heightStretchiness() {
3121 					return 1;
3122 				}
3123 				override int heightShrinkiness() {
3124 					return 1;
3125 				}
3126 				override int maxHeight() {
3127 					return lineEdit.maxHeight;
3128 				}
3129 			};
3130 			//btn.addDirectEventListener("focus", &lineEdit.focus);
3131 			btn.addEventListener("triggered", &this.popup);
3132 			addEventListener(EventType.change, (Event event) {
3133 				lineEdit.content = event.stringValue;
3134 				lineEdit.focus();
3135 				redraw();
3136 			});
3137 		}
3138 		else static assert(false);
3139 
3140 		this.options = options;
3141 	}
3142 
3143 	string content() {
3144 		version(win32_widgets)
3145 			assert(0, "not implemented");
3146 		else version(custom_widgets)
3147 			return lineEdit.content;
3148 		else static assert(0);
3149 	}
3150 
3151 	void content(string s) {
3152 		version(win32_widgets)
3153 			assert(0, "not implemented");
3154 		else version(custom_widgets)
3155 			lineEdit.content = s;
3156 		else static assert(0);
3157 	}
3158 
3159 	version(custom_widgets) {
3160 		LineEdit lineEdit;
3161 
3162 		override int widthStretchiness() {
3163 			return lineEdit ? lineEdit.widthStretchiness : super.widthStretchiness;
3164 		}
3165 		override int flexBasisWidth() {
3166 			return lineEdit ? lineEdit.flexBasisWidth : super.flexBasisWidth;
3167 		}
3168 	}
3169 }
3170 
3171 /++
3172 	A combination of free entry with a list below it.
3173 +/
3174 class ComboBox : ComboboxBase {
3175 	this(Widget parent) {
3176 		version(win32_widgets)
3177 			super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent);
3178 		else version(custom_widgets) {
3179 			super(parent);
3180 			lineEdit = new LineEdit(this);
3181 			listWidget = new ListWidget(this);
3182 			listWidget.multiSelect = false;
3183 			listWidget.addEventListener(EventType.change, delegate(Widget, Event) {
3184 				string c = null;
3185 				foreach(option; listWidget.options)
3186 					if(option.selected) {
3187 						c = option.label;
3188 						break;
3189 					}
3190 				lineEdit.content = c;
3191 			});
3192 
3193 			listWidget.tabStop = false;
3194 			this.tabStop = false;
3195 			listWidget.addEventListener("focusin", &lineEdit.focus);
3196 			this.addEventListener("focusin", &lineEdit.focus);
3197 
3198 			addDirectEventListener(EventType.change, {
3199 				listWidget.setSelection(selection_);
3200 				if(selection_ != -1)
3201 					lineEdit.content = options[selection_];
3202 				lineEdit.focus();
3203 				redraw();
3204 			});
3205 
3206 			lineEdit.addEventListener("focusin", &lineEdit.selectAll);
3207 
3208 			listWidget.addDirectEventListener(EventType.change, {
3209 				int set = -1;
3210 				foreach(idx, opt; listWidget.options)
3211 					if(opt.selected) {
3212 						set = cast(int) idx;
3213 						break;
3214 					}
3215 				if(set != selection_)
3216 					this.setSelection(set);
3217 			});
3218 		} else static assert(false);
3219 	}
3220 
3221 	override int minHeight() { return defaultLineHeight * 3; }
3222 	override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; }
3223 	override int heightStretchiness() { return 5; }
3224 
3225 	version(custom_widgets) {
3226 		LineEdit lineEdit;
3227 		ListWidget listWidget;
3228 
3229 		override void addOption(string s) {
3230 			listWidget.addOption(s);
3231 			ComboboxBase.addOption(s);
3232 		}
3233 
3234 		override void scrollSelectionIntoView() {
3235 			listWidget.scrollSelectionIntoView();
3236 		}
3237 	}
3238 }
3239 
3240 /+
3241 class Spinner : Widget {
3242 	version(win32_widgets)
3243 	this(Widget parent) {
3244 		super(parent);
3245 		parentWindow = parent.parentWindow;
3246 		auto hlayout = new HorizontalLayout(this);
3247 		lineEdit = new LineEdit(hlayout);
3248 		upDownControl = new UpDownControl(hlayout);
3249 	}
3250 
3251 	LineEdit lineEdit;
3252 	UpDownControl upDownControl;
3253 }
3254 
3255 class UpDownControl : Widget {
3256 	version(win32_widgets)
3257 	this(Widget parent) {
3258 		super(parent);
3259 		parentWindow = parent.parentWindow;
3260 		createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */);
3261 	}
3262 
3263 	override int minHeight() { return defaultLineHeight; }
3264 	override int maxHeight() { return defaultLineHeight * 3/2; }
3265 
3266 	override int minWidth() { return defaultLineHeight * 3/2; }
3267 	override int maxWidth() { return defaultLineHeight * 3/2; }
3268 }
3269 +/
3270 
3271 /+
3272 class DataView : Widget {
3273 	// this is the omnibus data viewer
3274 	// the internal data layout is something like:
3275 	// string[string][] but also each node can have parents
3276 }
3277 +/
3278 
3279 
3280 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS
3281 
3282 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d
3283 
3284 // FIXME: menus should prolly capture the mouse. ugh i kno.
3285 /*
3286 	TextEdit needs:
3287 
3288 	* caret manipulation
3289 	* selection control
3290 	* convenience functions for appendText, insertText, insertTextAtCaret, etc.
3291 
3292 	For example:
3293 
3294 	connect(paste, &textEdit.insertTextAtCaret);
3295 
3296 	would be nice.
3297 
3298 
3299 
3300 	I kinda want an omnibus dataview that combines list, tree,
3301 	and table - it can be switched dynamically between them.
3302 
3303 	Flattening policy: only show top level, show recursive, show grouped
3304 	List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer)
3305 
3306 	Single select, multi select, organization, drag+drop
3307 */
3308 
3309 //static if(UsingSimpledisplayX11)
3310 version(win32_widgets) {}
3311 else version(custom_widgets) {
3312 	enum scrollClickRepeatInterval = 50;
3313 
3314 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") {
3315 	enum windowBackgroundColor = Color(212, 212, 212); // used to be 192
3316 	enum activeTabColor = lightAccentColor;
3317 	enum hoveringColor = Color(228, 228, 228);
3318 	enum buttonColor = windowBackgroundColor;
3319 	enum depressedButtonColor = darkAccentColor;
3320 	enum activeListXorColor = Color(255, 255, 127);
3321 	enum progressBarColor = Color(0, 0, 128);
3322 	enum activeMenuItemColor = Color(0, 0, 128);
3323 
3324 }}
3325 else static assert(false);
3326 deprecated("Get these properties off the `visualTheme` instead.") {
3327 	// these are used by horizontal rule so not just custom_widgets. for now at least.
3328 	enum darkAccentColor = Color(172, 172, 172);
3329 	enum lightAccentColor = Color(223, 223, 223); // used to be 223
3330 }
3331 
3332 private const(wchar)* toWstringzInternal(in char[] s) {
3333 	wchar[] str;
3334 	str.reserve(s.length + 1);
3335 	foreach(dchar ch; s)
3336 		str ~= ch;
3337 	str ~= '\0';
3338 	return str.ptr;
3339 }
3340 
3341 static if(SimpledisplayTimerAvailable)
3342 void setClickRepeat(Widget w, int interval, int delay = 250) {
3343 	Timer timer;
3344 	int delayRemaining = delay / interval;
3345 	if(delayRemaining <= 1)
3346 		delayRemaining = 2;
3347 
3348 	immutable originalDelayRemaining = delayRemaining;
3349 
3350 	w.addDirectEventListener((scope MouseDownEvent ev) {
3351 		if(ev.srcElement !is w)
3352 			return;
3353 		if(timer !is null) {
3354 			timer.destroy();
3355 			timer = null;
3356 		}
3357 		delayRemaining = originalDelayRemaining;
3358 		timer = new Timer(interval, () {
3359 			if(delayRemaining > 0)
3360 				delayRemaining--;
3361 			else {
3362 				auto ev = new Event("triggered", w);
3363 				ev.sendDirectly();
3364 			}
3365 		});
3366 	});
3367 
3368 	w.addDirectEventListener((scope MouseUpEvent ev) {
3369 		if(ev.srcElement !is w)
3370 			return;
3371 		if(timer !is null) {
3372 			timer.destroy();
3373 			timer = null;
3374 		}
3375 	});
3376 
3377 	w.addDirectEventListener((scope MouseLeaveEvent ev) {
3378 		if(ev.srcElement !is w)
3379 			return;
3380 		if(timer !is null) {
3381 			timer.destroy();
3382 			timer = null;
3383 		}
3384 	});
3385 
3386 }
3387 else
3388 void setClickRepeat(Widget w, int interval, int delay = 250) {}
3389 
3390 enum FrameStyle {
3391 	none, ///
3392 	risen, /// a 3d pop-out effect (think Windows 95 button)
3393 	sunk, /// a 3d sunken effect (think Windows 95 button as you click on it)
3394 	solid, ///
3395 	dotted, ///
3396 	fantasy, /// a style based on a popular fantasy video game
3397 	rounded, /// a rounded rectangle
3398 }
3399 
3400 version(custom_widgets)
3401 deprecated
3402 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) {
3403 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
3404 }
3405 
3406 version(custom_widgets)
3407 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) {
3408 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, background);
3409 }
3410 
3411 version(custom_widgets)
3412 deprecated
3413 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) {
3414 	draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
3415 }
3416 
3417 int getBorderWidth(FrameStyle style) {
3418 	final switch(style) {
3419 		case FrameStyle.sunk, FrameStyle.risen:
3420 			return 2;
3421 		case FrameStyle.none:
3422 			return 0;
3423 		case FrameStyle.solid:
3424 			return 1;
3425 		case FrameStyle.dotted:
3426 			return 1;
3427 		case FrameStyle.fantasy:
3428 			return 3;
3429 		case FrameStyle.rounded:
3430 			return 2;
3431 	}
3432 }
3433 
3434 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) {
3435 	int borderWidth = getBorderWidth(style);
3436 	final switch(style) {
3437 		case FrameStyle.sunk, FrameStyle.risen:
3438 			// outer layer
3439 			painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black;
3440 		break;
3441 		case FrameStyle.none:
3442 			painter.outlineColor = background;
3443 		break;
3444 		case FrameStyle.solid:
3445 		case FrameStyle.rounded:
3446 			painter.pen = Pen(border, 1);
3447 		break;
3448 		case FrameStyle.dotted:
3449 			painter.pen = Pen(border, 1, Pen.Style.Dotted);
3450 		break;
3451 		case FrameStyle.fantasy:
3452 			painter.pen = Pen(border, 3);
3453 		break;
3454 	}
3455 
3456 	painter.fillColor = background;
3457 
3458 	if(style == FrameStyle.rounded) {
3459 		painter.drawRectangleRounded(Point(x, y), Size(width, height), 6);
3460 	} else {
3461 		painter.drawRectangle(Point(x + 0, y + 0), width, height);
3462 
3463 		if(style == FrameStyle.sunk || style == FrameStyle.risen) {
3464 			// 3d effect
3465 			auto vt = WidgetPainter.visualTheme;
3466 
3467 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor;
3468 			painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0));
3469 			painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1));
3470 
3471 			// inner layer
3472 			//right, bottom
3473 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor;
3474 			painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2));
3475 			painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2));
3476 			// left, top
3477 			painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white;
3478 			painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1));
3479 			painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2));
3480 		} else if(style == FrameStyle.fantasy) {
3481 			painter.pen = Pen(Color.white, 1, Pen.Style.Solid);
3482 			painter.fillColor = Color.transparent;
3483 			painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1));
3484 		}
3485 	}
3486 
3487 	return borderWidth;
3488 }
3489 
3490 /++
3491 	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.
3492 
3493 	See_Also:
3494 		[MenuItem]
3495 		[ToolButton]
3496 		[Menu.addItem]
3497 +/
3498 class Action {
3499 	version(win32_widgets) {
3500 		private int id;
3501 		private static int lastId = 9000;
3502 		private static Action[int] mapping;
3503 	}
3504 
3505 	KeyEvent accelerator;
3506 
3507 	// FIXME: disable message
3508 	// and toggle thing?
3509 	// ??? and trigger arguments too ???
3510 
3511 	/++
3512 		Params:
3513 			label = the textual label
3514 			icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons.
3515 			triggered = initial handler, more can be added via the [triggered] member.
3516 	+/
3517 	this(string label, ushort icon = 0, void delegate() triggered = null) {
3518 		this.label = label;
3519 		this.iconId = icon;
3520 		if(triggered !is null)
3521 			this.triggered ~= triggered;
3522 		version(win32_widgets) {
3523 			id = ++lastId;
3524 			mapping[id] = this;
3525 		}
3526 	}
3527 
3528 	private string label;
3529 	private ushort iconId;
3530 	// icon
3531 
3532 	// when it is triggered, the triggered event is fired on the window
3533 	/// The list of handlers when it is triggered.
3534 	void delegate()[] triggered;
3535 }
3536 
3537 /*
3538 	plan:
3539 		keyboard accelerators
3540 
3541 		* menus (and popups and tooltips)
3542 		* status bar
3543 		* toolbars and buttons
3544 
3545 		sortable table view
3546 
3547 		maybe notification area icons
3548 		basic clipboard
3549 
3550 		* radio box
3551 		splitter
3552 		toggle buttons (optionally mutually exclusive, like in Paint)
3553 		label, rich text display, multi line plain text (selectable)
3554 		* fieldset
3555 		* nestable grid layout
3556 		single line text input
3557 		* multi line text input
3558 		slider
3559 		spinner
3560 		list box
3561 		drop down
3562 		combo box
3563 		auto complete box
3564 		* progress bar
3565 
3566 		terminal window/widget (on unix it might even be a pty but really idk)
3567 
3568 		ok button
3569 		cancel button
3570 
3571 		keyboard hotkeys
3572 
3573 		scroll widget
3574 
3575 		event redirections and network transparency
3576 		script integration
3577 */
3578 
3579 
3580 /*
3581 	MENUS
3582 
3583 	auto bar = new MenuBar(window);
3584 	window.menuBar = bar;
3585 
3586 	auto fileMenu = bar.addItem(new Menu("&File"));
3587 	fileMenu.addItem(new MenuItem("&Exit"));
3588 
3589 
3590 	EVENTS
3591 
3592 	For controls, you should usually use "triggered" rather than "click", etc., because
3593 	triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation.
3594 	This is the case on menus and pushbuttons.
3595 
3596 	"click", on the other hand, currently only fires when it is literally clicked by the mouse.
3597 */
3598 
3599 
3600 /*
3601 enum LinePreference {
3602 	AlwaysOnOwnLine, // always on its own line
3603 	PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way
3604 	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
3605 }
3606 */
3607 
3608 /++
3609 	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.
3610 
3611 	---
3612 	class MyWidget : Widget {
3613 		this(Widget parent) { super(parent); }
3614 
3615 		// set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go:
3616 		mixin Padding!q{4};
3617 
3618 		// set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go:
3619 		mixin Margin!q{8};
3620 
3621 		// but if I specify one outside, it overrides the override, so now marginLeft is 2,
3622 		// while Top/Bottom/Right remain 8 from the mixin above.
3623 		override int marginLeft() { return 2; }
3624 	}
3625 	---
3626 
3627 
3628 	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]).
3629 
3630 	Padding is the area inside a widget where its background is drawn, but the content avoids.
3631 
3632 	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!).
3633 
3634 	* 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.
3635 +/
3636 mixin template Padding(string code) {
3637 	override int paddingLeft() { return mixin(code);}
3638 	override int paddingRight() { return mixin(code);}
3639 	override int paddingTop() { return mixin(code);}
3640 	override int paddingBottom() { return mixin(code);}
3641 }
3642 
3643 /// ditto
3644 mixin template Margin(string code) {
3645 	override int marginLeft() { return mixin(code);}
3646 	override int marginRight() { return mixin(code);}
3647 	override int marginTop() { return mixin(code);}
3648 	override int marginBottom() { return mixin(code);}
3649 }
3650 
3651 private
3652 void recomputeChildLayout(string relevantMeasure)(Widget parent) {
3653 	enum calcingV = relevantMeasure == "height";
3654 
3655 	parent.registerMovement();
3656 
3657 	if(parent.children.length == 0)
3658 		return;
3659 
3660 	auto parentStyle = parent.getComputedStyle();
3661 
3662 	enum firstThingy = relevantMeasure == "height" ? "Top" : "Left";
3663 	enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right";
3664 
3665 	enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top";
3666 	enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom";
3667 
3668 	// my own width and height should already be set by the caller of this function...
3669 	int spaceRemaining = mixin("parent." ~ relevantMeasure) -
3670 		mixin("parentStyle.padding"~firstThingy~"()") -
3671 		mixin("parentStyle.padding"~secondThingy~"()");
3672 
3673 	int stretchinessSum;
3674 	int stretchyChildSum;
3675 	int lastMargin = 0;
3676 
3677 	int shrinkinessSum;
3678 	int shrinkyChildSum;
3679 
3680 	// set initial size
3681 	foreach(child; parent.children) {
3682 
3683 		auto childStyle = child.getComputedStyle();
3684 
3685 		if(cast(StaticPosition) child)
3686 			continue;
3687 		if(child.hidden)
3688 			continue;
3689 
3690 		const iw = child.flexBasisWidth();
3691 		const ih = child.flexBasisHeight();
3692 
3693 		static if(calcingV) {
3694 			child.width = parent.width -
3695 				mixin("childStyle.margin"~otherFirstThingy~"()") -
3696 				mixin("childStyle.margin"~otherSecondThingy~"()") -
3697 				mixin("parentStyle.padding"~otherFirstThingy~"()") -
3698 				mixin("parentStyle.padding"~otherSecondThingy~"()");
3699 
3700 			if(child.width < 0)
3701 				child.width = 0;
3702 			if(child.width > childStyle.maxWidth())
3703 				child.width = childStyle.maxWidth();
3704 
3705 			if(iw > 0) {
3706 				auto totalPossible = child.width;
3707 				if(child.width > iw && child.widthStretchiness() == 0)
3708 					child.width = iw;
3709 			}
3710 
3711 			child.height = mymax(childStyle.minHeight(), ih);
3712 		} else {
3713 			// set to take all the space
3714 			child.height = parent.height -
3715 				mixin("childStyle.margin"~firstThingy~"()") -
3716 				mixin("childStyle.margin"~secondThingy~"()") -
3717 				mixin("parentStyle.padding"~firstThingy~"()") -
3718 				mixin("parentStyle.padding"~secondThingy~"()");
3719 
3720 			// then clamp it
3721 			if(child.height < 0)
3722 				child.height = 0;
3723 			if(child.height > childStyle.maxHeight())
3724 				child.height = childStyle.maxHeight();
3725 
3726 			// and if possible, respect the ideal target
3727 			if(ih > 0) {
3728 				auto totalPossible = child.height;
3729 				if(child.height > ih && child.heightStretchiness() == 0)
3730 					child.height = ih;
3731 			}
3732 
3733 			// if we have an ideal, try to respect it, otehrwise, just use the minimum
3734 			child.width = mymax(childStyle.minWidth(), iw);
3735 		}
3736 
3737 		spaceRemaining -= mixin("child." ~ relevantMeasure);
3738 
3739 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3740 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3741 		lastMargin = margin;
3742 		spaceRemaining -= thisMargin + margin;
3743 
3744 		auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3745 		stretchinessSum += s;
3746 		if(s > 0)
3747 			stretchyChildSum++;
3748 
3749 		auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()");
3750 		shrinkinessSum += s2;
3751 		if(s2 > 0)
3752 			shrinkyChildSum++;
3753 	}
3754 
3755 	if(spaceRemaining < 0 && shrinkyChildSum) {
3756 		// shrink to get into the space if it is possible
3757 		auto toRemove = -spaceRemaining;
3758 		auto removalPerItem = toRemove / shrinkinessSum;
3759 		auto remainder = toRemove % shrinkinessSum;
3760 
3761 		// FIXME: wtf why am i shrinking things with no shrinkiness?
3762 
3763 		foreach(child; parent.children) {
3764 			auto childStyle = child.getComputedStyle();
3765 			if(cast(StaticPosition) child)
3766 				continue;
3767 			if(child.hidden)
3768 				continue;
3769 			static if(calcingV) {
3770 				auto minimum = childStyle.minHeight();
3771 				auto stretch = childStyle.heightShrinkiness();
3772 			} else {
3773 				auto minimum = childStyle.minWidth();
3774 				auto stretch = childStyle.widthShrinkiness();
3775 			}
3776 
3777 			if(mixin("child._" ~ relevantMeasure) <= minimum)
3778 				continue;
3779 			// import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch);
3780 
3781 			mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh.
3782 
3783 			spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum;
3784 		}
3785 	}
3786 
3787 	// stretch to fill space
3788 	while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) {
3789 		auto spacePerChild = spaceRemaining / stretchinessSum;
3790 		bool spreadEvenly;
3791 		bool giveToBiggest;
3792 		if(spacePerChild <= 0) {
3793 			spacePerChild = spaceRemaining / stretchyChildSum;
3794 			spreadEvenly = true;
3795 		}
3796 		if(spacePerChild <= 0) {
3797 			giveToBiggest = true;
3798 		}
3799 		int previousSpaceRemaining = spaceRemaining;
3800 		stretchinessSum = 0;
3801 		Widget mostStretchy;
3802 		int mostStretchyS;
3803 		foreach(child; parent.children) {
3804 			auto childStyle = child.getComputedStyle();
3805 			if(cast(StaticPosition) child)
3806 				continue;
3807 			if(child.hidden)
3808 				continue;
3809 			static if(calcingV) {
3810 				auto maximum = childStyle.maxHeight();
3811 			} else {
3812 				auto maximum = childStyle.maxWidth();
3813 			}
3814 
3815 			if(mixin("child." ~ relevantMeasure) >= maximum) {
3816 				auto adj = mixin("child." ~ relevantMeasure) - maximum;
3817 				mixin("child._" ~ relevantMeasure) -= adj;
3818 				spaceRemaining += adj;
3819 				continue;
3820 			}
3821 			auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3822 			if(s <= 0)
3823 				continue;
3824 			auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s);
3825 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3826 			spaceRemaining -= spaceAdjustment;
3827 			if(mixin("child." ~ relevantMeasure) > maximum) {
3828 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3829 				mixin("child._" ~ relevantMeasure) -= diff;
3830 				spaceRemaining += diff;
3831 			} else if(mixin("child._" ~ relevantMeasure) < maximum) {
3832 				stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3833 				if(mostStretchy is null || s >= mostStretchyS) {
3834 					mostStretchy = child;
3835 					mostStretchyS = s;
3836 				}
3837 			}
3838 		}
3839 
3840 		if(giveToBiggest && mostStretchy !is null) {
3841 			auto child = mostStretchy;
3842 			auto childStyle = child.getComputedStyle();
3843 			int spaceAdjustment = spaceRemaining;
3844 
3845 			static if(calcingV)
3846 				auto maximum = childStyle.maxHeight();
3847 			else
3848 				auto maximum = childStyle.maxWidth();
3849 
3850 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3851 			spaceRemaining -= spaceAdjustment;
3852 			if(mixin("child._" ~ relevantMeasure) > maximum) {
3853 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3854 				mixin("child._" ~ relevantMeasure) -= diff;
3855 				spaceRemaining += diff;
3856 			}
3857 		}
3858 
3859 		if(spaceRemaining == previousSpaceRemaining) {
3860 			if(mostStretchy !is null) {
3861 				static if(calcingV)
3862 					auto maximum = mostStretchy.maxHeight();
3863 				else
3864 					auto maximum = mostStretchy.maxWidth();
3865 
3866 				mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining;
3867 				if(mixin("mostStretchy._" ~ relevantMeasure) > maximum)
3868 					mixin("mostStretchy._" ~ relevantMeasure) = maximum;
3869 			}
3870 			break; // apparently nothing more we can do
3871 		}
3872 	}
3873 
3874 	foreach(child; parent.children) {
3875 		auto childStyle = child.getComputedStyle();
3876 		if(cast(StaticPosition) child)
3877 			continue;
3878 		if(child.hidden)
3879 			continue;
3880 
3881 		static if(calcingV)
3882 			auto maximum = childStyle.maxHeight();
3883 		else
3884 			auto maximum = childStyle.maxWidth();
3885 		if(mixin("child._" ~ relevantMeasure) > maximum)
3886 			mixin("child._" ~ relevantMeasure) = maximum;
3887 	}
3888 
3889 	// position
3890 	lastMargin = 0;
3891 	int currentPos = mixin("parent.padding"~firstThingy~"()");
3892 	foreach(child; parent.children) {
3893 		auto childStyle = child.getComputedStyle();
3894 		if(cast(StaticPosition) child) {
3895 			child.recomputeChildLayout();
3896 			continue;
3897 		}
3898 		if(child.hidden)
3899 			continue;
3900 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3901 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3902 		currentPos += thisMargin;
3903 		static if(calcingV) {
3904 			child.x = parentStyle.paddingLeft() + childStyle.marginLeft();
3905 			child.y = currentPos;
3906 		} else {
3907 			child.x = currentPos;
3908 			child.y = parentStyle.paddingTop() + childStyle.marginTop();
3909 
3910 		}
3911 		currentPos += mixin("child." ~ relevantMeasure);
3912 		currentPos += margin;
3913 		lastMargin = margin;
3914 
3915 		child.recomputeChildLayout();
3916 	}
3917 }
3918 
3919 int mymax(int a, int b) { return a > b ? a : b; }
3920 int mymax(int a, int b, int c) {
3921 	auto d = mymax(a, b);
3922 	return c > d ? c : d;
3923 }
3924 
3925 // OK so we need to make getting at the native window stuff possible in simpledisplay.d
3926 // and here, it must be integrable with the layout, the event system, and not be painted over.
3927 version(win32_widgets) {
3928 
3929 	// this function just does stuff that a parent window needs for redirection
3930 	int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
3931 		this_.hookedWndProc(msg, wParam, lParam);
3932 
3933 		switch(msg) {
3934 
3935 			case WM_VSCROLL, WM_HSCROLL:
3936 				auto pos = HIWORD(wParam);
3937 				auto m = LOWORD(wParam);
3938 
3939 				auto scrollbarHwnd = cast(HWND) lParam;
3940 
3941 				if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) {
3942 
3943 					//auto smw = cast(ScrollMessageWidget) widgetp.parent;
3944 
3945 					switch(m) {
3946 						/+
3947 						// I don't think those messages are ever actually sent normally by the widget itself,
3948 						// they are more used for the keyboard interface. methinks.
3949 						case SB_BOTTOM:
3950 							// writeln("end");
3951 							auto event = new Event("scrolltoend", *widgetp);
3952 							event.dispatch();
3953 							//if(!event.defaultPrevented)
3954 						break;
3955 						case SB_TOP:
3956 							// writeln("top");
3957 							auto event = new Event("scrolltobeginning", *widgetp);
3958 							event.dispatch();
3959 						break;
3960 						case SB_ENDSCROLL:
3961 							// idk
3962 						break;
3963 						+/
3964 						case SB_LINEDOWN:
3965 							(*widgetp).emitCommand!"scrolltonextline"();
3966 						return 0;
3967 						case SB_LINEUP:
3968 							(*widgetp).emitCommand!"scrolltopreviousline"();
3969 						return 0;
3970 						case SB_PAGEDOWN:
3971 							(*widgetp).emitCommand!"scrolltonextpage"();
3972 						return 0;
3973 						case SB_PAGEUP:
3974 							(*widgetp).emitCommand!"scrolltopreviouspage"();
3975 						return 0;
3976 						case SB_THUMBPOSITION:
3977 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3978 							ev.dispatch();
3979 						return 0;
3980 						case SB_THUMBTRACK:
3981 							// eh kinda lying but i like the real time update display
3982 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3983 							ev.dispatch();
3984 
3985 							// the event loop doesn't seem to carry on with a requested redraw..
3986 							// so we request it to get our dirty bit set...
3987 							// then we need to immediately actually redraw it too for instant feedback to user
3988 							SimpleWindow.processAllCustomEvents();
3989 							SimpleWindow.processAllCustomEvents();
3990 							//if(this_.parentWindow)
3991 								//this_.parentWindow.actualRedraw();
3992 
3993 							// and this ensures the WM_PAINT message is sent fairly quickly
3994 							// still seems to lag a little in large windows but meh it basically works.
3995 							if(this_.parentWindow) {
3996 								// FIXME: if painting is slow, this does still lag
3997 								// we probably will want to expose some user hook to ScrollWindowEx
3998 								// or something.
3999 								UpdateWindow(this_.parentWindow.hwnd);
4000 							}
4001 						return 0;
4002 						default:
4003 					}
4004 				}
4005 			break;
4006 
4007 			case WM_CONTEXTMENU:
4008 				auto hwndFrom = cast(HWND) wParam;
4009 
4010 				auto xPos = cast(short) LOWORD(lParam);
4011 				auto yPos = cast(short) HIWORD(lParam);
4012 
4013 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
4014 					POINT p;
4015 					p.x = xPos;
4016 					p.y = yPos;
4017 					ScreenToClient(hwnd, &p);
4018 					auto clientX = cast(ushort) p.x;
4019 					auto clientY = cast(ushort) p.y;
4020 
4021 					auto wap = widgetAtPoint(*widgetp, clientX, clientY);
4022 
4023 					if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) {
4024 						return 0;
4025 					}
4026 				}
4027 			break;
4028 
4029 			case WM_DRAWITEM:
4030 				auto dis = cast(DRAWITEMSTRUCT*) lParam;
4031 				if(auto widgetp = dis.hwndItem in Widget.nativeMapping) {
4032 					return (*widgetp).handleWmDrawItem(dis);
4033 				}
4034 			break;
4035 
4036 			case WM_NOTIFY:
4037 				auto hdr = cast(NMHDR*) lParam;
4038 				auto hwndFrom = hdr.hwndFrom;
4039 				auto code = hdr.code;
4040 
4041 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
4042 					return (*widgetp).handleWmNotify(hdr, code, mustReturn);
4043 				}
4044 			break;
4045 			case WM_COMMAND:
4046 				auto handle = cast(HWND) lParam;
4047 				auto cmd = HIWORD(wParam);
4048 				return processWmCommand(hwnd, handle, cmd, LOWORD(wParam));
4049 
4050 			default:
4051 				// pass it on
4052 		}
4053 		return 0;
4054 	}
4055 
4056 
4057 
4058 	extern(Windows)
4059 	private
4060 	// this is called by native child windows, whereas the other hook is done by simpledisplay windows
4061 	// but can i merge them?!
4062 	LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
4063 		// try { writeln(iMessage); } catch(Exception e) {};
4064 
4065 		if(auto te = hWnd in Widget.nativeMapping) {
4066 			try {
4067 
4068 				te.hookedWndProc(iMessage, wParam, lParam);
4069 
4070 				int mustReturn;
4071 				auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn);
4072 				if(mustReturn)
4073 					return ret;
4074 
4075 				if(iMessage == WM_SETFOCUS) {
4076 					auto lol = *te;
4077 					while(lol !is null && lol.implicitlyCreated)
4078 						lol = lol.parent;
4079 					lol.focus();
4080 					//(*te).parentWindow.focusedWidget = lol;
4081 				}
4082 
4083 
4084 				if(iMessage == WM_CTLCOLOREDIT) {
4085 
4086 				}
4087 				if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) {
4088 					SetBkMode(cast(HDC) wParam, TRANSPARENT);
4089 					return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color...
4090 						//GetStockObject(NULL_BRUSH);
4091 				}
4092 
4093 				auto pos = getChildPositionRelativeToParentOrigin(*te);
4094 				lastDefaultPrevented = false;
4095 				// try { writeln(typeid(*te)); } catch(Exception e) {}
4096 				if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented)
4097 					return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam);
4098 				else {
4099 					// it was something we recognized, should only call the window procedure if the default was not prevented
4100 				}
4101 			} catch(Exception e) {
4102 				assert(0, e.toString());
4103 			}
4104 			return 0;
4105 		}
4106 		assert(0, "shouldn't be receiving messages for this window....");
4107 		//assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen
4108 	}
4109 
4110 	extern(Windows)
4111 	private
4112 	// see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/
4113 	LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
4114 		if(iMessage == WM_ERASEBKGND) {
4115 			auto dc = GetDC(hWnd);
4116 			auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE));
4117 			auto p = SelectObject(dc, GetStockObject(NULL_PEN));
4118 			RECT r;
4119 			GetWindowRect(hWnd, &r);
4120 			// since the pen is null, to fill the whole space, we need the +1 on both.
4121 			gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1);
4122 			SelectObject(dc, p);
4123 			SelectObject(dc, b);
4124 			ReleaseDC(hWnd, dc);
4125 			InvalidateRect(hWnd, null, false); // redraw the border
4126 			return 1;
4127 		}
4128 		return HookedWndProc(hWnd, iMessage, wParam, lParam);
4129 	}
4130 
4131 	/++
4132 		Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create
4133 		needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest
4134 		of minigui's expectations.
4135 
4136 		This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window
4137 		member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor.
4138 
4139 		It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`.
4140 
4141 		To check if you can use this, use `static if(UsingWin32Widgets)`.
4142 	+/
4143 	void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) {
4144 		assert(p.parentWindow !is null);
4145 		assert(p.parentWindow.win.impl.hwnd !is null);
4146 
4147 		auto bsgroupbox = style == BS_GROUPBOX;
4148 
4149 		HWND phwnd;
4150 
4151 		auto wtf = p.parent;
4152 		while(wtf) {
4153 			if(wtf.hwnd !is null) {
4154 				phwnd = wtf.hwnd;
4155 				break;
4156 			}
4157 			wtf = wtf.parent;
4158 		}
4159 
4160 		if(phwnd is null)
4161 			phwnd = p.parentWindow.win.impl.hwnd;
4162 
4163 		assert(phwnd !is null);
4164 
4165 		WCharzBuffer wt = WCharzBuffer(windowText);
4166 
4167 		style |= WS_VISIBLE | WS_CHILD;
4168 		//if(className != WC_TABCONTROL)
4169 			style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
4170 		p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style,
4171 				CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
4172 				phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null);
4173 
4174 		assert(p.hwnd !is null);
4175 
4176 
4177 		static HFONT font;
4178 		if(font is null) {
4179 			NONCLIENTMETRICS params;
4180 			params.cbSize = params.sizeof;
4181 			if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, &params, 0)) {
4182 				font = CreateFontIndirect(&params.lfMessageFont);
4183 			}
4184 		}
4185 
4186 		if(font)
4187 			SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true);
4188 
4189 		p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd);
4190 		p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
4191 		Widget.nativeMapping[p.hwnd] = p;
4192 
4193 		if(bsgroupbox)
4194 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK);
4195 		else
4196 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4197 
4198 		EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
4199 
4200 		p.registerMovement();
4201 	}
4202 }
4203 
4204 version(win32_widgets)
4205 private
4206 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) {
4207 	if(hwnd is null || hwnd in Widget.nativeMapping)
4208 		return true;
4209 	auto parent = cast(Widget) cast(void*) lparam;
4210 	Widget p = new Widget(null);
4211 	p._parent = parent;
4212 	p.parentWindow = parent.parentWindow;
4213 	p.hwnd = hwnd;
4214 	p.implicitlyCreated = true;
4215 	Widget.nativeMapping[p.hwnd] = p;
4216 	p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4217 	return true;
4218 }
4219 
4220 /++
4221 	Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness.
4222 +/
4223 struct WidgetPainter {
4224 	this(ScreenPainter screenPainter, Widget drawingUpon) {
4225 		this.drawingUpon = drawingUpon;
4226 		this.screenPainter = screenPainter;
4227 		if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi))
4228 			this.screenPainter.setFont(font);
4229 	}
4230 
4231 	/++
4232 		EXPERIMENTAL. subject to change.
4233 
4234 		When you draw a cursor, you can draw this to notify your window of where it is,
4235 		for IME systems to use.
4236 	+/
4237 	void notifyCursorPosition(int x, int y, int width, int height) {
4238 		if(auto a = drawingUpon.parentWindow)
4239 		if(auto w = a.inputProxy) {
4240 			w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height);
4241 		}
4242 	}
4243 
4244 
4245 	///
4246 	ScreenPainter screenPainter;
4247 	/// Forward to the screen painter for other methods
4248 	alias screenPainter this;
4249 
4250 	private Widget drawingUpon;
4251 
4252 	/++
4253 		This is the list of rectangles that actually need to be redrawn.
4254 
4255 		Not actually implemented yet.
4256 	+/
4257 	Rectangle[] invalidatedRectangles;
4258 
4259 	private static BaseVisualTheme _visualTheme;
4260 
4261 	/++
4262 		Functions to access the visual theme and helpers to easily use it.
4263 
4264 		These are aware of the current widget's computed style out of the theme.
4265 	+/
4266 	static @property BaseVisualTheme visualTheme() {
4267 		if(_visualTheme is null)
4268 			_visualTheme = new DefaultVisualTheme();
4269 		return _visualTheme;
4270 	}
4271 
4272 	/// ditto
4273 	static @property void visualTheme(BaseVisualTheme theme) {
4274 		_visualTheme = theme;
4275 
4276 		// FIXME: notify all windows about the new theme, they should recompute layout and redraw.
4277 	}
4278 
4279 	/// ditto
4280 	Color themeForeground() {
4281 		return drawingUpon.getComputedStyle().foregroundColor();
4282 	}
4283 
4284 	/// ditto
4285 	Color themeBackground() {
4286 		return drawingUpon.getComputedStyle().background.color;
4287 	}
4288 
4289 	int isDarkTheme() {
4290 		return 0; // unspecified, yes, no as enum. FIXME
4291 	}
4292 
4293 	/++
4294 		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.
4295 
4296 		It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding.
4297 
4298 		If you change teh clip rectangle, you should change it back before you return.
4299 
4300 
4301 		The sequence it uses is:
4302 			background
4303 			content (delegated to you)
4304 			border
4305 			focused outline
4306 			selected overlay
4307 
4308 		Example code:
4309 
4310 		---
4311 		void paint(WidgetPainter painter) {
4312 			painter.drawThemed((bounds) {
4313 				return bounds; // if the selection overlay should be contained, you can return it here.
4314 			});
4315 		}
4316 		---
4317 	+/
4318 	void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) {
4319 		drawThemed((WidgetPainter painter, const Rectangle bounds) {
4320 			return drawBody(bounds);
4321 		});
4322 	}
4323 	// this overload is actually mroe for setting the delegate to a virtual function
4324 	void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) {
4325 		Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height);
4326 
4327 		auto cs = drawingUpon.getComputedStyle();
4328 
4329 		auto bg = cs.background.color;
4330 
4331 		auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor);
4332 
4333 		rect.left += borderWidth;
4334 		rect.right -= borderWidth;
4335 		rect.top += borderWidth;
4336 		rect.bottom -= borderWidth;
4337 
4338 		auto insideBorderRect = rect;
4339 
4340 		rect.left += cs.paddingLeft;
4341 		rect.right -= cs.paddingRight;
4342 		rect.top += cs.paddingTop;
4343 		rect.bottom -= cs.paddingBottom;
4344 
4345 		this.outlineColor = this.themeForeground;
4346 		this.fillColor = bg;
4347 
4348 		auto widgetFont = cs.fontCached;
4349 		if(widgetFont !is null)
4350 			this.setFont(widgetFont);
4351 
4352 		rect = drawBody(this, rect);
4353 
4354 		if(widgetFont !is null) {
4355 			if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi))
4356 				this.setFont(vtFont);
4357 			else
4358 				this.setFont(null);
4359 		}
4360 
4361 		if(auto os = cs.outlineStyle()) {
4362 			this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid);
4363 			this.fillColor = Color.transparent;
4364 			this.drawRectangle(insideBorderRect);
4365 		}
4366 	}
4367 
4368 	/++
4369 		First, draw the background.
4370 		Then draw your content.
4371 		Next, draw the border.
4372 		And the focused indicator.
4373 		And the is-selected box.
4374 
4375 		If it is focused i can draw the outline too...
4376 
4377 		If selected i can even do the xor action but that's at the end.
4378 	+/
4379 	void drawThemeBackground() {
4380 
4381 	}
4382 
4383 	void drawThemeBorder() {
4384 
4385 	}
4386 
4387 	// all this stuff is a dangerous experiment....
4388 	static class ScriptableVersion {
4389 		ScreenPainterImplementation* p;
4390 		int originX, originY;
4391 
4392 		@scriptable:
4393 		void drawRectangle(int x, int y, int width, int height) {
4394 			p.drawRectangle(x + originX, y + originY, width, height);
4395 		}
4396 		void drawLine(int x1, int y1, int x2, int y2) {
4397 			p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY);
4398 		}
4399 		void drawText(int x, int y, string text) {
4400 			p.drawText(x + originX, y + originY, 100000, 100000, text, 0);
4401 		}
4402 		void setOutlineColor(int r, int g, int b) {
4403 			p.pen = Pen(Color(r,g,b), 1);
4404 		}
4405 		void setFillColor(int r, int g, int b) {
4406 			p.fillColor = Color(r,g,b);
4407 		}
4408 	}
4409 
4410 	ScriptableVersion toArsdJsvar() {
4411 		auto sv = new ScriptableVersion;
4412 		sv.p = this.screenPainter.impl;
4413 		sv.originX = this.screenPainter.originX;
4414 		sv.originY = this.screenPainter.originY;
4415 		return sv;
4416 	}
4417 
4418 	static WidgetPainter fromJsVar(T)(T t) {
4419 		return WidgetPainter.init;
4420 	}
4421 	// done..........
4422 }
4423 
4424 
4425 struct Style {
4426 	static struct helper(string m, T) {
4427 		enum method = m;
4428 		T v;
4429 
4430 		mixin template MethodOverride(typeof(this) v) {
4431 			mixin("override typeof(v.v) "~v.method~"() { return v.v; }");
4432 		}
4433 	}
4434 
4435 	static auto opDispatch(string method, T)(T value) {
4436 		return helper!(method, T)(value);
4437 	}
4438 }
4439 
4440 /++
4441 	Implementation detail of the [ControlledBy] UDA.
4442 
4443 	History:
4444 		Added Oct 28, 2020
4445 +/
4446 struct ControlledBy_(T, Args...) {
4447 	Args args;
4448 
4449 	static if(Args.length)
4450 	this(Args args) {
4451 		this.args = args;
4452 	}
4453 
4454 	private T construct(Widget parent) {
4455 		return new T(args, parent);
4456 	}
4457 }
4458 
4459 /++
4460 	User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them.
4461 
4462 	History:
4463 		Added Oct 28, 2020
4464 +/
4465 auto ControlledBy(T, Args...)(Args args) {
4466 	return ControlledBy_!(T, Args)(args);
4467 }
4468 
4469 struct ContainerMeta {
4470 	string name;
4471 	ContainerMeta[] children;
4472 	Widget function(Widget parent) factory;
4473 
4474 	Widget instantiate(Widget parent) {
4475 		auto n = factory(parent);
4476 		n.name = name;
4477 		foreach(child; children)
4478 			child.instantiate(n);
4479 		return n;
4480 	}
4481 }
4482 
4483 /++
4484 	This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See
4485 	http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information.
4486 
4487 	Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level
4488 	structures. It works fine on structs declared inside functions though.
4489 
4490 	See: https://issues.dlang.org/show_bug.cgi?id=21984
4491 +/
4492 template Container(CArgs...) {
4493 	static if(CArgs.length && is(CArgs[0] : Widget)) {
4494 		private alias Super = CArgs[0];
4495 		private alias CArgs2 = CArgs[1 .. $];
4496 	} else {
4497 		private alias Super = Layout;
4498 		private alias CArgs2 = CArgs;
4499 	}
4500 
4501 	class Container : Super {
4502 		this(Widget parent) { super(parent); }
4503 
4504 		// just to partially support old gdc versions
4505 		version(GNU) {
4506 			static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); }
4507 			static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); }
4508 			static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); }
4509 			static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time");
4510 		} else mixin(q{
4511 			static foreach(Arg; CArgs2) {
4512 				mixin Arg.MethodOverride!(Arg);
4513 			}
4514 		});
4515 
4516 		static ContainerMeta opCall(string name, ContainerMeta[] children...) {
4517 			return ContainerMeta(
4518 				name,
4519 				children.dup,
4520 				function (Widget parent) { return new typeof(this)(parent); }
4521 			);
4522 		}
4523 
4524 		static ContainerMeta opCall(ContainerMeta[] children...) {
4525 			return opCall(null, children);
4526 		}
4527 	}
4528 }
4529 
4530 /++
4531 	The data controller widget is created by reflecting over the given
4532 	data type. You can use [ControlledBy] as a UDA on a struct or
4533 	just let it create things automatically.
4534 
4535 	Unlike [dialog], this uses real-time updating of the data and
4536 	you add it to another window yourself.
4537 
4538 	---
4539 		struct Test {
4540 			int x;
4541 			int y;
4542 		}
4543 
4544 		auto window = new Window();
4545 		auto dcw = new DataControllerWidget!Test(new Test, window);
4546 	---
4547 
4548 	The way it works is any public members are given a widget based
4549 	on their data type, and public methods trigger an action button
4550 	if no relevant parameters or a dialog action if it does have
4551 	parameters, similar to the [menu] facility.
4552 
4553 	If you change data programmatically, without going through the
4554 	DataControllerWidget methods, you will have to tell it something
4555 	has changed and it needs to redraw. This is done with the `invalidate`
4556 	method.
4557 
4558 	History:
4559 		Added Oct 28, 2020
4560 +/
4561 /// Group: generating_from_code
4562 class DataControllerWidget(T) : WidgetContainer {
4563 	static if(is(T == class) || is(T == interface) || is(T : const E[], E))
4564 		private alias Tref = T;
4565 	else
4566 		private alias Tref = T*;
4567 
4568 	Tref datum;
4569 
4570 	/++
4571 		See_also: [addDataControllerWidget]
4572 	+/
4573 	this(Tref datum, Widget parent) {
4574 		this.datum = datum;
4575 
4576 		Widget cp = this;
4577 
4578 		super(parent);
4579 
4580 		foreach(attr; __traits(getAttributes, T))
4581 			static if(is(typeof(attr) == ContainerMeta)) {
4582 				cp = attr.instantiate(this);
4583 			}
4584 
4585 		auto def = this.getByName("default");
4586 		if(def !is null)
4587 			cp = def;
4588 
4589 		Widget helper(string name) {
4590 			auto maybe = this.getByName(name);
4591 			if(maybe is null)
4592 				return cp;
4593 			return maybe;
4594 
4595 		}
4596 
4597 		foreach(member; __traits(allMembers, T))
4598 		static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011
4599 		static if(is(typeof(__traits(getMember, this.datum, member))))
4600 		static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") {
4601 			void delegate() update;
4602 
4603 			auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update);
4604 
4605 			if(update)
4606 				updaters ~= update;
4607 
4608 			static if(is(typeof(__traits(getMember, this.datum, member)) == function)) {
4609 				w.addEventListener("triggered", delegate() {
4610 					makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))();
4611 					notifyDataUpdated();
4612 				});
4613 			} else static if(is(typeof(w.isChecked) == bool)) {
4614 				w.addEventListener(EventType.change, (Event ev) {
4615 					__traits(getMember, this.datum, member) = w.isChecked;
4616 				});
4617 			} else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) {
4618 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } );
4619 			} else static if(is(typeof(w.value) == int)) {
4620 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4621 			} else static if(is(typeof(w) == DropDownSelection)) {
4622 				// special case for this to kinda support enums and such. coudl be better though
4623 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4624 			} else {
4625 				//static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof);
4626 			}
4627 		}
4628 	}
4629 
4630 	/++
4631 		If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages.
4632 
4633 		History:
4634 			Added May 28, 2021
4635 	+/
4636 	void notifyDataUpdated() {
4637 		foreach(updater; updaters)
4638 			updater();
4639 
4640 		this.emit!(ChangeEvent!void)(delegate{});
4641 	}
4642 
4643 	private Widget[string] memberWidgets;
4644 	private void delegate()[] updaters;
4645 
4646 	mixin Emits!(ChangeEvent!void);
4647 }
4648 
4649 private int saturatedSum(int[] values...) {
4650 	int sum;
4651 	foreach(value; values) {
4652 		if(value == int.max)
4653 			return int.max;
4654 		sum += value;
4655 	}
4656 	return sum;
4657 }
4658 
4659 void genericSetValue(T, W)(T* where, W what) {
4660 	import std.conv;
4661 	*where = to!T(what);
4662 	//*where = cast(T) stringToLong(what);
4663 }
4664 
4665 /++
4666 	Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent.
4667 
4668 	The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget.
4669 
4670 	Note that this creates the widget but does not attach any event handlers to it.
4671 +/
4672 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) {
4673 
4674 	string displayName = __traits(identifier, tt).beautify;
4675 
4676 	static if(controlledByCount!tt == 1) {
4677 		foreach(i, attr; __traits(getAttributes, tt)) {
4678 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) {
4679 				auto w = attr.construct(parent);
4680 				static if(__traits(compiles, w.setPosition(*valptr)))
4681 					update = () { w.setPosition(*valptr); };
4682 				else static if(__traits(compiles, w.setValue(*valptr)))
4683 					update = () { w.setValue(*valptr); };
4684 
4685 				if(update)
4686 					update();
4687 				return w;
4688 			}
4689 		}
4690 	} else static if(controlledByCount!tt == 0) {
4691 		static if(is(typeof(tt) == enum)) {
4692 			// FIXME: update
4693 			auto dds = new DropDownSelection(parent);
4694 			foreach(idx, option; __traits(allMembers, typeof(tt))) {
4695 				dds.addOption(option);
4696 				if(__traits(getMember, typeof(tt), option) == *valptr)
4697 					dds.setSelection(cast(int) idx);
4698 			}
4699 			return dds;
4700 		} else static if(is(typeof(tt) == bool)) {
4701 			auto box = new Checkbox(displayName, parent);
4702 			update = () { box.isChecked = *valptr; };
4703 			update();
4704 			return box;
4705 		} else static if(is(typeof(tt) : const long)) {
4706 			auto le = new LabeledLineEdit(displayName, parent);
4707 			update = () { le.content = toInternal!string(*valptr); };
4708 			update();
4709 			return le;
4710 		} else static if(is(typeof(tt) : const double)) {
4711 			auto le = new LabeledLineEdit(displayName, parent);
4712 			import std.conv;
4713 			update = () { le.content = to!string(*valptr); };
4714 			update();
4715 			return le;
4716 		} else static if(is(typeof(tt) : const string)) {
4717 			auto le = new LabeledLineEdit(displayName, parent);
4718 			update = () { le.content = *valptr; };
4719 			update();
4720 			return le;
4721 		} else static if(is(typeof(tt) == function)) {
4722 			auto w = new Button(displayName, parent);
4723 			return w;
4724 		} else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) {
4725 			return parent.addDataControllerWidget(tt);
4726 		} else static assert(0, typeof(tt).stringof);
4727 	} else static assert(0, "multiple controllers not yet supported");
4728 }
4729 
4730 private template controlledByCount(alias tt) {
4731 	static int helper() {
4732 		int count;
4733 		foreach(i, attr; __traits(getAttributes, tt))
4734 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...))
4735 				count++;
4736 		return count;
4737 	}
4738 
4739 	enum controlledByCount = helper;
4740 }
4741 
4742 /++
4743 	Intended for UFCS action like `window.addDataControllerWidget(new MyObject());`
4744 
4745 	If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method.
4746 
4747 	History:
4748 		The `redrawOnChange` parameter was added on May 28, 2021.
4749 +/
4750 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) {
4751 	auto dcw = new DataControllerWidget!T(t, parent);
4752 	initializeDataControllerWidget(dcw, redrawOnChange);
4753 	return dcw;
4754 }
4755 
4756 /// ditto
4757 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) {
4758 	auto dcw = new DataControllerWidget!T(t, parent);
4759 	initializeDataControllerWidget(dcw, redrawOnChange);
4760 	return dcw;
4761 }
4762 
4763 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) {
4764 	if(redrawOnChange !is null)
4765 		w.addEventListener("change", delegate() { redrawOnChange.redraw(); });
4766 }
4767 
4768 /++
4769 	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.
4770 
4771 	History:
4772 		Finalized on June 3, 2021 for the dub v10.0 release
4773 +/
4774 struct StyleInformation {
4775 	private Widget w;
4776 	private BaseVisualTheme visualTheme;
4777 
4778 	private this(Widget w) {
4779 		this.w = w;
4780 		this.visualTheme = WidgetPainter.visualTheme;
4781 	}
4782 
4783 	/++
4784 		Forwards to [Widget.Style]
4785 
4786 		Bugs:
4787 			It is supposed to fall back to the [VisualTheme] if
4788 			the style doesn't override the default, but that is
4789 			not generally implemented. Many of them may end up
4790 			being explicit overloads instead of the generic
4791 			opDispatch fallback, like [font] is now.
4792 	+/
4793 	public @property opDispatch(string name)() {
4794 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4795 		w.useStyleProperties((scope Widget.Style props) {
4796 		//visualTheme.useStyleProperties(w, (props) {
4797 			prop = __traits(getMember, props, name);
4798 		});
4799 		return prop;
4800 	}
4801 
4802 	/++
4803 		Returns the cached font object associated with the widget,
4804 		if overridden by the [Widget.Style|Style], or the [VisualTheme] if not.
4805 
4806 		History:
4807 			Prior to March 21, 2022 (dub v10.7), `font` went through
4808 			[opDispatch], which did not use the cache. You can now call it
4809 			repeatedly without guilt.
4810 	+/
4811 	public @property OperatingSystemFont font() {
4812 		OperatingSystemFont prop;
4813 		w.useStyleProperties((scope Widget.Style props) {
4814 			prop = props.fontCached;
4815 		});
4816 		if(prop is null) {
4817 			prop = visualTheme.defaultFontCached(w.currentDpi);
4818 		}
4819 		return prop;
4820 	}
4821 
4822 	@property {
4823 		// Layout helpers. Currently just forwarding since I haven't made up my mind on a better way.
4824 		/** */ int paddingLeft() { return w.paddingLeft(); }
4825 		/** */ int paddingRight() { return w.paddingRight(); }
4826 		/** */ int paddingTop() { return w.paddingTop(); }
4827 		/** */ int paddingBottom() { return w.paddingBottom(); }
4828 
4829 		/** */ int marginLeft() { return w.marginLeft(); }
4830 		/** */ int marginRight() { return w.marginRight(); }
4831 		/** */ int marginTop() { return w.marginTop(); }
4832 		/** */ int marginBottom() { return w.marginBottom(); }
4833 
4834 		/** */ int maxHeight() { return w.maxHeight(); }
4835 		/** */ int minHeight() { return w.minHeight(); }
4836 
4837 		/** */ int maxWidth() { return w.maxWidth(); }
4838 		/** */ int minWidth() { return w.minWidth(); }
4839 
4840 		/** */ int flexBasisWidth() { return w.flexBasisWidth(); }
4841 		/** */ int flexBasisHeight() { return w.flexBasisHeight(); }
4842 
4843 		/** */ int heightStretchiness() { return w.heightStretchiness(); }
4844 		/** */ int widthStretchiness() { return w.widthStretchiness(); }
4845 
4846 		/** */ int heightShrinkiness() { return w.heightShrinkiness(); }
4847 		/** */ int widthShrinkiness() { return w.widthShrinkiness(); }
4848 
4849 		// Global helpers some of these are unstable.
4850 		static:
4851 		/** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4852 		/** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4853 		/** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4854 		/** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4855 		/** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); }
4856 		/** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4857 
4858 		/** */ Color activeTabColor() { return lightAccentColor; }
4859 		/** */ Color buttonColor() { return windowBackgroundColor; }
4860 		/** */ Color depressedButtonColor() { return darkAccentColor; }
4861 		/** the background color of the widget when mouse hovering over it, if it responds to mouse hovers */ Color hoveringColor() { return lightAccentColor; }
4862 		deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() {
4863 			auto c = WidgetPainter.visualTheme.selectionColor();
4864 			return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4865 		}
4866 		/** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4867 		/** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
4868 	}
4869 
4870 
4871 
4872 	/+
4873 
4874 	private static auto extractStyleProperty(string name)(Widget w) {
4875 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
4876 		w.useStyleProperties((props) {
4877 			prop = __traits(getMember, props, name);
4878 		});
4879 		return prop;
4880 	}
4881 
4882 	// FIXME: clear this upon a X server disconnect
4883 	private static OperatingSystemFont[string] fontCache;
4884 
4885 	T getProperty(T)(string name, lazy T default_) {
4886 		if(visualTheme !is null) {
4887 			auto str = visualTheme.getPropertyString(w, name);
4888 			if(str is null)
4889 				return default_;
4890 			static if(is(T == Color))
4891 				return Color.fromString(str);
4892 			else static if(is(T == Measurement))
4893 				return Measurement(cast(int) toInternal!int(str));
4894 			else static if(is(T == WidgetBackground))
4895 				return WidgetBackground.fromString(str);
4896 			else static if(is(T == OperatingSystemFont)) {
4897 				if(auto f = str in fontCache)
4898 					return *f;
4899 				else
4900 					return fontCache[str] = new OperatingSystemFont(str);
4901 			} else static if(is(T == FrameStyle)) {
4902 				switch(str) {
4903 					default:
4904 						return FrameStyle.none;
4905 					foreach(style; __traits(allMembers, FrameStyle))
4906 					case style:
4907 						return __traits(getMember, FrameStyle, style);
4908 				}
4909 			} else static assert(0);
4910 		} else
4911 			return default_;
4912 	}
4913 
4914 	static struct Measurement {
4915 		int value;
4916 		alias value this;
4917 	}
4918 
4919 	@property:
4920 
4921 	int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); }
4922 	int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); }
4923 	int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); }
4924 	int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); }
4925 
4926 	int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); }
4927 	int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); }
4928 	int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); }
4929 	int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); }
4930 
4931 	int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); }
4932 	int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); }
4933 
4934 	int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); }
4935 	int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); }
4936 
4937 
4938 	WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); }
4939 	Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); }
4940 
4941 	OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); }
4942 
4943 	FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); }
4944 	Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); }
4945 
4946 	FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); }
4947 	Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); }
4948 
4949 
4950 	Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
4951 	Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
4952 	Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
4953 	Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
4954 
4955 	Color activeTabColor() { return lightAccentColor; }
4956 	Color buttonColor() { return windowBackgroundColor; }
4957 	Color depressedButtonColor() { return darkAccentColor; }
4958 	Color hoveringColor() { return Color(228, 228, 228); }
4959 	Color activeListXorColor() {
4960 		auto c = WidgetPainter.visualTheme.selectionColor();
4961 		return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
4962 	}
4963 	Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); }
4964 	Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); }
4965 	+/
4966 }
4967 
4968 
4969 
4970 // pragma(msg, __traits(classInstanceSize, Widget));
4971 
4972 /*private*/ template EventString(E) {
4973 	static if(is(typeof(E.EventString)))
4974 		enum EventString = E.EventString;
4975 	else
4976 		enum EventString = E.mangleof; // FIXME fqn? or something more user friendly
4977 }
4978 
4979 /*private*/ template EventStringIdentifier(E) {
4980 	string helper() {
4981 		auto es = EventString!E;
4982 		char[] id = new char[](es.length * 2);
4983 		size_t idx;
4984 		foreach(char ch; es) {
4985 			id[idx++] = cast(char)('a' + (ch >> 4));
4986 			id[idx++] = cast(char)('a' + (ch & 0x0f));
4987 		}
4988 		return cast(string) id;
4989 	}
4990 
4991 	enum EventStringIdentifier = helper();
4992 }
4993 
4994 
4995 template classStaticallyEmits(This, EventType) {
4996 	static if(is(This Base == super))
4997 		static if(is(Base : Widget))
4998 			enum baseEmits = classStaticallyEmits!(Base, EventType);
4999 		else
5000 			enum baseEmits = false;
5001 	else
5002 		enum baseEmits = false;
5003 
5004 	enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]);
5005 
5006 	enum classStaticallyEmits = thisEmits || baseEmits;
5007 }
5008 
5009 /++
5010 	A helper to make widgets out of other native windows.
5011 
5012 	History:
5013 		Factored out of OpenGlWidget on November 5, 2021
5014 +/
5015 class NestedChildWindowWidget : Widget {
5016 	SimpleWindow win;
5017 
5018 	/++
5019 		Used on X to send focus to the appropriate child window when requested by the window manager.
5020 
5021 		Normally returns its own nested window. Can also return another child or null to revert to the parent
5022 		if you override it in a child class.
5023 
5024 		History:
5025 			Added April 2, 2022 (dub v10.8)
5026 	+/
5027 	SimpleWindow focusableWindow() {
5028 		return win;
5029 	}
5030 
5031 	///
5032 	// win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
5033 	this(SimpleWindow win, Widget parent) {
5034 		this.parentWindow = parent.parentWindow;
5035 		this.win = win;
5036 
5037 		super(parent);
5038 		windowsetup(win);
5039 	}
5040 
5041 	static protected SimpleWindow getParentWindow(Widget parent) {
5042 		assert(parent !is null);
5043 		SimpleWindow pwin = parent.parentWindow.win;
5044 
5045 		version(win32_widgets) {
5046 			HWND phwnd;
5047 			auto wtf = parent;
5048 			while(wtf) {
5049 				if(wtf.hwnd) {
5050 					phwnd = wtf.hwnd;
5051 					break;
5052 				}
5053 				wtf = wtf.parent;
5054 			}
5055 			// kinda a hack here just because the ctor below just needs a SimpleWindow wrapper....
5056 			if(phwnd)
5057 				pwin = new SimpleWindow(phwnd);
5058 		}
5059 
5060 		return pwin;
5061 	}
5062 
5063 	/++
5064 		Called upon the nested window being destroyed.
5065 		Remember the window has already been destroyed at
5066 		this point, so don't use the native handle for anything.
5067 
5068 		History:
5069 			Added April 3, 2022 (dub v10.8)
5070 	+/
5071 	protected void dispose() {
5072 
5073 	}
5074 
5075 	protected void windowsetup(SimpleWindow w) {
5076 		/*
5077 		win.onFocusChange = (bool getting) {
5078 			if(getting)
5079 				this.focus();
5080 		};
5081 		*/
5082 
5083 		/+
5084 		win.onFocusChange = (bool getting) {
5085 			if(getting) {
5086 				this.parentWindow.focusedWidget = this;
5087 				this.emit!FocusEvent();
5088 				this.emit!FocusInEvent();
5089 			} else {
5090 				this.emit!BlurEvent();
5091 				this.emit!FocusOutEvent();
5092 			}
5093 		};
5094 		+/
5095 
5096 		win.onDestroyed = () {
5097 			this.dispose();
5098 		};
5099 
5100 		version(win32_widgets) {
5101 			Widget.nativeMapping[win.hwnd] = this;
5102 			this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
5103 		} else {
5104 			win.setEventHandlers(
5105 				(MouseEvent e) {
5106 					Widget p = this;
5107 					while(p ! is parentWindow) {
5108 						e.x += p.x;
5109 						e.y += p.y;
5110 						p = p.parent;
5111 					}
5112 					parentWindow.dispatchMouseEvent(e);
5113 				},
5114 				(KeyEvent e) {
5115 					//writefln("%s %x   %s", cast(void*) win, cast(uint) e.key, e.key);
5116 					parentWindow.dispatchKeyEvent(e);
5117 				},
5118 				(dchar e) {
5119 					parentWindow.dispatchCharEvent(e);
5120 				},
5121 			);
5122 		}
5123 
5124 	}
5125 
5126 	override bool showOrHideIfNativeWindow(bool shouldShow) {
5127 		auto cur = hidden;
5128 		win.hidden = !shouldShow;
5129 		if(cur != shouldShow && shouldShow)
5130 			redraw();
5131 		return true;
5132 	}
5133 
5134 	/// OpenGL widgets cannot have child widgets. Do not call this.
5135 	/* @disable */ final override void addChild(Widget, int) {
5136 		throw new Error("cannot add children to OpenGL widgets");
5137 	}
5138 
5139 	/// When an opengl widget is laid out, it will adjust the glViewport for you automatically.
5140 	/// Keep in mind that events like mouse coordinates are still relative to your size.
5141 	override void registerMovement() {
5142 		// writefln("%d %d %d %d", x,y,width,height);
5143 		version(win32_widgets)
5144 			auto pos = getChildPositionRelativeToParentHwnd(this);
5145 		else
5146 			auto pos = getChildPositionRelativeToParentOrigin(this);
5147 		win.moveResize(pos[0], pos[1], width, height);
5148 
5149 		registerMovementAdditionalWork();
5150 		sendResizeEvent();
5151 	}
5152 
5153 	abstract void registerMovementAdditionalWork();
5154 }
5155 
5156 /++
5157 	Nests an opengl capable window inside this window as a widget.
5158 
5159 	You may also just want to create an additional [SimpleWindow] with
5160 	[OpenGlOptions.yes] yourself.
5161 
5162 	An OpenGL widget cannot have child widgets. It will throw if you try.
5163 +/
5164 static if(OpenGlEnabled)
5165 class OpenGlWidget : NestedChildWindowWidget {
5166 
5167 	override void registerMovementAdditionalWork() {
5168 		win.setAsCurrentOpenGlContext();
5169 	}
5170 
5171 	///
5172 	this(Widget parent) {
5173 		auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
5174 		super(win, parent);
5175 	}
5176 
5177 	override void paint(WidgetPainter painter) {
5178 		win.setAsCurrentOpenGlContext();
5179 		glViewport(0, 0, this.width, this.height);
5180 		win.redrawOpenGlSceneNow();
5181 	}
5182 
5183 	void redrawOpenGlScene(void delegate() dg) {
5184 		win.redrawOpenGlScene = dg;
5185 	}
5186 }
5187 
5188 /++
5189 	This demo shows how to draw text in an opengl scene.
5190 +/
5191 unittest {
5192 	import arsd.minigui;
5193 	import arsd.ttf;
5194 
5195 	void main() {
5196 		auto window = new Window();
5197 
5198 		auto widget = new OpenGlWidget(window);
5199 
5200 		// old means non-shader code so compatible with glBegin etc.
5201 		// tbh I haven't implemented new one in font yet...
5202 		// anyway, declaring here, will construct soon.
5203 		OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont;
5204 
5205 		// this is a little bit awkward, calling some methods through
5206 		// the underlying SimpleWindow `win` method, and you can't do this
5207 		// on a nanovega widget due to conflicts so I should probably fix
5208 		// the api to be a bit easier. But here it will work.
5209 		//
5210 		// Alternatively, you could load the font on the first draw, inside
5211 		// the redrawOpenGlScene, and keep a flag so you don't do it every
5212 		// time. That'd be a bit easier since the lib sets up the context
5213 		// by then guaranteed.
5214 		//
5215 		// But still, I wanna show this.
5216 		widget.win.visibleForTheFirstTime = delegate {
5217 			// must set the opengl context
5218 			widget.win.setAsCurrentOpenGlContext();
5219 
5220 			// if you were doing a OpenGL 3+ shader, this
5221 			// gets especially important to do in order. With
5222 			// old-style opengl, I think you can even do it
5223 			// in main(), but meh, let's show it more correctly.
5224 
5225 			// Anyway, now it is time to load the font from the
5226 			// OS (you can alternatively load one from a .ttf file
5227 			// you bundle with the application), then load the
5228 			// font into texture for drawing.
5229 
5230 			auto osfont = new OperatingSystemFont("DejaVu Sans", 18);
5231 
5232 			assert(!osfont.isNull()); // make sure it actually loaded
5233 
5234 			// using typeof to avoid repeating the long name lol
5235 			glfont = new typeof(glfont)(
5236 				// get the raw data from the font for loading in here
5237 				// since it doesn't use the OS function to draw the
5238 				// text, we gotta treat it more as a file than as
5239 				// a drawing api.
5240 				osfont.getTtfBytes(),
5241 				18, // need to respecify size since opengl world is different coordinate system
5242 
5243 				// these last two numbers are why it is called
5244 				// "Limited" font. It only loads the characters
5245 				// in the given range, since the texture atlas
5246 				// it references is all a big image generated ahead
5247 				// of time. You could maybe do the whole thing but
5248 				// idk how much memory that is.
5249 				//
5250 				// But here, 0-128 represents the ASCII range, so
5251 				// good enough for most English things, numeric labels,
5252 				// etc.
5253 				0,
5254 				128
5255 			);
5256 		};
5257 
5258 		widget.redrawOpenGlScene = () {
5259 			// now we can use the glfont's drawString function
5260 
5261 			// first some opengl setup. You can do this in one place
5262 			// on window first visible too in many cases, just showing
5263 			// here cuz it is easier for me.
5264 
5265 			// gonna need some alpha blending or it just looks awful
5266 			glEnable(GL_BLEND);
5267 			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
5268 			glClearColor(0,0,0,0);
5269 			glDepthFunc(GL_LEQUAL);
5270 
5271 			// Also need to enable 2d textures, since it draws the
5272 			// font characters as images baked in
5273 			glMatrixMode(GL_MODELVIEW);
5274 			glLoadIdentity();
5275 			glDisable(GL_DEPTH_TEST);
5276 			glEnable(GL_TEXTURE_2D);
5277 
5278 			// the orthographic matrix is best for 2d things like text
5279 			// so let's set that up. This matrix makes the coordinates
5280 			// in the opengl scene be one-to-one with the actual pixels
5281 			// on screen. (Not necessarily best, you may wish to scale
5282 			// things, but it does help keep fonts looking normal.)
5283 			glMatrixMode(GL_PROJECTION);
5284 			glLoadIdentity();
5285 			glOrtho(0, widget.width, widget.height, 0, 0, 1);
5286 
5287 			// you can do other glScale, glRotate, glTranslate, etc
5288 			// to the matrix here of course if you want.
5289 
5290 			// note the x,y coordinates here are for the text baseline
5291 			// NOT the upper-left corner. The baseline is like the line
5292 			// in the notebook you write on. Most the letters are actually
5293 			// above it, but some, like p and q, dip a bit below it.
5294 			//
5295 			// So if you're used to the upper left coordinate like the
5296 			// rest of simpledisplay/minigui usually do, do the
5297 			// y + glfont.ascent to bring it down a little. So this
5298 			// example puts the string in the upper left of the window.
5299 			glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green);
5300 
5301 			// re color btw: the function sets a solid color internally,
5302 			// but you actually COULD do your own thing for rainbow effects
5303 			// and the sort if you wanted too, by pulling its guts out.
5304 			// Just view its source for an idea of how it actually draws:
5305 			// http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332
5306 
5307 			// it gets a bit complicated with the character positioning,
5308 			// but the opengl parts are fairly simple: bind a texture,
5309 			// set the color, draw a quad for each letter.
5310 
5311 
5312 			// the last optional argument there btw is a bounding box
5313 			// it will/ use to word wrap and return an object you can
5314 			// use to implement scrolling or pagination; it tells how
5315 			// much of the string didn't fit in the box. But for simple
5316 			// labels we can just ignore that.
5317 
5318 
5319 			// I'd suggest drawing text as the last step, after you
5320 			// do your other drawing. You might use the push/pop matrix
5321 			// stuff to keep your place. You, in theory, should be able
5322 			// to do text in a 3d space but I've never actually tried
5323 			// that....
5324 		};
5325 
5326 		window.loop();
5327 	}
5328 }
5329 
5330 version(custom_widgets)
5331 private class TextListViewWidget : GenericListViewWidget {
5332 	static class TextListViewItem : GenericListViewItem {
5333 		ListWidget controller;
5334 		this(ListWidget controller, Widget parent) {
5335 			this.controller = controller;
5336 			this.tabStop = false;
5337 			super(parent);
5338 		}
5339 
5340 		ListWidget.Option* showing;
5341 
5342 		override void showItem(int idx) {
5343 			showing = idx < controller.options.length ? &controller.options[idx] : null;
5344 			redraw(); // is this necessary? the generic thing might call it...
5345 		}
5346 
5347 		override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
5348 			if(showing is null)
5349 				return bounds;
5350 			painter.drawText(bounds.upperLeft, showing.label);
5351 			return bounds;
5352 		}
5353 
5354 		static class Style : Widget.Style {
5355 			override WidgetBackground background() {
5356 				// FIXME: change it if it is focused or not
5357 				// 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
5358 				auto tlvi = cast(TextListViewItem) widget;
5359 				if(tlvi && tlvi.showing && tlvi && tlvi.showing.selected)
5360 					return WidgetBackground(true /*widget.parent.isFocused*/ ? WidgetPainter.visualTheme.selectionBackgroundColor : Color(128, 128, 128)); // FIXME: don't hardcode
5361 				return super.background();
5362 			}
5363 
5364 			override Color foregroundColor() {
5365 				auto tlvi = cast(TextListViewItem) widget;
5366 				return tlvi && tlvi.showing && tlvi && tlvi.showing.selected ? WidgetPainter.visualTheme.selectionForegroundColor : super.foregroundColor();
5367 			}
5368 
5369 			override FrameStyle outlineStyle() {
5370 				// FIXME: change it if it is focused or not
5371 				auto tlvi = cast(TextListViewItem) widget;
5372 				return (tlvi && tlvi.currentIndexLoaded() == tlvi.controller.focusOn) ? FrameStyle.dotted : super.outlineStyle();
5373 			}
5374 		}
5375 		mixin OverrideStyle!Style;
5376 
5377 		mixin Padding!q{2};
5378 
5379 		override void defaultEventHandler_click(ClickEvent event) {
5380 			if(event.button == MouseButton.left) {
5381 				controller.setSelection(currentIndexLoaded());
5382 				controller.focusOn = currentIndexLoaded();
5383 			}
5384 		}
5385 
5386 	}
5387 
5388 	ListWidget controller;
5389 
5390 	this(ListWidget parent) {
5391 		this.controller = parent;
5392 		this.tabStop = false; // this is only used as a child of the ListWidget
5393 		super(parent);
5394 
5395 		smw.movementPerButtonClick(1, itemSize().height);
5396 	}
5397 
5398 	override Size itemSize() {
5399 		return Size(0, defaultLineHeight + scaleWithDpi(4 /* the top and bottom padding */));
5400 	}
5401 
5402 	override GenericListViewItem itemFactory(Widget parent) {
5403 		return new TextListViewItem(controller, parent);
5404 	}
5405 
5406 	static class Style : Widget.Style {
5407 		override FrameStyle borderStyle() {
5408 			return FrameStyle.sunk;
5409 		}
5410 
5411 		override WidgetBackground background() {
5412 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
5413 		}
5414 	}
5415 	mixin OverrideStyle!Style;
5416 }
5417 
5418 /++
5419 	A list widget contains a list of strings that the user can examine and select.
5420 
5421 
5422 	In the future, items in the list may be possible to be more than just strings.
5423 
5424 	See_Also:
5425 		[TableView]
5426 +/
5427 class ListWidget : Widget {
5428 	/// 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.
5429 	mixin Emits!(ChangeEvent!void);
5430 
5431 	version(custom_widgets)
5432 		TextListViewWidget glvw;
5433 
5434 	static struct Option {
5435 		string label;
5436 		bool selected;
5437 		void* tag;
5438 	}
5439 	private Option[] options;
5440 
5441 	/++
5442 		Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete.
5443 	+/
5444 	void setSelection(int y) {
5445 		if(!multiSelect)
5446 			foreach(ref opt; options)
5447 				opt.selected = false;
5448 		if(y >= 0 && y < options.length)
5449 			options[y].selected = !options[y].selected;
5450 
5451 		version(custom_widgets)
5452 			focusOn = y;
5453 
5454 		this.emit!(ChangeEvent!void)(delegate {});
5455 
5456 		version(custom_widgets)
5457 			redraw();
5458 	}
5459 
5460 	/++
5461 		Gets the index of the selected item. In case of multi select, the index of the first selected item is returned.
5462 		Returns -1 if nothing is selected.
5463 	+/
5464 	int getSelection()
5465 	{
5466 		foreach(i, opt; options) {
5467 			if (opt.selected)
5468 				return cast(int) i;
5469 		}
5470 		return -1;
5471 	}
5472 
5473 	version(custom_widgets)
5474 	private int focusOn;
5475 
5476 	this(Widget parent) {
5477 		super(parent);
5478 
5479 		version(custom_widgets)
5480 			glvw = new TextListViewWidget(this);
5481 
5482 		version(win32_widgets)
5483 			createWin32Window(this, WC_LISTBOX, "",
5484 				0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0);
5485 	}
5486 
5487 	version(win32_widgets)
5488 	override void handleWmCommand(ushort code, ushort id) {
5489 		switch(code) {
5490 			case LBN_SELCHANGE:
5491 				auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0);
5492 				setSelection(cast(int) sel);
5493 			break;
5494 			default:
5495 		}
5496 	}
5497 
5498 
5499 	void addOption(string text, void* tag = null) {
5500 		options ~= Option(text, false, tag);
5501 		version(win32_widgets) {
5502 			WCharzBuffer buffer = WCharzBuffer(text);
5503 			SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr);
5504 		}
5505 		version(custom_widgets) {
5506 			glvw.setItemCount(cast(int) options.length);
5507 			//setContentSize(width, cast(int) (options.length * defaultLineHeight));
5508 			redraw();
5509 		}
5510 	}
5511 
5512 	void clear() {
5513 		options = null;
5514 		version(win32_widgets) {
5515 			while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0)
5516 				{}
5517 
5518 		} else version(custom_widgets) {
5519 			focusOn = -1;
5520 			glvw.setItemCount(0);
5521 			redraw();
5522 		}
5523 	}
5524 
5525 	version(custom_widgets)
5526 	override void defaultEventHandler_keydown(KeyDownEvent kde) {
5527 		void changedFocusOn() {
5528 			scrollFocusIntoView();
5529 			if(multiSelect)
5530 				redraw();
5531 			else
5532 				setSelection(focusOn);
5533 		}
5534 		switch(kde.key) {
5535 			case Key.Up:
5536 				if(focusOn) {
5537 					focusOn--;
5538 					changedFocusOn();
5539 				}
5540 			break;
5541 			case Key.Down:
5542 				if(focusOn + 1 < options.length) {
5543 					focusOn++;
5544 					changedFocusOn();
5545 				}
5546 			break;
5547 			case Key.Home:
5548 				if(focusOn) {
5549 					focusOn = 0;
5550 					changedFocusOn();
5551 				}
5552 			break;
5553 			case Key.End:
5554 				if(options.length && focusOn + 1 != options.length) {
5555 					focusOn = cast(int) options.length - 1;
5556 					changedFocusOn();
5557 				}
5558 			break;
5559 			case Key.PageUp:
5560 				auto n = glvw.numberOfCurrentlyFullyVisibleItems;
5561 				focusOn -= n;
5562 				if(focusOn < 0)
5563 					focusOn = 0;
5564 				changedFocusOn();
5565 			break;
5566 			case Key.PageDown:
5567 				if(options.length == 0)
5568 					break;
5569 				auto n = glvw.numberOfCurrentlyFullyVisibleItems;
5570 				focusOn += n;
5571 				if(focusOn >= options.length)
5572 					focusOn = cast(int) options.length - 1;
5573 				changedFocusOn();
5574 			break;
5575 
5576 			default:
5577 		}
5578 	}
5579 
5580 	version(custom_widgets)
5581 	override void defaultEventHandler_char(CharEvent ce) {
5582 		if(ce.character == '\n' || ce.character == ' ') {
5583 			setSelection(focusOn);
5584 		} else {
5585 			// search for the item that best matches and jump to it
5586 			// FIXME this sucks in tons of ways. the normal thing toolkits
5587 			// do here is to search for a substring on a timer, but i'd kinda
5588 			// rather make an actual little dialog with some options. still meh for now.
5589 			dchar search = ce.character;
5590 			if(search >= 'A' && search <= 'Z')
5591 				search += 32;
5592 			foreach(idx, option; options) {
5593 				auto ch = option.label.length ? option.label[0] : 0;
5594 				if(ch >= 'A' && ch <= 'Z')
5595 					ch += 32;
5596 				if(ch == search) {
5597 					setSelection(cast(int) idx);
5598 					scrollSelectionIntoView();
5599 					break;
5600 				}
5601 			}
5602 
5603 		}
5604 	}
5605 
5606 	version(win32_widgets)
5607 		enum multiSelect = false; /// not implemented yet
5608 	else
5609 		bool multiSelect;
5610 
5611 	override int heightStretchiness() { return 6; }
5612 
5613 	version(custom_widgets)
5614 	void scrollFocusIntoView() {
5615 		glvw.ensureItemVisibleInScroll(focusOn);
5616 	}
5617 
5618 	void scrollSelectionIntoView() {
5619 		// FIXME: implement on Windows
5620 
5621 		version(custom_widgets)
5622 			glvw.ensureItemVisibleInScroll(getSelection());
5623 	}
5624 
5625 	/*
5626 	version(custom_widgets)
5627 	override void defaultEventHandler_focusout(Event foe) {
5628 		glvw.redraw();
5629 	}
5630 
5631 	version(custom_widgets)
5632 	override void defaultEventHandler_focusin(Event foe) {
5633 		glvw.redraw();
5634 	}
5635 	*/
5636 
5637 }
5638 
5639 
5640 
5641 /// For [ScrollableWidget], determines when to show the scroll bar to the user.
5642 /// NEVER USED
5643 enum ScrollBarShowPolicy {
5644 	automatic, /// automatically show the scroll bar if it is necessary
5645 	never, /// never show the scroll bar (scrolling must be done programmatically)
5646 	always /// always show the scroll bar, even if it is disabled
5647 }
5648 
5649 /++
5650 	A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner.
5651 
5652 	It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code.
5653 +/
5654 // FIXME ScrollBarShowPolicy
5655 // FIXME: use the ScrollMessageWidget in here now that it exists
5656 // deprecated("Use ScrollMessageWidget or ScrollableContainerWidget instead") // ugh compiler won't let me do it
5657 class ScrollableWidget : Widget {
5658 	// FIXME: make line size configurable
5659 	// FIXME: add keyboard controls
5660 	version(win32_widgets) {
5661 		override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) {
5662 			if(msg == WM_VSCROLL || msg == WM_HSCROLL) {
5663 				auto pos = HIWORD(wParam);
5664 				auto m = LOWORD(wParam);
5665 
5666 				// FIXME: I can reintroduce the
5667 				// scroll bars now by using this
5668 				// in the top-level window handler
5669 				// to forward comamnds
5670 				auto scrollbarHwnd = lParam;
5671 				switch(m) {
5672 					case SB_BOTTOM:
5673 						if(msg == WM_HSCROLL)
5674 							horizontalScrollTo(contentWidth_);
5675 						else
5676 							verticalScrollTo(contentHeight_);
5677 					break;
5678 					case SB_TOP:
5679 						if(msg == WM_HSCROLL)
5680 							horizontalScrollTo(0);
5681 						else
5682 							verticalScrollTo(0);
5683 					break;
5684 					case SB_ENDSCROLL:
5685 						// idk
5686 					break;
5687 					case SB_LINEDOWN:
5688 						if(msg == WM_HSCROLL)
5689 							horizontalScroll(scaleWithDpi(16));
5690 						else
5691 							verticalScroll(scaleWithDpi(16));
5692 					break;
5693 					case SB_LINEUP:
5694 						if(msg == WM_HSCROLL)
5695 							horizontalScroll(scaleWithDpi(-16));
5696 						else
5697 							verticalScroll(scaleWithDpi(-16));
5698 					break;
5699 					case SB_PAGEDOWN:
5700 						if(msg == WM_HSCROLL)
5701 							horizontalScroll(scaleWithDpi(100));
5702 						else
5703 							verticalScroll(scaleWithDpi(100));
5704 					break;
5705 					case SB_PAGEUP:
5706 						if(msg == WM_HSCROLL)
5707 							horizontalScroll(scaleWithDpi(-100));
5708 						else
5709 							verticalScroll(scaleWithDpi(-100));
5710 					break;
5711 					case SB_THUMBPOSITION:
5712 					case SB_THUMBTRACK:
5713 						if(msg == WM_HSCROLL)
5714 							horizontalScrollTo(pos);
5715 						else
5716 							verticalScrollTo(pos);
5717 
5718 						if(m == SB_THUMBTRACK) {
5719 							// the event loop doesn't seem to carry on with a requested redraw..
5720 							// so we request it to get our dirty bit set...
5721 							redraw();
5722 
5723 							// then we need to immediately actually redraw it too for instant feedback to user
5724 
5725 							SimpleWindow.processAllCustomEvents();
5726 							//if(parentWindow)
5727 								//parentWindow.actualRedraw();
5728 						}
5729 					break;
5730 					default:
5731 				}
5732 			}
5733 			return super.hookedWndProc(msg, wParam, lParam);
5734 		}
5735 	}
5736 	///
5737 	this(Widget parent) {
5738 		this.parentWindow = parent.parentWindow;
5739 
5740 		version(win32_widgets) {
5741 			createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "",
5742 				0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0);
5743 			super(parent);
5744 		} else version(custom_widgets) {
5745 			outerContainer = new InternalScrollableContainerWidget(this, parent);
5746 			super(outerContainer);
5747 		} else static assert(0);
5748 	}
5749 
5750 	version(custom_widgets)
5751 		InternalScrollableContainerWidget outerContainer;
5752 
5753 	override void defaultEventHandler_click(ClickEvent event) {
5754 		if(event.button == MouseButton.wheelUp)
5755 			verticalScroll(scaleWithDpi(-16));
5756 		if(event.button == MouseButton.wheelDown)
5757 			verticalScroll(scaleWithDpi(16));
5758 		super.defaultEventHandler_click(event);
5759 	}
5760 
5761 	override void defaultEventHandler_keydown(KeyDownEvent event) {
5762 		switch(event.key) {
5763 			case Key.Left:
5764 				horizontalScroll(scaleWithDpi(-16));
5765 			break;
5766 			case Key.Right:
5767 				horizontalScroll(scaleWithDpi(16));
5768 			break;
5769 			case Key.Up:
5770 				verticalScroll(scaleWithDpi(-16));
5771 			break;
5772 			case Key.Down:
5773 				verticalScroll(scaleWithDpi(16));
5774 			break;
5775 			case Key.Home:
5776 				verticalScrollTo(0);
5777 			break;
5778 			case Key.End:
5779 				verticalScrollTo(contentHeight);
5780 			break;
5781 			case Key.PageUp:
5782 				verticalScroll(scaleWithDpi(-160));
5783 			break;
5784 			case Key.PageDown:
5785 				verticalScroll(scaleWithDpi(160));
5786 			break;
5787 			default:
5788 		}
5789 		super.defaultEventHandler_keydown(event);
5790 	}
5791 
5792 
5793 	version(win32_widgets)
5794 	override void recomputeChildLayout() {
5795 		super.recomputeChildLayout();
5796 		SCROLLINFO info;
5797 		info.cbSize = info.sizeof;
5798 		info.nPage = viewportHeight;
5799 		info.fMask = SIF_PAGE | SIF_RANGE;
5800 		info.nMin = 0;
5801 		info.nMax = contentHeight_;
5802 		SetScrollInfo(hwnd, SB_VERT, &info, true);
5803 
5804 		info.cbSize = info.sizeof;
5805 		info.nPage = viewportWidth;
5806 		info.fMask = SIF_PAGE | SIF_RANGE;
5807 		info.nMin = 0;
5808 		info.nMax = contentWidth_;
5809 		SetScrollInfo(hwnd, SB_HORZ, &info, true);
5810 	}
5811 
5812 	/*
5813 		Scrolling
5814 		------------
5815 
5816 		You are assigned a width and a height by the layout engine, which
5817 		is your viewport box. However, you may draw more than that by setting
5818 		a contentWidth and contentHeight.
5819 
5820 		If these can be contained by the viewport, no scrollbar is displayed.
5821 		If they cannot fit though, it will automatically show scroll as necessary.
5822 
5823 		If contentWidth == 0, no horizontal scrolling is performed. If contentHeight
5824 		is zero, no vertical scrolling is performed.
5825 
5826 		If scrolling is necessary, the lib will automatically work with the bars.
5827 		When you redraw, the origin and clipping info in the painter is set so if
5828 		you just draw everything, it will work, but you can be more efficient by checking
5829 		the viewportWidth, viewportHeight, and scrollOrigin members.
5830 	*/
5831 
5832 	///
5833 	final @property int viewportWidth() {
5834 		return width - (showingVerticalScroll ? scaleWithDpi(16) : 0);
5835 	}
5836 	///
5837 	final @property int viewportHeight() {
5838 		return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0);
5839 	}
5840 
5841 	// FIXME property
5842 	Point scrollOrigin_;
5843 
5844 	///
5845 	final const(Point) scrollOrigin() {
5846 		return scrollOrigin_;
5847 	}
5848 
5849 	// the user sets these two
5850 	private int contentWidth_ = 0;
5851 	private int contentHeight_ = 0;
5852 
5853 	///
5854 	int contentWidth() { return contentWidth_; }
5855 	///
5856 	int contentHeight() { return contentHeight_; }
5857 
5858 	///
5859 	void setContentSize(int width, int height) {
5860 		contentWidth_ = width;
5861 		contentHeight_ = height;
5862 
5863 		version(custom_widgets) {
5864 			if(showingVerticalScroll || showingHorizontalScroll) {
5865 				outerContainer.queueRecomputeChildLayout();
5866 			}
5867 
5868 			if(showingVerticalScroll())
5869 				outerContainer.verticalScrollBar.redraw();
5870 			if(showingHorizontalScroll())
5871 				outerContainer.horizontalScrollBar.redraw();
5872 		} else version(win32_widgets) {
5873 			queueRecomputeChildLayout();
5874 		} else static assert(0);
5875 	}
5876 
5877 	///
5878 	void verticalScroll(int delta) {
5879 		verticalScrollTo(scrollOrigin.y + delta);
5880 	}
5881 	///
5882 	void verticalScrollTo(int pos) {
5883 		scrollOrigin_.y = pos;
5884 		if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight))
5885 			scrollOrigin_.y = contentHeight - viewportHeight;
5886 
5887 		if(scrollOrigin_.y < 0)
5888 			scrollOrigin_.y = 0;
5889 
5890 		version(win32_widgets) {
5891 			SCROLLINFO info;
5892 			info.cbSize = info.sizeof;
5893 			info.fMask = SIF_POS;
5894 			info.nPos = scrollOrigin_.y;
5895 			SetScrollInfo(hwnd, SB_VERT, &info, true);
5896 		} else version(custom_widgets) {
5897 			outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y);
5898 		} else static assert(0);
5899 
5900 		redraw();
5901 	}
5902 
5903 	///
5904 	void horizontalScroll(int delta) {
5905 		horizontalScrollTo(scrollOrigin.x + delta);
5906 	}
5907 	///
5908 	void horizontalScrollTo(int pos) {
5909 		scrollOrigin_.x = pos;
5910 		if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth))
5911 			scrollOrigin_.x = contentWidth - viewportWidth;
5912 
5913 		if(scrollOrigin_.x < 0)
5914 			scrollOrigin_.x = 0;
5915 
5916 		version(win32_widgets) {
5917 			SCROLLINFO info;
5918 			info.cbSize = info.sizeof;
5919 			info.fMask = SIF_POS;
5920 			info.nPos = scrollOrigin_.x;
5921 			SetScrollInfo(hwnd, SB_HORZ, &info, true);
5922 		} else version(custom_widgets) {
5923 			outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x);
5924 		} else static assert(0);
5925 
5926 		redraw();
5927 	}
5928 	///
5929 	void scrollTo(Point p) {
5930 		verticalScrollTo(p.y);
5931 		horizontalScrollTo(p.x);
5932 	}
5933 
5934 	///
5935 	void ensureVisibleInScroll(Point p) {
5936 		auto rect = viewportRectangle();
5937 		if(rect.contains(p))
5938 			return;
5939 		if(p.x < rect.left)
5940 			horizontalScroll(p.x - rect.left);
5941 		else if(p.x > rect.right)
5942 			horizontalScroll(p.x - rect.right);
5943 
5944 		if(p.y < rect.top)
5945 			verticalScroll(p.y - rect.top);
5946 		else if(p.y > rect.bottom)
5947 			verticalScroll(p.y - rect.bottom);
5948 	}
5949 
5950 	///
5951 	void ensureVisibleInScroll(Rectangle rect) {
5952 		ensureVisibleInScroll(rect.upperLeft);
5953 		ensureVisibleInScroll(rect.lowerRight);
5954 	}
5955 
5956 	///
5957 	Rectangle viewportRectangle() {
5958 		return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight));
5959 	}
5960 
5961 	///
5962 	bool showingHorizontalScroll() {
5963 		return contentWidth > width;
5964 	}
5965 	///
5966 	bool showingVerticalScroll() {
5967 		return contentHeight > height;
5968 	}
5969 
5970 	/// This is called before the ordinary paint delegate,
5971 	/// giving you a chance to draw the window frame, etc,
5972 	/// before the scroll clip takes effect
5973 	void paintFrameAndBackground(WidgetPainter painter) {
5974 		version(win32_widgets) {
5975 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
5976 			auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
5977 			// since the pen is null, to fill the whole space, we need the +1 on both.
5978 			gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
5979 			SelectObject(painter.impl.hdc, p);
5980 			SelectObject(painter.impl.hdc, b);
5981 		}
5982 
5983 	}
5984 
5985 	// make space for the scroll bar, and that's it.
5986 	final override int paddingRight() { return scaleWithDpi(16); }
5987 	final override int paddingBottom() { return scaleWithDpi(16); }
5988 
5989 	/*
5990 		END SCROLLING
5991 	*/
5992 
5993 	override WidgetPainter draw() {
5994 		int x = this.x, y = this.y;
5995 		auto parent = this.parent;
5996 		while(parent) {
5997 			x += parent.x;
5998 			y += parent.y;
5999 			parent = parent.parent;
6000 		}
6001 
6002 		//version(win32_widgets) {
6003 			//auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
6004 		//} else {
6005 			auto painter = parentWindow.win.draw(true);
6006 		//}
6007 		painter.originX = x;
6008 		painter.originY = y;
6009 
6010 		painter.originX = painter.originX - scrollOrigin.x;
6011 		painter.originY = painter.originY - scrollOrigin.y;
6012 		painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight());
6013 
6014 		return WidgetPainter(painter, this);
6015 	}
6016 
6017 	mixin ScrollableChildren;
6018 }
6019 
6020 // you need to have a Point scrollOrigin in the class somewhere
6021 // and a paintFrameAndBackground
6022 private mixin template ScrollableChildren() {
6023 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
6024 		if(hidden)
6025 			return;
6026 
6027 		//version(win32_widgets)
6028 			//painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
6029 
6030 		painter.originX = lox + x;
6031 		painter.originY = loy + y;
6032 
6033 		bool actuallyPainted = false;
6034 
6035 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height)));
6036 		if(clip == Rectangle.init)
6037 			return;
6038 
6039 		if(force || redrawRequested) {
6040 			//painter.setClipRectangle(scrollOrigin, width, height);
6041 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
6042 			paintFrameAndBackground(painter);
6043 		}
6044 
6045 		/+
6046 		version(win32_widgets) {
6047 			if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW);
6048 		}
6049 		+/
6050 
6051 		painter.originX = painter.originX - scrollOrigin.x;
6052 		painter.originY = painter.originY - scrollOrigin.y;
6053 		if(force || redrawRequested) {
6054 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4);
6055 			//painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4);
6056 
6057 			//erase(painter); // we paintFrameAndBackground above so no need
6058 			if(painter.visualTheme)
6059 				painter.visualTheme.doPaint(this, painter);
6060 			else
6061 				paint(painter);
6062 
6063 			if(invalidate) {
6064 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
6065 				// children are contained inside this, so no need to do extra work
6066 				invalidate = false;
6067 			}
6068 
6069 
6070 			actuallyPainted = true;
6071 			redrawRequested = false;
6072 		}
6073 
6074 		foreach(child; children) {
6075 			if(cast(FixedPosition) child)
6076 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
6077 			else
6078 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
6079 		}
6080 	}
6081 }
6082 
6083 private class InternalScrollableContainerInsideWidget : ContainerWidget {
6084 	ScrollableContainerWidget scw;
6085 
6086 	this(ScrollableContainerWidget parent) {
6087 		scw = parent;
6088 		super(parent);
6089 	}
6090 
6091 	version(custom_widgets)
6092 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
6093 		if(hidden)
6094 			return;
6095 
6096 		bool actuallyPainted = false;
6097 
6098 		auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_);
6099 
6100 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_)));
6101 		if(clip == Rectangle.init)
6102 			return;
6103 
6104 		painter.originX = lox + x - scrollOrigin.x;
6105 		painter.originY = loy + y - scrollOrigin.y;
6106 		if(force || redrawRequested) {
6107 			painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
6108 
6109 			erase(painter);
6110 			if(painter.visualTheme)
6111 				painter.visualTheme.doPaint(this, painter);
6112 			else
6113 				paint(painter);
6114 
6115 			if(invalidate) {
6116 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
6117 				// children are contained inside this, so no need to do extra work
6118 				invalidate = false;
6119 			}
6120 
6121 			actuallyPainted = true;
6122 			redrawRequested = false;
6123 		}
6124 		foreach(child; children) {
6125 			if(cast(FixedPosition) child)
6126 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
6127 			else
6128 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
6129 		}
6130 	}
6131 
6132 	version(custom_widgets)
6133 	override protected void addScrollPosition(ref int x, ref int y) {
6134 		x += scw.scrollX_;
6135 		y += scw.scrollY_;
6136 	}
6137 }
6138 
6139 /++
6140 	A widget meant to contain other widgets that may need to scroll.
6141 
6142 	Currently buggy.
6143 
6144 	History:
6145 		Added July 1, 2021 (dub v10.2)
6146 
6147 		On January 3, 2022, I tried to use it in a few other cases
6148 		and found it only worked well in the original test case. Since
6149 		it still sucks, I think I'm going to rewrite it again.
6150 +/
6151 class ScrollableContainerWidget : ContainerWidget {
6152 	///
6153 	this(Widget parent) {
6154 		super(parent);
6155 
6156 		container = new InternalScrollableContainerInsideWidget(this);
6157 		hsb = new HorizontalScrollbar(this);
6158 		vsb = new VerticalScrollbar(this);
6159 
6160 		tabStop = false;
6161 		container.tabStop = false;
6162 		magic = true;
6163 
6164 
6165 		vsb.addEventListener("scrolltonextline", () {
6166 			scrollBy(0, scaleWithDpi(16));
6167 		});
6168 		vsb.addEventListener("scrolltopreviousline", () {
6169 			scrollBy(0,scaleWithDpi( -16));
6170 		});
6171 		vsb.addEventListener("scrolltonextpage", () {
6172 			scrollBy(0, container.height);
6173 		});
6174 		vsb.addEventListener("scrolltopreviouspage", () {
6175 			scrollBy(0, -container.height);
6176 		});
6177 		vsb.addEventListener((scope ScrollToPositionEvent spe) {
6178 			scrollTo(scrollX_, spe.value);
6179 		});
6180 
6181 		this.addEventListener(delegate (scope ClickEvent e) {
6182 			if(e.button == MouseButton.wheelUp) {
6183 				if(!e.defaultPrevented)
6184 					scrollBy(0, scaleWithDpi(-16));
6185 				e.stopPropagation();
6186 			} else if(e.button == MouseButton.wheelDown) {
6187 				if(!e.defaultPrevented)
6188 					scrollBy(0, scaleWithDpi(16));
6189 				e.stopPropagation();
6190 			}
6191 		});
6192 	}
6193 
6194 	/+
6195 	override void defaultEventHandler_click(ClickEvent e) {
6196 	}
6197 	+/
6198 
6199 	override void removeAllChildren() {
6200 		container.removeAllChildren();
6201 	}
6202 
6203 	void scrollTo(int x, int y) {
6204 		scrollBy(x - scrollX_, y - scrollY_);
6205 	}
6206 
6207 	void scrollBy(int x, int y) {
6208 		auto ox = scrollX_;
6209 		auto oy = scrollY_;
6210 
6211 		auto nx = ox + x;
6212 		auto ny = oy + y;
6213 
6214 		if(nx < 0)
6215 			nx = 0;
6216 		if(ny < 0)
6217 			ny = 0;
6218 
6219 		auto maxX = hsb.max - container.width;
6220 		if(maxX < 0) maxX = 0;
6221 		auto maxY = vsb.max - container.height;
6222 		if(maxY < 0) maxY = 0;
6223 
6224 		if(nx > maxX)
6225 			nx = maxX;
6226 		if(ny > maxY)
6227 			ny = maxY;
6228 
6229 		auto dx = nx - ox;
6230 		auto dy = ny - oy;
6231 
6232 		if(dx || dy) {
6233 			version(win32_widgets)
6234 				ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
6235 			else {
6236 				redraw();
6237 			}
6238 
6239 			hsb.setPosition = nx;
6240 			vsb.setPosition = ny;
6241 
6242 			scrollX_ = nx;
6243 			scrollY_ = ny;
6244 		}
6245 	}
6246 
6247 	private int scrollX_;
6248 	private int scrollY_;
6249 
6250 	void setTotalArea(int width, int height) {
6251 		hsb.setMax(width);
6252 		vsb.setMax(height);
6253 	}
6254 
6255 	///
6256 	void setViewableArea(int width, int height) {
6257 		hsb.setViewableArea(width);
6258 		vsb.setViewableArea(height);
6259 	}
6260 
6261 	private bool magic;
6262 	override void addChild(Widget w, int position = int.max) {
6263 		if(magic)
6264 			container.addChild(w, position);
6265 		else
6266 			super.addChild(w, position);
6267 	}
6268 
6269 	override void recomputeChildLayout() {
6270 		if(hsb is null || vsb is null || container is null) return;
6271 
6272 		/+
6273 		writeln(x, " ", y , " ", width, " ", height);
6274 		writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight());
6275 		+/
6276 
6277 		registerMovement();
6278 
6279 		hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane?
6280 		hsb.x = 0;
6281 		hsb.y = this.height - hsb.height;
6282 		hsb.width = this.width - scaleWithDpi(16);
6283 		hsb.recomputeChildLayout();
6284 
6285 		vsb.width = scaleWithDpi(16); // FIXME?
6286 		vsb.x = this.width - vsb.width;
6287 		vsb.y = 0;
6288 		vsb.height = this.height - scaleWithDpi(16);
6289 		vsb.recomputeChildLayout();
6290 
6291 		container.x = 0;
6292 		container.y = 0;
6293 		container.width = this.width - vsb.width;
6294 		container.height = this.height - hsb.height;
6295 		container.recomputeChildLayout();
6296 
6297 		scrollX_ = 0;
6298 		scrollY_ = 0;
6299 
6300 		hsb.setPosition(0);
6301 		vsb.setPosition(0);
6302 
6303 		int mw, mh;
6304 		Widget c = container;
6305 		// FIXME: hack here to handle a layout inside...
6306 		if(c.children.length == 1 && cast(Layout) c.children[0])
6307 			c = c.children[0];
6308 		foreach(child; c.children) {
6309 			auto w = child.x + child.width;
6310 			auto h = child.y + child.height;
6311 
6312 			if(w > mw) mw = w;
6313 			if(h > mh) mh = h;
6314 		}
6315 
6316 		setTotalArea(mw, mh);
6317 		setViewableArea(width, height);
6318 	}
6319 
6320 	override int minHeight() { return scaleWithDpi(64); }
6321 
6322 	HorizontalScrollbar hsb;
6323 	VerticalScrollbar vsb;
6324 	ContainerWidget container;
6325 }
6326 
6327 
6328 version(custom_widgets)
6329 // deprecated // i can't deprecate it w/o stupid messages ugh
6330 private class InternalScrollableContainerWidget : Widget {
6331 
6332 	ScrollableWidget sw;
6333 
6334 	VerticalScrollbar verticalScrollBar;
6335 	HorizontalScrollbar horizontalScrollBar;
6336 
6337 	this(ScrollableWidget sw, Widget parent) {
6338 		this.sw = sw;
6339 
6340 		this.tabStop = false;
6341 
6342 		super(parent);
6343 
6344 		horizontalScrollBar = new HorizontalScrollbar(this);
6345 		verticalScrollBar = new VerticalScrollbar(this);
6346 
6347 		horizontalScrollBar.showing_ = false;
6348 		verticalScrollBar.showing_ = false;
6349 
6350 		horizontalScrollBar.addEventListener("scrolltonextline", {
6351 			horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
6352 			sw.horizontalScrollTo(horizontalScrollBar.position);
6353 		});
6354 		horizontalScrollBar.addEventListener("scrolltopreviousline", {
6355 			horizontalScrollBar.setPosition(horizontalScrollBar.position - 1);
6356 			sw.horizontalScrollTo(horizontalScrollBar.position);
6357 		});
6358 		verticalScrollBar.addEventListener("scrolltonextline", {
6359 			verticalScrollBar.setPosition(verticalScrollBar.position + 1);
6360 			sw.verticalScrollTo(verticalScrollBar.position);
6361 		});
6362 		verticalScrollBar.addEventListener("scrolltopreviousline", {
6363 			verticalScrollBar.setPosition(verticalScrollBar.position - 1);
6364 			sw.verticalScrollTo(verticalScrollBar.position);
6365 		});
6366 		horizontalScrollBar.addEventListener("scrolltonextpage", {
6367 			horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_);
6368 			sw.horizontalScrollTo(horizontalScrollBar.position);
6369 		});
6370 		horizontalScrollBar.addEventListener("scrolltopreviouspage", {
6371 			horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_);
6372 			sw.horizontalScrollTo(horizontalScrollBar.position);
6373 		});
6374 		verticalScrollBar.addEventListener("scrolltonextpage", {
6375 			verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_);
6376 			sw.verticalScrollTo(verticalScrollBar.position);
6377 		});
6378 		verticalScrollBar.addEventListener("scrolltopreviouspage", {
6379 			verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_);
6380 			sw.verticalScrollTo(verticalScrollBar.position);
6381 		});
6382 		horizontalScrollBar.addEventListener("scrolltoposition", (Event event) {
6383 			horizontalScrollBar.setPosition(event.intValue);
6384 			sw.horizontalScrollTo(horizontalScrollBar.position);
6385 		});
6386 		verticalScrollBar.addEventListener("scrolltoposition", (Event event) {
6387 			verticalScrollBar.setPosition(event.intValue);
6388 			sw.verticalScrollTo(verticalScrollBar.position);
6389 		});
6390 		horizontalScrollBar.addEventListener("scrolltrack", (Event event) {
6391 			horizontalScrollBar.setPosition(event.intValue);
6392 			sw.horizontalScrollTo(horizontalScrollBar.position);
6393 		});
6394 		verticalScrollBar.addEventListener("scrolltrack", (Event event) {
6395 			verticalScrollBar.setPosition(event.intValue);
6396 		});
6397 	}
6398 
6399 	// this is supposed to be basically invisible...
6400 	override int minWidth() { return sw.minWidth; }
6401 	override int minHeight() { return sw.minHeight; }
6402 	override int maxWidth() { return sw.maxWidth; }
6403 	override int maxHeight() { return sw.maxHeight; }
6404 	override int widthStretchiness() { return sw.widthStretchiness; }
6405 	override int heightStretchiness() { return sw.heightStretchiness; }
6406 	override int marginLeft() { return sw.marginLeft; }
6407 	override int marginRight() { return sw.marginRight; }
6408 	override int marginTop() { return sw.marginTop; }
6409 	override int marginBottom() { return sw.marginBottom; }
6410 	override int paddingLeft() { return sw.paddingLeft; }
6411 	override int paddingRight() { return sw.paddingRight; }
6412 	override int paddingTop() { return sw.paddingTop; }
6413 	override int paddingBottom() { return sw.paddingBottom; }
6414 	override void focus() { sw.focus(); }
6415 
6416 
6417 	override void recomputeChildLayout() {
6418 		// The stupid thing needs to calculate if a scroll bar is needed...
6419 		recomputeChildLayoutHelper();
6420 		// then running it again will position things correctly if the bar is NOT needed
6421 		recomputeChildLayoutHelper();
6422 
6423 		// this sucks but meh it barely works
6424 	}
6425 
6426 	private void recomputeChildLayoutHelper() {
6427 		if(sw is null) return;
6428 
6429 		bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll;
6430 		if(horizontalScrollBar && verticalScrollBar) {
6431 			horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0);
6432 			horizontalScrollBar.height = horizontalScrollBar.minHeight();
6433 			horizontalScrollBar.x = 0;
6434 			horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight();
6435 
6436 			verticalScrollBar.width = verticalScrollBar.minWidth();
6437 			verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2;
6438 			verticalScrollBar.x = this.width - verticalScrollBar.minWidth();
6439 			verticalScrollBar.y = 0 + 2;
6440 
6441 			sw.x = 0;
6442 			sw.y = 0;
6443 			sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0);
6444 			sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0);
6445 
6446 			if(sw.contentWidth_ <= this.width)
6447 				sw.scrollOrigin_.x = 0;
6448 			if(sw.contentHeight_ <= this.height)
6449 				sw.scrollOrigin_.y = 0;
6450 
6451 			horizontalScrollBar.recomputeChildLayout();
6452 			verticalScrollBar.recomputeChildLayout();
6453 			sw.recomputeChildLayout();
6454 		}
6455 
6456 		if(sw.contentWidth_ <= this.width)
6457 			sw.scrollOrigin_.x = 0;
6458 		if(sw.contentHeight_ <= this.height)
6459 			sw.scrollOrigin_.y = 0;
6460 
6461 		if(sw.showingHorizontalScroll())
6462 			horizontalScrollBar.showing(true, false);
6463 		else
6464 			horizontalScrollBar.showing(false, false);
6465 		if(sw.showingVerticalScroll())
6466 			verticalScrollBar.showing(true, false);
6467 		else
6468 			verticalScrollBar.showing(false, false);
6469 
6470 		verticalScrollBar.setViewableArea(sw.viewportHeight());
6471 		verticalScrollBar.setMax(sw.contentHeight);
6472 		verticalScrollBar.setPosition(sw.scrollOrigin.y);
6473 
6474 		horizontalScrollBar.setViewableArea(sw.viewportWidth());
6475 		horizontalScrollBar.setMax(sw.contentWidth);
6476 		horizontalScrollBar.setPosition(sw.scrollOrigin.x);
6477 	}
6478 }
6479 
6480 /*
6481 class ScrollableClientWidget : Widget {
6482 	this(Widget parent) {
6483 		super(parent);
6484 	}
6485 	override void paint(WidgetPainter p) {
6486 		parent.paint(p);
6487 	}
6488 }
6489 */
6490 
6491 /++
6492 	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.
6493 +/
6494 abstract class Slider : Widget {
6495 	this(int min, int max, int step, Widget parent) {
6496 		min_ = min;
6497 		max_ = max;
6498 		step_ = step;
6499 		page_ = step;
6500 		super(parent);
6501 	}
6502 
6503 	private int min_;
6504 	private int max_;
6505 	private int step_;
6506 	private int position_;
6507 	private int page_;
6508 
6509 	// selection start and selection end
6510 	// tics
6511 	// tooltip?
6512 	// some way to see and just type the value
6513 	// win32 buddy controls are labels
6514 
6515 	///
6516 	void setMin(int a) {
6517 		min_ = a;
6518 		version(custom_widgets)
6519 			redraw();
6520 		version(win32_widgets)
6521 			SendMessage(hwnd, TBM_SETRANGEMIN, true, a);
6522 	}
6523 	///
6524 	int min() {
6525 		return min_;
6526 	}
6527 	///
6528 	void setMax(int a) {
6529 		max_ = a;
6530 		version(custom_widgets)
6531 			redraw();
6532 		version(win32_widgets)
6533 			SendMessage(hwnd, TBM_SETRANGEMAX, true, a);
6534 	}
6535 	///
6536 	int max() {
6537 		return max_;
6538 	}
6539 	///
6540 	void setPosition(int a) {
6541 		if(a > max)
6542 			a = max;
6543 		if(a < min)
6544 			a = min;
6545 		position_ = a;
6546 		version(custom_widgets)
6547 			setPositionCustom(a);
6548 
6549 		version(win32_widgets)
6550 			setPositionWindows(a);
6551 	}
6552 	version(win32_widgets) {
6553 		protected abstract void setPositionWindows(int a);
6554 	}
6555 
6556 	protected abstract int win32direction();
6557 
6558 	/++
6559 		Alias for [position] for better compatibility with generic code.
6560 
6561 		History:
6562 			Added October 5, 2021
6563 	+/
6564 	@property int value() {
6565 		return position;
6566 	}
6567 
6568 	///
6569 	int position() {
6570 		return position_;
6571 	}
6572 	///
6573 	void setStep(int a) {
6574 		step_ = a;
6575 		version(win32_widgets)
6576 			SendMessage(hwnd, TBM_SETLINESIZE, 0, a);
6577 	}
6578 	///
6579 	int step() {
6580 		return step_;
6581 	}
6582 	///
6583 	void setPageSize(int a) {
6584 		page_ = a;
6585 		version(win32_widgets)
6586 			SendMessage(hwnd, TBM_SETPAGESIZE, 0, a);
6587 	}
6588 	///
6589 	int pageSize() {
6590 		return page_;
6591 	}
6592 
6593 	private void notify() {
6594 		auto event = new ChangeEvent!int(this, &this.position);
6595 		event.dispatch();
6596 	}
6597 
6598 	version(win32_widgets)
6599 	void win32Setup(int style) {
6600 		createWin32Window(this, TRACKBAR_CLASS, "",
6601 			0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0);
6602 
6603 		// the trackbar sends the same messages as scroll, which
6604 		// our other layer sends as these... just gonna translate
6605 		// here
6606 		this.addDirectEventListener("scrolltoposition", (Event event) {
6607 			event.stopPropagation();
6608 			this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue);
6609 			notify();
6610 		});
6611 		this.addDirectEventListener("scrolltonextline", (Event event) {
6612 			event.stopPropagation();
6613 			this.setPosition(this.position + this.step_ * this.win32direction);
6614 			notify();
6615 		});
6616 		this.addDirectEventListener("scrolltopreviousline", (Event event) {
6617 			event.stopPropagation();
6618 			this.setPosition(this.position - this.step_ * this.win32direction);
6619 			notify();
6620 		});
6621 		this.addDirectEventListener("scrolltonextpage", (Event event) {
6622 			event.stopPropagation();
6623 			this.setPosition(this.position + this.page_ * this.win32direction);
6624 			notify();
6625 		});
6626 		this.addDirectEventListener("scrolltopreviouspage", (Event event) {
6627 			event.stopPropagation();
6628 			this.setPosition(this.position - this.page_ * this.win32direction);
6629 			notify();
6630 		});
6631 
6632 		setMin(min_);
6633 		setMax(max_);
6634 		setStep(step_);
6635 		setPageSize(page_);
6636 	}
6637 
6638 	version(custom_widgets) {
6639 		protected MouseTrackingWidget thumb;
6640 
6641 		protected abstract void setPositionCustom(int a);
6642 
6643 		override void defaultEventHandler_keydown(KeyDownEvent event) {
6644 			switch(event.key) {
6645 				case Key.Up:
6646 				case Key.Right:
6647 					setPosition(position() - step() * win32direction);
6648 					changed();
6649 				break;
6650 				case Key.Down:
6651 				case Key.Left:
6652 					setPosition(position() + step() * win32direction);
6653 					changed();
6654 				break;
6655 				case Key.Home:
6656 					setPosition(win32direction > 0 ? min() : max());
6657 					changed();
6658 				break;
6659 				case Key.End:
6660 					setPosition(win32direction > 0 ? max() : min());
6661 					changed();
6662 				break;
6663 				case Key.PageUp:
6664 					setPosition(position() - pageSize() * win32direction);
6665 					changed();
6666 				break;
6667 				case Key.PageDown:
6668 					setPosition(position() + pageSize() * win32direction);
6669 					changed();
6670 				break;
6671 				default:
6672 			}
6673 			super.defaultEventHandler_keydown(event);
6674 		}
6675 
6676 		protected void changed() {
6677 			auto ev = new ChangeEvent!int(this, &position);
6678 			ev.dispatch();
6679 		}
6680 	}
6681 }
6682 
6683 /++
6684 
6685 +/
6686 class VerticalSlider : Slider {
6687 	this(int min, int max, int step, Widget parent) {
6688 		version(custom_widgets)
6689 			initialize();
6690 
6691 		super(min, max, step, parent);
6692 
6693 		version(win32_widgets)
6694 			win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */);
6695 	}
6696 
6697 	protected override int win32direction() {
6698 		return -1;
6699 	}
6700 
6701 	version(win32_widgets)
6702 	protected override void setPositionWindows(int a) {
6703 		// the windows thing makes the top 0 and i don't like that.
6704 		SendMessage(hwnd, TBM_SETPOS, true, max - a);
6705 	}
6706 
6707 	version(custom_widgets)
6708 	private void initialize() {
6709 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this);
6710 
6711 		thumb.tabStop = false;
6712 
6713 		thumb.thumbWidth = width;
6714 		thumb.thumbHeight = scaleWithDpi(16);
6715 
6716 		thumb.addEventListener(EventType.change, () {
6717 			auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16));
6718 			sx = max - sx;
6719 			//informProgramThatUserChangedPosition(sx);
6720 
6721 			position_ = sx;
6722 
6723 			changed();
6724 		});
6725 	}
6726 
6727 	version(custom_widgets)
6728 	override void recomputeChildLayout() {
6729 		thumb.thumbWidth = this.width;
6730 		super.recomputeChildLayout();
6731 		setPositionCustom(position_);
6732 	}
6733 
6734 	version(custom_widgets)
6735 	protected override void setPositionCustom(int a) {
6736 		if(max())
6737 			thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max();
6738 		redraw();
6739 	}
6740 }
6741 
6742 /++
6743 
6744 +/
6745 class HorizontalSlider : Slider {
6746 	this(int min, int max, int step, Widget parent) {
6747 		version(custom_widgets)
6748 			initialize();
6749 
6750 		super(min, max, step, parent);
6751 
6752 		version(win32_widgets)
6753 			win32Setup(TBS_HORZ);
6754 	}
6755 
6756 	version(win32_widgets)
6757 	protected override void setPositionWindows(int a) {
6758 		SendMessage(hwnd, TBM_SETPOS, true, a);
6759 	}
6760 
6761 	protected override int win32direction() {
6762 		return 1;
6763 	}
6764 
6765 	version(custom_widgets)
6766 	private void initialize() {
6767 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this);
6768 
6769 		thumb.tabStop = false;
6770 
6771 		thumb.thumbWidth = scaleWithDpi(16);
6772 		thumb.thumbHeight = height;
6773 
6774 		thumb.addEventListener(EventType.change, () {
6775 			auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16));
6776 			//informProgramThatUserChangedPosition(sx);
6777 
6778 			position_ = sx;
6779 
6780 			changed();
6781 		});
6782 	}
6783 
6784 	version(custom_widgets)
6785 	override void recomputeChildLayout() {
6786 		thumb.thumbHeight = this.height;
6787 		super.recomputeChildLayout();
6788 		setPositionCustom(position_);
6789 	}
6790 
6791 	version(custom_widgets)
6792 	protected override void setPositionCustom(int a) {
6793 		if(max())
6794 			thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max();
6795 		redraw();
6796 	}
6797 }
6798 
6799 
6800 ///
6801 abstract class ScrollbarBase : Widget {
6802 	///
6803 	this(Widget parent) {
6804 		super(parent);
6805 		tabStop = false;
6806 		step_ = scaleWithDpi(16);
6807 	}
6808 
6809 	private int viewableArea_;
6810 	private int max_;
6811 	private int step_;// = 16;
6812 	private int position_;
6813 
6814 	///
6815 	bool atEnd() {
6816 		return position_ + viewableArea_ >= max_;
6817 	}
6818 
6819 	///
6820 	bool atStart() {
6821 		return position_ == 0;
6822 	}
6823 
6824 	///
6825 	void setViewableArea(int a) {
6826 		viewableArea_ = a;
6827 		version(custom_widgets)
6828 			redraw();
6829 	}
6830 	///
6831 	void setMax(int a) {
6832 		max_ = a;
6833 		version(custom_widgets)
6834 			redraw();
6835 	}
6836 	///
6837 	int max() {
6838 		return max_;
6839 	}
6840 	///
6841 	void setPosition(int a) {
6842 		auto logicalMax = max_ - viewableArea_;
6843 		if(a == int.max)
6844 			a = logicalMax;
6845 
6846 		if(a > logicalMax)
6847 			a = logicalMax;
6848 		if(a < 0)
6849 			a = 0;
6850 
6851 		position_ = a;
6852 
6853 		version(custom_widgets)
6854 			redraw();
6855 	}
6856 	///
6857 	int position() {
6858 		return position_;
6859 	}
6860 	///
6861 	void setStep(int a) {
6862 		step_ = a;
6863 	}
6864 	///
6865 	int step() {
6866 		return step_;
6867 	}
6868 
6869 	// FIXME: remove this.... maybe
6870 	/+
6871 	protected void informProgramThatUserChangedPosition(int n) {
6872 		position_ = n;
6873 		auto evt = new Event(EventType.change, this);
6874 		evt.intValue = n;
6875 		evt.dispatch();
6876 	}
6877 	+/
6878 
6879 	version(custom_widgets) {
6880 		enum MIN_THUMB_SIZE = 8;
6881 
6882 		abstract protected int getBarDim();
6883 		int thumbSize() {
6884 			if(viewableArea_ >= max_ || max_ == 0)
6885 				return getBarDim();
6886 
6887 			int res = viewableArea_ * getBarDim() / max_;
6888 
6889 			if(res < scaleWithDpi(MIN_THUMB_SIZE))
6890 				res = scaleWithDpi(MIN_THUMB_SIZE);
6891 
6892 			return res;
6893 		}
6894 
6895 		int thumbPosition() {
6896 			/*
6897 				viewableArea_ is the viewport height/width
6898 				position_ is where we are
6899 			*/
6900 			//if(position_ + viewableArea_ >= max_)
6901 				//return getBarDim - thumbSize;
6902 
6903 			auto maximumPossibleValue = getBarDim() - thumbSize;
6904 			auto maximiumLogicalValue = max_ - viewableArea_;
6905 
6906 			auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0;
6907 
6908 			return p;
6909 		}
6910 	}
6911 }
6912 
6913 //public import mgt;
6914 
6915 /++
6916 	A mouse tracking widget is one that follows the mouse when dragged inside it.
6917 
6918 	Concrete subclasses may include a scrollbar thumb and a volume control.
6919 +/
6920 //version(custom_widgets)
6921 class MouseTrackingWidget : Widget {
6922 
6923 	///
6924 	int positionX() { return positionX_; }
6925 	///
6926 	int positionY() { return positionY_; }
6927 
6928 	///
6929 	void positionX(int p) { positionX_ = p; }
6930 	///
6931 	void positionY(int p) { positionY_ = p; }
6932 
6933 	private int positionX_;
6934 	private int positionY_;
6935 
6936 	///
6937 	enum Orientation {
6938 		horizontal, ///
6939 		vertical, ///
6940 		twoDimensional, ///
6941 	}
6942 
6943 	private int thumbWidth_;
6944 	private int thumbHeight_;
6945 
6946 	///
6947 	int thumbWidth() { return thumbWidth_; }
6948 	///
6949 	int thumbHeight() { return thumbHeight_; }
6950 	///
6951 	int thumbWidth(int a) { return thumbWidth_ = a; }
6952 	///
6953 	int thumbHeight(int a) { return thumbHeight_ = a; }
6954 
6955 	private bool dragging;
6956 	private bool hovering;
6957 	private int startMouseX, startMouseY;
6958 
6959 	///
6960 	this(Orientation orientation, Widget parent) {
6961 		super(parent);
6962 
6963 		//assert(parentWindow !is null);
6964 
6965 		addEventListener((MouseDownEvent event) {
6966 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
6967 				dragging = true;
6968 				startMouseX = event.clientX - positionX;
6969 				startMouseY = event.clientY - positionY;
6970 				parentWindow.captureMouse(this);
6971 			} else {
6972 				if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
6973 					positionX = event.clientX - thumbWidth / 2;
6974 				if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
6975 					positionY = event.clientY - thumbHeight / 2;
6976 
6977 				if(positionX + thumbWidth > this.width)
6978 					positionX = this.width - thumbWidth;
6979 				if(positionY + thumbHeight > this.height)
6980 					positionY = this.height - thumbHeight;
6981 
6982 				if(positionX < 0)
6983 					positionX = 0;
6984 				if(positionY < 0)
6985 					positionY = 0;
6986 
6987 
6988 				// this.emit!(ChangeEvent!void)();
6989 				auto evt = new Event(EventType.change, this);
6990 				evt.sendDirectly();
6991 
6992 				redraw();
6993 
6994 			}
6995 		});
6996 
6997 		addEventListener(EventType.mouseup, (Event event) {
6998 			dragging = false;
6999 			parentWindow.releaseMouseCapture();
7000 		});
7001 
7002 		addEventListener(EventType.mouseout, (Event event) {
7003 			if(!hovering)
7004 				return;
7005 			hovering = false;
7006 			redraw();
7007 		});
7008 
7009 		int lpx, lpy;
7010 
7011 		addEventListener((MouseMoveEvent event) {
7012 			auto oh = hovering;
7013 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
7014 				hovering = true;
7015 			} else {
7016 				hovering = false;
7017 			}
7018 			if(!dragging) {
7019 				if(hovering != oh)
7020 					redraw();
7021 				return;
7022 			}
7023 
7024 			if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
7025 				positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it
7026 			if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
7027 				positionY = event.clientY - startMouseY;
7028 
7029 			if(positionX + thumbWidth > this.width)
7030 				positionX = this.width - thumbWidth;
7031 			if(positionY + thumbHeight > this.height)
7032 				positionY = this.height - thumbHeight;
7033 
7034 			if(positionX < 0)
7035 				positionX = 0;
7036 			if(positionY < 0)
7037 				positionY = 0;
7038 
7039 			if(positionX != lpx || positionY != lpy) {
7040 				lpx = positionX;
7041 				lpy = positionY;
7042 
7043 				auto evt = new Event(EventType.change, this);
7044 				evt.sendDirectly();
7045 			}
7046 
7047 			redraw();
7048 		});
7049 	}
7050 
7051 	version(custom_widgets)
7052 	override void paint(WidgetPainter painter) {
7053 		auto cs = getComputedStyle();
7054 		auto c = darken(cs.windowBackgroundColor, 0.2);
7055 		painter.outlineColor = c;
7056 		painter.fillColor = c;
7057 		painter.drawRectangle(Point(0, 0), this.width, this.height);
7058 
7059 		auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor;
7060 		draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color);
7061 	}
7062 }
7063 
7064 //version(custom_widgets)
7065 //private
7066 class HorizontalScrollbar : ScrollbarBase {
7067 
7068 	version(custom_widgets) {
7069 		private MouseTrackingWidget thumb;
7070 
7071 		override int getBarDim() {
7072 			return thumb.width;
7073 		}
7074 	}
7075 
7076 	override void setViewableArea(int a) {
7077 		super.setViewableArea(a);
7078 
7079 		version(win32_widgets) {
7080 			SCROLLINFO info;
7081 			info.cbSize = info.sizeof;
7082 			info.nPage = a + 1;
7083 			info.fMask = SIF_PAGE;
7084 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7085 		} else version(custom_widgets) {
7086 			thumb.positionX = thumbPosition;
7087 			thumb.thumbWidth = thumbSize;
7088 			thumb.redraw();
7089 		} else static assert(0);
7090 
7091 	}
7092 
7093 	override void setMax(int a) {
7094 		super.setMax(a);
7095 		version(win32_widgets) {
7096 			SCROLLINFO info;
7097 			info.cbSize = info.sizeof;
7098 			info.nMin = 0;
7099 			info.nMax = max;
7100 			info.fMask = SIF_RANGE;
7101 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7102 		} else version(custom_widgets) {
7103 			thumb.positionX = thumbPosition;
7104 			thumb.thumbWidth = thumbSize;
7105 			thumb.redraw();
7106 		}
7107 	}
7108 
7109 	override void setPosition(int a) {
7110 		super.setPosition(a);
7111 		version(win32_widgets) {
7112 			SCROLLINFO info;
7113 			info.cbSize = info.sizeof;
7114 			info.fMask = SIF_POS;
7115 			info.nPos = position;
7116 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7117 		} else version(custom_widgets) {
7118 			thumb.positionX = thumbPosition();
7119 			thumb.thumbWidth = thumbSize;
7120 			thumb.redraw();
7121 		} else static assert(0);
7122 	}
7123 
7124 	this(Widget parent) {
7125 		super(parent);
7126 
7127 		version(win32_widgets) {
7128 			createWin32Window(this, "Scrollbar"w, "",
7129 				0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0);
7130 		} else version(custom_widgets) {
7131 			auto vl = new HorizontalLayout(this);
7132 			auto leftButton = new ArrowButton(ArrowDirection.left, vl);
7133 			leftButton.setClickRepeat(scrollClickRepeatInterval);
7134 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl);
7135 			auto rightButton = new ArrowButton(ArrowDirection.right, vl);
7136 			rightButton.setClickRepeat(scrollClickRepeatInterval);
7137 
7138 			leftButton.tabStop = false;
7139 			rightButton.tabStop = false;
7140 			thumb.tabStop = false;
7141 
7142 			leftButton.addEventListener(EventType.triggered, () {
7143 				this.emitCommand!"scrolltopreviousline"();
7144 				//informProgramThatUserChangedPosition(position - step());
7145 			});
7146 			rightButton.addEventListener(EventType.triggered, () {
7147 				this.emitCommand!"scrolltonextline"();
7148 				//informProgramThatUserChangedPosition(position + step());
7149 			});
7150 
7151 			thumb.thumbWidth = this.minWidth;
7152 			thumb.thumbHeight = scaleWithDpi(16);
7153 
7154 			thumb.addEventListener(EventType.change, () {
7155 				auto maximumPossibleValue = thumb.width - thumb.thumbWidth;
7156 				auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0;
7157 
7158 				//informProgramThatUserChangedPosition(sx);
7159 
7160 				auto ev = new ScrollToPositionEvent(this, sx);
7161 				ev.dispatch();
7162 			});
7163 		}
7164 	}
7165 
7166 	override int minHeight() { return scaleWithDpi(16); }
7167 	override int maxHeight() { return scaleWithDpi(16); }
7168 	override int minWidth() { return scaleWithDpi(48); }
7169 }
7170 
7171 final class ScrollToPositionEvent : Event {
7172 	enum EventString = "scrolltoposition";
7173 
7174 	this(Widget target, int value) {
7175 		this.value = value;
7176 		super(EventString, target);
7177 	}
7178 
7179 	immutable int value;
7180 
7181 	override @property int intValue() {
7182 		return value;
7183 	}
7184 }
7185 
7186 //version(custom_widgets)
7187 //private
7188 class VerticalScrollbar : ScrollbarBase {
7189 
7190 	version(custom_widgets) {
7191 		override int getBarDim() {
7192 			return thumb.height;
7193 		}
7194 
7195 		private MouseTrackingWidget thumb;
7196 	}
7197 
7198 	override void setViewableArea(int a) {
7199 		super.setViewableArea(a);
7200 
7201 		version(win32_widgets) {
7202 			SCROLLINFO info;
7203 			info.cbSize = info.sizeof;
7204 			info.nPage = a + 1;
7205 			info.fMask = SIF_PAGE;
7206 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7207 		} else version(custom_widgets) {
7208 			thumb.positionY = thumbPosition;
7209 			thumb.thumbHeight = thumbSize;
7210 			thumb.redraw();
7211 		} else static assert(0);
7212 
7213 	}
7214 
7215 	override void setMax(int a) {
7216 		super.setMax(a);
7217 		version(win32_widgets) {
7218 			SCROLLINFO info;
7219 			info.cbSize = info.sizeof;
7220 			info.nMin = 0;
7221 			info.nMax = max;
7222 			info.fMask = SIF_RANGE;
7223 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7224 		} else version(custom_widgets) {
7225 			thumb.positionY = thumbPosition;
7226 			thumb.thumbHeight = thumbSize;
7227 			thumb.redraw();
7228 		}
7229 	}
7230 
7231 	override void setPosition(int a) {
7232 		super.setPosition(a);
7233 		version(win32_widgets) {
7234 			SCROLLINFO info;
7235 			info.cbSize = info.sizeof;
7236 			info.fMask = SIF_POS;
7237 			info.nPos = position;
7238 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7239 		} else version(custom_widgets) {
7240 			thumb.positionY = thumbPosition;
7241 			thumb.thumbHeight = thumbSize;
7242 			thumb.redraw();
7243 		} else static assert(0);
7244 	}
7245 
7246 	this(Widget parent) {
7247 		super(parent);
7248 
7249 		version(win32_widgets) {
7250 			createWin32Window(this, "Scrollbar"w, "",
7251 				0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0);
7252 		} else version(custom_widgets) {
7253 			auto vl = new VerticalLayout(this);
7254 			auto upButton = new ArrowButton(ArrowDirection.up, vl);
7255 			upButton.setClickRepeat(scrollClickRepeatInterval);
7256 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl);
7257 			auto downButton = new ArrowButton(ArrowDirection.down, vl);
7258 			downButton.setClickRepeat(scrollClickRepeatInterval);
7259 
7260 			upButton.addEventListener(EventType.triggered, () {
7261 				this.emitCommand!"scrolltopreviousline"();
7262 				//informProgramThatUserChangedPosition(position - step());
7263 			});
7264 			downButton.addEventListener(EventType.triggered, () {
7265 				this.emitCommand!"scrolltonextline"();
7266 				//informProgramThatUserChangedPosition(position + step());
7267 			});
7268 
7269 			thumb.thumbWidth = this.minWidth;
7270 			thumb.thumbHeight = scaleWithDpi(16);
7271 
7272 			thumb.addEventListener(EventType.change, () {
7273 				auto maximumPossibleValue = thumb.height - thumb.thumbHeight;
7274 				auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0;
7275 
7276 				auto ev = new ScrollToPositionEvent(this, sy);
7277 				ev.dispatch();
7278 
7279 				//informProgramThatUserChangedPosition(sy);
7280 			});
7281 
7282 			upButton.tabStop = false;
7283 			downButton.tabStop = false;
7284 			thumb.tabStop = false;
7285 		}
7286 	}
7287 
7288 	override int minWidth() { return scaleWithDpi(16); }
7289 	override int maxWidth() { return scaleWithDpi(16); }
7290 	override int minHeight() { return scaleWithDpi(48); }
7291 }
7292 
7293 
7294 /++
7295 	EXPERIMENTAL
7296 
7297 	A widget specialized for being a container for other widgets.
7298 
7299 	History:
7300 		Added May 29, 2021. Not stabilized at this time.
7301 +/
7302 class WidgetContainer : Widget {
7303 	this(Widget parent) {
7304 		tabStop = false;
7305 		super(parent);
7306 	}
7307 
7308 	override int maxHeight() {
7309 		if(this.children.length == 1) {
7310 			return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom);
7311 		} else {
7312 			return int.max;
7313 		}
7314 	}
7315 
7316 	override int maxWidth() {
7317 		if(this.children.length == 1) {
7318 			return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight);
7319 		} else {
7320 			return int.max;
7321 		}
7322 	}
7323 
7324 	/+
7325 
7326 	override int minHeight() {
7327 		int largest = 0;
7328 		int margins = 0;
7329 		int lastMargin = 0;
7330 		foreach(child; children) {
7331 			auto mh = child.minHeight();
7332 			if(mh > largest)
7333 				largest = mh;
7334 			margins += mymax(lastMargin, child.marginTop());
7335 			lastMargin = child.marginBottom();
7336 		}
7337 		return largest + margins;
7338 	}
7339 
7340 	override int maxHeight() {
7341 		int largest = 0;
7342 		int margins = 0;
7343 		int lastMargin = 0;
7344 		foreach(child; children) {
7345 			auto mh = child.maxHeight();
7346 			if(mh == int.max)
7347 				return int.max;
7348 			if(mh > largest)
7349 				largest = mh;
7350 			margins += mymax(lastMargin, child.marginTop());
7351 			lastMargin = child.marginBottom();
7352 		}
7353 		return largest + margins;
7354 	}
7355 
7356 	override int minWidth() {
7357 		int min;
7358 		foreach(child; children) {
7359 			auto cm = child.minWidth;
7360 			if(cm > min)
7361 				min = cm;
7362 		}
7363 		return min + paddingLeft + paddingRight;
7364 	}
7365 
7366 	override int minHeight() {
7367 		int min;
7368 		foreach(child; children) {
7369 			auto cm = child.minHeight;
7370 			if(cm > min)
7371 				min = cm;
7372 		}
7373 		return min + paddingTop + paddingBottom;
7374 	}
7375 
7376 	override int maxHeight() {
7377 		int largest = 0;
7378 		int margins = 0;
7379 		int lastMargin = 0;
7380 		foreach(child; children) {
7381 			auto mh = child.maxHeight();
7382 			if(mh == int.max)
7383 				return int.max;
7384 			if(mh > largest)
7385 				largest = mh;
7386 			margins += mymax(lastMargin, child.marginTop());
7387 			lastMargin = child.marginBottom();
7388 		}
7389 		return largest + margins;
7390 	}
7391 
7392 	override int heightStretchiness() {
7393 		int max;
7394 		foreach(child; children) {
7395 			auto c = child.heightStretchiness;
7396 			if(c > max)
7397 				max = c;
7398 		}
7399 		return max;
7400 	}
7401 
7402 	override int marginTop() {
7403 		if(this.children.length)
7404 			return this.children[0].marginTop;
7405 		return 0;
7406 	}
7407 	+/
7408 }
7409 
7410 ///
7411 abstract class Layout : Widget {
7412 	this(Widget parent) {
7413 		tabStop = false;
7414 		super(parent);
7415 	}
7416 }
7417 
7418 /++
7419 	Makes all children minimum width and height, placing them down
7420 	left to right, top to bottom.
7421 
7422 	Useful if you want to make a list of buttons that automatically
7423 	wrap to a new line when necessary.
7424 +/
7425 class InlineBlockLayout : Layout {
7426 	///
7427 	this(Widget parent) { super(parent); }
7428 
7429 	override void recomputeChildLayout() {
7430 		registerMovement();
7431 
7432 		int x = this.paddingLeft, y = this.paddingTop;
7433 
7434 		int lineHeight;
7435 		int previousMargin = 0;
7436 		int previousMarginBottom = 0;
7437 
7438 		foreach(child; children) {
7439 			if(child.hidden)
7440 				continue;
7441 			if(cast(FixedPosition) child) {
7442 				child.recomputeChildLayout();
7443 				continue;
7444 			}
7445 			child.width = child.flexBasisWidth();
7446 			if(child.width == 0)
7447 				child.width = child.minWidth();
7448 			if(child.width == 0)
7449 				child.width = 32;
7450 
7451 			child.height = child.flexBasisHeight();
7452 			if(child.height == 0)
7453 				child.height = child.minHeight();
7454 			if(child.height == 0)
7455 				child.height = 32;
7456 
7457 			if(x + child.width + paddingRight > this.width) {
7458 				x = this.paddingLeft;
7459 				y += lineHeight;
7460 				lineHeight = 0;
7461 				previousMargin = 0;
7462 				previousMarginBottom = 0;
7463 			}
7464 
7465 			auto margin = child.marginLeft;
7466 			if(previousMargin > margin)
7467 				margin = previousMargin;
7468 
7469 			x += margin;
7470 
7471 			child.x = x;
7472 			child.y = y;
7473 
7474 			int marginTopApplied;
7475 			if(child.marginTop > previousMarginBottom) {
7476 				child.y += child.marginTop;
7477 				marginTopApplied = child.marginTop;
7478 			}
7479 
7480 			x += child.width;
7481 			previousMargin = child.marginRight;
7482 
7483 			if(child.marginBottom > previousMarginBottom)
7484 				previousMarginBottom = child.marginBottom;
7485 
7486 			auto h = child.height + previousMarginBottom + marginTopApplied;
7487 			if(h > lineHeight)
7488 				lineHeight = h;
7489 
7490 			child.recomputeChildLayout();
7491 		}
7492 
7493 	}
7494 
7495 	override int minWidth() {
7496 		int min;
7497 		foreach(child; children) {
7498 			auto cm = child.minWidth;
7499 			if(cm > min)
7500 				min = cm;
7501 		}
7502 		return min + paddingLeft + paddingRight;
7503 	}
7504 
7505 	override int minHeight() {
7506 		int min;
7507 		foreach(child; children) {
7508 			auto cm = child.minHeight;
7509 			if(cm > min)
7510 				min = cm;
7511 		}
7512 		return min + paddingTop + paddingBottom;
7513 	}
7514 }
7515 
7516 /++
7517 	A TabMessageWidget is a clickable row of tabs followed by a content area, very similar
7518 	to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas
7519 	the [TabWidget] will automatically change pages of child widgets.
7520 
7521 	This allows you to react to it however you see fit rather than having to
7522 	be tied to just the new sets of child widgets.
7523 
7524 	It sends the message in the form of `this.emitCommand!"changetab"();`.
7525 
7526 	History:
7527 		Added December 24, 2021 (dub v10.5)
7528 +/
7529 class TabMessageWidget : Widget {
7530 
7531 	protected void tabIndexClicked(int item) {
7532 		this.emitCommand!"changetab"();
7533 	}
7534 
7535 	/++
7536 		Adds the a new tab to the control with the given title.
7537 
7538 		Returns:
7539 			The index of the newly added tab. You will need to know
7540 			this index to refer to it later and to know which tab to
7541 			change to when you get a changetab message.
7542 	+/
7543 	int addTab(string title, int pos = int.max) {
7544 		version(win32_widgets) {
7545 			TCITEM item;
7546 			item.mask = TCIF_TEXT;
7547 			WCharzBuffer buf = WCharzBuffer(title);
7548 			item.pszText = buf.ptr;
7549 			return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item);
7550 		} else version(custom_widgets) {
7551 			if(pos >= tabs.length) {
7552 				tabs ~= title;
7553 				redraw();
7554 				return cast(int) tabs.length - 1;
7555 			} else if(pos <= 0) {
7556 				tabs = title ~ tabs;
7557 				redraw();
7558 				return 0;
7559 			} else {
7560 				tabs = tabs[0 .. pos] ~ title ~ title[pos .. $];
7561 				redraw();
7562 				return pos;
7563 			}
7564 		}
7565 	}
7566 
7567 	override void addChild(Widget child, int pos = int.max) {
7568 		if(container)
7569 			container.addChild(child, pos);
7570 		else
7571 			super.addChild(child, pos);
7572 	}
7573 
7574 	protected Widget makeContainer() {
7575 		return new Widget(this);
7576 	}
7577 
7578 	private Widget container;
7579 
7580 	override void recomputeChildLayout() {
7581 		version(win32_widgets) {
7582 			this.registerMovement();
7583 
7584 			RECT rect;
7585 			GetWindowRect(hwnd, &rect);
7586 
7587 			auto left = rect.left;
7588 			auto top = rect.top;
7589 
7590 			TabCtrl_AdjustRect(hwnd, false, &rect);
7591 			foreach(child; children) {
7592 				if(!child.showing) continue;
7593 				child.x = rect.left - left;
7594 				child.y = rect.top - top;
7595 				child.width = rect.right - rect.left;
7596 				child.height = rect.bottom - rect.top;
7597 				child.recomputeChildLayout();
7598 			}
7599 		} else version(custom_widgets) {
7600 			this.registerMovement();
7601 			foreach(child; children) {
7602 				if(!child.showing) continue;
7603 				child.x = 2;
7604 				child.y = tabBarHeight + 2; // for the border
7605 				child.width = width - 4; // for the border
7606 				child.height = height - tabBarHeight - 2 - 2; // for the border
7607 				child.recomputeChildLayout();
7608 			}
7609 		} else static assert(0);
7610 	}
7611 
7612 	version(custom_widgets)
7613 		string[] tabs;
7614 
7615 	this(Widget parent) {
7616 		super(parent);
7617 
7618 		tabStop = false;
7619 
7620 		version(win32_widgets) {
7621 			createWin32Window(this, WC_TABCONTROL, "", 0);
7622 		} else version(custom_widgets) {
7623 			addEventListener((ClickEvent event) {
7624 				if(event.target !is this)
7625 					return;
7626 				if(event.clientY >= 0 && event.clientY < tabBarHeight) {
7627 					auto t = (event.clientX / tabWidth);
7628 					if(t >= 0 && t < tabs.length) {
7629 						currentTab_ = t;
7630 						tabIndexClicked(t);
7631 						redraw();
7632 					}
7633 				}
7634 			});
7635 		} else static assert(0);
7636 
7637 		this.container = makeContainer();
7638 	}
7639 
7640 	override int marginTop() { return 4; }
7641 	override int paddingBottom() { return 4; }
7642 
7643 	override int minHeight() {
7644 		int max = 0;
7645 		foreach(child; children)
7646 			max = mymax(child.minHeight, max);
7647 
7648 
7649 		version(win32_widgets) {
7650 			RECT rect;
7651 			rect.right = this.width;
7652 			rect.bottom = max;
7653 			TabCtrl_AdjustRect(hwnd, true, &rect);
7654 
7655 			max = rect.bottom;
7656 		} else {
7657 			max += defaultLineHeight + 4;
7658 		}
7659 
7660 
7661 		return max;
7662 	}
7663 
7664 	version(win32_widgets)
7665 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
7666 		switch(code) {
7667 			case TCN_SELCHANGE:
7668 				auto sel = TabCtrl_GetCurSel(hwnd);
7669 				tabIndexClicked(sel);
7670 			break;
7671 			default:
7672 		}
7673 		return 0;
7674 	}
7675 
7676 	version(custom_widgets) {
7677 		private int currentTab_;
7678 		private int tabBarHeight() { return defaultLineHeight; }
7679 		int tabWidth() { return scaleWithDpi(80); }
7680 	}
7681 
7682 	version(win32_widgets)
7683 	override void paint(WidgetPainter painter) {}
7684 
7685 	version(custom_widgets)
7686 	override void paint(WidgetPainter painter) {
7687 		auto cs = getComputedStyle();
7688 
7689 		draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color);
7690 
7691 		int posX = 0;
7692 		foreach(idx, title; tabs) {
7693 			auto isCurrent = idx == getCurrentTab();
7694 
7695 			painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight);
7696 
7697 			draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1));
7698 			painter.outlineColor = cs.foregroundColor;
7699 			painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter);
7700 
7701 			if(isCurrent) {
7702 				painter.outlineColor = cs.windowBackgroundColor;
7703 				painter.fillColor = Color.transparent;
7704 				painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1));
7705 				painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2));
7706 
7707 				painter.outlineColor = Color.white;
7708 				painter.drawPixel(Point(posX + 1, tabBarHeight - 1));
7709 				painter.drawPixel(Point(posX + 1, tabBarHeight - 2));
7710 				painter.outlineColor = cs.activeTabColor;
7711 				painter.drawPixel(Point(posX, tabBarHeight - 1));
7712 			}
7713 
7714 			posX += tabWidth - 2;
7715 		}
7716 	}
7717 
7718 	///
7719 	@scriptable
7720 	void setCurrentTab(int item) {
7721 		version(win32_widgets)
7722 			TabCtrl_SetCurSel(hwnd, item);
7723 		else version(custom_widgets)
7724 			currentTab_ = item;
7725 		else static assert(0);
7726 
7727 		tabIndexClicked(item);
7728 	}
7729 
7730 	///
7731 	@scriptable
7732 	int getCurrentTab() {
7733 		version(win32_widgets)
7734 			return TabCtrl_GetCurSel(hwnd);
7735 		else version(custom_widgets)
7736 			return currentTab_; // FIXME
7737 		else static assert(0);
7738 	}
7739 
7740 	///
7741 	@scriptable
7742 	void removeTab(int item) {
7743 		if(item && item == getCurrentTab())
7744 			setCurrentTab(item - 1);
7745 
7746 		version(win32_widgets) {
7747 			TabCtrl_DeleteItem(hwnd, item);
7748 		}
7749 
7750 		for(int a = item; a < children.length - 1; a++)
7751 			this._children[a] = this._children[a + 1];
7752 		this._children = this._children[0 .. $-1];
7753 	}
7754 
7755 }
7756 
7757 
7758 /++
7759 	A tab widget is a set of clickable tab buttons followed by a content area.
7760 
7761 
7762 	Tabs can change existing content or can be new pages.
7763 
7764 	When the user picks a different tab, a `change` message is generated.
7765 +/
7766 class TabWidget : TabMessageWidget {
7767 	this(Widget parent) {
7768 		super(parent);
7769 	}
7770 
7771 	override protected Widget makeContainer() {
7772 		return null;
7773 	}
7774 
7775 	override void addChild(Widget child, int pos = int.max) {
7776 		if(auto twp = cast(TabWidgetPage) child) {
7777 			Widget.addChild(child, pos);
7778 			if(pos == int.max)
7779 				pos = cast(int) this.children.length - 1;
7780 
7781 			super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop...
7782 
7783 			if(pos != getCurrentTab) {
7784 				child.showing = false;
7785 			}
7786 		} else {
7787 			assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)");
7788 		}
7789 	}
7790 
7791 	// FIXME: add tab icons at some point, Windows supports them
7792 	/++
7793 		Adds a page and its associated tab with the given label to the widget.
7794 
7795 		Returns:
7796 			The added page object, to which you can add other widgets.
7797 	+/
7798 	@scriptable
7799 	TabWidgetPage addPage(string title) {
7800 		return new TabWidgetPage(title, this);
7801 	}
7802 
7803 	/++
7804 		Gets the page at the given tab index, or `null` if the index is bad.
7805 
7806 		History:
7807 			Added December 24, 2021.
7808 	+/
7809 	TabWidgetPage getPage(int index) {
7810 		if(index < this.children.length)
7811 			return null;
7812 		return cast(TabWidgetPage) this.children[index];
7813 	}
7814 
7815 	/++
7816 		While you can still use the addTab from the parent class,
7817 		*strongly* recommend you use [addPage] insteaad.
7818 
7819 		History:
7820 			Added December 24, 2021 to fulful the interface
7821 			requirement that came from adding [TabMessageWidget].
7822 
7823 			You should not use it though since the [addPage] function
7824 			is much easier to use here.
7825 	+/
7826 	override int addTab(string title, int pos = int.max) {
7827 		auto p = addPage(title);
7828 		foreach(idx, child; this.children)
7829 			if(child is p)
7830 				return cast(int) idx;
7831 		return -1;
7832 	}
7833 
7834 	protected override void tabIndexClicked(int item) {
7835 		foreach(idx, child; children) {
7836 			child.showing(false, false); // batch the recalculates for the end
7837 		}
7838 
7839 		foreach(idx, child; children) {
7840 			if(idx == item) {
7841 				child.showing(true, false);
7842 				if(parentWindow) {
7843 					auto f = parentWindow.getFirstFocusable(child);
7844 					if(f)
7845 						f.focus();
7846 				}
7847 				recomputeChildLayout();
7848 			}
7849 		}
7850 
7851 		version(win32_widgets) {
7852 			InvalidateRect(hwnd, null, true);
7853 		} else version(custom_widgets) {
7854 			this.redraw();
7855 		}
7856 	}
7857 
7858 }
7859 
7860 /++
7861 	A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget".
7862 
7863 	You add [TabWidgetPage]s to it.
7864 +/
7865 class PageWidget : Widget {
7866 	this(Widget parent) {
7867 		super(parent);
7868 	}
7869 
7870 	override int minHeight() {
7871 		int max = 0;
7872 		foreach(child; children)
7873 			max = mymax(child.minHeight, max);
7874 
7875 		return max;
7876 	}
7877 
7878 
7879 	override void addChild(Widget child, int pos = int.max) {
7880 		if(auto twp = cast(TabWidgetPage) child) {
7881 			super.addChild(child, pos);
7882 			if(pos == int.max)
7883 				pos = cast(int) this.children.length - 1;
7884 
7885 			if(pos != getCurrentTab) {
7886 				child.showing = false;
7887 			}
7888 		} else {
7889 			assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)");
7890 		}
7891 	}
7892 
7893 	override void recomputeChildLayout() {
7894 		this.registerMovement();
7895 		foreach(child; children) {
7896 			child.x = 0;
7897 			child.y = 0;
7898 			child.width = width;
7899 			child.height = height;
7900 			child.recomputeChildLayout();
7901 		}
7902 	}
7903 
7904 	private int currentTab_;
7905 
7906 	///
7907 	@scriptable
7908 	void setCurrentTab(int item) {
7909 		currentTab_ = item;
7910 
7911 		showOnly(item);
7912 	}
7913 
7914 	///
7915 	@scriptable
7916 	int getCurrentTab() {
7917 		return currentTab_;
7918 	}
7919 
7920 	///
7921 	@scriptable
7922 	void removeTab(int item) {
7923 		if(item && item == getCurrentTab())
7924 			setCurrentTab(item - 1);
7925 
7926 		for(int a = item; a < children.length - 1; a++)
7927 			this._children[a] = this._children[a + 1];
7928 		this._children = this._children[0 .. $-1];
7929 	}
7930 
7931 	///
7932 	@scriptable
7933 	TabWidgetPage addPage(string title) {
7934 		return new TabWidgetPage(title, this);
7935 	}
7936 
7937 	private void showOnly(int item) {
7938 		foreach(idx, child; children)
7939 			if(idx == item) {
7940 				child.show();
7941 				child.queueRecomputeChildLayout();
7942 			} else {
7943 				child.hide();
7944 			}
7945 	}
7946 }
7947 
7948 /++
7949 
7950 +/
7951 class TabWidgetPage : Widget {
7952 	string title;
7953 	this(string title, Widget parent) {
7954 		this.title = title;
7955 		this.tabStop = false;
7956 		super(parent);
7957 
7958 		///*
7959 		version(win32_widgets) {
7960 			createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0);
7961 		}
7962 		//*/
7963 	}
7964 
7965 	override int minHeight() {
7966 		int sum = 0;
7967 		foreach(child; children)
7968 			sum += child.minHeight();
7969 		return sum;
7970 	}
7971 }
7972 
7973 version(none)
7974 /++
7975 	A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button.
7976 
7977 	I think I need to modify the layout algorithms to support this.
7978 +/
7979 class CollapsableSidebar : Widget {
7980 
7981 }
7982 
7983 /// Stacks the widgets vertically, taking all the available width for each child.
7984 class VerticalLayout : Layout {
7985 	// most of this is intentionally blank - widget's default is vertical layout right now
7986 	///
7987 	this(Widget parent) { super(parent); }
7988 
7989 	/++
7990 		Sets a max width for the layout so you don't have to subclass. The max width
7991 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
7992 
7993 		History:
7994 			Added November 29, 2021 (dub v10.5)
7995 	+/
7996 	this(int maxWidth, Widget parent) {
7997 		this.mw = maxWidth;
7998 		super(parent);
7999 	}
8000 
8001 	private int mw = int.max;
8002 
8003 	override int maxWidth() { return scaleWithDpi(mw); }
8004 }
8005 
8006 /// Stacks the widgets horizontally, taking all the available height for each child.
8007 class HorizontalLayout : Layout {
8008 	///
8009 	this(Widget parent) { super(parent); }
8010 
8011 	/++
8012 		Sets a max height for the layout so you don't have to subclass. The max height
8013 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
8014 
8015 		History:
8016 			Added November 29, 2021 (dub v10.5)
8017 	+/
8018 	this(int maxHeight, Widget parent) {
8019 		this.mh = maxHeight;
8020 		super(parent);
8021 	}
8022 
8023 	private int mh = 0;
8024 
8025 
8026 
8027 	override void recomputeChildLayout() {
8028 		.recomputeChildLayout!"width"(this);
8029 	}
8030 
8031 	override int minHeight() {
8032 		int largest = 0;
8033 		int margins = 0;
8034 		int lastMargin = 0;
8035 		foreach(child; children) {
8036 			auto mh = child.minHeight();
8037 			if(mh > largest)
8038 				largest = mh;
8039 			margins += mymax(lastMargin, child.marginTop());
8040 			lastMargin = child.marginBottom();
8041 		}
8042 		return largest + margins;
8043 	}
8044 
8045 	override int maxHeight() {
8046 		if(mh != 0)
8047 			return mymax(minHeight, scaleWithDpi(mh));
8048 
8049 		int largest = 0;
8050 		int margins = 0;
8051 		int lastMargin = 0;
8052 		foreach(child; children) {
8053 			auto mh = child.maxHeight();
8054 			if(mh == int.max)
8055 				return int.max;
8056 			if(mh > largest)
8057 				largest = mh;
8058 			margins += mymax(lastMargin, child.marginTop());
8059 			lastMargin = child.marginBottom();
8060 		}
8061 		return largest + margins;
8062 	}
8063 
8064 	override int heightStretchiness() {
8065 		int max;
8066 		foreach(child; children) {
8067 			auto c = child.heightStretchiness;
8068 			if(c > max)
8069 				max = c;
8070 		}
8071 		return max;
8072 	}
8073 }
8074 
8075 version(win32_widgets)
8076 private
8077 extern(Windows)
8078 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow {
8079 	Widget* pwin = hwnd in Widget.nativeMapping;
8080 	if(pwin is null)
8081 		return DefWindowProc(hwnd, message, wparam, lparam);
8082 	SimpleWindow win = pwin.simpleWindowWrappingHwnd;
8083 	if(win is null)
8084 		return DefWindowProc(hwnd, message, wparam, lparam);
8085 
8086 	switch(message) {
8087 		case WM_SIZE:
8088 			auto width = LOWORD(lparam);
8089 			auto height = HIWORD(lparam);
8090 
8091 			auto hdc = GetDC(hwnd);
8092 			auto hdcBmp = CreateCompatibleDC(hdc);
8093 
8094 			// FIXME: could this be more efficient? it never relinquishes a large bitmap
8095 			if(width > win.bmpWidth || height > win.bmpHeight) {
8096 				auto oldBuffer = win.buffer;
8097 				win.buffer = CreateCompatibleBitmap(hdc, width, height);
8098 
8099 				if(oldBuffer)
8100 					DeleteObject(oldBuffer);
8101 
8102 				win.bmpWidth = width;
8103 				win.bmpHeight = height;
8104 			}
8105 
8106 			// just always erase it upon resizing so minigui can draw over with a clean slate
8107 			auto oldBmp = SelectObject(hdcBmp, win.buffer);
8108 
8109 			auto brush = GetSysColorBrush(COLOR_3DFACE);
8110 			RECT r;
8111 			r.left = 0;
8112 			r.top = 0;
8113 			r.right = width;
8114 			r.bottom = height;
8115 			FillRect(hdcBmp, &r, brush);
8116 
8117 			SelectObject(hdcBmp, oldBmp);
8118 			DeleteDC(hdcBmp);
8119 			ReleaseDC(hwnd, hdc);
8120 		break;
8121 		case WM_PAINT:
8122 			if(win.buffer is null)
8123 				goto default;
8124 
8125 			BITMAP bm;
8126 			PAINTSTRUCT ps;
8127 
8128 			HDC hdc = BeginPaint(hwnd, &ps);
8129 
8130 			HDC hdcMem = CreateCompatibleDC(hdc);
8131 			HBITMAP hbmOld = SelectObject(hdcMem, win.buffer);
8132 
8133 			GetObject(win.buffer, bm.sizeof, &bm);
8134 
8135 			BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
8136 
8137 			SelectObject(hdcMem, hbmOld);
8138 			DeleteDC(hdcMem);
8139 			EndPaint(hwnd, &ps);
8140 		break;
8141 		default:
8142 			return DefWindowProc(hwnd, message, wparam, lparam);
8143 	}
8144 
8145 	return 0;
8146 }
8147 
8148 private wstring Win32Class(wstring name)() {
8149 	static bool classRegistered;
8150 	if(!classRegistered) {
8151 		HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null);
8152 		WNDCLASSEX wc;
8153 		wc.cbSize = wc.sizeof;
8154 		wc.hInstance = hInstance;
8155 		wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH);
8156 		wc.lpfnWndProc = &DoubleBufferWndProc;
8157 		wc.lpszClassName = name.ptr;
8158 		if(!RegisterClassExW(&wc))
8159 			throw new Exception("RegisterClass ");// ~ to!string(GetLastError()));
8160 		classRegistered = true;
8161 	}
8162 
8163 		return name;
8164 }
8165 
8166 /+
8167 version(win32_widgets)
8168 extern(Windows)
8169 private
8170 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
8171 	switch(iMessage) {
8172 		case WM_PAINT:
8173 			if(auto te = hWnd in Widget.nativeMapping) {
8174 				try {
8175 					//te.redraw();
8176 					writeln(te, " drawing");
8177 				} catch(Exception) {}
8178 			}
8179 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
8180 		default:
8181 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
8182 	}
8183 }
8184 +/
8185 
8186 
8187 /++
8188 	A widget specifically designed to hold other widgets.
8189 
8190 	History:
8191 		Added July 1, 2021
8192 +/
8193 class ContainerWidget : Widget {
8194 	this(Widget parent) {
8195 		super(parent);
8196 		this.tabStop = false;
8197 
8198 		version(win32_widgets) {
8199 			createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0);
8200 		}
8201 	}
8202 }
8203 
8204 /++
8205 	A widget that takes your widget, puts scroll bars around it, and sends
8206 	messages to it when the user scrolls. Unlike [ScrollableWidget], it makes
8207 	no effort to automatically scroll or clip its child widgets - it just sends
8208 	the messages.
8209 
8210 
8211 	A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed.
8212 	The scroll coordinates are all given in a unit you interpret as you wish. One
8213 	of these units is moved on each press of the arrow buttons and represents the
8214 	smallest amount the user can scroll. The intention is for this to be one line,
8215 	one item in a list, one row in a table, etc. Whatever makes sense for your widget
8216 	in each direction that the user might be interested in.
8217 
8218 	You can set a "page size" with the [step] property. (Yes, I regret the name...)
8219 	This is the amount it jumps when the user pressed page up and page down, or clicks
8220 	in the exposed part of the scroll bar.
8221 
8222 	You should add child content to the ScrollMessageWidget. However, it is important to
8223 	note that the coordinates are always independent of the scroll position! It is YOUR
8224 	responsibility to do any necessary transforms, clipping, etc., while drawing the
8225 	content and interpreting mouse events if they are supposed to change with the scroll.
8226 	This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries
8227 	to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives
8228 	you more control (which can be considerably more efficient and adapted to your actual data)
8229 	at the expense of you also needing to be aware of its reality.
8230 
8231 	Please note that it does NOT react to mouse wheel events or various keyboard events as of
8232 	version 10.3. Maybe this will change in the future.... but for now you must call
8233 	[addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself.
8234 +/
8235 class ScrollMessageWidget : Widget {
8236 	this(Widget parent) {
8237 		super(parent);
8238 
8239 		container = new Widget(this);
8240 		hsb = new HorizontalScrollbar(this);
8241 		vsb = new VerticalScrollbar(this);
8242 
8243 		hsb.addEventListener("scrolltonextline", {
8244 			hsb.setPosition(hsb.position + movementPerButtonClickH_);
8245 			notify();
8246 		});
8247 		hsb.addEventListener("scrolltopreviousline", {
8248 			hsb.setPosition(hsb.position - movementPerButtonClickH_);
8249 			notify();
8250 		});
8251 		vsb.addEventListener("scrolltonextline", {
8252 			vsb.setPosition(vsb.position + movementPerButtonClickV_);
8253 			notify();
8254 		});
8255 		vsb.addEventListener("scrolltopreviousline", {
8256 			vsb.setPosition(vsb.position - movementPerButtonClickV_);
8257 			notify();
8258 		});
8259 		hsb.addEventListener("scrolltonextpage", {
8260 			hsb.setPosition(hsb.position + hsb.step_);
8261 			notify();
8262 		});
8263 		hsb.addEventListener("scrolltopreviouspage", {
8264 			hsb.setPosition(hsb.position - hsb.step_);
8265 			notify();
8266 		});
8267 		vsb.addEventListener("scrolltonextpage", {
8268 			vsb.setPosition(vsb.position + vsb.step_);
8269 			notify();
8270 		});
8271 		vsb.addEventListener("scrolltopreviouspage", {
8272 			vsb.setPosition(vsb.position - vsb.step_);
8273 			notify();
8274 		});
8275 		hsb.addEventListener("scrolltoposition", (Event event) {
8276 			hsb.setPosition(event.intValue);
8277 			notify();
8278 		});
8279 		vsb.addEventListener("scrolltoposition", (Event event) {
8280 			vsb.setPosition(event.intValue);
8281 			notify();
8282 		});
8283 
8284 
8285 		tabStop = false;
8286 		container.tabStop = false;
8287 		magic = true;
8288 	}
8289 
8290 	private int movementPerButtonClickH_ = 1;
8291 	private int movementPerButtonClickV_ = 1;
8292 	public void movementPerButtonClick(int h, int v) {
8293 		movementPerButtonClickH_ = h;
8294 		movementPerButtonClickV_ = v;
8295 	}
8296 
8297 	/++
8298 		Add default event listeners for keyboard and mouse wheel scrolling shortcuts.
8299 
8300 
8301 		The defaults for [addDefaultWheelListeners] are:
8302 
8303 			$(LIST
8304 				* Mouse wheel scrolls vertically
8305 				* Alt key + mouse wheel scrolls horiontally
8306 				* Shift + mouse wheel scrolls faster.
8307 				* Any mouse click or wheel event will focus the inner widget if it has `tabStop = true`
8308 			)
8309 
8310 		The defaults for [addDefaultKeyboardListeners] are:
8311 
8312 			$(LIST
8313 				* Arrow keys scroll by the given amounts
8314 				* Shift+arrow keys scroll by the given amounts times the given shiftMultiplier
8315 				* Page up and down scroll by the vertical viewable area
8316 				* Home and end scroll to the start and end of the verticle viewable area.
8317 				* Alt + page up / page down / home / end will horizonally scroll instead of vertical.
8318 			)
8319 
8320 		My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line.
8321 
8322 		Params:
8323 			horizontalArrowScrollAmount =
8324 			verticalArrowScrollAmount =
8325 			verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel
8326 			horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel
8327 			shiftMultiplier = multiplies the scroll amount by this when shift is held
8328 	+/
8329 	void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) {
8330 		defaultKeyboardListener_verticalArrowScrollAmount = verticalArrowScrollAmount;
8331 		defaultKeyboardListener_horizontalArrowScrollAmount = horizontalArrowScrollAmount;
8332 		defaultKeyboardListener_shiftMultiplier = shiftMultiplier;
8333 
8334 		container.addEventListener(&defaultKeyboardListener);
8335 	}
8336 
8337 	/// ditto
8338 	void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) {
8339 		auto _this = this;
8340 		container.addEventListener((scope ClickEvent ce) {
8341 
8342 			//if(ce.target && ce.target.tabStop)
8343 				//ce.target.focus();
8344 
8345 			// ctrl is reserved for the application
8346 			if(ce.ctrlKey)
8347 				return;
8348 
8349 			if(horizontalWheelScrollAmount == 0 && ce.altKey)
8350 				return;
8351 
8352 			if(shiftMultiplier == 0 && ce.shiftKey)
8353 				return;
8354 
8355 			if(ce.button == MouseButton.wheelDown) {
8356 				if(ce.altKey)
8357 					_this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8358 				else
8359 					_this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8360 			} else if(ce.button == MouseButton.wheelUp) {
8361 				if(ce.altKey)
8362 					_this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8363 				else
8364 					_this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8365 			}
8366 		});
8367 	}
8368 
8369 	int defaultKeyboardListener_verticalArrowScrollAmount = 1;
8370 	int defaultKeyboardListener_horizontalArrowScrollAmount = 1;
8371 	int defaultKeyboardListener_shiftMultiplier = 3;
8372 
8373 	void defaultKeyboardListener(scope KeyDownEvent ke) {
8374 		switch(ke.key) {
8375 			case Key.Left:
8376 				this.scrollLeft(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8377 			break;
8378 			case Key.Right:
8379 				this.scrollRight(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8380 			break;
8381 			case Key.Up:
8382 				this.scrollUp(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8383 			break;
8384 			case Key.Down:
8385 				this.scrollDown(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8386 			break;
8387 			case Key.PageUp:
8388 				if(ke.altKey)
8389 					this.scrollLeft(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8390 				else
8391 					this.scrollUp(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8392 			break;
8393 			case Key.PageDown:
8394 				if(ke.altKey)
8395 					this.scrollRight(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8396 				else
8397 					this.scrollDown(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8398 			break;
8399 			case Key.Home:
8400 				if(ke.altKey)
8401 					this.scrollLeft(short.max * 16);
8402 				else
8403 					this.scrollUp(short.max * 16);
8404 			break;
8405 			case Key.End:
8406 				if(ke.altKey)
8407 					this.scrollRight(short.max * 16);
8408 				else
8409 					this.scrollDown(short.max * 16);
8410 			break;
8411 
8412 			default:
8413 				// ignore, not for us.
8414 		}
8415 	}
8416 
8417 	/++
8418 		Scrolls the given amount.
8419 
8420 		History:
8421 			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.
8422 	+/
8423 	void scrollUp(int amount = 1) {
8424 		vsb.setPosition(vsb.position.NonOverflowingInt - amount);
8425 		notify();
8426 	}
8427 	/// ditto
8428 	void scrollDown(int amount = 1) {
8429 		vsb.setPosition(vsb.position.NonOverflowingInt + amount);
8430 		notify();
8431 	}
8432 	/// ditto
8433 	void scrollLeft(int amount = 1) {
8434 		hsb.setPosition(hsb.position.NonOverflowingInt - amount);
8435 		notify();
8436 	}
8437 	/// ditto
8438 	void scrollRight(int amount = 1) {
8439 		hsb.setPosition(hsb.position.NonOverflowingInt + amount);
8440 		notify();
8441 	}
8442 
8443 	///
8444 	VerticalScrollbar verticalScrollBar() { return vsb; }
8445 	///
8446 	HorizontalScrollbar horizontalScrollBar() { return hsb; }
8447 
8448 	void notify() {
8449 		static bool insideNotify;
8450 
8451 		if(insideNotify)
8452 			return; // avoid the recursive call, even if it isn't strictly correct
8453 
8454 		insideNotify = true;
8455 		scope(exit) insideNotify = false;
8456 
8457 		this.emit!ScrollEvent();
8458 	}
8459 
8460 	mixin Emits!ScrollEvent;
8461 
8462 	///
8463 	Point position() {
8464 		return Point(hsb.position, vsb.position);
8465 	}
8466 
8467 	///
8468 	void setPosition(int x, int y) {
8469 		hsb.setPosition(x);
8470 		vsb.setPosition(y);
8471 	}
8472 
8473 	///
8474 	void setPageSize(int unitsX, int unitsY) {
8475 		hsb.setStep(unitsX);
8476 		vsb.setStep(unitsY);
8477 	}
8478 
8479 	/// Always call this BEFORE setViewableArea
8480 	void setTotalArea(int width, int height) {
8481 		hsb.setMax(width);
8482 		vsb.setMax(height);
8483 	}
8484 
8485 	/++
8486 		Always set the viewable area AFTER setitng the total area if you are going to change both.
8487 		NEVER call this from inside a scroll event. This includes through recomputeChildLayout.
8488 		If you need to do that, use [queueRecomputeChildLayout].
8489 	+/
8490 	void setViewableArea(int width, int height) {
8491 
8492 		// actually there IS A need to dothis cuz the max might have changed since then
8493 		//if(width == hsb.viewableArea_ && height == vsb.viewableArea_)
8494 			//return; // no need to do what is already done
8495 		hsb.setViewableArea(width);
8496 		vsb.setViewableArea(height);
8497 
8498 		bool needsNotify = false;
8499 
8500 		// FIXME: if at any point the rhs is outside the scrollbar, we need
8501 		// to reset to 0. but it should remember the old position in case the
8502 		// window resizes again, so it can kinda return ot where it was.
8503 		//
8504 		// so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0)
8505 		if(width >= hsb.max) {
8506 			// there's plenty of room to display it all so we need to reset to zero
8507 			// FIXME: adjust so it matches the note above
8508 			hsb.setPosition(0);
8509 			needsNotify = true;
8510 		}
8511 		if(height >= vsb.max) {
8512 			// there's plenty of room to display it all so we need to reset to zero
8513 			// FIXME: adjust so it matches the note above
8514 			vsb.setPosition(0);
8515 			needsNotify = true;
8516 		}
8517 		if(needsNotify)
8518 			notify();
8519 	}
8520 
8521 	private bool magic;
8522 	override void addChild(Widget w, int position = int.max) {
8523 		if(magic)
8524 			container.addChild(w, position);
8525 		else
8526 			super.addChild(w, position);
8527 	}
8528 
8529 	override void recomputeChildLayout() {
8530 		if(hsb is null || vsb is null || container is null) return;
8531 
8532 		registerMovement();
8533 
8534 		enum BUTTON_SIZE = 16;
8535 
8536 		hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane?
8537 		hsb.x = 0;
8538 		hsb.y = this.height - hsb.height;
8539 
8540 		vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME?
8541 		vsb.x = this.width - vsb.width;
8542 		vsb.y = 0;
8543 
8544 		auto vsb_width = vsb.showing ? vsb.width : 0;
8545 		auto hsb_height = hsb.showing ? hsb.height : 0;
8546 
8547 		hsb.width = this.width - vsb_width;
8548 		vsb.height = this.height - hsb_height;
8549 
8550 		hsb.recomputeChildLayout();
8551 		vsb.recomputeChildLayout();
8552 
8553 		if(this.header is null) {
8554 			container.x = 0;
8555 			container.y = 0;
8556 			container.width = this.width - vsb_width;
8557 			container.height = this.height - hsb_height;
8558 			container.recomputeChildLayout();
8559 		} else {
8560 			header.x = 0;
8561 			header.y = 0;
8562 			header.width = this.width - vsb_width;
8563 			header.height = scaleWithDpi(BUTTON_SIZE); // size of the button
8564 			header.recomputeChildLayout();
8565 
8566 			container.x = 0;
8567 			container.y = scaleWithDpi(BUTTON_SIZE);
8568 			container.width = this.width - vsb_width;
8569 			container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE);
8570 			container.recomputeChildLayout();
8571 		}
8572 	}
8573 
8574 	private HorizontalScrollbar hsb;
8575 	private VerticalScrollbar vsb;
8576 	Widget container;
8577 	private Widget header;
8578 
8579 	/++
8580 		Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button.
8581 
8582 		History:
8583 			Added September 27, 2021 (dub v10.3)
8584 	+/
8585 	Widget getHeader() {
8586 		if(this.header is null) {
8587 			magic = false;
8588 			scope(exit) magic = true;
8589 			this.header = new Widget(this);
8590 			queueRecomputeChildLayout();
8591 		}
8592 		return this.header;
8593 	}
8594 
8595 	/++
8596 		Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary.
8597 
8598 		History:
8599 			Added January 3, 2023 (dub v11.0)
8600 	+/
8601 	void scrollIntoView(Rectangle rect) {
8602 		Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_));
8603 
8604 		// import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1)));
8605 
8606 		// the lower right is exclusive normally
8607 		auto test = rect.lowerRight;
8608 		if(test.x > 0) test.x--;
8609 		if(test.y > 0) test.y--;
8610 
8611 		if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) {
8612 			// try to scroll only one dimension at a time if we can
8613 			if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y)))
8614 				setPosition(rect.upperLeft.x, position.y);
8615 			if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y)))
8616 				setPosition(position.x, rect.upperLeft.y);
8617 		}
8618 
8619 	}
8620 
8621 	override int minHeight() {
8622 		int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0));
8623 		if(header !is null)
8624 			min += header.minHeight;
8625 		if(horizontalScrollBar.showing)
8626 			min += horizontalScrollBar.minHeight;
8627 		return min;
8628 	}
8629 
8630 	override int maxHeight() {
8631 		int max = container ? container.maxHeight : int.max;
8632 		if(max == int.max)
8633 			return max;
8634 		if(horizontalScrollBar.showing)
8635 			max += horizontalScrollBar.minHeight;
8636 		return max;
8637 	}
8638 
8639 	static class Style : Widget.Style {
8640 		override WidgetBackground background() {
8641 			return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
8642 		}
8643 	}
8644 	mixin OverrideStyle!Style;
8645 }
8646 
8647 /++
8648 	$(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")
8649 	$(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.)
8650 +/
8651 version(minigui_screenshots)
8652 @Screenshot("ScrollMessageWidget")
8653 unittest {
8654 	auto window = new Window("ScrollMessageWidget");
8655 
8656 	auto smw = new ScrollMessageWidget(window);
8657 	smw.addDefaultKeyboardListeners();
8658 	smw.addDefaultWheelListeners();
8659 
8660 	window.loop();
8661 }
8662 
8663 /++
8664 	Bypasses automatic layout for its children, using manual positioning and sizing only.
8665 	While you need to manually position them, you must ensure they are inside the StaticLayout's
8666 	bounding box to avoid undefined behavior.
8667 
8668 	You should almost never use this.
8669 +/
8670 class StaticLayout : Layout {
8671 	///
8672 	this(Widget parent) { super(parent); }
8673 	override void recomputeChildLayout() {
8674 		registerMovement();
8675 		foreach(child; children)
8676 			child.recomputeChildLayout();
8677 	}
8678 }
8679 
8680 /++
8681 	Bypasses automatic positioning when being laid out. It is your responsibility to make
8682 	room for this widget in the parent layout.
8683 
8684 	Its children are laid out normally, unless there is exactly one, in which case it takes
8685 	on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you
8686 	can do that with `padding`).
8687 +/
8688 class StaticPosition : Layout {
8689 	///
8690 	this(Widget parent) { super(parent); }
8691 
8692 	override void recomputeChildLayout() {
8693 		registerMovement();
8694 		if(this.children.length == 1) {
8695 			auto child = children[0];
8696 			child.x = 0;
8697 			child.y = 0;
8698 			child.width = this.width;
8699 			child.height = this.height;
8700 			child.recomputeChildLayout();
8701 		} else
8702 		foreach(child; children)
8703 			child.recomputeChildLayout();
8704 	}
8705 
8706 	alias width = typeof(super).width;
8707 	alias height = typeof(super).height;
8708 
8709 	@property int width(int w) @nogc pure @safe nothrow {
8710 		return this._width = w;
8711 	}
8712 
8713 	@property int height(int w) @nogc pure @safe nothrow {
8714 		return this._height = w;
8715 	}
8716 
8717 }
8718 
8719 /++
8720 	FixedPosition is like [StaticPosition], but its coordinates
8721 	are always relative to the viewport, meaning they do not scroll with
8722 	the parent content.
8723 +/
8724 class FixedPosition : StaticPosition {
8725 	///
8726 	this(Widget parent) { super(parent); }
8727 }
8728 
8729 version(win32_widgets)
8730 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) {
8731 	if(true) {
8732 		// cmd == 0 = menu, cmd == 1 = accelerator
8733 		if(auto item = idm in Action.mapping) {
8734 			foreach(handler; (*item).triggered)
8735 				handler();
8736 		/*
8737 			auto event = new Event("triggered", *item);
8738 			event.button = idm;
8739 			event.dispatch();
8740 		*/
8741 			return 0;
8742 		}
8743 	}
8744 	if(handle)
8745 	if(auto widgetp = handle in Widget.nativeMapping) {
8746 		(*widgetp).handleWmCommand(cmd, idm);
8747 		return 0;
8748 	}
8749 	return 1;
8750 }
8751 
8752 
8753 ///
8754 class Window : Widget {
8755 	Widget[] mouseCapturedBy;
8756 	void captureMouse(Widget byWhom) {
8757 		assert(byWhom !is null);
8758 		if(mouseCapturedBy.length > 0) {
8759 			auto cc = mouseCapturedBy[$-1];
8760 			if(cc is byWhom)
8761 				return; // or should it throw?
8762 			auto par = byWhom;
8763 			while(par) {
8764 				if(cc is par)
8765 					goto allowed;
8766 				par = par.parent;
8767 			}
8768 
8769 			throw new Exception("mouse is already captured by other widget");
8770 		}
8771 		allowed:
8772 		mouseCapturedBy ~= byWhom;
8773 		if(mouseCapturedBy.length == 1)
8774 			win.grabInput(false, true, false);
8775 		//void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) {
8776 	}
8777 	void releaseMouseCapture() {
8778 		if(mouseCapturedBy.length == 0)
8779 			return; // or should it throw?
8780 		mouseCapturedBy = mouseCapturedBy[0 .. $-1];
8781 		mouseCapturedBy.assumeSafeAppend();
8782 		if(mouseCapturedBy.length == 0)
8783 			win.releaseInputGrab();
8784 	}
8785 
8786 
8787 	/++
8788 
8789 	+/
8790 	MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
8791 		return .messageBox(this, title, message, style, icon);
8792 	}
8793 
8794 	/// ditto
8795 	int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
8796 		return messageBox(null, message, style, icon);
8797 	}
8798 
8799 
8800 	/++
8801 		Sets the window icon which is often seen in title bars and taskbars.
8802 
8803 		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.
8804 
8805 		History:
8806 			Added April 5, 2022 (dub v10.8)
8807 	+/
8808 	@property void icon(MemoryImage icon) {
8809 		if(win && icon)
8810 			win.icon = icon;
8811 	}
8812 
8813 	// 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
8814 	// this does NOT change the icon on the window! That's what the other overload is for
8815 	static @property .icon icon(GenericIcons i) {
8816 		return .icon(i);
8817 	}
8818 
8819 	///
8820 	@scriptable
8821 	@property bool focused() {
8822 		return win.focused;
8823 	}
8824 
8825 	static class Style : Widget.Style {
8826 		override WidgetBackground background() {
8827 			version(custom_widgets)
8828 				return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
8829 			else version(win32_widgets)
8830 				return WidgetBackground(Color.transparent);
8831 			else static assert(0);
8832 		}
8833 	}
8834 	mixin OverrideStyle!Style;
8835 
8836 	/++
8837 		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.
8838 	+/
8839 	deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() {
8840 		return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback();
8841 	}
8842 
8843 	private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() {
8844 		OperatingSystemFont font;
8845 		if(auto vt = WidgetPainter.visualTheme) {
8846 			font = vt.defaultFontCached(96); // FIXME
8847 		}
8848 
8849 		if(font is null) {
8850 			static int defaultHeightCache;
8851 			if(defaultHeightCache == 0) {
8852 				font = new OperatingSystemFont;
8853 				font.loadDefault;
8854 				defaultHeightCache = font.height();// * 5 / 4;
8855 			}
8856 			return defaultHeightCache;
8857 		}
8858 
8859 		return font.height();// * 5 / 4;
8860 	}
8861 
8862 	Widget focusedWidget;
8863 
8864 	private SimpleWindow win_;
8865 
8866 	@property {
8867 		/++
8868 			Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers.
8869 
8870 			History:
8871 				Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property.
8872 		+/
8873 		public SimpleWindow win() {
8874 			return win_;
8875 		}
8876 		///
8877 		protected void win(SimpleWindow w) {
8878 			win_ = w;
8879 		}
8880 	}
8881 
8882 	/// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing.
8883 	this(Widget p) {
8884 		tabStop = false;
8885 		super(p);
8886 	}
8887 
8888 	private void actualRedraw() {
8889 		if(recomputeChildLayoutRequired)
8890 			recomputeChildLayoutEntry();
8891 		if(!showing) return;
8892 
8893 		assert(parentWindow !is null);
8894 
8895 		auto w = drawableWindow;
8896 		if(w is null)
8897 			w = parentWindow.win;
8898 
8899 		if(w.closed())
8900 			return;
8901 
8902 		auto ugh = this.parent;
8903 		int lox, loy;
8904 		while(ugh) {
8905 			lox += ugh.x;
8906 			loy += ugh.y;
8907 			ugh = ugh.parent;
8908 		}
8909 		auto painter = w.draw(true);
8910 		privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw());
8911 	}
8912 
8913 
8914 	private bool skipNextChar = false;
8915 
8916 	/++
8917 		Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers.
8918 
8919 		This constructor is intended primarily for internal use and may be changed to `protected` later.
8920 	+/
8921 	this(SimpleWindow win) {
8922 
8923 		static if(UsingSimpledisplayX11) {
8924 			win.discardAdditionalConnectionState = &discardXConnectionState;
8925 			win.recreateAdditionalConnectionState = &recreateXConnectionState;
8926 		}
8927 
8928 		tabStop = false;
8929 		super(null);
8930 		this.win = win;
8931 
8932 		win.addEventListener((Widget.RedrawEvent) {
8933 			if(win.eventQueued!RecomputeEvent) {
8934 				// writeln("skipping");
8935 				return; // let the recompute event do the actual redraw
8936 			}
8937 			this.actualRedraw();
8938 		});
8939 
8940 		win.addEventListener((Widget.RecomputeEvent) {
8941 			recomputeChildLayoutEntry();
8942 			if(win.eventQueued!RedrawEvent)
8943 				return; // let the queued one do it
8944 			else {
8945 				// writeln("drawing");
8946 				this.actualRedraw(); // if not queued, it needs to be done now anyway
8947 			}
8948 		});
8949 
8950 		this.width = win.width;
8951 		this.height = win.height;
8952 		this.parentWindow = this;
8953 
8954 		win.closeQuery = () {
8955 			if(this.emit!ClosingEvent())
8956 				win.close();
8957 		};
8958 		win.onClosing = () {
8959 			this.emit!ClosedEvent();
8960 		};
8961 
8962 		win.windowResized = (int w, int h) {
8963 			this.width = w;
8964 			this.height = h;
8965 			queueRecomputeChildLayout();
8966 			// this causes a HUGE performance problem for no apparent benefit, hence the commenting
8967 			//version(win32_widgets)
8968 				//InvalidateRect(hwnd, null, true);
8969 			redraw();
8970 		};
8971 
8972 		win.onFocusChange = (bool getting) {
8973 			// sdpyPrintDebugString("onFocusChange ", getting, " ", this.toString);
8974 			if(this.focusedWidget) {
8975 				if(getting) {
8976 					this.focusedWidget.emit!FocusEvent();
8977 					this.focusedWidget.emit!FocusInEvent();
8978 				} else {
8979 					this.focusedWidget.emit!BlurEvent();
8980 					this.focusedWidget.emit!FocusOutEvent();
8981 				}
8982 			}
8983 
8984 			if(getting) {
8985 				this.emit!FocusEvent();
8986 				this.emit!FocusInEvent();
8987 			} else {
8988 				this.emit!BlurEvent();
8989 				this.emit!FocusOutEvent();
8990 			}
8991 		};
8992 
8993 		win.onDpiChanged = {
8994 			this.queueRecomputeChildLayout();
8995 			auto event = new DpiChangedEvent(this);
8996 			event.sendDirectly();
8997 
8998 			privateDpiChanged();
8999 		};
9000 
9001 		win.setEventHandlers(
9002 			(MouseEvent e) {
9003 				dispatchMouseEvent(e);
9004 			},
9005 			(KeyEvent e) {
9006 				//writefln("%x   %s", cast(uint) e.key, e.key);
9007 				dispatchKeyEvent(e);
9008 			},
9009 			(dchar e) {
9010 				if(e == 13) e = 10; // hack?
9011 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
9012 				dispatchCharEvent(e);
9013 			},
9014 		);
9015 
9016 		addEventListener("char", (Widget, Event ev) {
9017 			if(skipNextChar) {
9018 				ev.preventDefault();
9019 				skipNextChar = false;
9020 			}
9021 		});
9022 
9023 		version(win32_widgets)
9024 		win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
9025 			if(hwnd !is this.win.impl.hwnd)
9026 				return 1; // we don't care... pass it on
9027 			auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn);
9028 			if(mustReturn)
9029 				return ret;
9030 			return 1; // pass it on
9031 		};
9032 
9033 		if(Window.newWindowCreated)
9034 			Window.newWindowCreated(this);
9035 	}
9036 
9037 	version(custom_widgets)
9038 	override void defaultEventHandler_click(ClickEvent event) {
9039 		if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) {
9040 			if(event.target && event.target.tabStop)
9041 				event.target.focus();
9042 		}
9043 	}
9044 
9045 	private static void delegate(Window) newWindowCreated;
9046 
9047 	version(win32_widgets)
9048 	override void paint(WidgetPainter painter) {
9049 		/*
9050 		RECT rect;
9051 		rect.right = this.width;
9052 		rect.bottom = this.height;
9053 		DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null);
9054 		*/
9055 		// 3dface is used as window backgrounds by Windows too, so that's why I'm using it here
9056 		auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
9057 		auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
9058 		// since the pen is null, to fill the whole space, we need the +1 on both.
9059 		gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
9060 		SelectObject(painter.impl.hdc, p);
9061 		SelectObject(painter.impl.hdc, b);
9062 	}
9063 	version(custom_widgets)
9064 	override void paint(WidgetPainter painter) {
9065 		auto cs = getComputedStyle();
9066 		painter.fillColor = cs.windowBackgroundColor;
9067 		painter.outlineColor = cs.windowBackgroundColor;
9068 		painter.drawRectangle(Point(0, 0), this.width, this.height);
9069 	}
9070 
9071 
9072 	override void defaultEventHandler_keydown(KeyDownEvent event) {
9073 		Widget _this = event.target;
9074 
9075 		if(event.key == Key.Tab) {
9076 			/* Window tab ordering is a recursive thingy with each group */
9077 
9078 			// FIXME inefficient
9079 			Widget[] helper(Widget p) {
9080 				if(p.hidden)
9081 					return null;
9082 				Widget[] childOrdering;
9083 
9084 				auto children = p.children.dup;
9085 
9086 				while(true) {
9087 					// UIs should be generally small, so gonna brute force it a little
9088 					// note that it must be a stable sort here; if all are index 0, it should be in order of declaration
9089 
9090 					Widget smallestTab;
9091 					foreach(ref c; children) {
9092 						if(c is null) continue;
9093 						if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) {
9094 							smallestTab = c;
9095 							c = null;
9096 						}
9097 					}
9098 					if(smallestTab !is null) {
9099 						if(smallestTab.tabStop && !smallestTab.hidden)
9100 							childOrdering ~= smallestTab;
9101 						if(!smallestTab.hidden)
9102 							childOrdering ~= helper(smallestTab);
9103 					} else
9104 						break;
9105 
9106 				}
9107 
9108 				return childOrdering;
9109 			}
9110 
9111 			Widget[] tabOrdering = helper(this);
9112 
9113 			Widget recipient;
9114 
9115 			if(tabOrdering.length) {
9116 				bool seenThis = false;
9117 				Widget previous;
9118 				foreach(idx, child; tabOrdering) {
9119 					if(child is focusedWidget) {
9120 
9121 						if(event.shiftKey) {
9122 							if(idx == 0)
9123 								recipient = tabOrdering[$-1];
9124 							else
9125 								recipient = tabOrdering[idx - 1];
9126 							break;
9127 						}
9128 
9129 						seenThis = true;
9130 						if(idx + 1 == tabOrdering.length) {
9131 							// we're at the end, either move to the next group
9132 							// or start back over
9133 							recipient = tabOrdering[0];
9134 						}
9135 						continue;
9136 					}
9137 					if(seenThis) {
9138 						recipient = child;
9139 						break;
9140 					}
9141 					previous = child;
9142 				}
9143 			}
9144 
9145 			if(recipient !is null) {
9146 				//  writeln(typeid(recipient));
9147 				recipient.focus();
9148 
9149 				skipNextChar = true;
9150 			}
9151 		}
9152 
9153 		debug if(event.key == Key.F12) {
9154 			if(devTools) {
9155 				devTools.close();
9156 				devTools = null;
9157 			} else {
9158 				devTools = new DevToolWindow(this);
9159 				devTools.show();
9160 			}
9161 		}
9162 	}
9163 
9164 	debug DevToolWindow devTools;
9165 
9166 
9167 	/++
9168 		Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display.
9169 
9170 		History:
9171 			Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead.
9172 
9173 			The width and height arguments were added to the overload that takes `string` first on June 21, 2021.
9174 	+/
9175 	this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
9176 		if(title is null) {
9177 			import core.runtime;
9178 			if(Runtime.args.length)
9179 				title = Runtime.args[0];
9180 		}
9181 		win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent);
9182 
9183 		static if(UsingSimpledisplayX11)
9184 		if(windowFlags & WindowFlags.managesChildWindowFocus) {
9185 		///+
9186 		// for input proxy
9187 		auto display = XDisplayConnection.get;
9188 		auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0);
9189 		XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask);
9190 		XMapWindow(display, inputProxy);
9191 		// writefln("input proxy: 0x%0x", inputProxy);
9192 		this.inputProxy = new SimpleWindow(inputProxy);
9193 
9194 		/+
9195 		this.inputProxy.onFocusChange = (bool getting) {
9196 			sdpyPrintDebugString("input proxy focus change ", getting);
9197 		};
9198 		+/
9199 
9200 		XEvent lastEvent;
9201 		this.inputProxy.handleNativeEvent = (XEvent ev) {
9202 			lastEvent = ev;
9203 			return 1;
9204 		};
9205 		this.inputProxy.setEventHandlers(
9206 			(MouseEvent e) {
9207 				dispatchMouseEvent(e);
9208 			},
9209 			(KeyEvent e) {
9210 				//writefln("%x   %s", cast(uint) e.key, e.key);
9211 				if(dispatchKeyEvent(e)) {
9212 					// FIXME: i should trap error
9213 					if(auto nw = cast(NestedChildWindowWidget) focusedWidget) {
9214 						auto thing = nw.focusableWindow();
9215 						if(thing && thing.window) {
9216 							lastEvent.xkey.window = thing.window;
9217 							// writeln("sending event ", lastEvent.xkey);
9218 							trapXErrors( {
9219 								XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent);
9220 							});
9221 						}
9222 					}
9223 				}
9224 			},
9225 			(dchar e) {
9226 				if(e == 13) e = 10; // hack?
9227 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
9228 				dispatchCharEvent(e);
9229 			},
9230 		);
9231 
9232 		this.inputProxy.populateXic();
9233 		// done
9234 		//+/
9235 		}
9236 
9237 
9238 
9239 		win.setRequestedInputFocus = &this.setRequestedInputFocus;
9240 
9241 		this(win);
9242 	}
9243 
9244 	SimpleWindow inputProxy;
9245 
9246 	private SimpleWindow setRequestedInputFocus() {
9247 		return inputProxy;
9248 	}
9249 
9250 	/// ditto
9251 	this(string title, int width = 500, int height = 500) {
9252 		this(width, height, title);
9253 	}
9254 
9255 	///
9256 	@property string title() { return parentWindow.win.title; }
9257 	///
9258 	@property void title(string title) { parentWindow.win.title = title; }
9259 
9260 	///
9261 	@scriptable
9262 	void close() {
9263 		win.close();
9264 		// I synchronize here upon window closing to ensure all child windows
9265 		// get updated too before the event loop. This avoids some random X errors.
9266 		static if(UsingSimpledisplayX11) {
9267 			runInGuiThread( {
9268 				XSync(XDisplayConnection.get, false);
9269 			});
9270 		}
9271 	}
9272 
9273 	bool dispatchKeyEvent(KeyEvent ev) {
9274 		auto wid = focusedWidget;
9275 		if(wid is null)
9276 			wid = this;
9277 		KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid);
9278 		event.originalKeyEvent = ev;
9279 		event.key = ev.key;
9280 		event.state = ev.modifierState;
9281 		event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
9282 		event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
9283 		event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
9284 		event.dispatch();
9285 
9286 		return !event.propagationStopped;
9287 	}
9288 
9289 	// returns true if propagation should continue into nested things.... prolly not a great thing to do.
9290 	bool dispatchCharEvent(dchar ch) {
9291 		if(focusedWidget) {
9292 			auto event = new CharEvent(focusedWidget, ch);
9293 			event.dispatch();
9294 			return !event.propagationStopped;
9295 		}
9296 		return true;
9297 	}
9298 
9299 	Widget mouseLastOver;
9300 	Widget mouseLastDownOn;
9301 	bool lastWasDoubleClick;
9302 	bool dispatchMouseEvent(MouseEvent ev) {
9303 		auto eleR = widgetAtPoint(this, ev.x, ev.y);
9304 		auto ele = eleR.widget;
9305 
9306 		auto captureEle = ele;
9307 
9308 		auto mouseCapturedBy = this.mouseCapturedBy.length ? this.mouseCapturedBy[$-1] : null;
9309 		if(mouseCapturedBy !is null) {
9310 			if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele))
9311 				captureEle = mouseCapturedBy;
9312 		}
9313 
9314 		// a hack to get it relative to the widget.
9315 		eleR.x = ev.x;
9316 		eleR.y = ev.y;
9317 		auto pain = captureEle;
9318 		while(pain) {
9319 			eleR.x -= pain.x;
9320 			eleR.y -= pain.y;
9321 			pain.addScrollPosition(eleR.x, eleR.y);
9322 			pain = pain.parent;
9323 		}
9324 
9325 		void populateMouseEventBase(MouseEventBase event) {
9326 			event.button = ev.button;
9327 			event.buttonLinear = ev.buttonLinear;
9328 			event.state = ev.modifierState;
9329 			event.clientX = eleR.x;
9330 			event.clientY = eleR.y;
9331 
9332 			event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
9333 			event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
9334 			event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
9335 		}
9336 
9337 		if(ev.type == MouseEventType.buttonPressed) {
9338 			{
9339 				auto event = new MouseDownEvent(captureEle);
9340 				populateMouseEventBase(event);
9341 				event.dispatch();
9342 			}
9343 
9344 			if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) {
9345 				auto event = new DoubleClickEvent(captureEle);
9346 				populateMouseEventBase(event);
9347 				event.dispatch();
9348 				lastWasDoubleClick = ev.doubleClick;
9349 			} else {
9350 				lastWasDoubleClick = false;
9351 			}
9352 
9353 			mouseLastDownOn = ele;
9354 		} else if(ev.type == MouseEventType.buttonReleased) {
9355 			{
9356 				auto event = new MouseUpEvent(captureEle);
9357 				populateMouseEventBase(event);
9358 				event.dispatch();
9359 			}
9360 			if(!lastWasDoubleClick && mouseLastDownOn is ele) {
9361 				auto event = new ClickEvent(captureEle);
9362 				populateMouseEventBase(event);
9363 				event.dispatch();
9364 			}
9365 		} else if(ev.type == MouseEventType.motion) {
9366 			// motion
9367 			{
9368 				auto event = new MouseMoveEvent(captureEle);
9369 				populateMouseEventBase(event); // fills in button which is meaningless but meh
9370 				event.dispatch();
9371 			}
9372 
9373 			if(mouseLastOver !is ele) {
9374 				if(ele !is null) {
9375 					if(!isAParentOf(ele, mouseLastOver)) {
9376 						ele.setDynamicState(DynamicState.hover, true);
9377 						auto event = new MouseEnterEvent(ele);
9378 						event.relatedTarget = mouseLastOver;
9379 						event.sendDirectly();
9380 
9381 						ele.useStyleProperties((scope Widget.Style s) {
9382 							ele.parentWindow.win.cursor = s.cursor;
9383 						});
9384 					}
9385 				}
9386 
9387 				if(mouseLastOver !is null) {
9388 					if(!isAParentOf(mouseLastOver, ele)) {
9389 						mouseLastOver.setDynamicState(DynamicState.hover, false);
9390 						auto event = new MouseLeaveEvent(mouseLastOver);
9391 						event.relatedTarget = ele;
9392 						event.sendDirectly();
9393 					}
9394 				}
9395 
9396 				if(ele !is null) {
9397 					auto event = new MouseOverEvent(ele);
9398 					event.relatedTarget = mouseLastOver;
9399 					event.dispatch();
9400 				}
9401 
9402 				if(mouseLastOver !is null) {
9403 					auto event = new MouseOutEvent(mouseLastOver);
9404 					event.relatedTarget = ele;
9405 					event.dispatch();
9406 				}
9407 
9408 				mouseLastOver = ele;
9409 			}
9410 		}
9411 
9412 		return true; // FIXME: the event default prevented?
9413 	}
9414 
9415 	/++
9416 		Shows the window and runs the application event loop.
9417 
9418 		Blocks until this window is closed.
9419 
9420 		Bugs:
9421 
9422 		$(PITFALL
9423 			You should always have one event loop live for your application.
9424 			If you make two windows in sequence, the second call to loop (or
9425 			simpledisplay's [SimpleWindow.eventLoop], upon which this is built)
9426 			might fail:
9427 
9428 			---
9429 			// don't do this!
9430 			auto window = new Window();
9431 			window.loop();
9432 
9433 			// or new Window or new MainWindow, all the same
9434 			auto window2 = new SimpleWindow();
9435 			window2.eventLoop(0); // problematic! might crash
9436 			---
9437 
9438 			simpledisplay's current implementation assumes that final cleanup is
9439 			done when the event loop refcount reaches zero. So after the first
9440 			eventLoop returns, when there isn't already another one active, it assumes
9441 			the program will exit soon and cleans up.
9442 
9443 			This is arguably a bug that it doesn't reinitialize, and I'll probably change
9444 			it eventually, but in the mean time, there's an easy solution:
9445 
9446 			---
9447 			// do this
9448 			EventLoop mainEventLoop = EventLoop.get; // just add this line
9449 
9450 			auto window = new Window();
9451 			window.loop();
9452 
9453 			// or any other type of Window etc.
9454 			auto window2 = new Window();
9455 			window2.loop(); // perfectly fine since mainEventLoop still alive
9456 			---
9457 
9458 			By adding a top-level reference to the event loop, it ensures the final cleanup
9459 			is not performed until it goes out of scope too, letting the individual window loops
9460 			work without trouble despite the bug.
9461 		)
9462 
9463 		History:
9464 			The [BlockingMode] parameter was added on December 8, 2021.
9465 			The default behavior is to block until the application quits
9466 			(so all windows have been closed), unless another minigui or
9467 			simpledisplay event loop is already running, in which case it
9468 			will block until this window closes specifically.
9469 	+/
9470 	@scriptable
9471 	void loop(BlockingMode bm = BlockingMode.automatic) {
9472 		if(win.closed)
9473 			return; // otherwise show will throw
9474 		show();
9475 		win.eventLoopWithBlockingMode(bm, 0);
9476 	}
9477 
9478 	private bool firstShow = true;
9479 
9480 	@scriptable
9481 	override void show() {
9482 		bool rd = false;
9483 		if(firstShow) {
9484 			firstShow = false;
9485 			queueRecomputeChildLayout();
9486 			// unless the programmer already called focus on something, pick something ourselves
9487 			auto f = focusedWidget is null ? getFirstFocusable(this) : focusedWidget; // FIXME: autofocus?
9488 			if(f)
9489 				f.focus();
9490 			redraw();
9491 		}
9492 		win.show();
9493 		super.show();
9494 	}
9495 	@scriptable
9496 	override void hide() {
9497 		win.hide();
9498 		super.hide();
9499 	}
9500 
9501 	static Widget getFirstFocusable(Widget start) {
9502 		if(start is null)
9503 			return null;
9504 
9505 		foreach(widget; &start.focusableWidgets) {
9506 			return widget;
9507 		}
9508 
9509 		return null;
9510 	}
9511 
9512 	static Widget getLastFocusable(Widget start) {
9513 		if(start is null)
9514 			return null;
9515 
9516 		Widget last;
9517 		foreach(widget; &start.focusableWidgets) {
9518 			last = widget;
9519 		}
9520 
9521 		return last;
9522 	}
9523 
9524 
9525 	mixin Emits!ClosingEvent;
9526 	mixin Emits!ClosedEvent;
9527 }
9528 
9529 /++
9530 	History:
9531 		Added January 12, 2022
9532 
9533 		Made `final` on January 3, 2025
9534 +/
9535 final class DpiChangedEvent : Event {
9536 	enum EventString = "dpichanged";
9537 
9538 	this(Widget target) {
9539 		super(EventString, target);
9540 	}
9541 }
9542 
9543 debug private class DevToolWindow : Window {
9544 	Window p;
9545 
9546 	TextEdit parentList;
9547 	TextEdit logWindow;
9548 	TextLabel clickX, clickY;
9549 
9550 	this(Window p) {
9551 		this.p = p;
9552 		super(400, 300, "Developer Toolbox");
9553 
9554 		logWindow = new TextEdit(this);
9555 		parentList = new TextEdit(this);
9556 
9557 		auto hl = new HorizontalLayout(this);
9558 		clickX = new TextLabel("", TextAlignment.Right, hl);
9559 		clickY = new TextLabel("", TextAlignment.Right, hl);
9560 
9561 		parentListeners ~= p.addEventListener("*", (Event ev) {
9562 			log(typeid(ev.source).name, " emitted ", typeid(ev).name);
9563 		});
9564 
9565 		parentListeners ~= p.addEventListener((ClickEvent ev) {
9566 			auto s = ev.srcElement;
9567 
9568 			string list;
9569 
9570 			void addInfo(Widget s) {
9571 				list ~= s.toString();
9572 				list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight);
9573 				list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight);
9574 				list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness);
9575 				list ~= "\n\theight: " ~ toInternal!string(s.height);
9576 				list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth);
9577 				list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth);
9578 				list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness);
9579 				list ~= "\n\twidth: " ~ toInternal!string(s.width);
9580 				list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop);
9581 				list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom);
9582 			}
9583 
9584 			addInfo(s);
9585 
9586 			s = s.parent;
9587 			while(s) {
9588 				list ~= "\n";
9589 				addInfo(s);
9590 				s = s.parent;
9591 			}
9592 			parentList.content = list;
9593 
9594 			clickX.label = toInternal!string(ev.clientX);
9595 			clickY.label = toInternal!string(ev.clientY);
9596 		});
9597 	}
9598 
9599 	EventListener[] parentListeners;
9600 
9601 	override void close() {
9602 		assert(p !is null);
9603 		foreach(p; parentListeners)
9604 			p.disconnect();
9605 		parentListeners = null;
9606 		p.devTools = null;
9607 		p = null;
9608 		super.close();
9609 	}
9610 
9611 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
9612 		if(ev.key == Key.F12) {
9613 			this.close();
9614 			if(p)
9615 				p.devTools = null;
9616 		} else {
9617 			super.defaultEventHandler_keydown(ev);
9618 		}
9619 	}
9620 
9621 	void log(T...)(T t) {
9622 		string str;
9623 		import std.conv;
9624 		foreach(i; t)
9625 			str ~= to!string(i);
9626 		str ~= "\n";
9627 		logWindow.addText(str);
9628 		logWindow.scrollToBottom();
9629 
9630 		//version(custom_widgets)
9631 		//logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox());
9632 	}
9633 }
9634 
9635 /++
9636 	A dialog is a transient window that intends to get information from
9637 	the user before being dismissed.
9638 +/
9639 class Dialog : Window {
9640 	///
9641 	this(Window parent, int width, int height, string title = null) {
9642 		super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win);
9643 
9644 		// this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
9645 	}
9646 
9647 	///
9648 	this(Window parent, string title, int width, int height) {
9649 		this(parent, width, height, title);
9650 	}
9651 
9652 	deprecated("Pass an explicit parent window, even if it is `null`")
9653 	this(int width, int height, string title = null) {
9654 		this(null, width, height, title);
9655 	}
9656 
9657 	///
9658 	void OK() {
9659 
9660 	}
9661 
9662 	///
9663 	void Cancel() {
9664 		this.close();
9665 	}
9666 }
9667 
9668 /++
9669 	A custom widget similar to the HTML5 <details> tag.
9670 +/
9671 version(none)
9672 class DetailsView : Widget {
9673 
9674 }
9675 
9676 // FIXME: maybe i should expose the other list views Windows offers too
9677 
9678 /++
9679 	A TableView is a widget made to display a table of data strings.
9680 
9681 
9682 	Future_Directions:
9683 		Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers.
9684 
9685 		I will add a selection changed event at some point, as well as item clicked events.
9686 	History:
9687 		Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet.
9688 	See_Also:
9689 		[ListWidget] which displays a list of strings without additional columns.
9690 +/
9691 class TableView : Widget {
9692 	/++
9693 
9694 	+/
9695 	this(Widget parent) {
9696 		super(parent);
9697 
9698 		version(win32_widgets) {
9699 			createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED);
9700 		} else version(custom_widgets) {
9701 			auto smw = new ScrollMessageWidget(this);
9702 			smw.addDefaultKeyboardListeners();
9703 			smw.addDefaultWheelListeners(1, scaleWithDpi(16));
9704 			tvwi = new TableViewWidgetInner(this, smw);
9705 		}
9706 	}
9707 
9708 	// FIXME: auto-size columns on double click of header thing like in Windows
9709 	// it need only make the currently displayed things fit well.
9710 
9711 
9712 	private ColumnInfo[] columns;
9713 	private int itemCount;
9714 
9715 	version(custom_widgets) private {
9716 		TableViewWidgetInner tvwi;
9717 	}
9718 
9719 	/// Passed to [setColumnInfo]
9720 	static struct ColumnInfo {
9721 		const(char)[] name; /// the name displayed in the header
9722 		/++
9723 			The default width, in pixels. As a special case, you can set this to -1
9724 			if you want the system to try to automatically size the width to fit visible
9725 			content. If it can't, it will try to pick a sensible default size.
9726 
9727 			Any other negative value is not allowed and may lead to unpredictable results.
9728 
9729 			History:
9730 				The -1 behavior was specified on December 3, 2021. It actually worked before
9731 				anyway on Win32 but now it is a formal feature with partial Linux support.
9732 
9733 			Bugs:
9734 				It doesn't actually attempt to calculate a best-fit width on Linux as of
9735 				December 3, 2021. I do plan to fix this in the future, but Windows is the
9736 				priority right now. At least it doesn't break things when you use it now.
9737 		+/
9738 		int width;
9739 
9740 		/++
9741 			Alignment of the text in the cell. Applies to the header as well as all data in this
9742 			column.
9743 
9744 			Bugs:
9745 				On Windows, the first column ignores this member and is always left aligned.
9746 				You can work around this by inserting a dummy first column with width = 0
9747 				then putting your actual data in the second column, which does respect the
9748 				alignment.
9749 
9750 				This is a quirk of the operating system's implementation going back a very
9751 				long time and is unlikely to ever be fixed.
9752 		+/
9753 		TextAlignment alignment;
9754 
9755 		/++
9756 			After all the pixel widths have been assigned, any left over
9757 			space is divided up among all columns and distributed to according
9758 			to the widthPercent field.
9759 
9760 
9761 			For example, if you have two fields, both with width 50 and one with
9762 			widthPercent of 25 and the other with widthPercent of 75, and the
9763 			container is 200 pixels wide, first both get their width of 50.
9764 			then the 100 remaining pixels are split up, so the one gets a total
9765 			of 75 pixels and the other gets a total of 125.
9766 
9767 			This is automatically applied as the window is resized.
9768 
9769 			If there is not enough space - that is, when a horizontal scrollbar
9770 			needs to appear - there are 0 pixels divided up, and thus everyone
9771 			gets 0. This can cause a column to shrink out of proportion when
9772 			passing the scroll threshold.
9773 
9774 			It is important to still set a fixed width (that is, to populate the
9775 			`width` field) even if you use the percents because that will be the
9776 			default minimum in the event of a scroll bar appearing.
9777 
9778 			The percents total in the column can never exceed 100 or be less than 0.
9779 			Doing this will trigger an assert error.
9780 
9781 			Implementation note:
9782 
9783 			Please note that percentages are only recalculated 1) upon original
9784 			construction and 2) upon resizing the control. If the user adjusts the
9785 			width of a column, the percentage items will not be updated.
9786 
9787 			On the other hand, if the user adjusts the width of a percentage column
9788 			then resizes the window, it is recalculated, meaning their hand adjustment
9789 			is discarded. This specific behavior may change in the future as it is
9790 			arguably a bug, but I'm not certain yet.
9791 
9792 			History:
9793 				Added November 10, 2021 (dub v10.4)
9794 		+/
9795 		int widthPercent;
9796 
9797 
9798 		private int calculatedWidth;
9799 	}
9800 	/++
9801 		Sets the number of columns along with information about the headers.
9802 
9803 		Please note: on Windows, the first column ignores your alignment preference
9804 		and is always left aligned.
9805 	+/
9806 	void setColumnInfo(ColumnInfo[] columns...) {
9807 
9808 		foreach(ref c; columns) {
9809 			c.name = c.name.idup;
9810 		}
9811 		this.columns = columns.dup;
9812 
9813 		updateCalculatedWidth(false);
9814 
9815 		version(custom_widgets) {
9816 			tvwi.header.updateHeaders();
9817 			tvwi.updateScrolls();
9818 		} else version(win32_widgets)
9819 		foreach(i, column; this.columns) {
9820 			LVCOLUMN lvColumn;
9821 			lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
9822 			lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth;
9823 
9824 			auto bfr = WCharzBuffer(column.name);
9825 			lvColumn.pszText = bfr.ptr;
9826 
9827 			if(column.alignment & TextAlignment.Center)
9828 				lvColumn.fmt = LVCFMT_CENTER;
9829 			else if(column.alignment & TextAlignment.Right)
9830 				lvColumn.fmt = LVCFMT_RIGHT;
9831 			else
9832 				lvColumn.fmt = LVCFMT_LEFT;
9833 
9834 			if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1)
9835 				throw new WindowsApiException("Insert Column Fail", GetLastError());
9836 		}
9837 	}
9838 
9839 	private int getActualSetSize(size_t i, bool askWindows) {
9840 		version(win32_widgets)
9841 			if(askWindows)
9842 				return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0);
9843 		auto w = columns[i].width;
9844 		if(w == -1)
9845 			return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME
9846 		return w;
9847 	}
9848 
9849 	private void updateCalculatedWidth(bool informWindows) {
9850 		int padding;
9851 		version(win32_widgets)
9852 			padding = 4;
9853 		int remaining = this.width;
9854 		foreach(i, column; columns)
9855 			remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding;
9856 		remaining -= padding;
9857 		if(remaining < 0)
9858 			remaining = 0;
9859 
9860 		int percentTotal;
9861 		foreach(i, ref column; columns) {
9862 			percentTotal += column.widthPercent;
9863 
9864 			auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100;
9865 
9866 			column.calculatedWidth = c;
9867 
9868 			version(win32_widgets)
9869 			if(informWindows)
9870 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
9871 		}
9872 
9873 		assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100.");
9874 		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).");
9875 
9876 
9877 	}
9878 
9879 	override void registerMovement() {
9880 		super.registerMovement();
9881 
9882 		updateCalculatedWidth(true);
9883 	}
9884 
9885 	/++
9886 		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.
9887 	+/
9888 	void setItemCount(int count) {
9889 		this.itemCount = count;
9890 		version(custom_widgets) {
9891 			tvwi.updateScrolls();
9892 			redraw();
9893 		} else version(win32_widgets) {
9894 			SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0);
9895 		}
9896 	}
9897 
9898 	/++
9899 		Clears all items;
9900 	+/
9901 	void clear() {
9902 		this.itemCount = 0;
9903 		this.columns = null;
9904 		version(custom_widgets) {
9905 			tvwi.header.updateHeaders();
9906 			tvwi.updateScrolls();
9907 			redraw();
9908 		} else version(win32_widgets) {
9909 			SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0);
9910 		}
9911 	}
9912 
9913 	/+
9914 	version(win32_widgets)
9915 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis)
9916 		auto itemId = dis.itemID;
9917 		auto hdc = dis.hDC;
9918 		auto rect = dis.rcItem;
9919 		switch(dis.itemAction) {
9920 			case ODA_DRAWENTIRE:
9921 
9922 				// FIXME: do other items
9923 				// FIXME: do the focus rectangle i guess
9924 				// FIXME: alignment
9925 				// FIXME: column width
9926 				// FIXME: padding left
9927 				// FIXME: check dpi scaling
9928 				// FIXME: don't owner draw unless it is necessary.
9929 
9930 				auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi
9931 				RECT itemRect;
9932 				itemRect.top = 1; // subitem idx, 1-based
9933 				itemRect.left = LVIR_BOUNDS;
9934 
9935 				SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect);
9936 				itemRect.left += padding;
9937 
9938 				getData(itemId, 0, (in char[] data) {
9939 					auto wdata = WCharzBuffer(data);
9940 					DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS);
9941 
9942 				});
9943 			goto case;
9944 			case ODA_FOCUS:
9945 				if(dis.itemState & ODS_FOCUS)
9946 					DrawFocusRect(hdc, &rect);
9947 			break;
9948 			case ODA_SELECT:
9949 				// itemState & ODS_SELECTED
9950 			break;
9951 			default:
9952 		}
9953 		return 1;
9954 	}
9955 	+/
9956 
9957 	version(win32_widgets) {
9958 		CellStyle last;
9959 		COLORREF defaultColor;
9960 		COLORREF defaultBackground;
9961 	}
9962 
9963 	version(win32_widgets)
9964 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
9965 		switch(code) {
9966 			case NM_CUSTOMDRAW:
9967 				auto s = cast(NMLVCUSTOMDRAW*) hdr;
9968 				switch(s.nmcd.dwDrawStage) {
9969 					case CDDS_PREPAINT:
9970 						if(getCellStyle is null)
9971 							return 0;
9972 
9973 						mustReturn = true;
9974 						return CDRF_NOTIFYITEMDRAW;
9975 					case CDDS_ITEMPREPAINT:
9976 						mustReturn = true;
9977 						return CDRF_NOTIFYSUBITEMDRAW;
9978 					case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
9979 						mustReturn = true;
9980 
9981 						if(getCellStyle is null) // this SHOULD never happen...
9982 							return 0;
9983 
9984 						if(s.iSubItem == 0) {
9985 							// Windows resets it per row so we'll use item 0 as a chance
9986 							// to capture these for later
9987 							defaultColor = s.clrText;
9988 							defaultBackground = s.clrTextBk;
9989 						}
9990 
9991 						auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem);
9992 						// if no special style and no reset needed...
9993 						if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init))
9994 							return 0; // allow default processing to continue
9995 
9996 						last = style;
9997 
9998 						// might still need to reset or use the preference.
9999 
10000 						if(style.flags & CellStyle.Flags.textColorSet)
10001 							s.clrText = style.textColor.asWindowsColorRef;
10002 						else
10003 							s.clrText = defaultColor; // reset in case it was set from last iteration not a fan
10004 						if(style.flags & CellStyle.Flags.backgroundColorSet)
10005 							s.clrTextBk = style.backgroundColor.asWindowsColorRef;
10006 						else
10007 							s.clrTextBk = defaultBackground; // need to reset it... not a fan of this
10008 
10009 						return CDRF_NEWFONT;
10010 					default:
10011 						return 0;
10012 
10013 				}
10014 			case NM_RETURN: // no need since i subclass keydown
10015 			break;
10016 			case LVN_COLUMNCLICK:
10017 				auto info = cast(LPNMLISTVIEW) hdr;
10018 				this.emit!HeaderClickedEvent(info.iSubItem);
10019 			break;
10020 			case NM_CLICK:
10021 			case NM_DBLCLK:
10022 			case NM_RCLICK:
10023 			case NM_RDBLCLK:
10024 				// the item/subitem is set here and that can be a useful notification
10025 				// even beyond the normal click notification
10026 			break;
10027 			case LVN_GETDISPINFO:
10028 				LV_DISPINFO* info = cast(LV_DISPINFO*) hdr;
10029 				if(info.item.mask & LVIF_TEXT) {
10030 					if(getData) {
10031 						getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) {
10032 							auto bfr = WCharzBuffer(dataReceived);
10033 							auto len = info.item.cchTextMax;
10034 							if(bfr.length < len)
10035 								len = cast(typeof(len)) bfr.length;
10036 							info.item.pszText[0 .. len] = bfr.ptr[0 .. len];
10037 							info.item.pszText[len] = 0;
10038 						});
10039 					} else {
10040 						info.item.pszText[0] = 0;
10041 					}
10042 					//info.item.iItem
10043 					//if(info.item.iSubItem)
10044 				}
10045 			break;
10046 			default:
10047 		}
10048 		return 0;
10049 	}
10050 
10051 	override bool encapsulatedChildren() {
10052 		return true;
10053 	}
10054 
10055 	/++
10056 		Informs the control that content has changed.
10057 
10058 		History:
10059 			Added November 10, 2021 (dub v10.4)
10060 	+/
10061 	void update() {
10062 		version(custom_widgets)
10063 			redraw();
10064 		else {
10065 			SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0));
10066 			UpdateWindow(hwnd);
10067 		}
10068 
10069 
10070 	}
10071 
10072 	/++
10073 		Called by the system to request the text content of an individual cell. You
10074 		should pass the text into the provided `sink` delegate. This function will be
10075 		called for each visible cell as-needed when drawing.
10076 	+/
10077 	void delegate(int row, int column, scope void delegate(in char[]) sink) getData;
10078 
10079 	/++
10080 		Available per-cell style customization options. Use one of the constructors
10081 		provided to set the values conveniently, or default construct it and set individual
10082 		values yourself. Just remember to set the `flags` so your values are actually used.
10083 		If the flag isn't set, the field is ignored and the system default is used instead.
10084 
10085 		This is returned by the [getCellStyle] delegate.
10086 
10087 		Examples:
10088 			---
10089 			// assumes you have a variables called `my_data` which is an array of arrays of numbers
10090 			auto table = new TableView(window);
10091 			// snip: you would set up columns here
10092 
10093 			// this is how you provide data to the table view class
10094 			table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) {
10095 				import std.conv;
10096 				sink(to!string(my_data[row][column]));
10097 			};
10098 
10099 			// and this is how you customize the colors
10100 			table.getCellStyle = delegate(int row, int column) {
10101 				return (my_data[row][column] < 0) ?
10102 					TableView.CellStyle(Color.red); // make negative numbers red
10103 					: TableView.CellStyle.init; // leave the rest alone
10104 			};
10105 			// snip: you would call table.setItemCount here then continue with the rest of your window setup work
10106 			---
10107 
10108 		History:
10109 			Added November 27, 2021 (dub v10.4)
10110 	+/
10111 	struct CellStyle {
10112 		/// 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.
10113 		this(Color textColor) {
10114 			this.textColor = textColor;
10115 			this.flags |= Flags.textColorSet;
10116 		}
10117 		/// Sets a custom text and background color.
10118 		this(Color textColor, Color backgroundColor) {
10119 			this.textColor = textColor;
10120 			this.backgroundColor = backgroundColor;
10121 			this.flags |= Flags.textColorSet | Flags.backgroundColorSet;
10122 		}
10123 
10124 		Color textColor;
10125 		Color backgroundColor;
10126 		int flags; /// bitmask of [Flags]
10127 		/// available options to combine into [flags]
10128 		enum Flags {
10129 			textColorSet = 1 << 0,
10130 			backgroundColorSet = 1 << 1,
10131 		}
10132 	}
10133 	/++
10134 		Companion delegate to [getData] that allows you to custom style each
10135 		cell of the table.
10136 
10137 		Returns:
10138 			A [CellStyle] structure that describes the desired style for the
10139 			given cell. `return CellStyle.init` if you want the default style.
10140 
10141 		History:
10142 			Added November 27, 2021 (dub v10.4)
10143 	+/
10144 	CellStyle delegate(int row, int column) getCellStyle;
10145 
10146 	// i want to be able to do things like draw little colored things to show red for negative numbers
10147 	// or background color indicators or even in-cell charts
10148 	// void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell;
10149 
10150 	/++
10151 		When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked.
10152 	+/
10153 	mixin Emits!HeaderClickedEvent;
10154 }
10155 
10156 /++
10157 	This is emitted by the [TableView] when a user clicks on a column header.
10158 
10159 	Its member `columnIndex` has the zero-based index of the column that was clicked.
10160 
10161 	The default behavior of this event is to do nothing, so `preventDefault` has no effect.
10162 
10163 	History:
10164 		Added November 27, 2021 (dub v10.4)
10165 
10166 		Made `final` on January 3, 2025
10167 +/
10168 final class HeaderClickedEvent : Event {
10169 	enum EventString = "HeaderClicked";
10170 	this(Widget target, int columnIndex) {
10171 		this.columnIndex = columnIndex;
10172 		super(EventString, target);
10173 	}
10174 
10175 	/// The index of the column
10176 	int columnIndex;
10177 
10178 	///
10179 	override @property int intValue() {
10180 		return columnIndex;
10181 	}
10182 }
10183 
10184 version(custom_widgets)
10185 private class TableViewWidgetInner : Widget {
10186 
10187 // wrap this thing in a ScrollMessageWidget
10188 
10189 	TableView tvw;
10190 	ScrollMessageWidget smw;
10191 	HeaderWidget header;
10192 
10193 	this(TableView tvw, ScrollMessageWidget smw) {
10194 		this.tvw = tvw;
10195 		this.smw = smw;
10196 		super(smw);
10197 
10198 		this.tabStop = true;
10199 
10200 		header = new HeaderWidget(this, smw.getHeader());
10201 
10202 		smw.addEventListener("scroll", () {
10203 			this.redraw();
10204 			header.redraw();
10205 		});
10206 
10207 
10208 		// I need headers outside the scroll area but rendered on the same line as the up arrow
10209 		// FIXME: add a fixed header to the SMW
10210 	}
10211 
10212 	enum padding = 3;
10213 
10214 	void updateScrolls() {
10215 		int w;
10216 		foreach(idx, column; tvw.columns) {
10217 			if(column.width == 0) continue;
10218 			w += tvw.getActualSetSize(idx, false);// + padding;
10219 		}
10220 		smw.setTotalArea(w, tvw.itemCount);
10221 		columnsWidth = w;
10222 	}
10223 
10224 	private int columnsWidth;
10225 
10226 	private int lh() { return scaleWithDpi(16); } // FIXME lineHeight
10227 
10228 	override void registerMovement() {
10229 		super.registerMovement();
10230 		// FIXME: actual column width. it might need to be done per-pixel instead of per-column
10231 		smw.setViewableArea(this.width, this.height / lh);
10232 	}
10233 
10234 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
10235 		int x;
10236 		int y;
10237 
10238 		int row = smw.position.y;
10239 
10240 		foreach(lol; 0 .. this.height / lh) {
10241 			if(row >= tvw.itemCount)
10242 				break;
10243 			x = 0;
10244 			foreach(columnNumber, column; tvw.columns) {
10245 				auto x2 = x + column.calculatedWidth;
10246 				auto smwx = smw.position.x;
10247 
10248 				if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) {
10249 					auto startX = x;
10250 					auto endX = x + column.calculatedWidth;
10251 					switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) {
10252 						case TextAlignment.Left: startX += padding; break;
10253 						case TextAlignment.Center: startX += padding; endX -= padding; break;
10254 						case TextAlignment.Right: endX -= padding; break;
10255 						default: /* broken */ break;
10256 					}
10257 					if(column.width != 0) // no point drawing an invisible column
10258 					tvw.getData(row, cast(int) columnNumber, (in char[] info) {
10259 						auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endX - smw.position.x, y + lh)));
10260 
10261 						void dotext(WidgetPainter painter) {
10262 							painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment);
10263 						}
10264 
10265 						if(tvw.getCellStyle !is null) {
10266 							auto style = tvw.getCellStyle(row, cast(int) columnNumber);
10267 
10268 							if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) {
10269 								auto tempPainter = painter;
10270 								tempPainter.fillColor = style.backgroundColor;
10271 								tempPainter.outlineColor = style.backgroundColor;
10272 
10273 								tempPainter.drawRectangle(Point(startX - smw.position.x, y),
10274 									Point(endX - smw.position.x, y + lh));
10275 							}
10276 							auto tempPainter = painter;
10277 							if(style.flags & TableView.CellStyle.Flags.textColorSet)
10278 								tempPainter.outlineColor = style.textColor;
10279 
10280 							dotext(tempPainter);
10281 						} else {
10282 							dotext(painter);
10283 						}
10284 					});
10285 				}
10286 
10287 				x += column.calculatedWidth;
10288 			}
10289 			row++;
10290 			y += lh;
10291 		}
10292 		return bounds;
10293 	}
10294 
10295 	static class Style : Widget.Style {
10296 		override WidgetBackground background() {
10297 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
10298 		}
10299 	}
10300 	mixin OverrideStyle!Style;
10301 
10302 	private static class HeaderWidget : Widget {
10303 		/+
10304 			maybe i should do a splitter thing on top of the other widgets
10305 			so the splitter itself isn't really drawn but still replies to mouse events?
10306 		+/
10307 		this(TableViewWidgetInner tvw, Widget parent) {
10308 			super(parent);
10309 			this.tvw = tvw;
10310 
10311 			this.remainder = new Button("", this);
10312 
10313 			this.addEventListener((scope ClickEvent ev) {
10314 				int header = -1;
10315 				foreach(idx, child; this.children[1 .. $]) {
10316 					if(child is ev.target) {
10317 						header = cast(int) idx;
10318 						break;
10319 					}
10320 				}
10321 
10322 				if(header != -1) {
10323 					auto hce = new HeaderClickedEvent(tvw.tvw, header);
10324 					hce.dispatch();
10325 				}
10326 
10327 			});
10328 		}
10329 
10330 		void updateHeaders() {
10331 			foreach(child; children[1 .. $])
10332 				child.removeWidget();
10333 
10334 			foreach(column; tvw.tvw.columns) {
10335 				// the cast is ok because I dup it above, just the type is never changed.
10336 				// all this is private so it should never get messed up.
10337 				new Button(ImageLabel(cast(string) column.name, column.alignment), this);
10338 			}
10339 		}
10340 
10341 		Button remainder;
10342 		TableViewWidgetInner tvw;
10343 
10344 		override void recomputeChildLayout() {
10345 			registerMovement();
10346 			int pos;
10347 			foreach(idx, child; children[1 .. $]) {
10348 				if(idx >= tvw.tvw.columns.length)
10349 					continue;
10350 				child.x = pos;
10351 				child.y = 0;
10352 				child.width = tvw.tvw.columns[idx].calculatedWidth;
10353 				child.height = scaleWithDpi(16);// this.height;
10354 				pos += child.width;
10355 
10356 				child.recomputeChildLayout();
10357 			}
10358 
10359 			if(remainder is null)
10360 				return;
10361 
10362 			remainder.x = pos;
10363 			remainder.y = 0;
10364 			if(pos < this.width)
10365 				remainder.width = this.width - pos;// + 4;
10366 			else
10367 				remainder.width = 0;
10368 			remainder.height = scaleWithDpi(16);
10369 
10370 			remainder.recomputeChildLayout();
10371 		}
10372 
10373 		// for the scrollable children mixin
10374 		Point scrollOrigin() {
10375 			return Point(tvw.smw.position.x, 0);
10376 		}
10377 		void paintFrameAndBackground(WidgetPainter painter) { }
10378 
10379 		mixin ScrollableChildren;
10380 	}
10381 }
10382 
10383 /+
10384 
10385 // given struct / array / number / string / etc, make it viewable and editable
10386 class DataViewerWidget : Widget {
10387 
10388 }
10389 +/
10390 
10391 /++
10392 	A line edit box with an associated label.
10393 
10394 	History:
10395 		On May 17, 2021, the default internal layout was changed from horizontal to vertical.
10396 
10397 		```
10398 		Old: ________
10399 
10400 		New:
10401 		____________
10402 		```
10403 
10404 		To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);`
10405 
10406 		You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a
10407 		horizontal label but left aligned. You may also consider a [GridLayout].
10408 +/
10409 alias LabeledLineEdit = Labeled!LineEdit;
10410 
10411 private int widthThatWouldFitChildLabels(Widget w) {
10412 	if(w is null)
10413 		return 0;
10414 
10415 	int max;
10416 
10417 	if(auto label = cast(TextLabel) w) {
10418 		return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight();
10419 	} else {
10420 		foreach(child; w.children) {
10421 			max = mymax(max, widthThatWouldFitChildLabels(child));
10422 		}
10423 	}
10424 
10425 	return max;
10426 }
10427 
10428 /++
10429 	History:
10430 		Added May 19, 2021
10431 +/
10432 class Labeled(T) : Widget {
10433 	///
10434 	this(string label, Widget parent) {
10435 		super(parent);
10436 		initialize!VerticalLayout(label, TextAlignment.Left, parent);
10437 	}
10438 
10439 	/++
10440 		History:
10441 			The alignment parameter was added May 17, 2021
10442 	+/
10443 	this(string label, TextAlignment alignment, Widget parent) {
10444 		super(parent);
10445 		initialize!HorizontalLayout(label, alignment, parent);
10446 	}
10447 
10448 	private void initialize(L)(string label, TextAlignment alignment, Widget parent) {
10449 		tabStop = false;
10450 		horizontal = is(L == HorizontalLayout);
10451 		auto hl = new L(this);
10452 		if(horizontal) {
10453 			static class SpecialTextLabel : TextLabel {
10454 				Widget outerParent;
10455 
10456 				this(string label, TextAlignment alignment, Widget outerParent, Widget parent) {
10457 					this.outerParent = outerParent;
10458 					super(label, alignment, parent);
10459 				}
10460 
10461 				override int flexBasisWidth() {
10462 					return widthThatWouldFitChildLabels(outerParent);
10463 				}
10464 				/+
10465 				override int widthShrinkiness() { return 0; }
10466 				override int widthStretchiness() { return 1; }
10467 				+/
10468 
10469 				override int paddingRight() { return 6; }
10470 				override int paddingLeft() { return 9; }
10471 
10472 				override int paddingTop() { return 3; }
10473 			}
10474 			this.label = new SpecialTextLabel(label, alignment, parent, hl);
10475 		} else
10476 			this.label = new TextLabel(label, alignment, hl);
10477 		this.lineEdit = new T(hl);
10478 
10479 		this.label.labelFor = this.lineEdit;
10480 	}
10481 
10482 	private bool horizontal;
10483 
10484 	TextLabel label; ///
10485 	T lineEdit; ///
10486 
10487 	override int flexBasisWidth() { return 250; }
10488 	override int widthShrinkiness() { return 1; }
10489 
10490 	override int minHeight() {
10491 		return this.children[0].minHeight;
10492 	}
10493 	override int maxHeight() { return minHeight(); }
10494 	override int marginTop() { return 4; }
10495 	override int marginBottom() { return 4; }
10496 
10497 	// FIXME: i should prolly call it value as well as content tbh
10498 
10499 	///
10500 	@property string content() {
10501 		return lineEdit.content;
10502 	}
10503 	///
10504 	@property void content(string c) {
10505 		return lineEdit.content(c);
10506 	}
10507 
10508 	///
10509 	void selectAll() {
10510 		lineEdit.selectAll();
10511 	}
10512 
10513 	override void focus() {
10514 		lineEdit.focus();
10515 	}
10516 }
10517 
10518 /++
10519 	A labeled password edit.
10520 
10521 	History:
10522 		Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021
10523 
10524 		The default parameters for the constructors were also removed on May 19, 2021
10525 +/
10526 alias LabeledPasswordEdit = Labeled!PasswordEdit;
10527 
10528 private string toMenuLabel(string s) {
10529 	string n;
10530 	n.reserve(s.length);
10531 	foreach(c; s)
10532 		if(c == '_')
10533 			n ~= ' ';
10534 		else
10535 			n ~= c;
10536 	return n;
10537 }
10538 
10539 private void autoExceptionHandler(Exception e) {
10540 	messageBox(e.msg);
10541 }
10542 
10543 void callAsIfClickedFromMenu(alias fn)(auto ref __traits(parent, fn) _this, Window window) {
10544 	makeAutomaticHandler!(fn)(window, &__traits(child, _this, fn))();
10545 }
10546 
10547 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) {
10548 	static if(is(T : void delegate())) {
10549 		return () {
10550 			try
10551 				t();
10552 			catch(Exception e)
10553 				autoExceptionHandler(e);
10554 		};
10555 	} else static if(is(typeof(fn) Params == __parameters)) {
10556 		static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) {
10557 			return () {
10558 				void onOK(string s) {
10559 					member = s;
10560 					try
10561 						t(Params[0](s));
10562 					catch(Exception e)
10563 						autoExceptionHandler(e);
10564 				}
10565 
10566 				if(
10567 					(type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export")))
10568 					|| type == FileDialogType.Save)
10569 				{
10570 					getSaveFileName(window, &onOK, member, filters, null);
10571 				} else
10572 					getOpenFileName(window, &onOK, member, filters, null);
10573 			};
10574 		} else {
10575 			struct S {
10576 				static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) {
10577 					pragma(msg, "warning: automatic handler of params not yet implemented on your compiler");
10578 				} else mixin(q{
10579 				static foreach(idx, ignore; Params) {
10580 					mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";");
10581 				}
10582 				});
10583 			}
10584 			return () {
10585 				dialog(window, (S s) {
10586 					try {
10587 						static if(is(typeof(t) Ret == return)) {
10588 							static if(is(Ret == void)) {
10589 								t(s.tupleof);
10590 							} else {
10591 								auto ret = t(s.tupleof);
10592 								import std.conv;
10593 								messageBox(to!string(ret), "Returned Value");
10594 							}
10595 						}
10596 					} catch(Exception e)
10597 						autoExceptionHandler(e);
10598 				}, null, __traits(identifier, fn));
10599 			};
10600 		}
10601 	}
10602 }
10603 
10604 private template hasAnyRelevantAnnotations(a...) {
10605 	bool helper() {
10606 		bool any;
10607 		foreach(attr; a) {
10608 			static if(is(typeof(attr) == .menu))
10609 				any = true;
10610 			else static if(is(typeof(attr) == .toolbar))
10611 				any = true;
10612 			else static if(is(attr == .separator))
10613 				any = true;
10614 			else static if(is(typeof(attr) == .accelerator))
10615 				any = true;
10616 			else static if(is(typeof(attr) == .hotkey))
10617 				any = true;
10618 			else static if(is(typeof(attr) == .icon))
10619 				any = true;
10620 			else static if(is(typeof(attr) == .label))
10621 				any = true;
10622 			else static if(is(typeof(attr) == .tip))
10623 				any = true;
10624 		}
10625 		return any;
10626 	}
10627 
10628 	enum bool hasAnyRelevantAnnotations = helper();
10629 }
10630 
10631 /++
10632 	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.
10633 +/
10634 class MainWindow : Window {
10635 	///
10636 	this(string title = null, int initialWidth = 500, int initialHeight = 500) {
10637 		super(initialWidth, initialHeight, title);
10638 
10639 		_clientArea = new ClientAreaWidget();
10640 		_clientArea.x = 0;
10641 		_clientArea.y = 0;
10642 		_clientArea.width = this.width;
10643 		_clientArea.height = this.height;
10644 		_clientArea.tabStop = false;
10645 
10646 		super.addChild(_clientArea);
10647 
10648 		statusBar = new StatusBar(this);
10649 	}
10650 
10651 	/++
10652 		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).
10653 
10654 		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')`.
10655 
10656 		You can also use `@separator` to put a separating line in the menu before the function.
10657 
10658 		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.
10659 
10660 		Let's look at a complete example:
10661 
10662 	---
10663 	import arsd.minigui;
10664 
10665 	void main() {
10666 		auto window = new MainWindow();
10667 
10668 		// we can add widgets before or after setting the menu, either way is fine.
10669 		// i'll do it before here so the local variables are available to the commands.
10670 
10671 		auto textEdit = new TextEdit(window);
10672 
10673 		// Remember, in D, you can define structs inside of functions
10674 		// and those structs can access the function's local variables.
10675 		//
10676 		// Of course, you might also want to do this separately, and if you
10677 		// do, make sure you keep a reference to the window as a struct data
10678 		// member so you can refer to it in cases like this Exit function.
10679 		struct Commands {
10680 			// the & in the string indicates that the next letter is the hotkey
10681 			// to access it from the keyboard (so here, alt+f will open the
10682 			// file menu)
10683 			@menu("&File") {
10684 				@accelerator("Ctrl+N")
10685 				@hotkey('n')
10686 				@icon(GenericIcons.New) // add an icon to the action
10687 				@toolbar("File") // adds it to a toolbar.
10688 				// The toolbar name is never visible to the user, but is used to group icons.
10689 				void New() {
10690 					previousFileReferenced = null;
10691 					textEdit.content = "";
10692 				}
10693 
10694 				@icon(GenericIcons.Open)
10695 				@toolbar("File")
10696 				@hotkey('s')
10697 				@accelerator("Ctrl+O")
10698 				void Open(FileName!() filename) {
10699 					import std.file;
10700 					textEdit.content = std.file.readText(filename);
10701 				}
10702 
10703 				@icon(GenericIcons.Save)
10704 				@toolbar("File")
10705 				@accelerator("Ctrl+S")
10706 				@hotkey('s')
10707 				void Save() {
10708 					// these are still functions, so of course you can
10709 					// still call them yourself too
10710 					Save_As(previousFileReferenced);
10711 				}
10712 
10713 				// underscores translate to spaces in the visible name
10714 				@hotkey('a')
10715 				void Save_As(FileName!() filename) {
10716 					import std.file;
10717 					std.file.write(previousFileReferenced, textEdit.content);
10718 				}
10719 
10720 				// you can put the annotations before or after the function name+args and it works the same way
10721 				@separator
10722 				void Exit() @accelerator("Alt+F4") @hotkey('x') {
10723 					window.close();
10724 				}
10725 			}
10726 
10727 			@menu("&Edit") {
10728 				// not putting accelerators here because the text edit widget
10729 				// does it locally, so no need to duplicate it globally.
10730 
10731 				@icon(GenericIcons.Undo)
10732 				void Undo() @toolbar("Undo") {
10733 					textEdit.undo();
10734 				}
10735 
10736 				@separator
10737 
10738 				@icon(GenericIcons.Cut)
10739 				void Cut() @toolbar("Edit") {
10740 					textEdit.cut();
10741 				}
10742 				@icon(GenericIcons.Copy)
10743 				void Copy() @toolbar("Edit") {
10744 					textEdit.copy();
10745 				}
10746 				@icon(GenericIcons.Paste)
10747 				void Paste() @toolbar("Edit") {
10748 					textEdit.paste();
10749 				}
10750 
10751 				@separator
10752 				void Select_All() {
10753 					textEdit.selectAll();
10754 				}
10755 			}
10756 
10757 			@menu("Help") {
10758 				void About() @accelerator("F1") {
10759 					window.messageBox("A minigui sample program.");
10760 				}
10761 
10762 				// @label changes the name in the menu from what is in the code
10763 				@label("In Menu Name")
10764 				void otherNameInCode() {}
10765 			}
10766 		}
10767 
10768 		// declare the object that holds the commands, and set
10769 		// and members you want from it
10770 		Commands commands;
10771 
10772 		// and now tell minigui to do its magic and create the ui for it!
10773 		window.setMenuAndToolbarFromAnnotatedCode(commands);
10774 
10775 		// then, loop the window normally;
10776 		window.loop();
10777 
10778 		// important to note that the `commands` variable must live through the window's whole life cycle,
10779 		// or you can have crashes. If you declare the variable and loop in different functions, make sure
10780 		// you do `new Commands` so the garbage collector can take over management of it for you.
10781 	}
10782 	---
10783 
10784 	Note that you can call this function multiple times and it will add the items in order to the given items.
10785 
10786 	+/
10787 	void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) {
10788 		setMenuAndToolbarFromAnnotatedCode_internal(t);
10789 	}
10790 	/// ditto
10791 	void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) {
10792 		setMenuAndToolbarFromAnnotatedCode_internal(t);
10793 	}
10794 	void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
10795 		auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
10796 		Menu[string] mcs;
10797 
10798 		alias ToolbarSection = ToolBar.ToolbarSection;
10799 		ToolbarSection[] toolbarSections;
10800 
10801 		foreach(menu; menuBar.subMenus) {
10802 			mcs[menu.label] = menu;
10803 		}
10804 
10805 		foreach(memberName; __traits(derivedMembers, T)) {
10806 			static if(memberName != "this")
10807 			static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
10808 				.menu menu;
10809 				.toolbar toolbar;
10810 				bool separator;
10811 				.accelerator accelerator;
10812 				.hotkey hotkey;
10813 				.icon icon;
10814 				string label;
10815 				string tip;
10816 				foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
10817 					static if(is(typeof(attr) == .menu))
10818 						menu = attr;
10819 					else static if(is(typeof(attr) == .toolbar))
10820 						toolbar = attr;
10821 					else static if(is(attr == .separator))
10822 						separator = true;
10823 					else static if(is(typeof(attr) == .accelerator))
10824 						accelerator = attr;
10825 					else static if(is(typeof(attr) == .hotkey))
10826 						hotkey = attr;
10827 					else static if(is(typeof(attr) == .icon))
10828 						icon = attr;
10829 					else static if(is(typeof(attr) == .label))
10830 						label = attr.label;
10831 					else static if(is(typeof(attr) == .tip))
10832 						tip = attr.tip;
10833 				}
10834 
10835 				if(menu !is .menu.init || toolbar !is .toolbar.init) {
10836 					ushort correctIcon = icon.id; // FIXME
10837 					if(label.length == 0)
10838 						label = memberName.toMenuLabel;
10839 
10840 					auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName));
10841 
10842 					auto action = new Action(label, correctIcon, handler);
10843 
10844 					if(accelerator.keyString.length) {
10845 						auto ke = KeyEvent.parse(accelerator.keyString);
10846 						action.accelerator = ke;
10847 						accelerators[ke.toStr] = handler;
10848 					}
10849 
10850 					if(toolbar !is .toolbar.init) {
10851 						bool found;
10852 						foreach(ref section; toolbarSections)
10853 							if(section.name == toolbar.groupName) {
10854 								section.actions ~= action;
10855 								found = true;
10856 								break;
10857 							}
10858 						if(!found) {
10859 							toolbarSections ~= ToolbarSection(toolbar.groupName, [action]);
10860 						}
10861 					}
10862 					if(menu !is .menu.init) {
10863 						Menu mc;
10864 						if(menu.name in mcs) {
10865 							mc = mcs[menu.name];
10866 						} else {
10867 							mc = new Menu(menu.name, this);
10868 							menuBar.addItem(mc);
10869 							mcs[menu.name] = mc;
10870 						}
10871 
10872 						if(separator)
10873 							mc.addSeparator();
10874 						auto mi = mc.addItem(new MenuItem(action));
10875 
10876 						if(hotkey !is .hotkey.init)
10877 							mi.hotkey = hotkey.ch;
10878 					}
10879 				}
10880 			}
10881 		}
10882 
10883 		this.menuBar = menuBar;
10884 
10885 		if(toolbarSections.length) {
10886 			auto tb = new ToolBar(toolbarSections, this);
10887 		}
10888 	}
10889 
10890 	void delegate()[string] accelerators;
10891 
10892 	override void defaultEventHandler_keydown(KeyDownEvent event) {
10893 		auto str = event.originalKeyEvent.toStr;
10894 		if(auto acl = str in accelerators)
10895 			(*acl)();
10896 
10897 		// Windows this this automatically so only on custom need we implement it
10898 		version(custom_widgets) {
10899 			if(event.altKey && this.menuBar) {
10900 				foreach(item; this.menuBar.items) {
10901 					if(item.hotkey == keyToLetterCharAssumingLotsOfThingsThatYouMightBetterNotAssume(event.key)) {
10902 						// FIXME this kinda sucks but meh just pretending to click on it to trigger other existing mediocre code
10903 						item.dynamicState = DynamicState.hover | DynamicState.depressed;
10904 						item.redraw();
10905 						auto e = new MouseDownEvent(item);
10906 						e.dispatch();
10907 						break;
10908 					}
10909 				}
10910 			}
10911 
10912 			if(event.key == Key.Menu) {
10913 				showContextMenu(-1, -1);
10914 			}
10915 		}
10916 
10917 		super.defaultEventHandler_keydown(event);
10918 	}
10919 
10920 	override void defaultEventHandler_mouseover(MouseOverEvent event) {
10921 		super.defaultEventHandler_mouseover(event);
10922 		if(this.statusBar !is null && event.target.statusTip.length)
10923 			this.statusBar.parts[0].content = event.target.statusTip;
10924 		else if(this.statusBar !is null && this.statusTip.length)
10925 			this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString();
10926 	}
10927 
10928 	override void addChild(Widget c, int position = int.max) {
10929 		if(auto tb = cast(ToolBar) c)
10930 			version(win32_widgets)
10931 				super.addChild(c, 0);
10932 			else version(custom_widgets)
10933 				super.addChild(c, menuBar ? 1 : 0);
10934 			else static assert(0);
10935 		else
10936 			clientArea.addChild(c, position);
10937 	}
10938 
10939 	ToolBar _toolBar;
10940 	///
10941 	ToolBar toolBar() { return _toolBar; }
10942 	///
10943 	ToolBar toolBar(ToolBar t) {
10944 		_toolBar = t;
10945 		foreach(child; this.children)
10946 			if(child is t)
10947 				return t;
10948 		version(win32_widgets)
10949 			super.addChild(t, 0);
10950 		else version(custom_widgets)
10951 			super.addChild(t, menuBar ? 1 : 0);
10952 		else static assert(0);
10953 		return t;
10954 	}
10955 
10956 	MenuBar _menu;
10957 	///
10958 	MenuBar menuBar() { return _menu; }
10959 	///
10960 	MenuBar menuBar(MenuBar m) {
10961 		if(m is _menu) {
10962 			version(custom_widgets)
10963 				queueRecomputeChildLayout();
10964 			return m;
10965 		}
10966 
10967 		if(_menu !is null) {
10968 			// make sure it is sanely removed
10969 			// FIXME
10970 		}
10971 
10972 		_menu = m;
10973 
10974 		version(win32_widgets) {
10975 			SetMenu(parentWindow.win.impl.hwnd, m.handle);
10976 		} else version(custom_widgets) {
10977 			super.addChild(m, 0);
10978 
10979 		//	clientArea.y = menu.height;
10980 		//	clientArea.height = this.height - menu.height;
10981 
10982 			queueRecomputeChildLayout();
10983 		} else static assert(false);
10984 
10985 		return _menu;
10986 	}
10987 	private Widget _clientArea;
10988 	///
10989 	@property Widget clientArea() { return _clientArea; }
10990 	protected @property void clientArea(Widget wid) {
10991 		_clientArea = wid;
10992 	}
10993 
10994 	private StatusBar _statusBar;
10995 	/++
10996 		Returns the window's [StatusBar]. Be warned it may be `null`.
10997 	+/
10998 	@property StatusBar statusBar() { return _statusBar; }
10999 	/// ditto
11000 	@property void statusBar(StatusBar bar) {
11001 		if(_statusBar !is null)
11002 			_statusBar.removeWidget();
11003 		_statusBar = bar;
11004 		if(bar !is null)
11005 			super.addChild(_statusBar);
11006 	}
11007 }
11008 
11009 /+
11010 	This is really an implementation detail of [MainWindow]
11011 +/
11012 private class ClientAreaWidget : Widget {
11013 	this() {
11014 		this.tabStop = false;
11015 		super(null);
11016 		//sa = new ScrollableWidget(this);
11017 	}
11018 	/*
11019 	ScrollableWidget sa;
11020 	override void addChild(Widget w, int position) {
11021 		if(sa is null)
11022 			super.addChild(w, position);
11023 		else {
11024 			sa.addChild(w, position);
11025 			sa.setContentSize(this.minWidth + 1, this.minHeight);
11026 			writeln(sa.contentWidth, "x", sa.contentHeight);
11027 		}
11028 	}
11029 	*/
11030 }
11031 
11032 /**
11033 	Toolbars are lists of buttons (typically icons) that appear under the menu.
11034 	Each button ought to correspond to a menu item, represented by [Action] objects.
11035 */
11036 class ToolBar : Widget {
11037 	version(win32_widgets) {
11038 		private int idealHeight;
11039 		override int minHeight() { return idealHeight; }
11040 		override int maxHeight() { return idealHeight; }
11041 	} else version(custom_widgets) {
11042 		override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; }
11043 		override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; }
11044 	} else static assert(false);
11045 	override int heightStretchiness() { return 0; }
11046 
11047 	static struct ToolbarSection {
11048 		string name;
11049 		Action[] actions;
11050 	}
11051 
11052 	version(win32_widgets) {
11053 		HIMAGELIST imageListSmall;
11054 		HIMAGELIST imageListLarge;
11055 	}
11056 
11057 	this(Widget parent) {
11058 		this(cast(ToolbarSection[]) null, parent);
11059 	}
11060 
11061 	version(win32_widgets)
11062 	void changeIconSize(bool useLarge) {
11063 		SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall));
11064 
11065 		/+
11066 		SIZE size;
11067 		import core.sys.windows.commctrl;
11068 		SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size);
11069 		idealHeight = size.cy + 4; // the plus 4 is a hack
11070 		+/
11071 
11072 		idealHeight = useLarge ? 34 : 26;
11073 
11074 		if(parent) {
11075 			parent.queueRecomputeChildLayout();
11076 			parent.redraw();
11077 		}
11078 
11079 		SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4));
11080 		SendMessageW(hwnd, TB_AUTOSIZE, 0, 0);
11081 	}
11082 
11083 	/++
11084 		History:
11085 			The `ToolbarSection` overload was added December 31, 2024
11086 	+/
11087 	this(Action[] actions, Widget parent) {
11088 		this([ToolbarSection(null, actions)], parent);
11089 	}
11090 
11091 	/// ditto
11092 	this(ToolbarSection[] sections, Widget parent) {
11093 		super(parent);
11094 
11095 		tabStop = false;
11096 
11097 		version(win32_widgets) {
11098 			// so i like how the flat thing looks on windows, but not on wine
11099 			// and eh, with windows visual styles enabled it looks cool anyway soooo gonna
11100 			// leave it commented
11101 			createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS);
11102 
11103 			SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/);
11104 
11105 			imageListSmall = ImageList_Create(
11106 				// width, height
11107 				16, 16,
11108 				ILC_COLOR16 | ILC_MASK,
11109 				16 /*numberOfButtons*/, 0);
11110 
11111 			imageListLarge = ImageList_Create(
11112 				// width, height
11113 				24, 24,
11114 				ILC_COLOR16 | ILC_MASK,
11115 				16 /*numberOfButtons*/, 0);
11116 
11117 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall);
11118 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL);
11119 
11120 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge);
11121 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL);
11122 
11123 			SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0);
11124 
11125 			TBBUTTON[] buttons;
11126 
11127 			// FIXME: I_IMAGENONE is if here is no icon
11128 			foreach(sidx, section; sections) {
11129 				if(sidx)
11130 					buttons ~= TBBUTTON(
11131 						scaleWithDpi(4),
11132 						0,
11133 						TBSTATE_ENABLED, // state
11134 						TBSTYLE_SEP | BTNS_SEP, // style
11135 						0, // reserved array, just zero it out
11136 						0, // dwData
11137 						-1
11138 					);
11139 
11140 				foreach(action; section.actions)
11141 					buttons ~= TBBUTTON(
11142 						MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0),
11143 						action.id,
11144 						TBSTATE_ENABLED, // state
11145 						0, // style
11146 						0, // reserved array, just zero it out
11147 						0, // dwData
11148 						cast(size_t) toWstringzInternal(action.label) // INT_PTR
11149 					);
11150 			}
11151 
11152 			SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0);
11153 			SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr);
11154 
11155 			/*
11156 			RECT rect;
11157 			GetWindowRect(hwnd, &rect);
11158 			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
11159 			*/
11160 
11161 			dpiChanged(); // to load the things calling changeIconSize the first time
11162 
11163 			assert(idealHeight);
11164 		} else version(custom_widgets) {
11165 			foreach(sidx, section; sections) {
11166 				if(sidx)
11167 					new HorizontalSpacer(4, this);
11168 				foreach(action; section.actions)
11169 					new ToolButton(action, this);
11170 			}
11171 		} else static assert(false);
11172 	}
11173 
11174 	override void recomputeChildLayout() {
11175 		.recomputeChildLayout!"width"(this);
11176 	}
11177 
11178 
11179 	version(win32_widgets)
11180 	override protected void dpiChanged() {
11181 		auto sz = scaleWithDpi(16);
11182 		if(sz >= 20)
11183 			changeIconSize(true);
11184 		else
11185 			changeIconSize(false);
11186 	}
11187 }
11188 
11189 /// 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.
11190 class ToolButton : Button {
11191 	///
11192 	this(Action action, Widget parent) {
11193 		super(action.label, parent);
11194 		tabStop = false;
11195 		this.action = action;
11196 	}
11197 
11198 	version(custom_widgets)
11199 	override void defaultEventHandler_click(ClickEvent event) {
11200 		foreach(handler; action.triggered)
11201 			handler();
11202 	}
11203 
11204 	Action action;
11205 
11206 	override int maxWidth() { return toolbarIconSize; }
11207 	override int minWidth() { return toolbarIconSize; }
11208 	override int maxHeight() { return toolbarIconSize; }
11209 	override int minHeight() { return toolbarIconSize; }
11210 
11211 	version(custom_widgets)
11212 	override void paint(WidgetPainter painter) {
11213 	painter.drawThemed(delegate Rectangle (const Rectangle bounds) {
11214 		painter.outlineColor = Color.black;
11215 
11216 		immutable multiplier = toolbarIconSize / 4;
11217 		immutable divisor = 16 / 4;
11218 
11219 		int ScaledNumber(int n) {
11220 			// return n * multiplier / divisor;
11221 			auto s = n * multiplier;
11222 			auto it = s / divisor;
11223 			auto rem = s % divisor;
11224 			if(rem && n >= 8) // cuz the original used 0 .. 16 and we want to try to stay centered so things in the bottom half tend to be added a it
11225 				it++;
11226 			return it;
11227 		}
11228 
11229 		arsd.color.Point Point(int x, int y) {
11230 			return arsd.color.Point(ScaledNumber(x), ScaledNumber(y));
11231 		}
11232 
11233 		switch(action.iconId) {
11234 			case GenericIcons.New:
11235 				painter.fillColor = Color.white;
11236 				painter.drawPolygon(
11237 					Point(3, 2), Point(3, 13), Point(12, 13), Point(12, 6),
11238 					Point(8, 2), Point(8, 6), Point(12, 6), Point(8, 2),
11239 					Point(3, 2), Point(3, 13)
11240 				);
11241 			break;
11242 			case GenericIcons.Save:
11243 				painter.fillColor = Color.white;
11244 				painter.outlineColor = Color.black;
11245 				painter.drawRectangle(Point(2, 2), Point(13, 13));
11246 
11247 				// the label
11248 				painter.drawRectangle(Point(4, 8), Point(11, 13));
11249 
11250 				// the slider
11251 				painter.fillColor = Color.black;
11252 				painter.outlineColor = Color.black;
11253 				painter.drawRectangle(Point(4, 3), Point(10, 6));
11254 
11255 				painter.fillColor = Color.white;
11256 				painter.outlineColor = Color.white;
11257 				// the disc window
11258 				painter.drawRectangle(Point(5, 3), Point(6, 5));
11259 			break;
11260 			case GenericIcons.Open:
11261 				painter.fillColor = Color.white;
11262 				painter.drawPolygon(
11263 					Point(4, 4), Point(4, 12), Point(13, 12), Point(13, 3),
11264 					Point(9, 3), Point(9, 4), Point(4, 4));
11265 				painter.drawPolygon(
11266 					Point(2, 6), Point(11, 6),
11267 					Point(12, 12), Point(4, 12),
11268 					Point(2, 6));
11269 				//painter.drawLine(Point(9, 6), Point(13, 7));
11270 			break;
11271 			case GenericIcons.Copy:
11272 				painter.fillColor = Color.white;
11273 				painter.drawRectangle(Point(3, 2), Point(9, 10));
11274 				painter.drawRectangle(Point(6, 5), Point(12, 13));
11275 			break;
11276 			case GenericIcons.Cut:
11277 				painter.fillColor = Color.transparent;
11278 				painter.outlineColor = getComputedStyle.foregroundColor();
11279 				painter.drawLine(Point(3, 2), Point(10, 9));
11280 				painter.drawLine(Point(4, 9), Point(11, 2));
11281 				painter.drawRectangle(Point(3, 9), Point(5, 13));
11282 				painter.drawRectangle(Point(9, 9), Point(11, 12));
11283 			break;
11284 			case GenericIcons.Paste:
11285 				painter.fillColor = Color.white;
11286 				painter.drawRectangle(Point(2, 3), Point(11, 11));
11287 				painter.drawRectangle(Point(6, 8), Point(13, 13));
11288 				painter.drawLine(Point(6, 2), Point(4, 5));
11289 				painter.drawLine(Point(6, 2), Point(9, 5));
11290 				painter.fillColor = Color.black;
11291 				painter.drawRectangle(Point(4, 5), Point(9, 6));
11292 			break;
11293 			case GenericIcons.Help:
11294 				painter.outlineColor = getComputedStyle.foregroundColor();
11295 				painter.drawText(arsd.color.Point(0, 0), "?", arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
11296 			break;
11297 			case GenericIcons.Undo:
11298 				painter.fillColor = Color.transparent;
11299 				painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64);
11300 				painter.outlineColor = Color.black;
11301 				painter.fillColor = Color.black;
11302 				painter.drawPolygon(
11303 					Point(4, 4),
11304 					Point(8, 2),
11305 					Point(8, 6),
11306 					Point(4, 4),
11307 				);
11308 			break;
11309 			case GenericIcons.Redo:
11310 				painter.fillColor = Color.transparent;
11311 				painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64);
11312 				painter.outlineColor = Color.black;
11313 				painter.fillColor = Color.black;
11314 				painter.drawPolygon(
11315 					Point(10, 4),
11316 					Point(6, 2),
11317 					Point(6, 6),
11318 					Point(10, 4),
11319 				);
11320 			break;
11321 			default:
11322 				painter.outlineColor = getComputedStyle.foregroundColor;
11323 				painter.drawText(arsd.color.Point(0, 0), action.label, arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
11324 		}
11325 		return bounds;
11326 		});
11327 	}
11328 
11329 }
11330 
11331 
11332 /++
11333 	You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode].
11334 +/
11335 class MenuBar : Widget {
11336 	MenuItem[] items;
11337 	Menu[] subMenus;
11338 
11339 	version(win32_widgets) {
11340 		HMENU handle;
11341 		///
11342 		this(Widget parent = null) {
11343 			super(parent);
11344 
11345 			handle = CreateMenu();
11346 			tabStop = false;
11347 		}
11348 	} else version(custom_widgets) {
11349 		///
11350 		this(Widget parent = null) {
11351 			tabStop = false; // these are selected some other way
11352 			super(parent);
11353 		}
11354 
11355 		mixin Padding!q{2};
11356 	} else static assert(false);
11357 
11358 	version(custom_widgets)
11359 	override void paint(WidgetPainter painter) {
11360 		draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color);
11361 	}
11362 
11363 	///
11364 	MenuItem addItem(MenuItem item) {
11365 		this.addChild(item);
11366 		items ~= item;
11367 		version(win32_widgets) {
11368 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
11369 		}
11370 		return item;
11371 	}
11372 
11373 
11374 	///
11375 	Menu addItem(Menu item) {
11376 
11377 		subMenus ~= item;
11378 
11379 		auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane
11380 
11381 		addChild(mbItem);
11382 		items ~= mbItem;
11383 
11384 		version(win32_widgets) {
11385 			AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label));
11386 		} else version(custom_widgets) {
11387 			mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) {
11388 				item.popup(mbItem);
11389 			};
11390 		} else static assert(false);
11391 
11392 		return item;
11393 	}
11394 
11395 	override void recomputeChildLayout() {
11396 		.recomputeChildLayout!"width"(this);
11397 	}
11398 
11399 	override int maxHeight() { return defaultLineHeight + 4; }
11400 	override int minHeight() { return defaultLineHeight + 4; }
11401 }
11402 
11403 
11404 /**
11405 	Status bars appear at the bottom of a MainWindow.
11406 	They are made out of Parts, with a width and content.
11407 
11408 	They can have multiple parts or be in simple mode. FIXME: implement simple mode.
11409 
11410 
11411 	sb.parts[0].content = "Status bar text!";
11412 */
11413 class StatusBar : Widget {
11414 	private Part[] partsArray;
11415 	///
11416 	struct Parts {
11417 		@disable this();
11418 		this(StatusBar owner) { this.owner = owner; }
11419 		//@disable this(this);
11420 		///
11421 		@property int length() { return cast(int) owner.partsArray.length; }
11422 		private StatusBar owner;
11423 		private this(StatusBar owner, Part[] parts) {
11424 			this.owner.partsArray = parts;
11425 			this.owner = owner;
11426 		}
11427 		///
11428 		Part opIndex(int p) {
11429 			if(owner.partsArray.length == 0)
11430 				this ~= new StatusBar.Part(0);
11431 			return owner.partsArray[p];
11432 		}
11433 
11434 		///
11435 		Part opOpAssign(string op : "~" )(Part p) {
11436 			assert(owner.partsArray.length < 255);
11437 			p.owner = this.owner;
11438 			p.idx = cast(int) owner.partsArray.length;
11439 			owner.partsArray ~= p;
11440 
11441 			owner.queueRecomputeChildLayout();
11442 
11443 			version(win32_widgets) {
11444 				int[256] pos;
11445 				int cpos;
11446 				foreach(idx, part; owner.partsArray) {
11447 					if(idx + 1 == owner.partsArray.length)
11448 						pos[idx] = -1;
11449 					else {
11450 						cpos += part.currentlyAssignedWidth;
11451 						pos[idx] = cpos;
11452 					}
11453 				}
11454 				SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr);
11455 			} else version(custom_widgets) {
11456 				owner.redraw();
11457 			} else static assert(false);
11458 
11459 			return p;
11460 		}
11461 
11462 		/++
11463 			Sets up proportional parts in one function call. You can use negative numbers to indicate device-independent pixels, and positive numbers to indicate proportions.
11464 
11465 			No given item should be 0.
11466 
11467 			History:
11468 				Added December 31, 2024
11469 		+/
11470 		void setSizes(int[] proportions...) {
11471 			assert(this.owner);
11472 			this.owner.partsArray = null;
11473 
11474 			foreach(n; proportions) {
11475 				assert(n, "do not give 0 to statusBar.parts.set, it would make an invisible part. Try 1 instead.");
11476 
11477 				this.opOpAssign!"~"(new StatusBar.Part(n > 0 ? n : -n, n > 0 ? StatusBar.Part.WidthUnits.Proportional : StatusBar.Part.WidthUnits.DeviceIndependentPixels));
11478 			}
11479 
11480 		}
11481 	}
11482 
11483 	private Parts _parts;
11484 	///
11485 	final @property Parts parts() {
11486 		return _parts;
11487 	}
11488 
11489 	/++
11490 
11491 	+/
11492 	static class Part {
11493 		/++
11494 			History:
11495 				Added September 1, 2023 (dub v11.1)
11496 		+/
11497 		enum WidthUnits {
11498 			/++
11499 				Unscaled pixels as they appear on screen.
11500 
11501 				If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui.
11502 			+/
11503 			DeviceDependentPixels,
11504 			/++
11505 				Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui.
11506 			+/
11507 			DeviceIndependentPixels,
11508 			/++
11509 				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`).
11510 			+/
11511 			ApproximateCharacters,
11512 			/++
11513 				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.
11514 
11515 				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.
11516 			+/
11517 			Proportional
11518 		}
11519 		private WidthUnits units;
11520 		private int width;
11521 		private StatusBar owner;
11522 
11523 		private int currentlyAssignedWidth;
11524 
11525 		/++
11526 			History:
11527 				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.
11528 
11529 				It now allows you to provide your own value for [WidthUnits].
11530 
11531 				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`.
11532 		+/
11533 		this(int w, WidthUnits units = WidthUnits.Proportional) {
11534 			this.units = units;
11535 			this.width = w;
11536 		}
11537 
11538 		/// ditto
11539 		this(int w = 0) {
11540 			if(w == 0)
11541 				this(w, WidthUnits.Proportional);
11542 			else
11543 				this(w, WidthUnits.DeviceDependentPixels);
11544 		}
11545 
11546 		private int idx;
11547 		private string _content;
11548 		///
11549 		@property string content() { return _content; }
11550 		///
11551 		@property void content(string s) {
11552 			version(win32_widgets) {
11553 				_content = s;
11554 				WCharzBuffer bfr = WCharzBuffer(s);
11555 				SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr);
11556 			} else version(custom_widgets) {
11557 				if(_content != s) {
11558 					_content = s;
11559 					owner.redraw();
11560 				}
11561 			} else static assert(false);
11562 		}
11563 	}
11564 	string simpleModeContent;
11565 	bool inSimpleMode;
11566 
11567 
11568 	///
11569 	this(Widget parent) {
11570 		super(null); // FIXME
11571 		_parts = Parts(this);
11572 		tabStop = false;
11573 		version(win32_widgets) {
11574 			parentWindow = parent.parentWindow;
11575 			createWin32Window(this, "msctls_statusbar32"w, "", 0);
11576 
11577 			RECT rect;
11578 			GetWindowRect(hwnd, &rect);
11579 			idealHeight = rect.bottom - rect.top;
11580 			assert(idealHeight);
11581 		} else version(custom_widgets) {
11582 		} else static assert(false);
11583 	}
11584 
11585 	override void recomputeChildLayout() {
11586 		int remainingLength = this.width;
11587 
11588 		int proportionalSum;
11589 		int proportionalCount;
11590 		foreach(idx, part; this.partsArray) {
11591 			with(Part.WidthUnits)
11592 			final switch(part.units) {
11593 				case DeviceDependentPixels:
11594 					part.currentlyAssignedWidth = part.width;
11595 					remainingLength -= part.currentlyAssignedWidth;
11596 				break;
11597 				case DeviceIndependentPixels:
11598 					part.currentlyAssignedWidth = scaleWithDpi(part.width);
11599 					remainingLength -= part.currentlyAssignedWidth;
11600 				break;
11601 				case ApproximateCharacters:
11602 					auto cs = getComputedStyle();
11603 					auto font = cs.font;
11604 
11605 					part.currentlyAssignedWidth = font.averageWidth * this.width;
11606 					remainingLength -= part.currentlyAssignedWidth;
11607 				break;
11608 				case Proportional:
11609 					proportionalSum += part.width;
11610 					proportionalCount ++;
11611 				break;
11612 			}
11613 		}
11614 
11615 		foreach(part; this.partsArray) {
11616 			if(part.units == Part.WidthUnits.Proportional) {
11617 				auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width;
11618 				if(proportion == 0)
11619 					proportion = 1;
11620 
11621 				if(proportionalSum == 0)
11622 					proportionalSum = proportionalCount;
11623 
11624 				part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum;
11625 			}
11626 		}
11627 
11628 		super.recomputeChildLayout();
11629 	}
11630 
11631 	version(win32_widgets)
11632 	override protected void dpiChanged() {
11633 		RECT rect;
11634 		GetWindowRect(hwnd, &rect);
11635 		idealHeight = rect.bottom - rect.top;
11636 		assert(idealHeight);
11637 	}
11638 
11639 	version(custom_widgets)
11640 	override void paint(WidgetPainter painter) {
11641 		auto cs = getComputedStyle();
11642 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
11643 		int cpos = 0;
11644 		foreach(idx, part; this.partsArray) {
11645 			auto partWidth = part.currentlyAssignedWidth;
11646 			// part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100);
11647 			painter.setClipRectangle(Point(cpos, 0), partWidth, height);
11648 			draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color);
11649 			painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4);
11650 
11651 			painter.outlineColor = cs.foregroundColor();
11652 			painter.fillColor = cs.foregroundColor();
11653 
11654 			painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter);
11655 			cpos += partWidth;
11656 		}
11657 	}
11658 
11659 
11660 	version(win32_widgets) {
11661 		private int idealHeight;
11662 		override int maxHeight() { return idealHeight; }
11663 		override int minHeight() { return idealHeight; }
11664 	} else version(custom_widgets) {
11665 		override int maxHeight() { return defaultLineHeight + 4; }
11666 		override int minHeight() { return defaultLineHeight + 4; }
11667 	} else static assert(false);
11668 }
11669 
11670 /// Displays an in-progress indicator without known values
11671 version(none)
11672 class IndefiniteProgressBar : Widget {
11673 	version(win32_widgets)
11674 	this(Widget parent) {
11675 		super(parent);
11676 		createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */);
11677 		tabStop = false;
11678 	}
11679 	override int minHeight() { return 10; }
11680 }
11681 
11682 /// A progress bar with a known endpoint and completion amount
11683 class ProgressBar : Widget {
11684 	/++
11685 		History:
11686 			Added March 16, 2022 (dub v10.7)
11687 	+/
11688 	this(int min, int max, Widget parent) {
11689 		this(parent);
11690 		setRange(cast(ushort) min, cast(ushort) max); // FIXME
11691 	}
11692 	this(Widget parent) {
11693 		version(win32_widgets) {
11694 			super(parent);
11695 			createWin32Window(this, "msctls_progress32"w, "", 0);
11696 			tabStop = false;
11697 		} else version(custom_widgets) {
11698 			super(parent);
11699 			max = 100;
11700 			step = 10;
11701 			tabStop = false;
11702 		} else static assert(0);
11703 	}
11704 
11705 	version(custom_widgets)
11706 	override void paint(WidgetPainter painter) {
11707 		auto cs = getComputedStyle();
11708 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
11709 		painter.fillColor = cs.progressBarColor;
11710 		painter.drawRectangle(Point(0, 0), width * current / max, height);
11711 	}
11712 
11713 
11714 	version(custom_widgets) {
11715 		int current;
11716 		int max;
11717 		int step;
11718 	}
11719 
11720 	///
11721 	void advanceOneStep() {
11722 		version(win32_widgets)
11723 			SendMessageW(hwnd, PBM_STEPIT, 0, 0);
11724 		else version(custom_widgets)
11725 			addToPosition(step);
11726 		else static assert(false);
11727 	}
11728 
11729 	///
11730 	void setStepIncrement(int increment) {
11731 		version(win32_widgets)
11732 			SendMessageW(hwnd, PBM_SETSTEP, increment, 0);
11733 		else version(custom_widgets)
11734 			step = increment;
11735 		else static assert(false);
11736 	}
11737 
11738 	///
11739 	void addToPosition(int amount) {
11740 		version(win32_widgets)
11741 			SendMessageW(hwnd, PBM_DELTAPOS, amount, 0);
11742 		else version(custom_widgets)
11743 			setPosition(current + amount);
11744 		else static assert(false);
11745 	}
11746 
11747 	///
11748 	void setPosition(int pos) {
11749 		version(win32_widgets)
11750 			SendMessageW(hwnd, PBM_SETPOS, pos, 0);
11751 		else version(custom_widgets) {
11752 			current = pos;
11753 			if(current > max)
11754 				current = max;
11755 			redraw();
11756 		}
11757 		else static assert(false);
11758 	}
11759 
11760 	///
11761 	void setRange(ushort min, ushort max) {
11762 		version(win32_widgets)
11763 			SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max));
11764 		else version(custom_widgets) {
11765 			this.max = max;
11766 		}
11767 		else static assert(false);
11768 	}
11769 
11770 	override int minHeight() { return 10; }
11771 }
11772 
11773 version(custom_widgets)
11774 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) {
11775 	thisLabel.reserve(label.length);
11776 	bool justSawAmpersand;
11777 	foreach(ch; label) {
11778 		if(justSawAmpersand) {
11779 			justSawAmpersand = false;
11780 			if(ch == '&') {
11781 				goto plain;
11782 			}
11783 			thisAccelerator = ch;
11784 		} else {
11785 			if(ch == '&') {
11786 				justSawAmpersand = true;
11787 				continue;
11788 			}
11789 			plain:
11790 			thisLabel ~= ch;
11791 		}
11792 	}
11793 }
11794 
11795 /++
11796 	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.
11797 
11798 
11799 	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
11800 
11801 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
11802 
11803 	History:
11804 		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.
11805 +/
11806 class Fieldset : Widget {
11807 	// FIXME: on Windows,it doesn't draw the background on the label
11808 	// on X, it doesn't fix the clipping rectangle for it
11809 	version(win32_widgets)
11810 		override int paddingTop() { return defaultLineHeight; }
11811 	else version(custom_widgets)
11812 		override int paddingTop() { return defaultLineHeight + 2; }
11813 	else static assert(false);
11814 	override int paddingBottom() { return 6; }
11815 	override int paddingLeft() { return 6; }
11816 	override int paddingRight() { return 6; }
11817 
11818 	override int marginLeft() { return 6; }
11819 	override int marginRight() { return 6; }
11820 	override int marginTop() { return 2; }
11821 	override int marginBottom() { return 2; }
11822 
11823 	string legend;
11824 
11825 	version(custom_widgets) private dchar accelerator;
11826 
11827 	this(string legend, Widget parent) {
11828 		version(win32_widgets) {
11829 			super(parent);
11830 			this.legend = legend;
11831 			createWin32Window(this, "button"w, legend, BS_GROUPBOX);
11832 			tabStop = false;
11833 		} else version(custom_widgets) {
11834 			super(parent);
11835 			tabStop = false;
11836 
11837 			legend.extractWindowsStyleLabel(this.legend, this.accelerator);
11838 		} else static assert(0);
11839 	}
11840 
11841 	version(custom_widgets)
11842 	override void paint(WidgetPainter painter) {
11843 		auto dlh = defaultLineHeight;
11844 
11845 		painter.fillColor = Color.transparent;
11846 		auto cs = getComputedStyle();
11847 		painter.pen = Pen(cs.foregroundColor, 1);
11848 		painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2);
11849 
11850 		auto tx = painter.textSize(legend);
11851 		painter.outlineColor = Color.transparent;
11852 
11853 		version(Windows) {
11854 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
11855 			painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height);
11856 			SelectObject(painter.impl.hdc, b);
11857 		} else static if(UsingSimpledisplayX11) {
11858 			painter.fillColor = getComputedStyle().windowBackgroundColor;
11859 			painter.drawRectangle(Point(8, 0), tx.width, tx.height);
11860 		}
11861 		painter.outlineColor = cs.foregroundColor;
11862 		painter.drawText(Point(8, 0), legend);
11863 	}
11864 
11865 	override int maxHeight() {
11866 		auto m = paddingTop() + paddingBottom();
11867 		foreach(child; children) {
11868 			auto mh = child.maxHeight();
11869 			if(mh == int.max)
11870 				return int.max;
11871 			m += mh;
11872 			m += child.marginBottom();
11873 			m += child.marginTop();
11874 		}
11875 		m += 6;
11876 		if(m < minHeight)
11877 			return minHeight;
11878 		return m;
11879 	}
11880 
11881 	override int minHeight() {
11882 		auto m = paddingTop() + paddingBottom();
11883 		foreach(child; children) {
11884 			m += child.minHeight();
11885 			m += child.marginBottom();
11886 			m += child.marginTop();
11887 		}
11888 		return m + 6;
11889 	}
11890 
11891 	override int minWidth() {
11892 		return 6 + cast(int) this.legend.length * 7;
11893 	}
11894 }
11895 
11896 /++
11897 	$(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")
11898 	$(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.)
11899 +/
11900 version(minigui_screenshots)
11901 @Screenshot("Fieldset")
11902 unittest {
11903 	auto window = new Window(200, 100);
11904 	auto set = new Fieldset("Baby will", window);
11905 	auto option1 = new Radiobox("Eat", set);
11906 	auto option2 = new Radiobox("Cry", set);
11907 	auto option3 = new Radiobox("Sleep", set);
11908 	window.loop();
11909 }
11910 
11911 /// Draws a line
11912 class HorizontalRule : Widget {
11913 	mixin Margin!q{ 2 };
11914 	override int minHeight() { return 2; }
11915 	override int maxHeight() { return 2; }
11916 
11917 	///
11918 	this(Widget parent) {
11919 		super(parent);
11920 	}
11921 
11922 	override void paint(WidgetPainter painter) {
11923 		auto cs = getComputedStyle();
11924 		painter.outlineColor = cs.darkAccentColor;
11925 		painter.drawLine(Point(0, 0), Point(width, 0));
11926 		painter.outlineColor = cs.lightAccentColor;
11927 		painter.drawLine(Point(0, 1), Point(width, 1));
11928 	}
11929 }
11930 
11931 version(minigui_screenshots)
11932 @Screenshot("HorizontalRule")
11933 /++
11934 	$(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.)
11935 
11936 +/
11937 unittest {
11938 	auto window = new Window(200, 100);
11939 	auto above = new TextLabel("Above the line", TextAlignment.Left, window);
11940 	new HorizontalRule(window);
11941 	auto below = new TextLabel("Below the line", TextAlignment.Left, window);
11942 	window.loop();
11943 }
11944 
11945 /// ditto
11946 class VerticalRule : Widget {
11947 	mixin Margin!q{ 2 };
11948 	override int minWidth() { return 2; }
11949 	override int maxWidth() { return 2; }
11950 
11951 	///
11952 	this(Widget parent) {
11953 		super(parent);
11954 	}
11955 
11956 	override void paint(WidgetPainter painter) {
11957 		auto cs = getComputedStyle();
11958 		painter.outlineColor = cs.darkAccentColor;
11959 		painter.drawLine(Point(0, 0), Point(0, height));
11960 		painter.outlineColor = cs.lightAccentColor;
11961 		painter.drawLine(Point(1, 0), Point(1, height));
11962 	}
11963 }
11964 
11965 
11966 ///
11967 class Menu : Window {
11968 	void remove() {
11969 		foreach(i, child; parentWindow.children)
11970 			if(child is this) {
11971 				parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $];
11972 				break;
11973 			}
11974 		parentWindow.redraw();
11975 
11976 		parentWindow.releaseMouseCapture();
11977 	}
11978 
11979 	///
11980 	void addSeparator() {
11981 		version(win32_widgets)
11982 			AppendMenu(handle, MF_SEPARATOR, 0, null);
11983 		else version(custom_widgets)
11984 			auto hr = new HorizontalRule(this);
11985 		else static assert(0);
11986 	}
11987 
11988 	override int paddingTop() { return 4; }
11989 	override int paddingBottom() { return 4; }
11990 	override int paddingLeft() { return 2; }
11991 	override int paddingRight() { return 2; }
11992 
11993 	version(win32_widgets) {}
11994 	else version(custom_widgets) {
11995 
11996 		Widget previouslyFocusedWidget;
11997 		Widget* previouslyFocusedWidgetBelongsIn;
11998 
11999 		SimpleWindow dropDown;
12000 		Widget menuParent;
12001 		void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
12002 			this.menuParent = parent;
12003 
12004 			previouslyFocusedWidget = parent.parentWindow.focusedWidget;
12005 			previouslyFocusedWidgetBelongsIn = &parent.parentWindow.focusedWidget;
12006 			parent.parentWindow.focusedWidget = this;
12007 
12008 			int w = 150;
12009 			int h = paddingTop + paddingBottom;
12010 			if(this.children.length) {
12011 				// hacking it to get the ideal height out of recomputeChildLayout
12012 				this.width = w;
12013 				this.height = h;
12014 				this.recomputeChildLayoutEntry();
12015 				h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
12016 				h += paddingBottom;
12017 
12018 				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
12019 			}
12020 
12021 			if(offsetY == int.min)
12022 				offsetY = parent.defaultLineHeight;
12023 
12024 			auto coord = parent.globalCoordinates();
12025 			dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
12026 			this.x = 0;
12027 			this.y = 0;
12028 			this.width = dropDown.width;
12029 			this.height = dropDown.height;
12030 			this.drawableWindow = dropDown;
12031 			this.recomputeChildLayoutEntry();
12032 
12033 			static if(UsingSimpledisplayX11)
12034 				XSync(XDisplayConnection.get, 0);
12035 
12036 			dropDown.visibilityChanged = (bool visible) {
12037 				if(visible) {
12038 					this.redraw();
12039 					dropDown.grabInput();
12040 				} else {
12041 					dropDown.releaseInputGrab();
12042 				}
12043 			};
12044 
12045 			dropDown.show();
12046 
12047 			clickListener = this.addEventListener((scope ClickEvent ev) {
12048 				unpopup();
12049 				// need to unlock asap just in case other user handlers block...
12050 				static if(UsingSimpledisplayX11)
12051 					flushGui();
12052 			}, true /* again for asap action */);
12053 		}
12054 
12055 		EventListener clickListener;
12056 	}
12057 	else static assert(false);
12058 
12059 	version(custom_widgets)
12060 	void unpopup() {
12061 		mouseLastOver = mouseLastDownOn = null;
12062 		dropDown.hide();
12063 		if(!menuParent.parentWindow.win.closed) {
12064 			if(auto maw = cast(MouseActivatedWidget) menuParent) {
12065 				maw.setDynamicState(DynamicState.depressed, false);
12066 				maw.setDynamicState(DynamicState.hover, false);
12067 				maw.redraw();
12068 			}
12069 			// menuParent.parentWindow.win.focus();
12070 		}
12071 		clickListener.disconnect();
12072 
12073 		if(previouslyFocusedWidgetBelongsIn)
12074 			*previouslyFocusedWidgetBelongsIn = previouslyFocusedWidget;
12075 	}
12076 
12077 	MenuItem[] items;
12078 
12079 	///
12080 	MenuItem addItem(MenuItem item) {
12081 		addChild(item);
12082 		items ~= item;
12083 		version(win32_widgets) {
12084 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
12085 		}
12086 		return item;
12087 	}
12088 
12089 	string label;
12090 
12091 	version(win32_widgets) {
12092 		HMENU handle;
12093 		///
12094 		this(string label, Widget parent) {
12095 			// not actually passing the parent since it effs up the drawing
12096 			super(cast(Widget) null);// parent);
12097 			this.label = label;
12098 			handle = CreatePopupMenu();
12099 		}
12100 	} else version(custom_widgets) {
12101 		///
12102 		this(string label, Widget parent) {
12103 
12104 			if(dropDown) {
12105 				dropDown.close();
12106 			}
12107 			dropDown = new SimpleWindow(
12108 				150, 4,
12109 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null);
12110 
12111 			this.label = label;
12112 
12113 			super(dropDown);
12114 		}
12115 	} else static assert(false);
12116 
12117 	override int maxHeight() { return defaultLineHeight; }
12118 	override int minHeight() { return defaultLineHeight; }
12119 
12120 	version(custom_widgets) {
12121 		Widget currentPlace;
12122 
12123 		void changeCurrentPlace(Widget n) {
12124 			if(currentPlace) {
12125 				currentPlace.dynamicState = 0;
12126 			}
12127 
12128 			if(n) {
12129 				n.dynamicState = DynamicState.hover;
12130 			}
12131 
12132 			currentPlace = n;
12133 		}
12134 
12135 		override void paint(WidgetPainter painter) {
12136 			this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color);
12137 		}
12138 
12139 		override void defaultEventHandler_keydown(KeyDownEvent ke) {
12140 			switch(ke.key) {
12141 				case Key.Down:
12142 					Widget next;
12143 					Widget first;
12144 					foreach(w; this.children) {
12145 						if((cast(MenuItem) w) is null)
12146 							continue;
12147 
12148 						if(first is null)
12149 							first = w;
12150 
12151 						if(next !is null) {
12152 							next = w;
12153 							break;
12154 						}
12155 
12156 						if(currentPlace is null) {
12157 							next = w;
12158 							break;
12159 						}
12160 
12161 						if(w is currentPlace) {
12162 							next = w;
12163 						}
12164 					}
12165 
12166 					if(next is currentPlace)
12167 						next = first;
12168 
12169 					changeCurrentPlace(next);
12170 					break;
12171 				case Key.Up:
12172 					Widget prev;
12173 					foreach(w; this.children) {
12174 						if((cast(MenuItem) w) is null)
12175 							continue;
12176 						if(w is currentPlace) {
12177 							if(prev is null) {
12178 								foreach_reverse(c; this.children) {
12179 									if((cast(MenuItem) c) !is null) {
12180 										prev = c;
12181 										break;
12182 									}
12183 								}
12184 							}
12185 							break;
12186 						}
12187 						prev = w;
12188 					}
12189 					changeCurrentPlace(prev);
12190 					break;
12191 				case Key.Left:
12192 				case Key.Right:
12193 					if(menuParent) {
12194 						Menu first;
12195 						Menu last;
12196 						Menu prev;
12197 						Menu next;
12198 						bool found;
12199 
12200 						size_t prev_idx;
12201 						size_t next_idx;
12202 
12203 						MenuBar mb = cast(MenuBar) menuParent.parent;
12204 
12205 						if(mb) {
12206 							foreach(idx, menu; mb.subMenus) {
12207 								if(first is null)
12208 									first = menu;
12209 								last = menu;
12210 								if(found && next is null) {
12211 									next = menu;
12212 									next_idx = idx;
12213 								}
12214 								if(menu is this)
12215 									found = true;
12216 								if(!found) {
12217 									prev = menu;
12218 									prev_idx = idx;
12219 								}
12220 							}
12221 
12222 							Menu nextMenu;
12223 							size_t nextMenuIdx;
12224 							if(ke.key == Key.Left) {
12225 								nextMenu = prev ? prev : last;
12226 								nextMenuIdx = prev ? prev_idx : mb.subMenus.length - 1;
12227 							} else {
12228 								nextMenu = next ? next : first;
12229 								nextMenuIdx = next ? next_idx : 0;
12230 							}
12231 
12232 							unpopup();
12233 
12234 							auto rent = mb.children[nextMenuIdx]; // FIXME thsi is not necessarily right
12235 							rent.dynamicState = DynamicState.depressed | DynamicState.hover;
12236 							nextMenu.popup(rent);
12237 						}
12238 					}
12239 					break;
12240 				case Key.Enter:
12241 				case Key.PadEnter:
12242 					// because the key up and char events will go back to the other window after we unpopup!
12243 					// we will wait for the char event to come (in the following method)
12244 					break;
12245 				case Key.Escape:
12246 					unpopup();
12247 					break;
12248 				default:
12249 			}
12250 		}
12251 		override void defaultEventHandler_char(CharEvent ke) {
12252 			// if one is selected, enter activates it
12253 			if(currentPlace) {
12254 				if(ke.character == '\n') {
12255 					// enter selects
12256 					auto event = new Event(EventType.triggered, currentPlace);
12257 					event.dispatch();
12258 					unpopup();
12259 					return;
12260 				}
12261 			}
12262 
12263 			// otherwise search for a hotkey
12264 			foreach(item; items) {
12265 				if(item.hotkey == ke.character) {
12266 					auto event = new Event(EventType.triggered, item);
12267 					event.dispatch();
12268 					unpopup();
12269 					return;
12270 				}
12271 			}
12272 		}
12273 		override void defaultEventHandler_mouseover(MouseOverEvent moe) {
12274 			if(moe.target && moe.target.parent is this)
12275 				changeCurrentPlace(moe.target);
12276 		}
12277 	}
12278 }
12279 
12280 /++
12281 	A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked.
12282 +/
12283 class MenuItem : MouseActivatedWidget {
12284 	Menu submenu;
12285 
12286 	Action action;
12287 	string label;
12288 	dchar hotkey;
12289 
12290 	override int paddingLeft() { return 4; }
12291 
12292 	override int maxHeight() { return defaultLineHeight + 4; }
12293 	override int minHeight() { return defaultLineHeight + 4; }
12294 	override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); }
12295 	override int maxWidth() {
12296 		if(cast(MenuBar) parent) {
12297 			return minWidth();
12298 		}
12299 		return int.max;
12300 	}
12301 	/// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu.
12302 	this(string lbl, Widget parent = null) {
12303 		super(parent);
12304 		//label = lbl; // FIXME
12305 		foreach(idx, char ch; lbl) // FIXME
12306 			if(ch != '&') { // FIXME
12307 				label ~= ch; // FIXME
12308 			} else {
12309 				if(idx + 1 < lbl.length) {
12310 					hotkey = lbl[idx + 1];
12311 					if(hotkey >= 'A' && hotkey <= 'Z')
12312 						hotkey += 32;
12313 				}
12314 			}
12315 		tabStop = false; // these are selected some other way
12316 	}
12317 
12318 	///
12319 	this(Action action, Widget parent = null) {
12320 		assert(action !is null);
12321 		this(action.label, parent);
12322 		this.action = action;
12323 		tabStop = false; // these are selected some other way
12324 	}
12325 
12326 	version(custom_widgets)
12327 	override void paint(WidgetPainter painter) {
12328 		auto cs = getComputedStyle();
12329 		if(dynamicState & DynamicState.depressed)
12330 			this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
12331 		else {
12332 			if(dynamicState & DynamicState.hover) {
12333 				painter.fillColor = cs.hoveringColor;
12334 				painter.outlineColor = Color.transparent;
12335 			} else {
12336 				painter.fillColor = cs.background.color;
12337 				painter.outlineColor = Color.transparent;
12338 			}
12339 
12340 			painter.drawRectangle(Point(0, 0), Size(this.width, this.height));
12341 		}
12342 
12343 		if(dynamicState & DynamicState.hover)
12344 			painter.outlineColor = cs.activeMenuItemColor;
12345 		else
12346 			painter.outlineColor = cs.foregroundColor;
12347 		painter.fillColor = Color.transparent;
12348 		painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
12349 		if(action && action.accelerator !is KeyEvent.init) {
12350 			painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter);
12351 
12352 		}
12353 	}
12354 
12355 	static class Style : Widget.Style {
12356 		override bool variesWithState(ulong dynamicStateFlags) {
12357 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
12358 		}
12359 	}
12360 	mixin OverrideStyle!Style;
12361 
12362 	override void defaultEventHandler_triggered(Event event) {
12363 		if(action)
12364 		foreach(handler; action.triggered)
12365 			handler();
12366 
12367 		if(auto pmenu = cast(Menu) this.parent)
12368 			pmenu.remove();
12369 
12370 		super.defaultEventHandler_triggered(event);
12371 	}
12372 }
12373 
12374 version(win32_widgets)
12375 /// A "mouse activiated widget" is really just an abstract variant of button.
12376 class MouseActivatedWidget : Widget {
12377 	@property bool isChecked() {
12378 		assert(hwnd);
12379 		return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED;
12380 
12381 	}
12382 	@property void isChecked(bool state) {
12383 		assert(hwnd);
12384 		SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0);
12385 
12386 	}
12387 
12388 	override void handleWmCommand(ushort cmd, ushort id) {
12389 		if(cmd == 0) {
12390 			auto event = new Event(EventType.triggered, this);
12391 			event.dispatch();
12392 		}
12393 	}
12394 
12395 	this(Widget parent) {
12396 		super(parent);
12397 	}
12398 }
12399 else version(custom_widgets)
12400 /// ditto
12401 class MouseActivatedWidget : Widget {
12402 	@property bool isChecked() { return isChecked_; }
12403 	@property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;}
12404 
12405 	private bool isChecked_;
12406 
12407 	this(Widget parent) {
12408 		super(parent);
12409 
12410 		addEventListener((MouseDownEvent ev) {
12411 			if(ev.button == MouseButton.left) {
12412 				setDynamicState(DynamicState.depressed, true);
12413 				setDynamicState(DynamicState.hover, true);
12414 				redraw();
12415 			}
12416 		});
12417 
12418 		addEventListener((MouseUpEvent ev) {
12419 			if(ev.button == MouseButton.left) {
12420 				setDynamicState(DynamicState.depressed, false);
12421 				setDynamicState(DynamicState.hover, false);
12422 				redraw();
12423 			}
12424 		});
12425 
12426 		addEventListener((MouseMoveEvent mme) {
12427 			if(!(mme.state & ModifierState.leftButtonDown)) {
12428 				if(dynamicState_ & DynamicState.depressed) {
12429 					setDynamicState(DynamicState.depressed, false);
12430 					redraw();
12431 				}
12432 			}
12433 		});
12434 	}
12435 
12436 	override void defaultEventHandler_focus(FocusEvent ev) {
12437 		super.defaultEventHandler_focus(ev);
12438 		this.redraw();
12439 	}
12440 	override void defaultEventHandler_blur(BlurEvent ev) {
12441 		super.defaultEventHandler_blur(ev);
12442 		setDynamicState(DynamicState.depressed, false);
12443 		this.redraw();
12444 	}
12445 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
12446 		super.defaultEventHandler_keydown(ev);
12447 		if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) {
12448 			setDynamicState(DynamicState.depressed, true);
12449 			setDynamicState(DynamicState.hover, true);
12450 			this.redraw();
12451 		}
12452 	}
12453 	override void defaultEventHandler_keyup(KeyUpEvent ev) {
12454 		super.defaultEventHandler_keyup(ev);
12455 		if(!(dynamicState & DynamicState.depressed))
12456 			return;
12457 		setDynamicState(DynamicState.depressed, false);
12458 		setDynamicState(DynamicState.hover, false);
12459 		this.redraw();
12460 
12461 		auto event = new Event(EventType.triggered, this);
12462 		event.sendDirectly();
12463 	}
12464 	override void defaultEventHandler_click(ClickEvent ev) {
12465 		super.defaultEventHandler_click(ev);
12466 		if(ev.button == MouseButton.left) {
12467 			auto event = new Event(EventType.triggered, this);
12468 			event.sendDirectly();
12469 		}
12470 	}
12471 
12472 }
12473 else static assert(false);
12474 
12475 /*
12476 /++
12477 	Like the tablet thing, it would have a label, a description, and a switch slider thingy.
12478 
12479 	Basically the same as a checkbox.
12480 +/
12481 class OnOffSwitch : MouseActivatedWidget {
12482 
12483 }
12484 */
12485 
12486 /++
12487 	History:
12488 		Added June 15, 2021 (dub v10.1)
12489 +/
12490 struct ImageLabel {
12491 	/++
12492 		Defines a label+image combo used by some widgets.
12493 
12494 		If you provide just a text label, that is all the widget will try to
12495 		display. Or just an image will display just that. If you provide both,
12496 		it may display both text and image side by side or display the image
12497 		and offer text on an input event depending on the widget.
12498 
12499 		History:
12500 			The `alignment` parameter was added on September 27, 2021
12501 	+/
12502 	this(string label, TextAlignment alignment = TextAlignment.Center) {
12503 		this.label = label;
12504 		this.displayFlags = DisplayFlags.displayText;
12505 		this.alignment = alignment;
12506 	}
12507 
12508 	/// ditto
12509 	this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
12510 		this.label = label;
12511 		this.image = image;
12512 		this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
12513 		this.alignment = alignment;
12514 	}
12515 
12516 	/// ditto
12517 	this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
12518 		this.image = image;
12519 		this.displayFlags = DisplayFlags.displayImage;
12520 		this.alignment = alignment;
12521 	}
12522 
12523 	/// ditto
12524 	this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) {
12525 		this.label = label;
12526 		this.image = image;
12527 		this.alignment = alignment;
12528 		this.displayFlags = displayFlags;
12529 	}
12530 
12531 	string label;
12532 	MemoryImage image;
12533 
12534 	enum DisplayFlags {
12535 		displayText = 1 << 0,
12536 		displayImage = 1 << 1,
12537 	}
12538 
12539 	int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
12540 
12541 	TextAlignment alignment;
12542 }
12543 
12544 /++
12545 	A basic checked or not checked box with an attached label.
12546 
12547 
12548 	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
12549 
12550 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
12551 
12552 	History:
12553 		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.
12554 +/
12555 class Checkbox : MouseActivatedWidget {
12556 	version(win32_widgets) {
12557 		override int maxHeight() { return scaleWithDpi(16); }
12558 		override int minHeight() { return scaleWithDpi(16); }
12559 	} else version(custom_widgets) {
12560 		private enum buttonSize = 16;
12561 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
12562 		override int minHeight() { return maxHeight(); }
12563 	} else static assert(0);
12564 
12565 	override int marginLeft() { return 4; }
12566 
12567 	override int flexBasisWidth() { return 24 + cast(int) label.length * 7; }
12568 
12569 	/++
12570 		Just an alias because I keep typing checked out of web habit.
12571 
12572 		History:
12573 			Added May 31, 2021
12574 	+/
12575 	alias checked = isChecked;
12576 
12577 	private string label;
12578 	private dchar accelerator;
12579 
12580 	/++
12581 	+/
12582 	this(string label, Widget parent) {
12583 		this(ImageLabel(label), Appearance.checkbox, parent);
12584 	}
12585 
12586 	/// ditto
12587 	this(string label, Appearance appearance, Widget parent) {
12588 		this(ImageLabel(label), appearance, parent);
12589 	}
12590 
12591 	/++
12592 		Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific.
12593 
12594 		History:
12595 			Added June 29, 2021 (dub v10.2)
12596 	+/
12597 	enum Appearance {
12598 		checkbox, /// a normal checkbox
12599 		pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad.
12600 		//sliderswitch,
12601 	}
12602 	private Appearance appearance;
12603 
12604 	/// ditto
12605 	private this(ImageLabel label, Appearance appearance, Widget parent) {
12606 		super(parent);
12607 		version(win32_widgets) {
12608 			this.label = label.label;
12609 
12610 			uint extraStyle;
12611 			final switch(appearance) {
12612 				case Appearance.checkbox:
12613 				break;
12614 				case Appearance.pushbutton:
12615 					extraStyle |= BS_PUSHLIKE;
12616 				break;
12617 			}
12618 
12619 			createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle);
12620 		} else version(custom_widgets) {
12621 			label.label.extractWindowsStyleLabel(this.label, this.accelerator);
12622 		} else static assert(0);
12623 	}
12624 
12625 	version(custom_widgets)
12626 	override void paint(WidgetPainter painter) {
12627 		auto cs = getComputedStyle();
12628 		if(isFocused()) {
12629 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
12630 			painter.fillColor = cs.windowBackgroundColor;
12631 			painter.drawRectangle(Point(0, 0), width, height);
12632 			painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
12633 		} else {
12634 			painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid);
12635 			painter.fillColor = cs.windowBackgroundColor;
12636 			painter.drawRectangle(Point(0, 0), width, height);
12637 		}
12638 
12639 
12640 		painter.outlineColor = Color.black;
12641 		painter.fillColor = Color.white;
12642 		enum rectOffset = 2;
12643 		painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset));
12644 
12645 		if(isChecked) {
12646 			auto size = scaleWithDpi(2);
12647 			painter.pen = Pen(Color.black, size);
12648 			// I'm using height so the checkbox is square
12649 			enum padding = 3;
12650 			painter.drawLine(
12651 				scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)),
12652 				scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2)
12653 			);
12654 			painter.drawLine(
12655 				scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0),
12656 				scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 -  size % 2)
12657 			);
12658 
12659 			painter.pen = Pen(Color.black, 1);
12660 		}
12661 
12662 		if(label !is null) {
12663 			painter.outlineColor = cs.foregroundColor();
12664 			painter.fillColor = cs.foregroundColor();
12665 
12666 			// i want the centerline of the text to be aligned with the centerline of the checkbox
12667 			/+
12668 			auto font = cs.font();
12669 			auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2;
12670 			painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label);
12671 			+/
12672 			painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter);
12673 		}
12674 	}
12675 
12676 	override void defaultEventHandler_triggered(Event ev) {
12677 		isChecked = !isChecked;
12678 
12679 		this.emit!(ChangeEvent!bool)(&isChecked);
12680 
12681 		redraw();
12682 	}
12683 
12684 	/// Emits a change event with the checked state
12685 	mixin Emits!(ChangeEvent!bool);
12686 }
12687 
12688 /// Adds empty space to a layout.
12689 class VerticalSpacer : Widget {
12690 	private int mh;
12691 
12692 	/++
12693 		History:
12694 			The overload with `maxHeight` was added on December 31, 2024
12695 	+/
12696 	this(Widget parent) {
12697 		this(0, parent);
12698 	}
12699 
12700 	/// ditto
12701 	this(int maxHeight, Widget parent) {
12702 		this.mh = maxHeight;
12703 		super(parent);
12704 		this.tabStop = false;
12705 	}
12706 
12707 	override int maxHeight() {
12708 		return mh ? scaleWithDpi(mh) : super.maxHeight();
12709 	}
12710 }
12711 
12712 
12713 /// ditto
12714 class HorizontalSpacer : Widget {
12715 	private int mw;
12716 
12717 	/++
12718 		History:
12719 			The overload with `maxWidth` was added on December 31, 2024
12720 	+/
12721 	this(Widget parent) {
12722 		this(0, parent);
12723 	}
12724 
12725 	/// ditto
12726 	this(int maxWidth, Widget parent) {
12727 		this.mw = maxWidth;
12728 		super(parent);
12729 		this.tabStop = false;
12730 	}
12731 
12732 	override int maxWidth() {
12733 		return mw ? scaleWithDpi(mw) : super.maxWidth();
12734 	}
12735 }
12736 
12737 
12738 /++
12739 	Creates a radio button with an associated label. These are usually put inside a [Fieldset].
12740 
12741 
12742 	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
12743 
12744 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
12745 
12746 	History:
12747 		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.
12748 +/
12749 class Radiobox : MouseActivatedWidget {
12750 
12751 	version(win32_widgets) {
12752 		override int maxHeight() { return scaleWithDpi(16); }
12753 		override int minHeight() { return scaleWithDpi(16); }
12754 	} else version(custom_widgets) {
12755 		private enum buttonSize = 16;
12756 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
12757 		override int minHeight() { return maxHeight(); }
12758 	} else static assert(0);
12759 
12760 	override int marginLeft() { return 4; }
12761 
12762 	// FIXME: make a label getter
12763 	private string label;
12764 	private dchar accelerator;
12765 
12766 	/++
12767 
12768 	+/
12769 	this(string label, Widget parent) {
12770 		super(parent);
12771 		version(win32_widgets) {
12772 			this.label = label;
12773 			createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON);
12774 		} else version(custom_widgets) {
12775 			label.extractWindowsStyleLabel(this.label, this.accelerator);
12776 			height = 16;
12777 			width = height + 4 + cast(int) label.length * 16;
12778 		}
12779 	}
12780 
12781 	version(custom_widgets)
12782 	override void paint(WidgetPainter painter) {
12783 		auto cs = getComputedStyle();
12784 
12785 		if(isFocused) {
12786 			painter.fillColor = cs.windowBackgroundColor;
12787 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
12788 		} else {
12789 			painter.fillColor = cs.windowBackgroundColor;
12790 			painter.outlineColor = cs.windowBackgroundColor;
12791 		}
12792 		painter.drawRectangle(Point(0, 0), width, height);
12793 
12794 		painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
12795 
12796 		painter.outlineColor = Color.black;
12797 		painter.fillColor = Color.white;
12798 		painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2)));
12799 		if(isChecked) {
12800 			painter.outlineColor = Color.black;
12801 			painter.fillColor = Color.black;
12802 			// I'm using height so the checkbox is square
12803 			auto size = scaleWithDpi(2);
12804 			painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2));
12805 		}
12806 
12807 		painter.outlineColor = cs.foregroundColor();
12808 		painter.fillColor = cs.foregroundColor();
12809 
12810 		painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
12811 	}
12812 
12813 
12814 	override void defaultEventHandler_triggered(Event ev) {
12815 		isChecked = true;
12816 
12817 		if(this.parent) {
12818 			foreach(child; this.parent.children) {
12819 				if(child is this) continue;
12820 				if(auto rb = cast(Radiobox) child) {
12821 					rb.isChecked = false;
12822 					rb.emit!(ChangeEvent!bool)(&rb.isChecked);
12823 					rb.redraw();
12824 				}
12825 			}
12826 		}
12827 
12828 		this.emit!(ChangeEvent!bool)(&this.isChecked);
12829 
12830 		redraw();
12831 	}
12832 
12833 	/// 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.
12834 	mixin Emits!(ChangeEvent!bool);
12835 }
12836 
12837 
12838 /++
12839 	Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event.
12840 
12841 
12842 	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
12843 
12844 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
12845 
12846 	History:
12847 		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.
12848 +/
12849 class Button : MouseActivatedWidget {
12850 	override int heightStretchiness() { return 3; }
12851 	override int widthStretchiness() { return 3; }
12852 
12853 	/++
12854 		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.
12855 
12856 		History:
12857 			Added July 2, 2021
12858 	+/
12859 	public bool triggersOnMultiClick;
12860 
12861 	private string label_;
12862 	private TextAlignment alignment;
12863 	private dchar accelerator;
12864 
12865 	///
12866 	string label() { return label_; }
12867 	///
12868 	void label(string l) {
12869 		label_ = l;
12870 		version(win32_widgets) {
12871 			WCharzBuffer bfr = WCharzBuffer(l);
12872 			SetWindowTextW(hwnd, bfr.ptr);
12873 		} else version(custom_widgets) {
12874 			redraw();
12875 		}
12876 	}
12877 
12878 	override void defaultEventHandler_dblclick(DoubleClickEvent ev) {
12879 		super.defaultEventHandler_dblclick(ev);
12880 		if(triggersOnMultiClick) {
12881 			if(ev.button == MouseButton.left) {
12882 				auto event = new Event(EventType.triggered, this);
12883 				event.sendDirectly();
12884 			}
12885 		}
12886 	}
12887 
12888 	private Sprite sprite;
12889 	private int displayFlags;
12890 
12891 	protected bool needsOwnerDraw() {
12892 		return &this.paint !is &Button.paint || &this.useStyleProperties !is &Button.useStyleProperties || &this.paintContent !is &Button.paintContent;
12893 	}
12894 
12895 	version(win32_widgets)
12896 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis) {
12897 		auto itemId = dis.itemID;
12898 		auto hdc = dis.hDC;
12899 		auto rect = dis.rcItem;
12900 		switch(dis.itemAction) {
12901 			// skipping setDynamicState because i don't want to queue the redraw unnecessarily
12902 			case ODA_SELECT:
12903 				dynamicState_ &= ~DynamicState.depressed;
12904 				if(dis.itemState & ODS_SELECTED)
12905 					dynamicState_ |= DynamicState.depressed;
12906 			goto case;
12907 			case ODA_FOCUS:
12908 				dynamicState_ &= ~DynamicState.focus;
12909 				if(dis.itemState & ODS_FOCUS)
12910 					dynamicState_ |= DynamicState.focus;
12911 			goto case;
12912 			case ODA_DRAWENTIRE:
12913 				auto painter = WidgetPainter(this.simpleWindowWrappingHwnd.draw(true), this);
12914 				//painter.impl.hdc = hdc;
12915 				paint(painter);
12916 			break;
12917 			default:
12918 		}
12919 		return 1;
12920 
12921 	}
12922 
12923 	/++
12924 		Creates a push button with the given label, which may be an image or some text.
12925 
12926 		Bugs:
12927 			If the image is bigger than the button, it may not be displayed in the right position on Linux.
12928 
12929 		History:
12930 			The [ImageLabel] overload was added on June 21, 2021 (dub v10.1).
12931 
12932 			The button with label and image will respect requests to show both on Windows as
12933 			of March 28, 2022 iff you provide a manifest file to opt into common controls v6.
12934 	+/
12935 	this(string label, Widget parent) {
12936 		this(ImageLabel(label), parent);
12937 	}
12938 
12939 	/// ditto
12940 	this(ImageLabel label, Widget parent) {
12941 		bool needsImage;
12942 		version(win32_widgets) {
12943 			super(parent);
12944 
12945 			// BS_BITMAP is set when we want image only, so checking for exactly that combination
12946 			enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText;
12947 			auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0;
12948 
12949 			// could also do a virtual method needsOwnerDraw which default returns true and we control it here. typeid(this) == typeid(Button) for override check.
12950 
12951 			if(needsOwnerDraw) {
12952 				extraStyle |= BS_OWNERDRAW;
12953 				needsImage = true;
12954 			}
12955 
12956 			// the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter
12957 			createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 );
12958 
12959 			if(label.image) {
12960 				sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true);
12961 
12962 				SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle);
12963 			}
12964 
12965 			this.label = label.label;
12966 		} else version(custom_widgets) {
12967 			super(parent);
12968 
12969 			label.label.extractWindowsStyleLabel(this.label_, this.accelerator);
12970 			needsImage = true;
12971 		}
12972 
12973 
12974 		if(needsImage && label.image) {
12975 			this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image);
12976 			this.displayFlags = label.displayFlags;
12977 		}
12978 
12979 		this.alignment = label.alignment;
12980 	}
12981 
12982 	override int minHeight() { return defaultLineHeight + 4; }
12983 
12984 	static class Style : Widget.Style {
12985 		override WidgetBackground background() {
12986 			auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive
12987 
12988 			auto pressed = DynamicState.depressed | DynamicState.hover;
12989 			if((widget.dynamicState & pressed) == pressed) {
12990 				return WidgetBackground(cs.depressedButtonColor());
12991 			} else if(widget.dynamicState & DynamicState.hover) {
12992 				return WidgetBackground(cs.hoveringColor());
12993 			} else {
12994 				return WidgetBackground(cs.buttonColor());
12995 			}
12996 		}
12997 
12998 		override FrameStyle borderStyle() {
12999 			auto pressed = DynamicState.depressed | DynamicState.hover;
13000 			if((widget.dynamicState & pressed) == pressed) {
13001 				return FrameStyle.sunk;
13002 			} else {
13003 				return FrameStyle.risen;
13004 			}
13005 
13006 		}
13007 
13008 		override bool variesWithState(ulong dynamicStateFlags) {
13009 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
13010 		}
13011 	}
13012 	mixin OverrideStyle!Style;
13013 
13014 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
13015 		if(sprite) {
13016 			sprite.drawAt(
13017 				painter,
13018 				bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2),
13019 				Point(0, 0)
13020 			);
13021 		} else {
13022 			Point pos = bounds.upperLeft;
13023 			if(this.height == 16)
13024 				pos.y -= 2; // total hack omg
13025 			painter.drawText(pos, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter);
13026 		}
13027 		return bounds;
13028 	}
13029 
13030 	override int flexBasisWidth() {
13031 		version(win32_widgets) {
13032 			SIZE size;
13033 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
13034 			if(size.cx == 0)
13035 				goto fallback;
13036 			return size.cx + scaleWithDpi(16);
13037 		}
13038 		fallback:
13039 			return scaleWithDpi(cast(int) label.length * 8 + 16);
13040 	}
13041 
13042 	override int flexBasisHeight() {
13043 		version(win32_widgets) {
13044 			SIZE size;
13045 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
13046 			if(size.cy == 0)
13047 				goto fallback;
13048 			return size.cy + scaleWithDpi(6);
13049 		}
13050 		fallback:
13051 			return defaultLineHeight + 4;
13052 	}
13053 }
13054 
13055 /++
13056 	A button with a custom appearance, even on systems where there is a standard button. You can subclass it to override its style, paint, or paintContent functions, or you can modify its members for common changes.
13057 
13058 	History:
13059 		Added January 14, 2024
13060 +/
13061 class CustomButton : Button {
13062 	this(ImageLabel label, Widget parent) {
13063 		super(label, parent);
13064 	}
13065 
13066 	this(string label, Widget parent) {
13067 		super(label, parent);
13068 	}
13069 
13070 	version(win32_widgets)
13071 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
13072 		// paint is driven by handleWmDrawItem instead of minigui's redraw events
13073 		if(hwnd)
13074 			InvalidateRect(hwnd, null, false); // get Windows to trigger the actual redraw
13075 		return;
13076 	}
13077 
13078 	override void paint(WidgetPainter painter) {
13079 		// the parent does `if(hwnd) return;` because
13080 		// normally we don't want to draw on standard controls,
13081 		// but this is an exception if it is an owner drawn button
13082 		// (which is determined in the constructor by testing,
13083 		// at runtime, for the existence of an overridden paint
13084 		// member anyway, so this needed to trigger BS_OWNERDRAW)
13085 		// sdpyPrintDebugString("drawing");
13086 		painter.drawThemed(&paintContent);
13087 	}
13088 }
13089 
13090 /++
13091 	A button with a consistent size, suitable for user commands like OK and CANCEL.
13092 +/
13093 class CommandButton : Button {
13094 	this(string label, Widget parent) {
13095 		super(label, parent);
13096 	}
13097 
13098 	// FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there
13099 
13100 	override int maxHeight() {
13101 		return defaultLineHeight + 4;
13102 	}
13103 
13104 	override int maxWidth() {
13105 		return defaultLineHeight * 4;
13106 	}
13107 
13108 	override int marginLeft() { return 12; }
13109 	override int marginRight() { return 12; }
13110 	override int marginTop() { return 12; }
13111 	override int marginBottom() { return 12; }
13112 }
13113 
13114 ///
13115 enum ArrowDirection {
13116 	left, ///
13117 	right, ///
13118 	up, ///
13119 	down ///
13120 }
13121 
13122 ///
13123 version(custom_widgets)
13124 class ArrowButton : Button {
13125 	///
13126 	this(ArrowDirection direction, Widget parent) {
13127 		super("", parent);
13128 		this.direction = direction;
13129 		triggersOnMultiClick = true;
13130 	}
13131 
13132 	private ArrowDirection direction;
13133 
13134 	override int minHeight() { return scaleWithDpi(16); }
13135 	override int maxHeight() { return scaleWithDpi(16); }
13136 	override int minWidth() { return scaleWithDpi(16); }
13137 	override int maxWidth() { return scaleWithDpi(16); }
13138 
13139 	override void paint(WidgetPainter painter) {
13140 		super.paint(painter);
13141 
13142 		auto cs = getComputedStyle();
13143 
13144 		painter.outlineColor = cs.foregroundColor;
13145 		painter.fillColor = cs.foregroundColor;
13146 
13147 		auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2);
13148 
13149 		final switch(direction) {
13150 			case ArrowDirection.up:
13151 				painter.drawPolygon(
13152 					scaleWithDpi(Point(2, 10) + offset),
13153 					scaleWithDpi(Point(7, 5) + offset),
13154 					scaleWithDpi(Point(12, 10) + offset),
13155 					scaleWithDpi(Point(2, 10) + offset)
13156 				);
13157 			break;
13158 			case ArrowDirection.down:
13159 				painter.drawPolygon(
13160 					scaleWithDpi(Point(2, 6) + offset),
13161 					scaleWithDpi(Point(7, 11) + offset),
13162 					scaleWithDpi(Point(12, 6) + offset),
13163 					scaleWithDpi(Point(2, 6) + offset)
13164 				);
13165 			break;
13166 			case ArrowDirection.left:
13167 				painter.drawPolygon(
13168 					scaleWithDpi(Point(10, 2) + offset),
13169 					scaleWithDpi(Point(5, 7) + offset),
13170 					scaleWithDpi(Point(10, 12) + offset),
13171 					scaleWithDpi(Point(10, 2) + offset)
13172 				);
13173 			break;
13174 			case ArrowDirection.right:
13175 				painter.drawPolygon(
13176 					scaleWithDpi(Point(6, 2) + offset),
13177 					scaleWithDpi(Point(11, 7) + offset),
13178 					scaleWithDpi(Point(6, 12) + offset),
13179 					scaleWithDpi(Point(6, 2) + offset)
13180 				);
13181 			break;
13182 		}
13183 	}
13184 }
13185 
13186 private
13187 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow {
13188 	int x, y;
13189 	Widget par = c;
13190 	while(par) {
13191 		x += par.x;
13192 		y += par.y;
13193 		par = par.parent;
13194 	}
13195 	return [x, y];
13196 }
13197 
13198 version(win32_widgets)
13199 private
13200 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow {
13201 // MapWindowPoints?
13202 	int x, y;
13203 	Widget par = c;
13204 	while(par) {
13205 		x += par.x;
13206 		y += par.y;
13207 		par = par.parent;
13208 		if(par !is null && par.useNativeDrawing())
13209 			break;
13210 	}
13211 	return [x, y];
13212 }
13213 
13214 ///
13215 class ImageBox : Widget {
13216 	private MemoryImage image_;
13217 
13218 	override int widthStretchiness() { return 1; }
13219 	override int heightStretchiness() { return 1; }
13220 	override int widthShrinkiness() { return 1; }
13221 	override int heightShrinkiness() { return 1; }
13222 
13223 	override int flexBasisHeight() {
13224 		return image_.height;
13225 	}
13226 
13227 	override int flexBasisWidth() {
13228 		return image_.width;
13229 	}
13230 
13231 	///
13232 	public void setImage(MemoryImage image){
13233 		this.image_ = image;
13234 		if(this.parentWindow && this.parentWindow.win) {
13235 			if(sprite)
13236 				sprite.dispose();
13237 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
13238 		}
13239 		redraw();
13240 	}
13241 
13242 	/// How to fit the image in the box if they aren't an exact match in size?
13243 	enum HowToFit {
13244 		center, /// centers the image, cropping around all the edges as needed
13245 		crop, /// always draws the image in the upper left, cropping the lower right if needed
13246 		// stretch, /// not implemented
13247 	}
13248 
13249 	private Sprite sprite;
13250 	private HowToFit howToFit_;
13251 
13252 	private Color backgroundColor_;
13253 
13254 	///
13255 	this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) {
13256 		this.image_ = image;
13257 		this.tabStop = false;
13258 		this.howToFit_ = howToFit;
13259 		this.backgroundColor_ = backgroundColor;
13260 		super(parent);
13261 		updateSprite();
13262 	}
13263 
13264 	/// ditto
13265 	this(MemoryImage image, HowToFit howToFit, Widget parent) {
13266 		this(image, howToFit, Color.transparent, parent);
13267 	}
13268 
13269 	private void updateSprite() {
13270 		if(sprite is null && this.parentWindow && this.parentWindow.win) {
13271 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
13272 		}
13273 	}
13274 
13275 	override void paint(WidgetPainter painter) {
13276 		updateSprite();
13277 		if(backgroundColor_.a) {
13278 			painter.fillColor = backgroundColor_;
13279 			painter.drawRectangle(Point(0, 0), width, height);
13280 		}
13281 		if(howToFit_ == HowToFit.crop)
13282 			sprite.drawAt(painter, Point(0, 0));
13283 		else if(howToFit_ == HowToFit.center) {
13284 			sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2));
13285 		}
13286 	}
13287 }
13288 
13289 ///
13290 class TextLabel : Widget {
13291 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; }
13292 	override int maxHeight() { return minHeight; }
13293 	override int minWidth() { return 32; }
13294 
13295 	override int flexBasisHeight() { return minHeight(); }
13296 	override int flexBasisWidth() { return defaultTextWidth(label); }
13297 
13298 	string label_;
13299 
13300 	/++
13301 		Indicates which other control this label is here for. Similar to HTML `for` attribute.
13302 
13303 		In practice this means a click on the label will focus the `labelFor`. In future versions
13304 		it will also set screen reader hints but that is not yet implemented.
13305 
13306 		History:
13307 			Added October 3, 2021 (dub v10.4)
13308 	+/
13309 	Widget labelFor;
13310 
13311 	///
13312 	@scriptable
13313 	string label() { return label_; }
13314 
13315 	///
13316 	@scriptable
13317 	void label(string l) {
13318 		label_ = l;
13319 		version(win32_widgets) {
13320 			WCharzBuffer bfr = WCharzBuffer(l);
13321 			SetWindowTextW(hwnd, bfr.ptr);
13322 		} else version(custom_widgets)
13323 			redraw();
13324 	}
13325 
13326 	override void defaultEventHandler_click(scope ClickEvent ce) {
13327 		if(this.labelFor !is null)
13328 			this.labelFor.focus();
13329 	}
13330 
13331 	/++
13332 		WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version.
13333 		For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly.
13334 	+/
13335 	this(string label, TextAlignment alignment, Widget parent) {
13336 		this.label_ = label;
13337 		this.alignment = alignment;
13338 		this.tabStop = false;
13339 		super(parent);
13340 
13341 		version(win32_widgets)
13342 		createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT);
13343 	}
13344 
13345 	/// ditto
13346 	this(string label, Widget parent) {
13347 		this(label, TextAlignment.Right, parent);
13348 	}
13349 
13350 	TextAlignment alignment;
13351 
13352 	version(custom_widgets)
13353 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
13354 		painter.outlineColor = getComputedStyle().foregroundColor;
13355 		painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment);
13356 		return bounds;
13357 	}
13358 }
13359 
13360 class TextDisplayHelper : Widget {
13361 	protected TextLayouter l;
13362 	protected ScrollMessageWidget smw;
13363 
13364 	private const(TextLayouter.State)*[] undoStack;
13365 	private const(TextLayouter.State)*[] redoStack;
13366 
13367 	private string preservedPrimaryText;
13368 	protected void selectionChanged() {
13369 		// sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString());
13370 		static if(UsingSimpledisplayX11)
13371 		with(l.selection()) {
13372 			if(!isEmpty()) {
13373 				//sdpyPrintDebugString("!isEmpty");
13374 
13375 				getPrimarySelection(parentWindow.win, (in char[] txt) {
13376 					// sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")");
13377 					// import std.stdio; writeln("txt: ", txt, " sel: ", getContentString);
13378 					if(txt.length) {
13379 						preservedPrimaryText = txt.idup;
13380 						// writeln(preservedPrimaryText);
13381 					}
13382 
13383 					setPrimarySelection(parentWindow.win, getContentString());
13384 				});
13385 			}
13386 		}
13387 	}
13388 
13389 	final TextLayouter layouter() {
13390 		return l;
13391 	}
13392 
13393 	bool readonly;
13394 	bool caretNavigation; // scroll lock can flip this
13395 	bool singleLine;
13396 	bool acceptsTabInput;
13397 
13398 	private Menu ctx;
13399 	override Menu contextMenu(int x, int y) {
13400 		if(ctx is null) {
13401 			ctx = new Menu("Actions", this);
13402 			if(!readonly) {
13403 				ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo)));
13404 				ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo)));
13405 				ctx.addSeparator();
13406 			}
13407 			if(!readonly)
13408 				ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut)));
13409 			ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, &copy)));
13410 			if(!readonly)
13411 				ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste)));
13412 			if(!readonly)
13413 				ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection)));
13414 			ctx.addSeparator();
13415 			ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll)));
13416 		}
13417 		return ctx;
13418 	}
13419 
13420 	override void defaultEventHandler_blur(BlurEvent ev) {
13421 		super.defaultEventHandler_blur(ev);
13422 		if(l.wasMutated()) {
13423 			auto evt = new ChangeEvent!string(this, &this.content);
13424 			evt.dispatch();
13425 			l.clearWasMutatedFlag();
13426 		}
13427 	}
13428 
13429 	private string content() {
13430 		return l.getTextString();
13431 	}
13432 
13433 	void undo() {
13434 		if(readonly) return;
13435 		if(undoStack.length) {
13436 			auto state = undoStack[$-1];
13437 			undoStack = undoStack[0 .. $-1];
13438 			undoStack.assumeSafeAppend();
13439 			redoStack ~= l.saveState();
13440 			l.restoreState(state);
13441 			adjustScrollbarSizes();
13442 			scrollForCaret();
13443 			redraw();
13444 			stateCheckpoint = true;
13445 		}
13446 	}
13447 
13448 	void redo() {
13449 		if(readonly) return;
13450 		if(redoStack.length) {
13451 			doStateCheckpoint();
13452 			auto state = redoStack[$-1];
13453 			redoStack = redoStack[0 .. $-1];
13454 			redoStack.assumeSafeAppend();
13455 			l.restoreState(state);
13456 			adjustScrollbarSizes();
13457 			scrollForCaret();
13458 			redraw();
13459 			stateCheckpoint = true;
13460 		}
13461 	}
13462 
13463 	void cut() {
13464 		if(readonly) return;
13465 		with(l.selection()) {
13466 			if(!isEmpty()) {
13467 				setClipboardText(parentWindow.win, getContentString());
13468 				doStateCheckpoint();
13469 				replaceContent("");
13470 				adjustScrollbarSizes();
13471 				scrollForCaret();
13472 				this.redraw();
13473 			}
13474 		}
13475 
13476 	}
13477 
13478 	void copy() {
13479 		with(l.selection()) {
13480 			if(!isEmpty()) {
13481 				setClipboardText(parentWindow.win, getContentString());
13482 				this.redraw();
13483 			}
13484 		}
13485 	}
13486 
13487 	void paste() {
13488 		if(readonly) return;
13489 		getClipboardText(parentWindow.win, (txt) {
13490 			doStateCheckpoint();
13491 			if(singleLine)
13492 				l.selection.replaceContent(txt.stripInternal());
13493 			else
13494 				l.selection.replaceContent(txt);
13495 			adjustScrollbarSizes();
13496 			scrollForCaret();
13497 			this.redraw();
13498 		});
13499 	}
13500 
13501 	void deleteContentOfSelection() {
13502 		if(readonly) return;
13503 		doStateCheckpoint();
13504 		l.selection.replaceContent("");
13505 		l.selection.setUserXCoordinate();
13506 		adjustScrollbarSizes();
13507 		scrollForCaret();
13508 		redraw();
13509 	}
13510 
13511 	void selectAll() {
13512 		with(l.selection) {
13513 			moveToStartOfDocument();
13514 			setAnchor();
13515 			moveToEndOfDocument();
13516 			setFocus();
13517 
13518 			selectionChanged();
13519 		}
13520 		redraw();
13521 	}
13522 
13523 	protected bool stateCheckpoint = true;
13524 
13525 	protected void doStateCheckpoint() {
13526 		if(stateCheckpoint) {
13527 			undoStack ~= l.saveState();
13528 			stateCheckpoint = false;
13529 		}
13530 	}
13531 
13532 	protected void adjustScrollbarSizes() {
13533 		// FIXME: will want a content area helper function instead of doing all these subtractions myself
13534 		auto borderWidth = 2;
13535 		this.smw.setTotalArea(l.width, l.height);
13536 		this.smw.setViewableArea(
13537 			this.width - this.paddingLeft - this.paddingRight - borderWidth * 2,
13538 			this.height - this.paddingTop - this.paddingBottom - borderWidth * 2);
13539 	}
13540 
13541 	protected void scrollForCaret() {
13542 		// writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom);
13543 		smw.scrollIntoView(l.selection.focusBoundingBox());
13544 	}
13545 
13546 	// FIXME: this should be a theme changed event listener instead
13547 	private BaseVisualTheme currentTheme;
13548 	override void recomputeChildLayout() {
13549 		if(currentTheme is null)
13550 			currentTheme = WidgetPainter.visualTheme;
13551 		if(WidgetPainter.visualTheme !is currentTheme) {
13552 			currentTheme = WidgetPainter.visualTheme;
13553 			auto ds = this.l.defaultStyle;
13554 			if(auto ms = cast(MyTextStyle) ds) {
13555 				auto cs = getComputedStyle();
13556 				auto font = cs.font();
13557 				if(font !is null)
13558 					ms.font_ = font;
13559 				else {
13560 					auto osc = new OperatingSystemFont();
13561 					osc.loadDefault;
13562 					ms.font_ = osc;
13563 				}
13564 			}
13565 		}
13566 		super.recomputeChildLayout();
13567 	}
13568 
13569 	private Point adjustForSingleLine(Point p) {
13570 		if(singleLine)
13571 			return Point(p.x, this.height / 2);
13572 		else
13573 			return p;
13574 	}
13575 
13576 	private bool wordWrapEnabled_;
13577 
13578 	this(TextLayouter l, ScrollMessageWidget parent) {
13579 		this.smw = parent;
13580 
13581 		smw.addDefaultWheelListeners(16, 16, 8);
13582 		smw.movementPerButtonClick(16, 16);
13583 
13584 		this.defaultPadding = Rectangle(2, 2, 2, 2);
13585 
13586 		this.l = l;
13587 		super(parent);
13588 
13589 		smw.addEventListener((scope ScrollEvent se) {
13590 			this.redraw();
13591 		});
13592 
13593 		this.addEventListener((scope ResizeEvent re) {
13594 			// FIXME: I should add a method to give this client area width thing
13595 			if(wordWrapEnabled_)
13596 				this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight;
13597 
13598 			adjustScrollbarSizes();
13599 			scrollForCaret();
13600 
13601 			this.redraw();
13602 		});
13603 
13604 	}
13605 
13606 	private {
13607 		bool mouseDown;
13608 		bool mouseActuallyMoved;
13609 
13610 		Point downAt;
13611 
13612 		Timer autoscrollTimer;
13613 		int autoscrollDirection;
13614 		int autoscrollAmount;
13615 
13616 		void autoscroll() {
13617 			switch(autoscrollDirection) {
13618 				case 0: smw.scrollUp(autoscrollAmount); break;
13619 				case 1: smw.scrollDown(autoscrollAmount); break;
13620 				case 2: smw.scrollLeft(autoscrollAmount); break;
13621 				case 3: smw.scrollRight(autoscrollAmount); break;
13622 				default: assert(0);
13623 			}
13624 
13625 			this.redraw();
13626 		}
13627 
13628 		void setAutoscrollTimer(int direction, int amount) {
13629 			if(autoscrollTimer is null) {
13630 				autoscrollTimer = new Timer(1000 / 60, &autoscroll);
13631 			}
13632 
13633 			autoscrollDirection = direction;
13634 			autoscrollAmount = amount;
13635 		}
13636 
13637 		void stopAutoscrollTimer() {
13638 			if(autoscrollTimer !is null) {
13639 				autoscrollTimer.dispose();
13640 				autoscrollTimer = null;
13641 			}
13642 			autoscrollAmount = 0;
13643 			autoscrollDirection = 0;
13644 		}
13645 	}
13646 
13647 	override void defaultEventHandler_mousemove(scope MouseMoveEvent ce) {
13648 		if(mouseDown) {
13649 			auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
13650 
13651 			// FIXME: when scrolling i actually do want a timer.
13652 			// i also want a zone near the sides of the window where i can auto scroll
13653 
13654 			auto scrollMultiplier = scaleWithDpi(16);
13655 			auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster
13656 
13657 			if(!singleLine && movedTo.y < 4) {
13658 				setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor);
13659 			} else
13660 			if(!singleLine && (movedTo.y + 6) > this.height) {
13661 				setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor);
13662 			} else
13663 			if(movedTo.x < 4) {
13664 				setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor);
13665 			} else
13666 			if((movedTo.x + 6) > this.width) {
13667 				setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor);
13668 			} else
13669 				stopAutoscrollTimer();
13670 
13671 			l.selection.moveTo(adjustForSingleLine(smw.position + movedTo));
13672 			l.selection.setFocus();
13673 			mouseActuallyMoved = true;
13674 			this.redraw();
13675 		}
13676 
13677 		super.defaultEventHandler_mousemove(ce);
13678 	}
13679 
13680 	override void defaultEventHandler_mouseup(scope MouseUpEvent ce) {
13681 		// FIXME: assert primary selection
13682 		if(mouseDown && ce.button == MouseButton.left) {
13683 			stateCheckpoint = true;
13684 			//l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop)));
13685 			//l.selection.setFocus();
13686 			mouseDown = false;
13687 			parentWindow.releaseMouseCapture();
13688 			stopAutoscrollTimer();
13689 			this.redraw();
13690 
13691 			if(mouseActuallyMoved)
13692 				selectionChanged();
13693 		}
13694 		//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
13695 
13696 		super.defaultEventHandler_mouseup(ce);
13697 	}
13698 
13699 	static if(UsingSimpledisplayX11)
13700 	override void defaultEventHandler_click(scope ClickEvent ce) {
13701 		if(ce.button == MouseButton.middle) {
13702 			parentWindow.win.getPrimarySelection((txt) {
13703 				doStateCheckpoint();
13704 
13705 				// import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText);
13706 
13707 				if(txt == l.selection.getContentString && preservedPrimaryText.length)
13708 					l.selection.replaceContent(preservedPrimaryText);
13709 				else
13710 					l.selection.replaceContent(txt);
13711 				redraw();
13712 			});
13713 		}
13714 
13715 		super.defaultEventHandler_click(ce);
13716 	}
13717 
13718 	override void defaultEventHandler_dblclick(scope DoubleClickEvent dce) {
13719 		if(dce.button == MouseButton.left) {
13720 			with(l.selection()) {
13721 				// FIXME: for a url or file picker i might wanna use / as a separator intead
13722 				scope dg = delegate const(char)[] (scope return const(char)[] ch) {
13723 					if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r")
13724 						return ch;
13725 					return null;
13726 				};
13727 				find(dg, 1, true).moveToEnd.setAnchor;
13728 				find(dg, 1, false).moveTo.setFocus;
13729 				selectionChanged();
13730 				redraw();
13731 			}
13732 		}
13733 
13734 		super.defaultEventHandler_dblclick(dce);
13735 	}
13736 
13737 	override void defaultEventHandler_mousedown(scope MouseDownEvent ce) {
13738 		if(ce.button == MouseButton.left) {
13739 			downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
13740 			l.selection.moveTo(adjustForSingleLine(smw.position + downAt));
13741 			if(ce.shiftKey)
13742 				l.selection.setFocus();
13743 			else
13744 				l.selection.setAnchor();
13745 			mouseDown = true;
13746 			mouseActuallyMoved = false;
13747 			parentWindow.captureMouse(this);
13748 			this.redraw();
13749 		}
13750 		//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
13751 
13752 		super.defaultEventHandler_mousedown(ce);
13753 	}
13754 
13755 	override void defaultEventHandler_char(scope CharEvent ce) {
13756 		super.defaultEventHandler_char(ce);
13757 
13758 		if(readonly)
13759 			return;
13760 		if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b')
13761 			return; // skip the ctrl+x characters we don't care about as plain text
13762 
13763 		if(singleLine && ce.character == '\n')
13764 			return;
13765 		if(!acceptsTabInput && ce.character == '\t')
13766 			return;
13767 
13768 		doStateCheckpoint();
13769 
13770 		char[4] buffer;
13771 		import arsd.core;
13772 		auto stride = encodeUtf8(buffer, ce.character);
13773 		l.selection.replaceContent(buffer[0 .. stride]);
13774 		l.selection.setUserXCoordinate();
13775 		adjustScrollbarSizes();
13776 		scrollForCaret();
13777 		redraw();
13778 
13779 	}
13780 
13781 	override void defaultEventHandler_keydown(scope KeyDownEvent kde) {
13782 		switch(kde.key) {
13783 			case Key.Up, Key.Down, Key.Left, Key.Right:
13784 			case Key.Home, Key.End:
13785 				stateCheckpoint = true;
13786 				bool setPosition = false;
13787 				switch(kde.key) {
13788 					case Key.Up: l.selection.moveUp(); break;
13789 					case Key.Down: l.selection.moveDown(); break;
13790 					case Key.Left: l.selection.moveLeft(); setPosition = true; break;
13791 					case Key.Right: l.selection.moveRight(); setPosition = true; break;
13792 					case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break;
13793 					case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break;
13794 					default: assert(0);
13795 				}
13796 
13797 				if(kde.shiftKey)
13798 					l.selection.setFocus();
13799 				else
13800 					l.selection.setAnchor();
13801 
13802 				selectionChanged();
13803 
13804 				if(setPosition)
13805 					l.selection.setUserXCoordinate();
13806 				scrollForCaret();
13807 				redraw();
13808 			break;
13809 			case Key.PageUp, Key.PageDown:
13810 				// want to act like the user clicked on the caret again
13811 				// after the scroll operation completed, so it would remain at
13812 				// about the same place on the viewport
13813 				auto oldY = smw.vsb.position;
13814 				smw.defaultKeyboardListener(kde);
13815 				auto newY = smw.vsb.position;
13816 				with(l.selection) {
13817 					auto uc = getUserCoordinate();
13818 					uc.y += newY - oldY;
13819 					moveTo(uc);
13820 
13821 					if(kde.shiftKey)
13822 						setFocus();
13823 					else
13824 						setAnchor();
13825 				}
13826 			break;
13827 			case Key.Delete:
13828 				if(l.selection.isEmpty()) {
13829 					l.selection.setAnchor();
13830 					l.selection.moveRight();
13831 					l.selection.setFocus();
13832 				}
13833 				deleteContentOfSelection();
13834 				adjustScrollbarSizes();
13835 				scrollForCaret();
13836 			break;
13837 			case Key.Insert:
13838 			break;
13839 			case Key.A:
13840 				if(kde.ctrlKey)
13841 					selectAll();
13842 			break;
13843 			case Key.F:
13844 				// find
13845 			break;
13846 			case Key.Z:
13847 				if(kde.ctrlKey)
13848 					undo();
13849 			break;
13850 			case Key.R:
13851 				if(kde.ctrlKey)
13852 					redo();
13853 			break;
13854 			case Key.X:
13855 				if(kde.ctrlKey)
13856 					cut();
13857 			break;
13858 			case Key.C:
13859 				if(kde.ctrlKey)
13860 					copy();
13861 			break;
13862 			case Key.V:
13863 				if(kde.ctrlKey)
13864 					paste();
13865 			break;
13866 			case Key.F1:
13867 				with(l.selection()) {
13868 					moveToStartOfLine();
13869 					setAnchor();
13870 					moveToEndOfLine();
13871 					moveToIncludeAdjacentEndOfLineMarker();
13872 					setFocus();
13873 					replaceContent("");
13874 				}
13875 
13876 				redraw();
13877 			break;
13878 			/*
13879 			case Key.F2:
13880 				l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle(
13881 					//(cast(MyTextStyle) old).font,
13882 					font2,
13883 					Color.red)));
13884 				redraw();
13885 			break;
13886 			*/
13887 			case Key.Tab:
13888 				// we process the char event, so don't want to change focus on it, unless the user overrides that with ctrl
13889 				if(acceptsTabInput && !kde.ctrlKey)
13890 					kde.preventDefault();
13891 			break;
13892 			default:
13893 		}
13894 
13895 		if(!kde.defaultPrevented)
13896 			super.defaultEventHandler_keydown(kde);
13897 	}
13898 
13899 	// we want to delegate all the Widget.Style stuff up to the other class that the user can see
13900 	override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) {
13901 		// this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidget Parent
13902 		if(parent && parent.parent && parent.parent.parent)
13903 			parent.parent.parent.useStyleProperties(dg);
13904 		else
13905 			super.useStyleProperties(dg);
13906 	}
13907 
13908 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; }
13909 	override int maxHeight() {
13910 		if(singleLine)
13911 			return minHeight;
13912 		else
13913 			return super.maxHeight();
13914 	}
13915 
13916 	void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
13917 		painter.setFont(myStyle.font);
13918 		painter.drawText(upperLeft, text);
13919 	}
13920 
13921 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
13922 		//painter.setFont(font);
13923 
13924 		auto cs = getComputedStyle();
13925 		auto defaultColor = cs.foregroundColor;
13926 
13927 		auto old = painter.setClipRectangle(bounds);
13928 		scope(exit) painter.setClipRectangle(old);
13929 
13930 		l.getDrawableText(delegate bool(txt, style, info, carets...) {
13931 			//writeln("Segment: ", txt);
13932 			assert(style !is null);
13933 
13934 			if(info.selections && info.boundingBox.width > 0) {
13935 				auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode
13936 				painter.fillColor = color;
13937 				painter.outlineColor = color;
13938 				painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size));
13939 				painter.outlineColor = cs.selectionForegroundColor;
13940 				//painter.fillColor = Color.white;
13941 			} else {
13942 				painter.outlineColor = defaultColor;
13943 			}
13944 
13945 			if(this.isFocused)
13946 			foreach(idx, caret; carets) {
13947 				if(idx == 0)
13948 					painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height);
13949 				painter.drawLine(
13950 					caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(),
13951 					bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position()
13952 				);
13953 			}
13954 
13955 			if(txt.stripInternal.length) {
13956 				// defaultColor = myStyle.color; // FIXME: so wrong
13957 				if(auto myStyle = cast(MyTextStyle) style)
13958 					drawTextSegment(myStyle, painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
13959 				else if(auto myStyle = cast(MyImageStyle) style)
13960 					myStyle.draw(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
13961 			}
13962 
13963 			if(info.boundingBox.upperLeft.y - smw.position().y > this.height) {
13964 				return false;
13965 			} else {
13966 				return true;
13967 			}
13968 		}, Rectangle(smw.position(), bounds.size));
13969 
13970 		/+
13971 		int place = 0;
13972 		int y = 75;
13973 		foreach(width; widths) {
13974 			painter.fillColor = Color.red;
13975 			painter.drawRectangle(Point(place, y), Size(width, 75));
13976 			//y += 15;
13977 			place += width;
13978 		}
13979 		+/
13980 
13981 		return bounds;
13982 	}
13983 
13984 	static class MyTextStyle : TextStyle {
13985 		OperatingSystemFont font_;
13986 		this(OperatingSystemFont font, bool passwordMode = false) {
13987 			this.font_ = font;
13988 		}
13989 
13990 		override OperatingSystemFont font() {
13991 			return font_;
13992 		}
13993 	}
13994 
13995 	static class MyImageStyle : TextStyle, MeasurableFont {
13996 		MemoryImage image_;
13997 		Image converted;
13998 		this(MemoryImage image) {
13999 			this.image_ =  image;
14000 			this.converted = Image.fromMemoryImage(image);
14001 		}
14002 
14003 		bool isMonospace() { return false; }
14004 		int averageWidth() { return image_.width; }
14005 		int height() { return image_.height; }
14006 		int ascent() { return image_.height; }
14007 		int descent() { return 0; }
14008 
14009 		int stringWidth(scope const(char)[] s, SimpleWindow window = null) {
14010 			return image_.width;
14011 		}
14012 
14013 		override MeasurableFont font() {
14014 			return this;
14015 		}
14016 
14017 		void draw(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
14018 			painter.drawImage(upperLeft, converted);
14019 		}
14020 	}
14021 }
14022 
14023 /+
14024 class TextWidget : Widget {
14025 	TextLayouter l;
14026 	ScrollMessageWidget smw;
14027 	TextDisplayHelper helper;
14028 	this(TextLayouter l, Widget parent) {
14029 		this.l = l;
14030 		super(parent);
14031 
14032 		smw = new ScrollMessageWidget(this);
14033 		//smw.horizontalScrollBar.hide;
14034 		//smw.verticalScrollBar.hide;
14035 		smw.addDefaultWheelListeners(16, 16, 8);
14036 		smw.movementPerButtonClick(16, 16);
14037 		helper = new TextDisplayHelper(l, smw);
14038 
14039 		// no need to do this here since there's gonna be a resize
14040 		// event immediately before any drawing
14041 		// smw.setTotalArea(l.width, l.height);
14042 		smw.setViewableArea(
14043 			this.width - this.paddingLeft - this.paddingRight,
14044 			this.height - this.paddingTop - this.paddingBottom);
14045 
14046 		/+
14047 		writeln(l.width, "x", l.height);
14048 		+/
14049 	}
14050 }
14051 +/
14052 
14053 
14054 
14055 
14056 /+
14057 	make sure it calls parentWindow.inputProxy.setIMEPopupLocation too
14058 +/
14059 
14060 /++
14061 	Contains the implementation of text editing and shared basic api. You should construct one of the child classes instead, like [TextEdit], [LineEdit], or [PasswordEdit].
14062 +/
14063 abstract class EditableTextWidget : Widget {
14064 	protected this(Widget parent) {
14065 		version(custom_widgets)
14066 			this(true, parent);
14067 		else
14068 			this(false, parent);
14069 	}
14070 
14071 	private bool useCustomWidget;
14072 
14073 	protected this(bool useCustomWidget, Widget parent) {
14074 		this.useCustomWidget = useCustomWidget;
14075 
14076 		super(parent);
14077 
14078 		if(useCustomWidget)
14079 			setupCustomTextEditing();
14080 	}
14081 
14082 	private bool wordWrapEnabled_;
14083 	/++
14084 		Enables or disables wrapping of long lines on word boundaries.
14085 	+/
14086 	void wordWrapEnabled(bool enabled) {
14087 		if(useCustomWidget) {
14088 			wordWrapEnabled_ = enabled;
14089 			if(tdh)
14090 				tdh.wordWrapEnabled_ = true;
14091 			textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME
14092 		} else version(win32_widgets) {
14093 			SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0);
14094 		}
14095 	}
14096 
14097 	override int minWidth() { return scaleWithDpi(16); }
14098 	override int widthStretchiness() { return 7; }
14099 	override int widthShrinkiness() { return 1; }
14100 
14101 	override int maxHeight() {
14102 		if(useCustomWidget)
14103 			return tdh.maxHeight;
14104 		else
14105 			return super.maxHeight();
14106 	}
14107 
14108 	override void focus() {
14109 		if(useCustomWidget && tdh)
14110 			tdh.focus();
14111 		else
14112 			super.focus();
14113 	}
14114 
14115 	override void defaultEventHandler_focusout(FocusOutEvent foe) {
14116 		if(tdh !is null && foe.target is tdh)
14117 			tdh.redraw();
14118 	}
14119 
14120 	override void defaultEventHandler_focusin(FocusInEvent foe) {
14121 		if(tdh !is null && foe.target is tdh)
14122 			tdh.redraw();
14123 	}
14124 
14125 
14126 	/++
14127 		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.
14128 	+/
14129 	void selectAll() {
14130 		if(useCustomWidget) {
14131 			tdh.selectAll();
14132 		} else version(win32_widgets) {
14133 			SendMessage(hwnd, EM_SETSEL, 0, -1);
14134 		}
14135 	}
14136 
14137 	/++
14138 		Basic clipboard operations.
14139 
14140 		History:
14141 			Added December 31, 2024
14142 	+/
14143 	void copy() {
14144 		if(useCustomWidget) {
14145 			tdh.copy();
14146 		} else version(win32_widgets) {
14147 			SendMessage(hwnd, WM_COPY, 0, 0);
14148 		}
14149 	}
14150 
14151 	/// ditto
14152 	void cut() {
14153 		if(useCustomWidget) {
14154 			tdh.cut();
14155 		} else version(win32_widgets) {
14156 			SendMessage(hwnd, WM_CUT, 0, 0);
14157 		}
14158 	}
14159 
14160 	/// ditto
14161 	void paste() {
14162 		if(useCustomWidget) {
14163 			tdh.paste();
14164 		} else version(win32_widgets) {
14165 			SendMessage(hwnd, WM_PASTE, 0, 0);
14166 		}
14167 	}
14168 
14169 	///
14170 	void undo() {
14171 		if(useCustomWidget) {
14172 			tdh.undo();
14173 		} else version(win32_widgets) {
14174 			SendMessage(hwnd, EM_UNDO, 0, 0);
14175 		}
14176 	}
14177 
14178 	// note that WM_CLEAR deletes the selection without copying it to the clipboard
14179 	// also windows supports margins, modified flag, and much more
14180 
14181 	// EM_UNDO and EM_CANUNDO. EM_REDO is only supported in rich text boxes here
14182 
14183 	// EM_GETSEL, EM_REPLACESEL, and EM_SETSEL might be usable for find etc.
14184 
14185 
14186 
14187 	/*protected*/ TextDisplayHelper tdh;
14188 	/*protected*/ TextLayouter textLayout;
14189 
14190 	/++
14191 		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.
14192 	+/
14193 	@property string content() {
14194 		if(useCustomWidget) {
14195 			return textLayout.getTextString();
14196 		} else version(win32_widgets) {
14197 			wchar[4096] bufferstack;
14198 			wchar[] buffer;
14199 			auto len = GetWindowTextLength(hwnd);
14200 			if(len < bufferstack.length)
14201 				buffer = bufferstack[0 .. len + 1];
14202 			else
14203 				buffer = new wchar[](len + 1);
14204 
14205 			auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length);
14206 			if(l >= 0)
14207 				return makeUtf8StringFromWindowsString(buffer[0 .. l]);
14208 			else
14209 				return null;
14210 		}
14211 
14212 		assert(0);
14213 	}
14214 	/// ditto
14215 	@property void content(string s) {
14216 		if(useCustomWidget) {
14217 			with(textLayout.selection) {
14218 				moveToStartOfDocument();
14219 				setAnchor();
14220 				moveToEndOfDocument();
14221 				setFocus();
14222 				replaceContent(s);
14223 			}
14224 
14225 			tdh.adjustScrollbarSizes();
14226 			// these don't seem to help
14227 			// tdh.smw.setPosition(0, 0);
14228 			// tdh.scrollForCaret();
14229 
14230 			redraw();
14231 		} else version(win32_widgets) {
14232 			WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines);
14233 			SetWindowTextW(hwnd, bfr.ptr);
14234 		}
14235 	}
14236 
14237 	/++
14238 		Appends some text to the widget at the end, without affecting the user selection or cursor position.
14239 	+/
14240 	void addText(string txt) {
14241 		if(useCustomWidget) {
14242 			textLayout.appendText(txt);
14243 			tdh.adjustScrollbarSizes();
14244 			redraw();
14245 		} else version(win32_widgets) {
14246 			// get the current selection
14247 			DWORD StartPos, EndPos;
14248 			SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) );
14249 
14250 			// move the caret to the end of the text
14251 			int outLength = GetWindowTextLengthW(hwnd);
14252 			SendMessageW( hwnd, EM_SETSEL, outLength, outLength );
14253 
14254 			// insert the text at the new caret position
14255 			WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines);
14256 			SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr );
14257 
14258 			// restore the previous selection
14259 			SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos );
14260 		}
14261 	}
14262 
14263 	// EM_SCROLLCARET scrolls the caret into view
14264 
14265 	void scrollToBottom() {
14266 		if(useCustomWidget) {
14267 			tdh.smw.scrollDown(int.max);
14268 		} else version(win32_widgets) {
14269 			SendMessageW( hwnd, EM_LINESCROLL, 0, int.max );
14270 		}
14271 	}
14272 
14273 	protected TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
14274 		return new TextDisplayHelper(textLayout, smw);
14275 	}
14276 
14277 	protected TextStyle defaultTextStyle() {
14278 		return new TextDisplayHelper.MyTextStyle(getUsedFont());
14279 	}
14280 
14281 	private OperatingSystemFont getUsedFont() {
14282 		auto cs = getComputedStyle();
14283 		auto font = cs.font;
14284 		if(font is null) {
14285 			font = new OperatingSystemFont;
14286 			font.loadDefault();
14287 		}
14288 		return font;
14289 	}
14290 
14291 	protected void setupCustomTextEditing() {
14292 		textLayout = new TextLayouter(defaultTextStyle());
14293 
14294 		auto smw = new ScrollMessageWidget(this);
14295 		if(!showingHorizontalScroll)
14296 			smw.horizontalScrollBar.hide();
14297 		if(!showingVerticalScroll)
14298 			smw.verticalScrollBar.hide();
14299 		this.tabStop = false;
14300 		smw.tabStop = false;
14301 		tdh = textDisplayHelperFactory(textLayout, smw);
14302 	}
14303 
14304 	override void newParentWindow(Window old, Window n) {
14305 		if(n is null) return;
14306 		this.parentWindow.addEventListener((scope DpiChangedEvent dce) {
14307 			if(textLayout) {
14308 				if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) {
14309 					// the dpi change can change the font, so this informs the layouter that it has changed too
14310 					style.font_ = getUsedFont();
14311 
14312 					// arsd.core.writeln(this.parentWindow.win.actualDpi);
14313 				}
14314 			}
14315 		});
14316 	}
14317 
14318 	static class Style : Widget.Style {
14319 		override WidgetBackground background() {
14320 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
14321 		}
14322 
14323 		override Color foregroundColor() {
14324 			return WidgetPainter.visualTheme.foregroundColor;
14325 		}
14326 
14327 		override FrameStyle borderStyle() {
14328 			return FrameStyle.sunk;
14329 		}
14330 
14331 		override MouseCursor cursor() {
14332 			return GenericCursor.Text;
14333 		}
14334 	}
14335 	mixin OverrideStyle!Style;
14336 
14337 	version(win32_widgets) {
14338 		private string lastContentBlur;
14339 
14340 		override void defaultEventHandler_blur(BlurEvent ev) {
14341 			super.defaultEventHandler_blur(ev);
14342 
14343 			if(!useCustomWidget)
14344 			if(this.content != lastContentBlur) {
14345 				auto evt = new ChangeEvent!string(this, &this.content);
14346 				evt.dispatch();
14347 				lastContentBlur = this.content;
14348 			}
14349 		}
14350 	}
14351 
14352 
14353 	bool showingVerticalScroll() { return true; }
14354 	bool showingHorizontalScroll() { return true; }
14355 }
14356 
14357 /++
14358 	A `LineEdit` is an editor of a single line of text, comparable to a HTML `<input type="text" />`.
14359 
14360 	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.
14361 
14362 	See_Also:
14363 		[PasswordEdit] for a `LineEdit` that obscures its input.
14364 
14365 		[TextEdit] for a multi-line plain text editor widget.
14366 
14367 		[TextLabel] for a single line piece of static text.
14368 
14369 		[TextDisplay] for a read-only display of a larger piece of plain text.
14370 +/
14371 class LineEdit : EditableTextWidget {
14372 	override bool showingVerticalScroll() { return false; }
14373 	override bool showingHorizontalScroll() { return false; }
14374 
14375 	override int flexBasisWidth() { return 250; }
14376 	override int widthShrinkiness() { return 10; }
14377 
14378 	///
14379 	this(Widget parent) {
14380 		super(parent);
14381 		version(win32_widgets) {
14382 			createWin32Window(this, "edit"w, "",
14383 				0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
14384 		} else version(custom_widgets) {
14385 		} else static assert(false);
14386 	}
14387 
14388 	private this(bool useCustomWidget, Widget parent) {
14389 		if(!useCustomWidget)
14390 			this(parent);
14391 		else
14392 			super(true, parent);
14393 	}
14394 
14395 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
14396 		auto tdh = new TextDisplayHelper(textLayout, smw);
14397 		tdh.singleLine = true;
14398 		return tdh;
14399 	}
14400 
14401 	version(win32_widgets) {
14402 		mixin Padding!q{0};
14403 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
14404 		override int maxHeight() { return minHeight; }
14405 	}
14406 
14407 	/+
14408 	@property void passwordMode(bool p) {
14409 		SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD);
14410 	}
14411 	+/
14412 }
14413 
14414 /// ditto
14415 class CustomLineEdit : LineEdit {
14416 	this(Widget parent) {
14417 		super(true, parent);
14418 	}
14419 }
14420 
14421 /++
14422 	A [LineEdit] that displays `*` in place of the actual characters.
14423 
14424 	Alas, Windows requires the window to be created differently to use this style,
14425 	so it had to be a new class instead of a toggle on and off on an existing object.
14426 
14427 	History:
14428 		Added January 24, 2021
14429 
14430 		Implemented on Linux on January 31, 2023.
14431 +/
14432 class PasswordEdit : EditableTextWidget {
14433 	override bool showingVerticalScroll() { return false; }
14434 	override bool showingHorizontalScroll() { return false; }
14435 
14436 	override int flexBasisWidth() { return 250; }
14437 
14438 	override TextStyle defaultTextStyle() {
14439 		auto cs = getComputedStyle();
14440 
14441 		auto osf = new class OperatingSystemFont {
14442 			this() {
14443 				super(cs.font);
14444 			}
14445 			override int stringWidth(scope const(char)[] text, SimpleWindow window = null) {
14446 				int count = 0;
14447 				foreach(dchar ch; text)
14448 					count++;
14449 				return count * super.stringWidth("*", window);
14450 			}
14451 		};
14452 
14453 		return new TextDisplayHelper.MyTextStyle(osf);
14454 	}
14455 
14456 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
14457 		static class TDH : TextDisplayHelper {
14458 			this(TextLayouter textLayout, ScrollMessageWidget smw) {
14459 				singleLine = true;
14460 				super(textLayout, smw);
14461 			}
14462 
14463 			override void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
14464 				char[256] buffer = void;
14465 				int bufferLength = 0;
14466 				foreach(dchar ch; text)
14467 					buffer[bufferLength++] = '*';
14468 				painter.setFont(myStyle.font);
14469 				painter.drawText(upperLeft, buffer[0..bufferLength]);
14470 			}
14471 		}
14472 
14473 		return new TDH(textLayout, smw);
14474 	}
14475 
14476 	///
14477 	this(Widget parent) {
14478 		super(parent);
14479 		version(win32_widgets) {
14480 			createWin32Window(this, "edit"w, "",
14481 				ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
14482 		} else version(custom_widgets) {
14483 		} else static assert(false);
14484 	}
14485 
14486 	private this(bool useCustomWidget, Widget parent) {
14487 		if(!useCustomWidget)
14488 			this(parent);
14489 		else
14490 			super(true, parent);
14491 	}
14492 
14493 	version(win32_widgets) {
14494 		mixin Padding!q{2};
14495 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
14496 		override int maxHeight() { return minHeight; }
14497 	}
14498 }
14499 
14500 /// ditto
14501 class CustomPasswordEdit : PasswordEdit {
14502 	this(Widget parent) {
14503 		super(true, parent);
14504 	}
14505 }
14506 
14507 
14508 /++
14509 	A `TextEdit` is a multi-line plain text editor, comparable to a HTML `<textarea>`.
14510 
14511 	See_Also:
14512 		[TextDisplay] for a read-only text display.
14513 
14514 		[LineEdit] for a single line text editor.
14515 
14516 		[PasswordEdit] for a single line text editor that obscures its input.
14517 +/
14518 class TextEdit : EditableTextWidget {
14519 	///
14520 	this(Widget parent) {
14521 		super(parent);
14522 		version(win32_widgets) {
14523 			createWin32Window(this, "edit"w, "",
14524 				0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE);
14525 		} else version(custom_widgets) {
14526 		} else static assert(false);
14527 	}
14528 
14529 	private this(bool useCustomWidget, Widget parent) {
14530 		if(!useCustomWidget)
14531 			this(parent);
14532 		else
14533 			super(true, parent);
14534 	}
14535 
14536 	override int maxHeight() { return int.max; }
14537 	override int heightStretchiness() { return 7; }
14538 
14539 	override int flexBasisWidth() { return 250; }
14540 	override int flexBasisHeight() { return 25; }
14541 }
14542 
14543 /// ditto
14544 class CustomTextEdit : TextEdit {
14545 	this(Widget parent) {
14546 		super(true, parent);
14547 	}
14548 }
14549 
14550 /+
14551 /++
14552 
14553 +/
14554 version(none)
14555 class RichTextDisplay : Widget {
14556 	@property void content(string c) {}
14557 	void appendContent(string c) {}
14558 }
14559 +/
14560 
14561 /++
14562 	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.
14563 
14564 	History:
14565 		Added October 31, 2023 (dub v11.3)
14566 +/
14567 class TextDisplay : EditableTextWidget {
14568 	this(string text, Widget parent) {
14569 		super(true, parent);
14570 		this.content = text;
14571 	}
14572 
14573 	override int maxHeight() { return int.max; }
14574 	override int minHeight() { return Window.defaultLineHeight; }
14575 	override int heightStretchiness() { return 7; }
14576 	override int heightShrinkiness() { return 2; }
14577 
14578 	override int flexBasisWidth() {
14579 		return scaleWithDpi(250);
14580 	}
14581 	override int flexBasisHeight() {
14582 		if(textLayout is null || this.tdh is null)
14583 			return Window.defaultLineHeight;
14584 
14585 		auto textHeight = borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textLayout.height))).height;
14586 		return this.tdh.borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textHeight))).height;
14587 	}
14588 
14589 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
14590 		return new MyTextDisplayHelper(textLayout, smw);
14591 	}
14592 
14593 	override void registerMovement() {
14594 		super.registerMovement();
14595 		this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally
14596 	}
14597 
14598 	static class MyTextDisplayHelper : TextDisplayHelper {
14599 		this(TextLayouter textLayout, ScrollMessageWidget smw) {
14600 			smw.verticalScrollBar.hide();
14601 			smw.horizontalScrollBar.hide();
14602 			super(textLayout, smw);
14603 			this.readonly = true;
14604 		}
14605 
14606 		override void registerMovement() {
14607 			super.registerMovement();
14608 
14609 			// FIXME: do the horizontal one too as needed and make sure that it does
14610 			// wordwrapping again
14611 			if(l.height + smw.horizontalScrollBar.height > this.height)
14612 				smw.verticalScrollBar.show();
14613 			else
14614 				smw.verticalScrollBar.hide();
14615 
14616 			l.wordWrapWidth = this.width;
14617 
14618 			smw.verticalScrollBar.setPosition = 0;
14619 		}
14620 	}
14621 
14622 	class Style : Widget.Style {
14623 		// just want the generic look for these
14624 	}
14625 
14626 	mixin OverrideStyle!Style;
14627 }
14628 
14629 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active
14630 /++
14631 	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.
14632 
14633 
14634 	When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`.
14635 
14636 	Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand.
14637 
14638 	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.
14639 
14640 	History:
14641 		Added August 12, 2024 (dub v11.6)
14642 +/
14643 abstract class GenericListViewWidget : Widget {
14644 	/++
14645 
14646 	+/
14647 	this(Widget parent) {
14648 		super(parent);
14649 
14650 		smw = new ScrollMessageWidget(this);
14651 		smw.addDefaultKeyboardListeners(itemSize.height, itemSize.width);
14652 		smw.addDefaultWheelListeners(itemSize.height, itemSize.width);
14653 		smw.hsb.hide(); // FIXME: this might actually be useful but we can't really communicate that yet
14654 
14655 		inner = new GenericListViewWidgetInner(this, smw, new GenericListViewInnerContainer(smw));
14656 		inner.tabStop = this.tabStop;
14657 		this.tabStop = false;
14658 	}
14659 
14660 	private ScrollMessageWidget smw;
14661 	private GenericListViewWidgetInner inner;
14662 
14663 	/++
14664 
14665 	+/
14666 	abstract GenericListViewItem itemFactory(Widget parent);
14667 	// in device-dependent pixels
14668 	/++
14669 
14670 	+/
14671 	abstract Size itemSize(); // use 0 to indicate it can stretch?
14672 
14673 	enum LayoutMode {
14674 		rows,
14675 		columns,
14676 		gridRowsFirst,
14677 		gridColumnsFirst
14678 	}
14679 	LayoutMode layoutMode() {
14680 		return LayoutMode.rows;
14681 	}
14682 
14683 	private int itemCount_;
14684 
14685 	/++
14686 		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.
14687 	+/
14688 	void setItemCount(int count) {
14689 		smw.setTotalArea(inner.width, count * itemSize().height);
14690 		smw.setViewableArea(inner.width, inner.height);
14691 		this.itemCount_ = count;
14692 	}
14693 
14694 	/++
14695 		Returns the current count of items expected to available in the list.
14696 	+/
14697 	int itemCount() {
14698 		return this.itemCount_;
14699 	}
14700 
14701 	/++
14702 		Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data.
14703 
14704 		Note you must $(I also) call [setItemCount] if the total item count has changed.
14705 	+/
14706 	void notifyItemsChanged(int index, int count = 1) {
14707 	}
14708 	/// ditto
14709 	void notifyItemsInserted(int index, int count = 1) {
14710 	}
14711 	/// ditto
14712 	void notifyItemsRemoved(int index, int count = 1) {
14713 	}
14714 	/// ditto
14715 	void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) {
14716 	}
14717 
14718 	/++
14719 		History:
14720 			Added January 1, 2025
14721 	+/
14722 	void ensureItemVisibleInScroll(int index) {
14723 		auto itemPos = index * itemSize().height;
14724 		auto vsb = smw.verticalScrollBar;
14725 		auto viewable = vsb.viewableArea_;
14726 
14727 		if(viewable == 0) {
14728 			// viewable == 0 isn't actually supposed to happen, this means
14729 			// this method is being called before having our size assigned, it should
14730 			// probably just queue it up for later.
14731 			queuedScroll = index;
14732 			return;
14733 		}
14734 
14735 		queuedScroll = int.min;
14736 
14737 		if(itemPos < vsb.position) {
14738 			// scroll up to it
14739 			vsb.setPosition(itemPos);
14740 			smw.notify();
14741 		} else if(itemPos + itemSize().height > (vsb.position + viewable)) {
14742 			// scroll down to it, so it is at the bottom
14743 
14744 			auto lastViewableItemPosition = (viewable - itemSize.height) / itemSize.height * itemSize.height;
14745 			// need the itemPos to be at the lastViewableItemPosition after scrolling, so subtraction does it
14746 
14747 			vsb.setPosition(itemPos - lastViewableItemPosition);
14748 			smw.notify();
14749 		}
14750 	}
14751 
14752 	/++
14753 		History:
14754 			Added January 1, 2025;
14755 	+/
14756 	int numberOfCurrentlyFullyVisibleItems() {
14757 		return smw.verticalScrollBar.viewableArea_ / itemSize.height;
14758 	}
14759 
14760 	private int queuedScroll = int.min;
14761 
14762 	override void recomputeChildLayout() {
14763 		super.recomputeChildLayout();
14764 		if(queuedScroll != int.min)
14765 			ensureItemVisibleInScroll(queuedScroll);
14766 	}
14767 
14768 	private GenericListViewItem[] items;
14769 
14770 	override void paint(WidgetPainter painter) {}
14771 }
14772 
14773 /// ditto
14774 abstract class GenericListViewItem : Widget {
14775 	/++
14776 	+/
14777 	this(Widget parent) {
14778 		super(parent);
14779 	}
14780 
14781 	private int _currentIndex = -1;
14782 
14783 	private void showItemPrivate(int idx) {
14784 		showItem(idx);
14785 		_currentIndex = idx;
14786 	}
14787 
14788 	/++
14789 		Implement this to show an item from your data backing to the list.
14790 
14791 		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.
14792 	+/
14793 	abstract void showItem(int idx);
14794 
14795 	/++
14796 		Maintained by the library after calling [showItem] so the object knows which data index it currently has.
14797 
14798 		It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent).
14799 
14800 		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.
14801 	+/
14802 	final int currentIndexLoaded() {
14803 		return _currentIndex;
14804 	}
14805 }
14806 
14807 ///
14808 unittest {
14809 	import arsd.minigui;
14810 
14811 	import std.conv;
14812 
14813 	void main() {
14814 		auto mw = new MainWindow();
14815 
14816 		static class MyListViewItem : GenericListViewItem {
14817 			this(Widget parent) {
14818 				super(parent);
14819 
14820 				label = new TextLabel("unloaded", TextAlignment.Left, this);
14821 				button = new Button("Click", this);
14822 
14823 				button.addEventListener("triggered", (){
14824 					messageBox(text("clicked ", currentIndexLoaded()));
14825 				});
14826 			}
14827 			override void showItem(int idx) {
14828 				label.label = "Item " ~ to!string(idx);
14829 			}
14830 
14831 			TextLabel label;
14832 			Button button;
14833 		}
14834 
14835 		auto widget = new class GenericListViewWidget {
14836 			this() {
14837 				super(mw);
14838 			}
14839 			override GenericListViewItem itemFactory(Widget parent) {
14840 				return new MyListViewItem(parent);
14841 			}
14842 			override Size itemSize() {
14843 				return Size(0, scaleWithDpi(80));
14844 			}
14845 		};
14846 
14847 		widget.setItemCount(5000);
14848 
14849 		mw.loop();
14850 	}
14851 }
14852 
14853 // this exists just to wrap the actual GenericListViewWidgetInner so borders
14854 // and padding and stuff can work
14855 private class GenericListViewInnerContainer : Widget {
14856 	this(Widget parent) {
14857 		super(parent);
14858 		this.tabStop = false;
14859 	}
14860 
14861 	override void recomputeChildLayout() {
14862 		registerMovement();
14863 
14864 		auto cs = getComputedStyle();
14865 		auto bw = getBorderWidth(cs.borderStyle);
14866 
14867 		assert(children.length < 2);
14868 		foreach(child; children) {
14869 			child.x = bw + paddingLeft();
14870 			child.y = bw + paddingTop();
14871 			child.width = this.width.NonOverflowingUint - bw - bw - paddingLeft() - paddingRight();
14872 			child.height = this.height.NonOverflowingUint - bw - bw - paddingTop() - paddingBottom();
14873 
14874 			child.recomputeChildLayout();
14875 		}
14876 	}
14877 
14878 	override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) {
14879 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
14880 			return parent.parent.parent.useStyleProperties(dg);
14881 		else
14882 			return super.useStyleProperties(dg);
14883 	}
14884 
14885 	override int paddingTop() {
14886 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
14887 			return parent.parent.parent.paddingTop();
14888 		else
14889 			return super.paddingTop();
14890 	}
14891 
14892 	override int paddingBottom() {
14893 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
14894 			return parent.parent.parent.paddingBottom();
14895 		else
14896 			return super.paddingBottom();
14897 	}
14898 
14899 	override int paddingLeft() {
14900 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
14901 			return parent.parent.parent.paddingLeft();
14902 		else
14903 			return super.paddingLeft();
14904 	}
14905 
14906 	override int paddingRight() {
14907 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
14908 			return parent.parent.parent.paddingRight();
14909 		else
14910 			return super.paddingRight();
14911 	}
14912 
14913 
14914 }
14915 
14916 private class GenericListViewWidgetInner : Widget {
14917 	this(GenericListViewWidget glvw, ScrollMessageWidget smw, GenericListViewInnerContainer parent) {
14918 		super(parent);
14919 		this.glvw = glvw;
14920 
14921 		reloadVisible();
14922 
14923 		smw.addEventListener("scroll", () {
14924 			reloadVisible();
14925 		});
14926 	}
14927 
14928 	override void registerMovement() {
14929 		super.registerMovement();
14930 		if(glvw && glvw.smw)
14931 			glvw.smw.setViewableArea(this.width, this.height);
14932 	}
14933 
14934 	void reloadVisible() {
14935 		auto y = glvw.smw.position.y / glvw.itemSize.height;
14936 
14937 		// idk why i had this here it doesn't seem to be ueful and actually made last items diasppear
14938 		//int offset = glvw.smw.position.y % glvw.itemSize.height;
14939 		//if(offset || y >= glvw.itemCount())
14940 			//y--;
14941 
14942 		if(y < 0)
14943 			y = 0;
14944 
14945 		recomputeChildLayout();
14946 
14947 		foreach(item; glvw.items) {
14948 			if(y < glvw.itemCount()) {
14949 				item.showItemPrivate(y);
14950 				item.show();
14951 			} else {
14952 				item.hide();
14953 			}
14954 			y++;
14955 		}
14956 
14957 		this.redraw();
14958 	}
14959 
14960 	private GenericListViewWidget glvw;
14961 
14962 	private bool inRcl;
14963 	override void recomputeChildLayout() {
14964 		if(inRcl)
14965 			return;
14966 		inRcl = true;
14967 		scope(exit)
14968 			inRcl = false;
14969 
14970 		registerMovement();
14971 
14972 		auto ih = glvw.itemSize().height;
14973 
14974 		auto itemCount = this.height / ih + 2; // extra for partial display before and after
14975 		bool hadNew;
14976 		while(glvw.items.length < itemCount) {
14977 			// FIXME: free the old items? maybe just set length
14978 			glvw.items ~= glvw.itemFactory(this);
14979 			hadNew = true;
14980 		}
14981 
14982 		if(hadNew)
14983 			reloadVisible();
14984 
14985 		int y = -(glvw.smw.position.y % ih) + this.paddingTop();
14986 		foreach(child; children) {
14987 			child.x = this.paddingLeft();
14988 			child.y = y;
14989 			y += glvw.itemSize().height;
14990 			child.width = this.width.NonOverflowingUint - this.paddingLeft() - this.paddingRight();
14991 			child.height = ih;
14992 
14993 			child.recomputeChildLayout();
14994 		}
14995 	}
14996 }
14997 
14998 
14999 
15000 /++
15001 	History:
15002 		It was a child of Window before, but as of September 29, 2024, it is now a child of `Dialog`.
15003 +/
15004 class MessageBox : Dialog {
15005 	private string message;
15006 	MessageBoxButton buttonPressed = MessageBoxButton.None;
15007 	/++
15008 
15009 		History:
15010 		The overload that takes `Window originator` was added on September 29, 2024.
15011 	+/
15012 	this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
15013 		this(null, message, buttons, buttonIds);
15014 	}
15015 	/// ditto
15016 	this(Window originator, string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
15017 		message = message.stripRightInternal;
15018 		int mainWidth;
15019 
15020 		// estimate longest line
15021 		int count;
15022 		foreach(ch; message) {
15023 			if(ch == '\n') {
15024 				if(count > mainWidth)
15025 					mainWidth = count;
15026 				count = 0;
15027 			} else {
15028 				count++;
15029 			}
15030 		}
15031 		mainWidth *= 8;
15032 		if(mainWidth < 300)
15033 			mainWidth = 300;
15034 		if(mainWidth > 600)
15035 			mainWidth = 600;
15036 
15037 		super(originator, mainWidth, 100);
15038 
15039 		assert(buttons.length);
15040 		assert(buttons.length ==  buttonIds.length);
15041 
15042 		this.message = message;
15043 
15044 		auto label = new TextDisplay(message, this);
15045 
15046 		auto hl = new HorizontalLayout(this);
15047 		auto spacer = new HorizontalSpacer(hl); // to right align
15048 
15049 		foreach(idx, buttonText; buttons) {
15050 			auto button = new CommandButton(buttonText, hl);
15051 
15052 			button.addEventListener(EventType.triggered, ((size_t idx) { return () {
15053 				this.buttonPressed = buttonIds[idx];
15054 				win.close();
15055 			}; })(idx));
15056 
15057 			if(idx == 0)
15058 				button.focus();
15059 		}
15060 
15061 		if(buttons.length == 1)
15062 			auto spacer2 = new HorizontalSpacer(hl); // to center it
15063 
15064 		auto size = label.flexBasisHeight() + hl.minHeight() + this.paddingTop + this.paddingBottom;
15065 		auto max = scaleWithDpi(600); // random max height
15066 		if(size > max)
15067 			size = max;
15068 
15069 		win.resize(scaleWithDpi(mainWidth), size);
15070 
15071 		win.show();
15072 		redraw();
15073 	}
15074 
15075 	override void OK() {
15076 		this.win.close();
15077 	}
15078 
15079 	mixin Padding!q{16};
15080 }
15081 
15082 ///
15083 enum MessageBoxStyle {
15084 	OK, ///
15085 	OKCancel, ///
15086 	RetryCancel, ///
15087 	YesNo, ///
15088 	YesNoCancel, ///
15089 	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.
15090 }
15091 
15092 ///
15093 enum MessageBoxIcon {
15094 	None, ///
15095 	Info, ///
15096 	Warning, ///
15097 	Error ///
15098 }
15099 
15100 /// Identifies the button the user pressed on a message box.
15101 enum MessageBoxButton {
15102 	None, /// The user closed the message box without clicking any of the buttons.
15103 	OK, ///
15104 	Cancel, ///
15105 	Retry, ///
15106 	Yes, ///
15107 	No, ///
15108 	Continue ///
15109 }
15110 
15111 
15112 /++
15113 	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.
15114 
15115 	Returns: the button pressed.
15116 +/
15117 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15118 	return messageBox(null, title, message, style, icon);
15119 }
15120 
15121 /// ditto
15122 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15123 	return messageBox(null, null, message, style, icon);
15124 }
15125 
15126 /++
15127 
15128 +/
15129 MessageBoxButton messageBox(Window originator, string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15130 	version(win32_widgets) {
15131 		WCharzBuffer t = WCharzBuffer(title);
15132 		WCharzBuffer m = WCharzBuffer(message);
15133 		UINT type;
15134 		with(MessageBoxStyle)
15135 		final switch(style) {
15136 			case OK: type |= MB_OK; break;
15137 			case OKCancel: type |= MB_OKCANCEL; break;
15138 			case RetryCancel: type |= MB_RETRYCANCEL; break;
15139 			case YesNo: type |= MB_YESNO; break;
15140 			case YesNoCancel: type |= MB_YESNOCANCEL; break;
15141 			case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break;
15142 		}
15143 		with(MessageBoxIcon)
15144 		final switch(icon) {
15145 			case None: break;
15146 			case Info: type |= MB_ICONINFORMATION; break;
15147 			case Warning: type |= MB_ICONWARNING; break;
15148 			case Error: type |= MB_ICONERROR; break;
15149 		}
15150 		switch(MessageBoxW(originator is null ? null : originator.win.hwnd, m.ptr, t.ptr, type)) {
15151 			case IDOK: return MessageBoxButton.OK;
15152 			case IDCANCEL: return MessageBoxButton.Cancel;
15153 			case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry;
15154 			case IDYES: return MessageBoxButton.Yes;
15155 			case IDNO: return MessageBoxButton.No;
15156 			case IDCONTINUE: return MessageBoxButton.Continue;
15157 			default: return MessageBoxButton.None;
15158 		}
15159 	} else {
15160 		string[] buttons;
15161 		MessageBoxButton[] buttonIds;
15162 		with(MessageBoxStyle)
15163 		final switch(style) {
15164 			case OK:
15165 				buttons = ["OK"];
15166 				buttonIds = [MessageBoxButton.OK];
15167 			break;
15168 			case OKCancel:
15169 				buttons = ["OK", "Cancel"];
15170 				buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel];
15171 			break;
15172 			case RetryCancel:
15173 				buttons = ["Retry", "Cancel"];
15174 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel];
15175 			break;
15176 			case YesNo:
15177 				buttons = ["Yes", "No"];
15178 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No];
15179 			break;
15180 			case YesNoCancel:
15181 				buttons = ["Yes", "No", "Cancel"];
15182 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel];
15183 			break;
15184 			case RetryCancelContinue:
15185 				buttons = ["Try Again", "Cancel", "Continue"];
15186 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue];
15187 			break;
15188 		}
15189 		auto mb = new MessageBox(originator, message, buttons, buttonIds);
15190 		EventLoop el = EventLoop.get;
15191 		el.run(() { return !mb.win.closed; });
15192 		return mb.buttonPressed;
15193 	}
15194 
15195 }
15196 
15197 /// ditto
15198 int messageBox(Window originator, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15199 	return messageBox(originator, null, message, style, icon);
15200 }
15201 
15202 
15203 ///
15204 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler;
15205 
15206 /++
15207 	This is an opaque type you can use to disconnect an event handler when you're no longer interested.
15208 
15209 	History:
15210 		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.
15211 +/
15212 struct EventListener {
15213 	private Widget widget;
15214 	private string event;
15215 	private EventHandler handler;
15216 	private bool useCapture;
15217 
15218 	///
15219 	void disconnect() {
15220 		widget.removeEventListener(this);
15221 	}
15222 }
15223 
15224 /++
15225 	The purpose of this enum was to give a compile-time checked version of various standard event strings.
15226 
15227 	Now, I recommend you use a statically typed event object instead.
15228 
15229 	See_Also: [Event]
15230 +/
15231 enum EventType : string {
15232 	click = "click", ///
15233 
15234 	mouseenter = "mouseenter", ///
15235 	mouseleave = "mouseleave", ///
15236 	mousein = "mousein", ///
15237 	mouseout = "mouseout", ///
15238 	mouseup = "mouseup", ///
15239 	mousedown = "mousedown", ///
15240 	mousemove = "mousemove", ///
15241 
15242 	keydown = "keydown", ///
15243 	keyup = "keyup", ///
15244 	char_ = "char", ///
15245 
15246 	focus = "focus", ///
15247 	blur = "blur", ///
15248 
15249 	triggered = "triggered", ///
15250 
15251 	change = "change", ///
15252 }
15253 
15254 /++
15255 	Represents an event that is currently being processed.
15256 
15257 
15258 	Minigui's event model is based on the web browser. An event has a name, a target,
15259 	and an associated data object. It starts from the window and works its way down through
15260 	the target through all intermediate [Widget]s, triggering capture phase handlers as it goes,
15261 	then goes back up again all the way back to the window, triggering bubble phase handlers. At
15262 	the end, if [Event.preventDefault] has not been called, it calls the target widget's default
15263 	handlers for the event (please note that default handlers will be called even if [Event.stopPropagation]
15264 	was called; that just stops it from calling other handlers in the widget tree, but the default happens
15265 	whenever propagation is done, not only if it gets to the end of the chain).
15266 
15267 	This model has several nice points:
15268 
15269 	$(LIST
15270 		* It is easy to delegate dynamic handlers to a parent. You can have a parent container
15271 		  with event handlers set, then add/remove children as much as you want without needing
15272 		  to manage the event handlers on them - the parent alone can manage everything.
15273 
15274 		* It is easy to create new custom events in your application.
15275 
15276 		* It is familiar to many web developers.
15277 	)
15278 
15279 	There's a few downsides though:
15280 
15281 	$(LIST
15282 		* There's not a lot of type safety.
15283 
15284 		* You don't get a static list of what events a widget can emit.
15285 
15286 		* Tracing where an event got cancelled along the chain can get difficult; the downside of
15287 		  the central delegation benefit is it can be lead to debugging of action at a distance.
15288 	)
15289 
15290 	In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript
15291 	while keeping the benefits - and most compatibility with - the existing model. The main idea is
15292 	to simply use a D object type which provides a static interface as well as a built-in event name.
15293 	Then, a new static interface allows you to see what an event can emit and attach handlers to it
15294 	similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still
15295 	delegate to a parent and use custom events as well as using the runtime dynamic access, in addition
15296 	to having a little more help from the D compiler and documentation generator.
15297 
15298 	Your code would change like this:
15299 
15300 	---
15301 	// old
15302 	widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture );
15303 
15304 	// new
15305 	widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture );
15306 	---
15307 
15308 	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.
15309 
15310 	All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class.
15311 
15312 	Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you!
15313 
15314 	Thus the family of functions are:
15315 
15316 	[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.
15317 
15318 	[Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate.
15319 
15320 	[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.
15321 
15322 	Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property:
15323 
15324 	---
15325 	class MyCheckbox : Widget {
15326 		/// This gives a chance to document it and generates a convenience function to send it and attach handlers.
15327 		/// It is NOT actually required but should be used whenever possible.
15328 		mixin Emits!(ChangeEvent!bool);
15329 
15330 		this(Widget parent) {
15331 			super(parent);
15332 			setDefaultEventHandler((ClickEvent) { checked = !checked; });
15333 		}
15334 
15335 		private bool _checked;
15336 		@property bool checked() { return _checked; }
15337 		@property void checked(bool set) {
15338 			_checked = set;
15339 			emit!(ChangeEvent!bool)(&checked);
15340 		}
15341 	}
15342 	---
15343 
15344 	## Creating Your Own Events
15345 
15346 	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. You should mark events `final` unless you specifically plan to use it as a shared base. Only `Widget` and final classes should actually be sent (and preferably, not even `Widget`), with few exceptions.
15347 
15348 	---
15349 	final class MyEvent : Event {
15350 		this(Widget target) { super(EventString, target); }
15351 		mixin Register; // adds EventString and other reflection information
15352 	}
15353 	---
15354 
15355 	Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it.
15356 
15357 	History:
15358 		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.
15359 
15360 		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.
15361 +/
15362 /+
15363 
15364 	## General Conventions
15365 
15366 	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.
15367 
15368 
15369 	## Qt-style signals and slots
15370 
15371 	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.
15372 
15373 	The intention is for events to be used when
15374 
15375 	---
15376 	class Demo : Widget {
15377 		this() {
15378 			myPropertyChanged = Signal!int(this);
15379 		}
15380 		@property myProperty(int v) {
15381 			myPropertyChanged.emit(v);
15382 		}
15383 
15384 		Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name...
15385 		// but it can just genuinely not care about `this` since that's not really passed.
15386 	}
15387 
15388 	class Foo : Widget {
15389 		// the slot uda is not necessary, but it helps the script and ui builder find it.
15390 		@slot void setValue(int v) { ... }
15391 	}
15392 
15393 	demo.myPropertyChanged.connect(&foo.setValue);
15394 	---
15395 
15396 	The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor.
15397 
15398 	Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically.
15399 
15400 	class StringChangeEvent : ChangeEvent, Signal!string {
15401 		mixin SignalImpl
15402 	}
15403 
15404 +/
15405 class Event : ReflectableProperties {
15406 	/// Creates an event without populating any members and without sending it. See [dispatch]
15407 	this(string eventName, Widget emittedBy) {
15408 		this.eventName = eventName;
15409 		this.srcElement = emittedBy;
15410 	}
15411 
15412 
15413 	/// Implementations for the [ReflectableProperties] interface/
15414 	void getPropertiesList(scope void delegate(string name) sink) const {}
15415 	/// ditto
15416 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { }
15417 	/// ditto
15418 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) {
15419 		return SetPropertyResult.notPermitted;
15420 	}
15421 
15422 
15423 	/+
15424 	/++
15425 		This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice.
15426 
15427 		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.
15428 	+/
15429 	protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) {
15430 		if(value.length == 0) {
15431 			finalSink(memberName, `""`);
15432 			return;
15433 		}
15434 
15435 		char[1024] bufferBacking;
15436 		char[] buffer = bufferBacking;
15437 		int bufferPosition;
15438 
15439 		void sink(char ch) {
15440 			if(bufferPosition >= buffer.length)
15441 				buffer.length = buffer.length + 1024;
15442 			buffer[bufferPosition++] = ch;
15443 		}
15444 
15445 		sink('"');
15446 
15447 		foreach(ch; value) {
15448 			switch(ch) {
15449 				case '\\':
15450 					sink('\\'); sink('\\');
15451 				break;
15452 				case '"':
15453 					sink('\\'); sink('"');
15454 				break;
15455 				case '\n':
15456 					sink('\\'); sink('n');
15457 				break;
15458 				case '\r':
15459 					sink('\\'); sink('r');
15460 				break;
15461 				case '\t':
15462 					sink('\\'); sink('t');
15463 				break;
15464 				default:
15465 					sink(ch);
15466 			}
15467 		}
15468 
15469 		sink('"');
15470 
15471 		finalSink(memberName, buffer[0 .. bufferPosition]);
15472 	}
15473 	+/
15474 
15475 	/+
15476 	enum EventInitiator {
15477 		system,
15478 		minigui,
15479 		user
15480 	}
15481 
15482 	immutable EventInitiator; initiatedBy;
15483 	+/
15484 
15485 	/++
15486 		Events should generally follow the propagation model, but there's some exceptions
15487 		to that rule. If so, they should override this to return false. In that case, only
15488 		bubbling event handlers on the target itself and capturing event handlers on the containing
15489 		window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal
15490 		capture -> target -> bubble process.)
15491 
15492 		History:
15493 			Added May 12, 2021
15494 	+/
15495 	bool propagates() const pure nothrow @nogc @safe {
15496 		return true;
15497 	}
15498 
15499 	/++
15500 		hints as to whether preventDefault will actually do anything. not entirely reliable.
15501 
15502 		History:
15503 			Added May 14, 2021
15504 	+/
15505 	bool cancelable() const pure nothrow @nogc @safe {
15506 		return true;
15507 	}
15508 
15509 	/++
15510 		You can mix this into child class to register some boilerplate. It includes the `EventString`
15511 		member, a constructor, and implementations of the dynamic get data interfaces.
15512 
15513 		If you fail to do this, your event will probably not have full compatibility but it might still work for you.
15514 
15515 
15516 		You can override the default EventString by simply providing your own in the form of
15517 		`enum string EventString = "some.name";` The default is the name of your class and its parent entity
15518 		which provides some namespace protection against conflicts in other libraries while still being fairly
15519 		easy to use.
15520 
15521 		If you provide your own constructor, it will override the default constructor provided here. A constructor
15522 		must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the
15523 		first argument to your constructor.
15524 
15525 		History:
15526 			Added May 13, 2021.
15527 	+/
15528 	protected static mixin template Register() {
15529 		public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~  __traits(identifier, typeof(this));
15530 		this(Widget target) { super(EventString, target); }
15531 
15532 		mixin ReflectableProperties.RegisterGetters;
15533 	}
15534 
15535 	/++
15536 		This is the widget that emitted the event.
15537 
15538 
15539 		The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms.
15540 
15541 		History:
15542 			The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms,
15543 			but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web
15544 			so I don't intend to remove these aliases.
15545 	+/
15546 	Widget source;
15547 	/// ditto
15548 	alias source target;
15549 	/// ditto
15550 	alias source srcElement;
15551 
15552 	Widget relatedTarget; /// Note: likely to be deprecated at some point.
15553 
15554 	/// Prevents the default event handler (if there is one) from being called
15555 	void preventDefault() {
15556 		lastDefaultPrevented = true;
15557 		defaultPrevented = true;
15558 	}
15559 
15560 	/// Stops the event propagation immediately.
15561 	void stopPropagation() {
15562 		propagationStopped = true;
15563 	}
15564 
15565 	private bool defaultPrevented;
15566 	private bool propagationStopped;
15567 	private string eventName;
15568 
15569 	private bool isBubbling;
15570 
15571 	/// 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.
15572 	protected void adjustScrolling() { }
15573 	/// ditto
15574 	protected void adjustClientCoordinates(int deltaX, int deltaY) { }
15575 
15576 	/++
15577 		this sends it only to the target. If you want propagation, use dispatch() instead.
15578 
15579 		This should be made private!!!
15580 
15581 	+/
15582 	void sendDirectly() {
15583 		if(srcElement is null)
15584 			return;
15585 
15586 		// 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.
15587 
15588 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
15589 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement);
15590 
15591 		adjustScrolling();
15592 
15593 		if(auto e = target.parentWindow) {
15594 			if(auto handlers = "*" in e.capturingEventHandlers)
15595 			foreach(handler; *handlers)
15596 				if(handler) handler(e, this);
15597 			if(auto handlers = eventName in e.capturingEventHandlers)
15598 			foreach(handler; *handlers)
15599 				if(handler) handler(e, this);
15600 		}
15601 
15602 		auto e = srcElement;
15603 
15604 		if(auto handlers = eventName in e.bubblingEventHandlers)
15605 		foreach(handler; *handlers)
15606 			if(handler) handler(e, this);
15607 
15608 		if(auto handlers = "*" in e.bubblingEventHandlers)
15609 		foreach(handler; *handlers)
15610 			if(handler) handler(e, this);
15611 
15612 		// there's never a default for a catch-all event
15613 		if(!defaultPrevented)
15614 			if(eventName in e.defaultEventHandlers)
15615 				e.defaultEventHandlers[eventName](e, this);
15616 	}
15617 
15618 	/// this dispatches the element using the capture -> target -> bubble process
15619 	void dispatch() {
15620 		if(srcElement is null)
15621 			return;
15622 
15623 		if(!propagates) {
15624 			sendDirectly;
15625 			return;
15626 		}
15627 
15628 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
15629 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement);
15630 
15631 		adjustScrolling();
15632 		// first capture, then bubble
15633 
15634 		Widget[] chain;
15635 		Widget curr = srcElement;
15636 		while(curr) {
15637 			auto l = curr;
15638 			chain ~= l;
15639 			curr = curr.parent;
15640 		}
15641 
15642 		isBubbling = false;
15643 
15644 		foreach_reverse(e; chain) {
15645 			if(auto handlers = "*" in e.capturingEventHandlers)
15646 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
15647 
15648 			if(propagationStopped)
15649 				break;
15650 
15651 			if(auto handlers = eventName in e.capturingEventHandlers)
15652 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
15653 
15654 			// the default on capture should really be to always do nothing
15655 
15656 			//if(!defaultPrevented)
15657 			//	if(eventName in e.defaultEventHandlers)
15658 			//		e.defaultEventHandlers[eventName](e.element, this);
15659 
15660 			if(propagationStopped)
15661 				break;
15662 		}
15663 
15664 		int adjustX;
15665 		int adjustY;
15666 
15667 		isBubbling = true;
15668 		if(!propagationStopped)
15669 		foreach(e; chain) {
15670 			if(auto handlers = eventName in e.bubblingEventHandlers)
15671 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
15672 
15673 			if(propagationStopped)
15674 				break;
15675 
15676 			if(auto handlers = "*" in e.bubblingEventHandlers)
15677 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
15678 
15679 			if(propagationStopped)
15680 				break;
15681 
15682 			if(e.encapsulatedChildren()) {
15683 				adjustClientCoordinates(adjustX, adjustY);
15684 				target = e;
15685 			} else {
15686 				adjustX += e.x;
15687 				adjustY += e.y;
15688 			}
15689 		}
15690 
15691 		if(!defaultPrevented)
15692 		foreach(e; chain) {
15693 			if(eventName in e.defaultEventHandlers)
15694 				e.defaultEventHandlers[eventName](e, this);
15695 		}
15696 	}
15697 
15698 
15699 	/* old compatibility things */
15700 	deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!")
15701 	final @property {
15702 		Key key() { return (cast(KeyEventBase) this).key; }
15703 		KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; }
15704 
15705 		bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; }
15706 		bool altKey() { return (cast(KeyEventBase) this).altKey; }
15707 		bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; }
15708 	}
15709 
15710 	deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!")
15711 	final @property {
15712 		int clientX() { return (cast(MouseEventBase) this).clientX; }
15713 		int clientY() { return (cast(MouseEventBase) this).clientY; }
15714 
15715 		int viewportX() { return (cast(MouseEventBase) this).viewportX; }
15716 		int viewportY() { return (cast(MouseEventBase) this).viewportY; }
15717 
15718 		int button() { return (cast(MouseEventBase) this).button; }
15719 		int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; }
15720 	}
15721 
15722 	deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward")
15723 	final @property {
15724 		int state() {
15725 			if(auto meb = cast(MouseEventBase) this)
15726 				return meb.state;
15727 			if(auto keb = cast(KeyEventBase) this)
15728 				return keb.state;
15729 			assert(0);
15730 		}
15731 	}
15732 
15733 	deprecated("Use a CharEvent instead of Event in your handler going forward")
15734 	final @property {
15735 		dchar character() {
15736 			if(auto ce = cast(CharEvent) this)
15737 				return ce.character;
15738 			return dchar.init;
15739 		}
15740 	}
15741 
15742 	// for change events
15743 	@property {
15744 		///
15745 		int intValue() { return 0; }
15746 		///
15747 		string stringValue() { return null; }
15748 	}
15749 }
15750 
15751 /++
15752 	This lets you statically verify you send the events you claim you send and gives you a hook to document them.
15753 
15754 	Please note that a widget may send events not listed as Emits. You can always construct and dispatch
15755 	dynamic and custom events, but the static list helps ensure you get them right.
15756 
15757 	If this is declared, you can use [Widget.emit] to send the event.
15758 
15759 	All events work the same way though, following the capture->widget->bubble model described under [Event].
15760 
15761 	History:
15762 		Added May 4, 2021
15763 +/
15764 mixin template Emits(EventType) {
15765 	import arsd.minigui : EventString;
15766 	static if(is(EventType : Event) && !is(EventType == Event))
15767 		mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";");
15768 	else
15769 		static assert(0, "You can only emit subclasses of Event");
15770 }
15771 
15772 /// ditto
15773 mixin template Emits(string eventString) {
15774 	mixin("private Event[0] emits_" ~ eventString ~";");
15775 }
15776 
15777 /*
15778 class SignalEvent(string name) : Event {
15779 
15780 }
15781 */
15782 
15783 /++
15784 	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".
15785 
15786 
15787 	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.
15788 
15789 	History:
15790 		Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things.
15791 +/
15792 class CommandEvent : Event {
15793 	enum EventString = "command";
15794 	this(Widget source, string CommandString = EventString) {
15795 		super(CommandString, source);
15796 	}
15797 }
15798 
15799 /++
15800 	A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments.
15801 +/
15802 class CommandEventWithArgs(Args...) : CommandEvent {
15803 	this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; }
15804 	Args args;
15805 }
15806 
15807 /++
15808 	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.
15809 
15810 	See [CommandEvent] for more information.
15811 
15812 	Returns:
15813 		The [EventListener] you can use to remove the handler.
15814 +/
15815 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) {
15816 	return w.addEventListener(CommandString, (Event ev) {
15817 		if(ev.target is w)
15818 			return; // it does not consume its own commands!
15819 		if(auto cev = cast(CommandEventWithArgs!Args) ev) {
15820 			handler(cev.args);
15821 			ev.stopPropagation();
15822 		}
15823 	});
15824 }
15825 
15826 /++
15827 	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.
15828 +/
15829 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) {
15830 	auto event = new CommandEventWithArgs!Args(w, CommandString, args);
15831 	event.dispatch();
15832 }
15833 
15834 /++
15835 	Widgets emit `ResizeEvent`s any time they are resized. You check [Widget.width] and [Widget.height] upon receiving this event to know the new size.
15836 
15837 	If you need to know the old size, you need to store it yourself.
15838 
15839 	History:
15840 		Made final on January 3, 2025 (dub v12.0)
15841 +/
15842 final class ResizeEvent : Event {
15843 	enum EventString = "resize";
15844 
15845 	this(Widget target) { super(EventString, target); }
15846 
15847 	override bool propagates() const { return false; }
15848 }
15849 
15850 /++
15851 	ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close.
15852 
15853 	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.
15854 
15855 	History:
15856 		Added June 21, 2021 (dub v10.1)
15857 
15858 		Made final on January 3, 2025 (dub v12.0)
15859 +/
15860 final class ClosingEvent : Event {
15861 	enum EventString = "closing";
15862 
15863 	this(Widget target) { super(EventString, target); }
15864 
15865 	override bool propagates() const { return false; }
15866 	override bool cancelable() const { return true; }
15867 }
15868 
15869 /// ditto
15870 final class ClosedEvent : Event {
15871 	enum EventString = "closed";
15872 
15873 	this(Widget target) { super(EventString, target); }
15874 
15875 	override bool propagates() const { return false; }
15876 	override bool cancelable() const { return false; }
15877 }
15878 
15879 ///
15880 final class BlurEvent : Event {
15881 	enum EventString = "blur";
15882 
15883 	// FIXME: related target?
15884 	this(Widget target) { super(EventString, target); }
15885 
15886 	override bool propagates() const { return false; }
15887 }
15888 
15889 ///
15890 final class FocusEvent : Event {
15891 	enum EventString = "focus";
15892 
15893 	// FIXME: related target?
15894 	this(Widget target) { super(EventString, target); }
15895 
15896 	override bool propagates() const { return false; }
15897 }
15898 
15899 /++
15900 	FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates.
15901 
15902 	History:
15903 		Added July 3, 2021
15904 +/
15905 final class FocusInEvent : Event {
15906 	enum EventString = "focusin";
15907 
15908 	// FIXME: related target?
15909 	this(Widget target) { super(EventString, target); }
15910 
15911 	override bool cancelable() const { return false; }
15912 }
15913 
15914 /// ditto
15915 final class FocusOutEvent : Event {
15916 	enum EventString = "focusout";
15917 
15918 	// FIXME: related target?
15919 	this(Widget target) { super(EventString, target); }
15920 
15921 	override bool cancelable() const { return false; }
15922 }
15923 
15924 ///
15925 final class ScrollEvent : Event {
15926 	enum EventString = "scroll";
15927 	this(Widget target) { super(EventString, target); }
15928 
15929 	override bool cancelable() const { return false; }
15930 }
15931 
15932 /++
15933 	Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget.
15934 
15935 	History:
15936 		Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class.
15937 +/
15938 final class CharEvent : Event {
15939 	enum EventString = "char";
15940 	this(Widget target, dchar ch) {
15941 		character = ch;
15942 		super(EventString, target);
15943 	}
15944 
15945 	immutable dchar character;
15946 }
15947 
15948 /++
15949 	You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information.
15950 +/
15951 abstract class ChangeEventBase : Event {
15952 	enum EventString = "change";
15953 	this(Widget target) {
15954 		super(EventString, target);
15955 	}
15956 
15957 	/+
15958 		// idk where or how exactly i want to do this.
15959 		// i might come back to it later.
15960 
15961 	// If a widget itself broadcasts one of theses itself, it stops propagation going down
15962 	// this way the source doesn't get too confused (think of a nested scroll widget)
15963 	//
15964 	// the idea is like the scroll bar emits a command event saying like "scroll left one line"
15965 	// then you consume that command and change you scroll x position to whatever. then you do
15966 	// some kind of change event that is broadcast back to the children and any horizontal scroll
15967 	// listeners are now able to update, without having an explicit connection between them.
15968 	void broadcastToChildren(string fieldName) {
15969 
15970 	}
15971 	+/
15972 }
15973 
15974 /++
15975 	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.
15976 
15977 
15978 	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).
15979 
15980 	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);`
15981 
15982 	Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything.
15983 
15984 	History:
15985 		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.
15986 +/
15987 final class ChangeEvent(T) : ChangeEventBase {
15988 	this(Widget target, T delegate() getNewValue) {
15989 		assert(getNewValue !is null);
15990 		this.getNewValue = getNewValue;
15991 		super(target);
15992 	}
15993 
15994 	private T delegate() getNewValue;
15995 
15996 	/++
15997 		Gets the new value that just changed.
15998 	+/
15999 	@property T value() {
16000 		return getNewValue();
16001 	}
16002 
16003 	/// compatibility method for old generic Events
16004 	static if(is(immutable T == immutable int))
16005 		override int intValue() { return value; }
16006 	/// ditto
16007 	static if(is(immutable T == immutable string))
16008 		override string stringValue() { return value; }
16009 }
16010 
16011 /++
16012 	Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s.
16013 
16014 
16015 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16016 
16017 	History:
16018 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
16019 +/
16020 abstract class KeyEventBase : Event {
16021 	this(string name, Widget target) {
16022 		super(name, target);
16023 	}
16024 
16025 	// for key events
16026 	Key key; ///
16027 
16028 	KeyEvent originalKeyEvent;
16029 
16030 	/++
16031 		Indicates the current state of the given keyboard modifier keys.
16032 
16033 		History:
16034 			Added to events on April 15, 2020.
16035 	+/
16036 	bool ctrlKey;
16037 
16038 	/// ditto
16039 	bool altKey;
16040 
16041 	/// ditto
16042 	bool shiftKey;
16043 
16044 	/++
16045 		The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey].
16046 
16047 		See [arsd.simpledisplay.ModifierState] for other possible flags.
16048 	+/
16049 	int state;
16050 
16051 	mixin Register;
16052 }
16053 
16054 /++
16055 	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].
16056 
16057 
16058 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16059 
16060 	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.
16061 
16062 	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.
16063 
16064 	See_Also: [KeyUpEvent], [CharEvent]
16065 
16066 	History:
16067 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners.
16068 +/
16069 final class KeyDownEvent : KeyEventBase {
16070 	enum EventString = "keydown";
16071 	this(Widget target) { super(EventString, target); }
16072 }
16073 
16074 /++
16075 	Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase].
16076 
16077 
16078 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16079 
16080 	See_Also: [KeyDownEvent], [CharEvent]
16081 
16082 	History:
16083 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners.
16084 +/
16085 final class KeyUpEvent : KeyEventBase {
16086 	enum EventString = "keyup";
16087 	this(Widget target) { super(EventString, target); }
16088 }
16089 
16090 /++
16091 	Contains shared properties for various mouse events;
16092 
16093 
16094 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16095 
16096 	History:
16097 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
16098 +/
16099 abstract class MouseEventBase : Event {
16100 	this(string name, Widget target) {
16101 		super(name, target);
16102 	}
16103 
16104 	// for mouse events
16105 	int clientX; /// The mouse event location relative to the target widget
16106 	int clientY; /// ditto
16107 
16108 	int viewportX; /// The mouse event location relative to the window origin
16109 	int viewportY; /// ditto
16110 
16111 	int button; /// See: [MouseEvent.button]
16112 	int buttonLinear; /// See: [MouseEvent.buttonLinear]
16113 
16114 	/++
16115 		Indicates the current state of the given keyboard modifier keys.
16116 
16117 		History:
16118 			Added to mouse events on September 28, 2010.
16119 	+/
16120 	bool ctrlKey;
16121 
16122 	/// ditto
16123 	bool altKey;
16124 
16125 	/// ditto
16126 	bool shiftKey;
16127 
16128 
16129 
16130 	int state; ///
16131 
16132 	/++
16133 		for consistent names with key event.
16134 
16135 		History:
16136 			Added September 28, 2021 (dub v10.3)
16137 	+/
16138 	alias modifierState = state;
16139 
16140 	/++
16141 		Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out.
16142 
16143 		History:
16144 			Added May 15, 2021
16145 	+/
16146 	bool isMouseWheel() {
16147 		return button == MouseButton.wheelUp || button == MouseButton.wheelDown;
16148 	}
16149 
16150 	// private
16151 	override void adjustClientCoordinates(int deltaX, int deltaY) {
16152 		clientX += deltaX;
16153 		clientY += deltaY;
16154 	}
16155 
16156 	override void adjustScrolling() {
16157 	version(custom_widgets) { // TEMP
16158 		viewportX = clientX;
16159 		viewportY = clientY;
16160 		if(auto se = cast(ScrollableWidget) srcElement) {
16161 			clientX += se.scrollOrigin.x;
16162 			clientY += se.scrollOrigin.y;
16163 		} else if(auto se = cast(ScrollableContainerWidget) srcElement) {
16164 			//clientX += se.scrollX_;
16165 			//clientY += se.scrollY_;
16166 		}
16167 	}
16168 	}
16169 
16170 	mixin Register;
16171 }
16172 
16173 /++
16174 	Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase].
16175 
16176 
16177 	$(WARNING
16178 		Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and
16179 		for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct
16180 		behavior.
16181 
16182 		Use [MouseEventBase.isMouseWheel] to filter wheel events while keeping others.
16183 	)
16184 
16185 	[MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement.
16186 
16187 	[MouseUpEvent] is sent when the user releases a mouse button.
16188 
16189 	[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.)
16190 
16191 	[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.
16192 
16193 	[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 different 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.
16194 
16195 	[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.
16196 
16197 	[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.
16198 
16199 	[MouseEnterEvent] is sent when the mouse enters the bounding box of a widget.
16200 
16201 	[MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget.
16202 
16203 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16204 
16205 	Rationale:
16206 
16207 		If you only want to do drag, mousedown/up works just fine being consistently sent.
16208 
16209 		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).
16210 
16211 		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.
16212 
16213 	History:
16214 		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.
16215 +/
16216 final class MouseUpEvent : MouseEventBase {
16217 	enum EventString = "mouseup"; ///
16218 	this(Widget target) { super(EventString, target); }
16219 }
16220 /// ditto
16221 final class MouseDownEvent : MouseEventBase {
16222 	enum EventString = "mousedown"; ///
16223 	this(Widget target) { super(EventString, target); }
16224 }
16225 /// ditto
16226 final class MouseMoveEvent : MouseEventBase {
16227 	enum EventString = "mousemove"; ///
16228 	this(Widget target) { super(EventString, target); }
16229 }
16230 /// ditto
16231 final class ClickEvent : MouseEventBase {
16232 	enum EventString = "click"; ///
16233 	this(Widget target) { super(EventString, target); }
16234 }
16235 /// ditto
16236 final class DoubleClickEvent : MouseEventBase {
16237 	enum EventString = "dblclick"; ///
16238 	this(Widget target) { super(EventString, target); }
16239 }
16240 /// ditto
16241 final class MouseOverEvent : Event {
16242 	enum EventString = "mouseover"; ///
16243 	this(Widget target) { super(EventString, target); }
16244 }
16245 /// ditto
16246 final class MouseOutEvent : Event {
16247 	enum EventString = "mouseout"; ///
16248 	this(Widget target) { super(EventString, target); }
16249 }
16250 /// ditto
16251 final class MouseEnterEvent : Event {
16252 	enum EventString = "mouseenter"; ///
16253 	this(Widget target) { super(EventString, target); }
16254 
16255 	override bool propagates() const { return false; }
16256 }
16257 /// ditto
16258 final class MouseLeaveEvent : Event {
16259 	enum EventString = "mouseleave"; ///
16260 	this(Widget target) { super(EventString, target); }
16261 
16262 	override bool propagates() const { return false; }
16263 }
16264 
16265 private bool isAParentOf(Widget a, Widget b) {
16266 	if(a is null || b is null)
16267 		return false;
16268 
16269 	while(b !is null) {
16270 		if(a is b)
16271 			return true;
16272 		b = b.parent;
16273 	}
16274 
16275 	return false;
16276 }
16277 
16278 private struct WidgetAtPointResponse {
16279 	Widget widget;
16280 
16281 	// x, y relative to the widget in the response.
16282 	int x;
16283 	int y;
16284 }
16285 
16286 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) {
16287 	assert(starting !is null);
16288 
16289 	starting.addScrollPosition(x, y);
16290 
16291 	auto child = starting.getChildAtPosition(x, y);
16292 	while(child) {
16293 		if(child.hidden)
16294 			continue;
16295 		starting = child;
16296 		x -= child.x;
16297 		y -= child.y;
16298 		auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y);
16299 		child = r.widget;
16300 		if(child is starting)
16301 			break;
16302 	}
16303 	return WidgetAtPointResponse(starting, x, y);
16304 }
16305 
16306 version(win32_widgets) {
16307 private:
16308 	import core.sys.windows.commctrl;
16309 
16310 	pragma(lib, "comctl32");
16311 	shared static this() {
16312 		// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx
16313 		INITCOMMONCONTROLSEX ic;
16314 		ic.dwSize = cast(DWORD) ic.sizeof;
16315 		ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES;
16316 		if(!InitCommonControlsEx(&ic)) {
16317 			//writeln("ICC failed");
16318 		}
16319 	}
16320 
16321 
16322 	// everything from here is just win32 headers copy pasta
16323 private:
16324 extern(Windows):
16325 
16326 	alias HANDLE HMENU;
16327 	HMENU CreateMenu();
16328 	bool SetMenu(HWND, HMENU);
16329 	HMENU CreatePopupMenu();
16330 	enum MF_POPUP = 0x10;
16331 	enum MF_STRING = 0;
16332 
16333 
16334 	BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*);
16335 	struct INITCOMMONCONTROLSEX {
16336 		DWORD dwSize;
16337 		DWORD dwICC;
16338 	}
16339 	enum HINST_COMMCTRL = cast(HINSTANCE) (-1);
16340 enum {
16341         IDB_STD_SMALL_COLOR,
16342         IDB_STD_LARGE_COLOR,
16343         IDB_VIEW_SMALL_COLOR = 4,
16344         IDB_VIEW_LARGE_COLOR = 5
16345 }
16346 enum {
16347         STD_CUT,
16348         STD_COPY,
16349         STD_PASTE,
16350         STD_UNDO,
16351         STD_REDOW,
16352         STD_DELETE,
16353         STD_FILENEW,
16354         STD_FILEOPEN,
16355         STD_FILESAVE,
16356         STD_PRINTPRE,
16357         STD_PROPERTIES,
16358         STD_HELP,
16359         STD_FIND,
16360         STD_REPLACE,
16361         STD_PRINT // = 14
16362 }
16363 
16364 alias HANDLE HIMAGELIST;
16365 	HIMAGELIST ImageList_Create(int, int, UINT, int, int);
16366 	int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP);
16367         BOOL ImageList_Destroy(HIMAGELIST);
16368 
16369 uint MAKELONG(ushort a, ushort b) {
16370         return cast(uint) ((b << 16) | a);
16371 }
16372 
16373 
16374 struct TBBUTTON {
16375 	int   iBitmap;
16376 	int   idCommand;
16377 	BYTE  fsState;
16378 	BYTE  fsStyle;
16379 	version(Win64)
16380 	BYTE[6] bReserved;
16381 	else
16382 	BYTE[2]  bReserved;
16383 	DWORD dwData;
16384 	INT_PTR   iString;
16385 }
16386 
16387 	enum {
16388 		TB_ADDBUTTONSA   = WM_USER + 20,
16389 		TB_INSERTBUTTONA = WM_USER + 21,
16390 		TB_GETIDEALSIZE = WM_USER + 99,
16391 	}
16392 
16393 struct SIZE {
16394 	LONG cx;
16395 	LONG cy;
16396 }
16397 
16398 
16399 enum {
16400 	TBSTATE_CHECKED       = 1,
16401 	TBSTATE_PRESSED       = 2,
16402 	TBSTATE_ENABLED       = 4,
16403 	TBSTATE_HIDDEN        = 8,
16404 	TBSTATE_INDETERMINATE = 16,
16405 	TBSTATE_WRAP          = 32
16406 }
16407 
16408 
16409 
16410 enum {
16411 	ILC_COLOR    = 0,
16412 	ILC_COLOR4   = 4,
16413 	ILC_COLOR8   = 8,
16414 	ILC_COLOR16  = 16,
16415 	ILC_COLOR24  = 24,
16416 	ILC_COLOR32  = 32,
16417 	ILC_COLORDDB = 254,
16418 	ILC_MASK     = 1,
16419 	ILC_PALETTE  = 2048
16420 }
16421 
16422 
16423 alias TBBUTTON*       PTBBUTTON, LPTBBUTTON;
16424 
16425 
16426 enum {
16427 	TB_ENABLEBUTTON          = WM_USER + 1,
16428 	TB_CHECKBUTTON,
16429 	TB_PRESSBUTTON,
16430 	TB_HIDEBUTTON,
16431 	TB_INDETERMINATE, //     = WM_USER + 5,
16432 	TB_ISBUTTONENABLED       = WM_USER + 9,
16433 	TB_ISBUTTONCHECKED,
16434 	TB_ISBUTTONPRESSED,
16435 	TB_ISBUTTONHIDDEN,
16436 	TB_ISBUTTONINDETERMINATE, // = WM_USER + 13,
16437 	TB_SETSTATE              = WM_USER + 17,
16438 	TB_GETSTATE              = WM_USER + 18,
16439 	TB_ADDBITMAP             = WM_USER + 19,
16440 	TB_DELETEBUTTON          = WM_USER + 22,
16441 	TB_GETBUTTON,
16442 	TB_BUTTONCOUNT,
16443 	TB_COMMANDTOINDEX,
16444 	TB_SAVERESTOREA,
16445 	TB_CUSTOMIZE,
16446 	TB_ADDSTRINGA,
16447 	TB_GETITEMRECT,
16448 	TB_BUTTONSTRUCTSIZE,
16449 	TB_SETBUTTONSIZE,
16450 	TB_SETBITMAPSIZE,
16451 	TB_AUTOSIZE, //          = WM_USER + 33,
16452 	TB_GETTOOLTIPS           = WM_USER + 35,
16453 	TB_SETTOOLTIPS           = WM_USER + 36,
16454 	TB_SETPARENT             = WM_USER + 37,
16455 	TB_SETROWS               = WM_USER + 39,
16456 	TB_GETROWS,
16457 	TB_GETBITMAPFLAGS,
16458 	TB_SETCMDID,
16459 	TB_CHANGEBITMAP,
16460 	TB_GETBITMAP,
16461 	TB_GETBUTTONTEXTA,
16462 	TB_REPLACEBITMAP, //     = WM_USER + 46,
16463 	TB_GETBUTTONSIZE         = WM_USER + 58,
16464 	TB_SETBUTTONWIDTH        = WM_USER + 59,
16465 	TB_GETBUTTONTEXTW        = WM_USER + 75,
16466 	TB_SAVERESTOREW          = WM_USER + 76,
16467 	TB_ADDSTRINGW            = WM_USER + 77,
16468 }
16469 
16470 extern(Windows)
16471 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM);
16472 
16473 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC;
16474 
16475 
16476 	enum {
16477 		TB_SETINDENT = WM_USER + 47,
16478 		TB_SETIMAGELIST,
16479 		TB_GETIMAGELIST,
16480 		TB_LOADIMAGES,
16481 		TB_GETRECT,
16482 		TB_SETHOTIMAGELIST,
16483 		TB_GETHOTIMAGELIST,
16484 		TB_SETDISABLEDIMAGELIST,
16485 		TB_GETDISABLEDIMAGELIST,
16486 		TB_SETSTYLE,
16487 		TB_GETSTYLE,
16488 		//TB_GETBUTTONSIZE,
16489 		//TB_SETBUTTONWIDTH,
16490 		TB_SETMAXTEXTROWS,
16491 		TB_GETTEXTROWS // = WM_USER + 61
16492 	}
16493 
16494 enum {
16495 	CCM_FIRST            = 0x2000,
16496 	CCM_LAST             = CCM_FIRST + 0x200,
16497 	CCM_SETBKCOLOR       = 8193,
16498 	CCM_SETCOLORSCHEME   = 8194,
16499 	CCM_GETCOLORSCHEME   = 8195,
16500 	CCM_GETDROPTARGET    = 8196,
16501 	CCM_SETUNICODEFORMAT = 8197,
16502 	CCM_GETUNICODEFORMAT = 8198,
16503 	CCM_SETVERSION       = 0x2007,
16504 	CCM_GETVERSION       = 0x2008,
16505 	CCM_SETNOTIFYWINDOW  = 0x2009
16506 }
16507 
16508 
16509 enum {
16510 	PBM_SETRANGE     = WM_USER + 1,
16511 	PBM_SETPOS,
16512 	PBM_DELTAPOS,
16513 	PBM_SETSTEP,
16514 	PBM_STEPIT,   // = WM_USER + 5
16515 	PBM_SETRANGE32   = 1030,
16516 	PBM_GETRANGE,
16517 	PBM_GETPOS,
16518 	PBM_SETBARCOLOR, // = 1033
16519 	PBM_SETBKCOLOR   = CCM_SETBKCOLOR
16520 }
16521 
16522 enum {
16523 	PBS_SMOOTH   = 1,
16524 	PBS_VERTICAL = 4
16525 }
16526 
16527 enum {
16528         ICC_LISTVIEW_CLASSES = 1,
16529         ICC_TREEVIEW_CLASSES = 2,
16530         ICC_BAR_CLASSES      = 4,
16531         ICC_TAB_CLASSES      = 8,
16532         ICC_UPDOWN_CLASS     = 16,
16533         ICC_PROGRESS_CLASS   = 32,
16534         ICC_HOTKEY_CLASS     = 64,
16535         ICC_ANIMATE_CLASS    = 128,
16536         ICC_WIN95_CLASSES    = 255,
16537         ICC_DATE_CLASSES     = 256,
16538         ICC_USEREX_CLASSES   = 512,
16539         ICC_COOL_CLASSES     = 1024,
16540 	ICC_STANDARD_CLASSES = 0x00004000,
16541 }
16542 
16543 	enum WM_USER = 1024;
16544 }
16545 
16546 version(win32_widgets)
16547 	pragma(lib, "comdlg32");
16548 
16549 
16550 ///
16551 enum GenericIcons : ushort {
16552 	None, ///
16553 	// these happen to match the win32 std icons numerically if you just subtract one from the value
16554 	Cut, ///
16555 	Copy, ///
16556 	Paste, ///
16557 	Undo, ///
16558 	Redo, ///
16559 	Delete, ///
16560 	New, ///
16561 	Open, ///
16562 	Save, ///
16563 	PrintPreview, ///
16564 	Properties, ///
16565 	Help, ///
16566 	Find, ///
16567 	Replace, ///
16568 	Print, ///
16569 }
16570 
16571 enum FileDialogType {
16572 	Automatic,
16573 	Open,
16574 	Save
16575 }
16576 
16577 /++
16578 	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.
16579 +/
16580 string previousFileReferenced;
16581 
16582 /++
16583 	Used in automatic menu functions to indicate that the user should be able to browse for a file.
16584 
16585 	Params:
16586 		storage = an alias to a `static string` variable that stores the last file referenced. It will
16587 		use this to pre-fill the dialog with a suggestion.
16588 
16589 		Please note that it MUST be `static` or you will get compile errors.
16590 
16591 		filters = the filters param to [getFileName]
16592 
16593 		type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will
16594 		guess based on the function name. If it has the word "Save" or "Export" in it, it will show
16595 		a save dialog box. Otherwise, it will show an open dialog box.
16596 +/
16597 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) {
16598 	string name;
16599 	alias name this;
16600 
16601 	@implicit this(string name) {
16602 		this.name = name;
16603 	}
16604 }
16605 
16606 /++
16607 	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.
16608 
16609 	History:
16610 		onCancel was added November 6, 2021.
16611 
16612 		The dialog itself on Linux was modified on December 2, 2021 to include
16613 		a directory picker in addition to the command line completion view.
16614 
16615 		The `initialDirectory` argument was added November 9, 2022 (dub v10.10)
16616 
16617 		The `owner` argument was added September 29, 2024. The overloads without this argument are likely to be deprecated in the next major version.
16618 	Future_directions:
16619 		I want to add some kind of custom preview and maybe thumbnail thing in the future,
16620 		at least on Linux, maybe on Windows too.
16621 +/
16622 void getOpenFileName(
16623 	Window owner,
16624 	void delegate(string) onOK,
16625 	string prefilledName = null,
16626 	string[] filters = null,
16627 	void delegate() onCancel = null,
16628 	string initialDirectory = null,
16629 )
16630 {
16631 	return getFileName(owner, true, onOK, prefilledName, filters, onCancel, initialDirectory);
16632 }
16633 
16634 /// ditto
16635 void getSaveFileName(
16636 	Window owner,
16637 	void delegate(string) onOK,
16638 	string prefilledName = null,
16639 	string[] filters = null,
16640 	void delegate() onCancel = null,
16641 	string initialDirectory = null,
16642 )
16643 {
16644 	return getFileName(owner, false, onOK, prefilledName, filters, onCancel, initialDirectory);
16645 }
16646 
16647 // 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.")
16648 /// ditto
16649 void getOpenFileName(
16650 	void delegate(string) onOK,
16651 	string prefilledName = null,
16652 	string[] filters = null,
16653 	void delegate() onCancel = null,
16654 	string initialDirectory = null,
16655 )
16656 {
16657 	return getFileName(null, true, onOK, prefilledName, filters, onCancel, initialDirectory);
16658 }
16659 
16660 /// ditto
16661 void getSaveFileName(
16662 	void delegate(string) onOK,
16663 	string prefilledName = null,
16664 	string[] filters = null,
16665 	void delegate() onCancel = null,
16666 	string initialDirectory = null,
16667 )
16668 {
16669 	return getFileName(null, false, onOK, prefilledName, filters, onCancel, initialDirectory);
16670 }
16671 
16672 /++
16673 	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.
16674 
16675 	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.
16676 
16677 	History:
16678 		Added January 1, 2025
16679 +/
16680 class FileDialogDelegate {
16681 
16682 	/++
16683 
16684 	+/
16685 	static abstract class PreviewWidget : Widget {
16686 		/// Call this from your subclass' constructor
16687 		this(Widget parent) {
16688 			super(parent);
16689 		}
16690 
16691 		/// Load the file given to you and show its preview inside the widget here
16692 		abstract void previewFile(string filename);
16693 	}
16694 
16695 	/++
16696 		Override this to add preview capabilities to the dialog for certain files.
16697 	+/
16698 	protected PreviewWidget makePreviewWidget(Widget parent) {
16699 		return null;
16700 	}
16701 
16702 	/++
16703 		Override this to change the dialog entirely.
16704 
16705 		This function IS allowed to block, but is NOT required to.
16706 	+/
16707 	protected void getFileName(
16708 		Window owner,
16709 		bool openOrSave, // true if open, false if save
16710 		void delegate(string) onOK,
16711 		string prefilledName,
16712 		string[] filters, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
16713 		void delegate() onCancel,
16714 		string initialDirectory,
16715 	)
16716 	{
16717 
16718 		version(win32_widgets) {
16719 			import core.sys.windows.commdlg;
16720 		/*
16721 		Ofn.lStructSize = sizeof(OPENFILENAME);
16722 		Ofn.hwndOwner = hWnd;
16723 		Ofn.lpstrFilter = szFilter;
16724 		Ofn.lpstrFile= szFile;
16725 		Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile);
16726 		Ofn.lpstrFileTitle = szFileTitle;
16727 		Ofn.nMaxFileTitle = sizeof(szFileTitle);
16728 		Ofn.lpstrInitialDir = (LPSTR)NULL;
16729 		Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT;
16730 		Ofn.lpstrTitle = szTitle;
16731 		 */
16732 
16733 
16734 			wchar[1024] file = 0;
16735 			wchar[1024] filterBuffer = 0;
16736 			makeWindowsString(prefilledName, file[]);
16737 			OPENFILENAME ofn;
16738 			ofn.lStructSize = ofn.sizeof;
16739 			ofn.hwndOwner = owner is null ? null : owner.win.hwnd;
16740 			if(filters.length) {
16741 				string filter;
16742 				foreach(i, f; filters) {
16743 					filter ~= f;
16744 					filter ~= "\0";
16745 				}
16746 				filter ~= "\0";
16747 				ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr;
16748 			}
16749 			ofn.lpstrFile = file.ptr;
16750 			ofn.nMaxFile = file.length;
16751 
16752 			wchar[1024] initialDir = 0;
16753 			if(initialDirectory !is null) {
16754 				makeWindowsString(initialDirectory, initialDir[]);
16755 				ofn.lpstrInitialDir = file.ptr;
16756 			}
16757 
16758 			if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn))
16759 			{
16760 				string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile);
16761 				if(okString.length && okString[$-1] == '\0')
16762 					okString = okString[0..$-1];
16763 				onOK(okString);
16764 			} else {
16765 				if(onCancel)
16766 					onCancel();
16767 			}
16768 		} else version(custom_widgets) {
16769 			filters ~= ["All Files\0*.*"];
16770 			auto picker = new FilePicker(openOrSave, prefilledName, filters, initialDirectory, owner);
16771 			picker.onOK = onOK;
16772 			picker.onCancel = onCancel;
16773 			picker.show();
16774 		}
16775 	}
16776 
16777 }
16778 
16779 /// ditto
16780 FileDialogDelegate fileDialogDelegate() {
16781 	if(fileDialogDelegate_ is null)
16782 		fileDialogDelegate_ = new FileDialogDelegate();
16783 	return fileDialogDelegate_;
16784 }
16785 
16786 /// ditto
16787 void fileDialogDelegate(FileDialogDelegate replacement) {
16788 	fileDialogDelegate_ = replacement;
16789 }
16790 
16791 private FileDialogDelegate fileDialogDelegate_;
16792 
16793 struct FileNameFilter {
16794 	string description;
16795 	string[] globPatterns;
16796 
16797 	string toString() {
16798 		string ret;
16799 		ret ~= description;
16800 		ret ~= " (";
16801 		foreach(idx, pattern; globPatterns) {
16802 			if(idx)
16803 				ret ~= "; ";
16804 			ret ~= pattern;
16805 		}
16806 		ret ~= ")";
16807 
16808 		return ret;
16809 	}
16810 
16811 	static FileNameFilter fromString(string s) {
16812 		size_t end = s.length;
16813 		size_t start = 0;
16814 		foreach_reverse(idx, ch; s) {
16815 			if(ch == ')' && end == s.length)
16816 				end = idx;
16817 			else if(ch == '(' && end != s.length) {
16818 				start = idx + 1;
16819 				break;
16820 			}
16821 		}
16822 
16823 		FileNameFilter fnf;
16824 		fnf.description = s[0 .. start ? start - 1 : 0];
16825 		size_t globStart = 0;
16826 		s = s[start .. end];
16827 		foreach(idx, ch; s)
16828 			if(ch == ';') {
16829 				auto ptn = stripInternal(s[globStart .. idx]);
16830 				if(ptn.length)
16831 					fnf.globPatterns ~= ptn;
16832 				globStart = idx + 1;
16833 
16834 			}
16835 		auto ptn = stripInternal(s[globStart .. $]);
16836 		if(ptn.length)
16837 			fnf.globPatterns ~= ptn;
16838 		return fnf;
16839 	}
16840 }
16841 
16842 struct FileNameFilterSet {
16843 	FileNameFilter[] filters;
16844 
16845 	static FileNameFilterSet fromWindowsFileNameFilterDescription(string[] filters) {
16846 		FileNameFilter[] ret;
16847 
16848 		foreach(filter; filters) {
16849 			FileNameFilter fnf;
16850 			size_t filterStartPoint;
16851 			foreach(idx, ch; filter) {
16852 				if(ch == 0) {
16853 					fnf.description = filter[0 .. idx];
16854 					filterStartPoint = idx + 1;
16855 				} else if(filterStartPoint && ch == ';') {
16856 					fnf.globPatterns ~= filter[filterStartPoint .. idx];
16857 					filterStartPoint = idx + 1;
16858 				}
16859 			}
16860 			fnf.globPatterns ~= filter[filterStartPoint .. $];
16861 
16862 			ret ~= fnf;
16863 		}
16864 
16865 		return FileNameFilterSet(ret);
16866 	}
16867 }
16868 
16869 void getFileName(
16870 	Window owner,
16871 	bool openOrSave,
16872 	void delegate(string) onOK,
16873 	string prefilledName = null,
16874 	string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
16875 	void delegate() onCancel = null,
16876 	string initialDirectory = null,
16877 )
16878 {
16879 	return fileDialogDelegate().getFileName(owner, openOrSave, onOK, prefilledName, filters, onCancel, initialDirectory);
16880 }
16881 
16882 version(custom_widgets)
16883 private
16884 class FilePicker : Dialog {
16885 	void delegate(string) onOK;
16886 	void delegate() onCancel;
16887 	LabeledLineEdit lineEdit;
16888 	bool isOpenDialogInsteadOfSave;
16889 
16890 	static struct HistoryItem {
16891 		string cwd;
16892 		FileNameFilter filters;
16893 	}
16894 	HistoryItem[] historyStack;
16895 	size_t historyStackPosition;
16896 
16897 	void back() {
16898 		if(historyStackPosition) {
16899 			historyStackPosition--;
16900 			currentDirectory = historyStack[historyStackPosition].cwd;
16901 			currentFilter = historyStack[historyStackPosition].filters;
16902 			filesOfType.content = currentFilter.toString();
16903 			loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true);
16904 			lineEdit.focus();
16905 		}
16906 	}
16907 
16908 	void forward() {
16909 		if(historyStackPosition + 1 < historyStack.length) {
16910 			historyStackPosition++;
16911 			currentDirectory = historyStack[historyStackPosition].cwd;
16912 			currentFilter = historyStack[historyStackPosition].filters;
16913 			filesOfType.content = currentFilter.toString();
16914 			loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true);
16915 			lineEdit.focus();
16916 		}
16917 	}
16918 
16919 	void up() {
16920 		currentDirectory = currentDirectory ~ "..";
16921 		loadFiles(currentDirectory, currentFilter);
16922 		lineEdit.focus();
16923 	}
16924 
16925 	void refresh() {
16926 		loadFiles(currentDirectory, currentFilter);
16927 		lineEdit.focus();
16928 	}
16929 
16930 	// returns common prefix
16931 	static struct CommonPrefixInfo {
16932 		string commonPrefix;
16933 		int fileCount;
16934 		string exactMatch;
16935 	}
16936 	CommonPrefixInfo loadFiles(string cwd, FileNameFilter filters, bool comingFromHistory = false) {
16937 
16938 		if(!comingFromHistory) {
16939 			if(historyStack.length) {
16940 				historyStack = historyStack[0 .. historyStackPosition + 1];
16941 				historyStack.assumeSafeAppend();
16942 			}
16943 			historyStack ~= HistoryItem(cwd, filters);
16944 			historyStackPosition = historyStack.length - 1;
16945 		}
16946 
16947 		string[] files;
16948 		string[] dirs;
16949 
16950 		dirs ~= "$HOME";
16951 		dirs ~= "$PWD";
16952 
16953 		string commonPrefix;
16954 		int commonPrefixCount;
16955 		string exactMatch;
16956 
16957 		bool matchesFilter(string name) {
16958 			foreach(filter; filters.globPatterns) {
16959 			if(
16960 				filter.length <= 1 ||
16961 				filter == "*.*" || // we always treat *.* the same as *, but it is a bit different than .*
16962 				(filter[0] == '*' && name.endsWith(filter[1 .. $])) ||
16963 				(filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1]))
16964 			)
16965 			{
16966 				if(name.length > 1 && name[0] == '.')
16967 					if(filter.length == 0 || filter[0] != '.')
16968 						return false;
16969 
16970 				return true;
16971 			}
16972 			}
16973 
16974 			return false;
16975 		}
16976 
16977 		void considerCommonPrefix(string name, bool prefiltered) {
16978 			if(!prefiltered && !matchesFilter(name))
16979 				return;
16980 
16981 			if(commonPrefix is null) {
16982 				commonPrefix = name;
16983 				commonPrefixCount = 1;
16984 				exactMatch = commonPrefix;
16985 			} else {
16986 				foreach(idx, char i; name) {
16987 					if(idx >= commonPrefix.length || i != commonPrefix[idx]) {
16988 						commonPrefix = commonPrefix[0 .. idx];
16989 						commonPrefixCount ++;
16990 						exactMatch = null;
16991 						break;
16992 					}
16993 				}
16994 			}
16995 		}
16996 
16997 		bool applyFilterToDirectories = true;
16998 		bool showDotFiles = false;
16999 		foreach(filter; filters.globPatterns) {
17000 			if(filter == ".*")
17001 				showDotFiles = true;
17002 			else foreach(ch; filter)
17003 				if(ch == '.') {
17004 					// a filter like *.exe should not apply to the directory
17005 					applyFilterToDirectories = false;
17006 					break;
17007 				}
17008 		}
17009 
17010 		try
17011 		getFiles(cwd, (string name, bool isDirectory) {
17012 			if(name == ".")
17013 				return; // skip this as unnecessary
17014 			if(isDirectory) {
17015 				if(applyFilterToDirectories) {
17016 					if(matchesFilter(name)) {
17017 						dirs ~= name;
17018 						considerCommonPrefix(name, false);
17019 					}
17020 				} else if(name != ".." && name.length > 1 && name[0] == '.') {
17021 					if(showDotFiles) {
17022 						dirs ~= name;
17023 						considerCommonPrefix(name, false);
17024 					}
17025 				} else {
17026 					dirs ~= name;
17027 					considerCommonPrefix(name, false);
17028 				}
17029 			} else {
17030 				if(matchesFilter(name)) {
17031 					files ~= name;
17032 
17033 					//if(filter.length > 0 && filter[$-1] == '*') {
17034 						considerCommonPrefix(name, true);
17035 					//}
17036 				}
17037 			}
17038 		});
17039 		catch(ArsdExceptionBase e) {
17040 			messageBox("Unable to read requested directory");
17041 			// FIXME: give them a chance to create it? or at least go back?
17042 			/+
17043 			comingFromHistory = true;
17044 			back();
17045 			return null;
17046 			+/
17047 		}
17048 
17049 		extern(C) static int comparator(scope const void* a, scope const void* b) {
17050 			auto sa = *cast(string*) a;
17051 			auto sb = *cast(string*) b;
17052 
17053 			/+
17054 				Goal here:
17055 
17056 				Dot first. This puts `foo.d` before `foo2.d`
17057 				Then numbers , natural sort order (so 9 comes before 10) for positive numbers
17058 				Then letters, in order Aa, Bb, Cc
17059 				Then other symbols in ascii order
17060 			+/
17061 			static int nextPiece(ref string whole) {
17062 				if(whole.length == 0)
17063 					return -1;
17064 
17065 				enum specialZoneSize = 1;
17066 
17067 				char current = whole[0];
17068 				if(current >= '0' && current <= '9') {
17069 					int accumulator;
17070 					do {
17071 						whole = whole[1 .. $];
17072 						accumulator *= 10;
17073 						accumulator += current - '0';
17074 						current = whole.length ? whole[0] : 0;
17075 					} while (current >= '0' && current <= '9');
17076 
17077 					return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols
17078 				} else {
17079 					whole = whole[1 .. $];
17080 
17081 					if(current == '.')
17082 						return 0; // the special case to put it before numbers
17083 
17084 					// anything above should be < specialZoneSize
17085 
17086 					int letterZoneSize = 26 * 2;
17087 					int base = int.max - letterZoneSize - char.max; // leaves space at end for symbols too if we want them after chars
17088 
17089 					if(current >= 'A' && current <= 'Z')
17090 						return base + (current - 'A') * 2;
17091 					if(current >= 'a' && current <= 'z')
17092 						return base + (current - 'a') * 2 + 1;
17093 					// return base + letterZoneSize + current; // would put symbols after numbers and letters
17094 					return specialZoneSize + current; // puts symbols before numbers and letters, but after the special zone
17095 				}
17096 			}
17097 
17098 			while(sa.length || sb.length) {
17099 				auto pa = nextPiece(sa);
17100 				auto pb = nextPiece(sb);
17101 
17102 				auto diff = pa - pb;
17103 				if(diff)
17104 					return diff;
17105 			}
17106 
17107 			return 0;
17108 		}
17109 
17110 		nonPhobosSort(files, &comparator);
17111 		nonPhobosSort(dirs, &comparator);
17112 
17113 		listWidget.clear();
17114 		dirWidget.clear();
17115 		foreach(name; dirs)
17116 			dirWidget.addOption(name);
17117 		foreach(name; files)
17118 			listWidget.addOption(name);
17119 
17120 		return CommonPrefixInfo(commonPrefix, commonPrefixCount, exactMatch);
17121 	}
17122 
17123 	ListWidget listWidget;
17124 	ListWidget dirWidget;
17125 
17126 	FreeEntrySelection filesOfType;
17127 	LineEdit directoryHolder;
17128 
17129 	string currentDirectory_;
17130 	FileNameFilter currentNonTabFilter;
17131 	FileNameFilter currentFilter;
17132 	FileNameFilterSet filterOptions;
17133 
17134 	void currentDirectory(string s) {
17135 		currentDirectory_ = FilePath(s).makeAbsolute(getCurrentWorkingDirectory()).toString();
17136 		directoryHolder.content = currentDirectory_;
17137 	}
17138 	string currentDirectory() {
17139 		return currentDirectory_;
17140 	}
17141 
17142 	private string getUserHomeDir() {
17143 		import core.stdc.stdlib;
17144 		version(Windows)
17145 			return (stringz(getenv("HOMEDRIVE")).borrow ~ stringz(getenv("HOMEPATH")).borrow).idup;
17146 		else
17147 			return (stringz(getenv("HOME")).borrow).idup;
17148 	}
17149 
17150 	private string expandTilde(string s) {
17151 		// FIXME: cannot look up other user dirs
17152 		if(s.length == 1 && s == "~")
17153 			return getUserHomeDir();
17154 		if(s.length > 1 && s[0] == '~' && s[1] == '/')
17155 			return getUserHomeDir() ~ s[1 .. $];
17156 		return s;
17157 	}
17158 
17159 	// FIXME: allow many files to be picked too sometimes
17160 
17161 	//string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
17162 	this(bool isOpenDialogInsteadOfSave, string prefilledName, string[] filtersInWindowsFormat, string initialDirectory, Window owner = null) {
17163 		this.filterOptions = FileNameFilterSet.fromWindowsFileNameFilterDescription(filtersInWindowsFormat);
17164 		this.isOpenDialogInsteadOfSave = isOpenDialogInsteadOfSave;
17165 		super(owner, 500, 400, "Choose File..."); // owner);
17166 
17167 		{
17168 			auto navbar = new HorizontalLayout(24, this);
17169 			auto backButton = new ToolButton(new Action("<", 0, &this.back), navbar);
17170 			auto forwardButton = new ToolButton(new Action(">", 0, &this.forward), navbar);
17171 			auto upButton = new ToolButton(new Action("^", 0, &this.up), navbar); // hmm with .. in the dir list we don't really need an up button
17172 
17173 			directoryHolder = new LineEdit(navbar);
17174 
17175 			directoryHolder.addEventListener(delegate(scope KeyDownEvent kde) {
17176 				if(kde.key == Key.Enter || kde.key == Key.PadEnter) {
17177 					kde.stopPropagation();
17178 
17179 					currentDirectory = directoryHolder.content;
17180 					loadFiles(currentDirectory, currentFilter);
17181 
17182 					lineEdit.focus();
17183 				}
17184 			});
17185 
17186 			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.
17187 
17188 			/+
17189 			auto newDirectoryButton = new ToolButton(new Action("N"), navbar);
17190 
17191 			// FIXME: make sure putting `.` in the dir filter goes back to the CWD
17192 			// and that ~ goes back to the home dir
17193 			// and blanking it goes back to the suggested dir
17194 
17195 			auto homeButton = new ToolButton(new Action("H"), navbar);
17196 			auto cwdButton = new ToolButton(new Action("."), navbar);
17197 			auto suggestedDirectoryButton = new ToolButton(new Action("*"), navbar);
17198 			+/
17199 
17200 			filesOfType = new class FreeEntrySelection {
17201 				this() {
17202 					string[] opt;
17203 					foreach(option; filterOptions.filters)
17204 						opt ~=  option.toString;
17205 					super(opt, navbar);
17206 				}
17207 				override int flexBasisWidth() {
17208 					return scaleWithDpi(150);
17209 				}
17210 				override int widthStretchiness() {
17211 					return 1;//super.widthStretchiness() / 2;
17212 				}
17213 			};
17214 			filesOfType.setSelection(0);
17215 			currentFilter = filterOptions.filters[0];
17216 			currentNonTabFilter = currentFilter;
17217 		}
17218 
17219 		{
17220 			auto mainGrid = new GridLayout(4, 1, this);
17221 
17222 			dirWidget = new ListWidget(mainGrid);
17223 			listWidget = new ListWidget(mainGrid);
17224 			listWidget.tabStop = false;
17225 			dirWidget.tabStop = false;
17226 
17227 			FileDialogDelegate.PreviewWidget previewWidget = fileDialogDelegate.makePreviewWidget(mainGrid);
17228 
17229 			mainGrid.setChildPosition(dirWidget, 0, 0, 1, 1);
17230 			mainGrid.setChildPosition(listWidget, 1, 0, previewWidget !is null ? 2 : 3, 1);
17231 			if(previewWidget)
17232 				mainGrid.setChildPosition(previewWidget, 2, 0, 1, 1);
17233 
17234 			// double click events normally trigger something else but
17235 			// here user might be clicking kinda fast and we'd rather just
17236 			// keep it
17237 			dirWidget.addEventListener((scope DoubleClickEvent dev) {
17238 				auto ce = new ChangeEvent!void(dirWidget, () {});
17239 				ce.dispatch();
17240 				lineEdit.focus();
17241 			});
17242 
17243 			dirWidget.addEventListener((scope ChangeEvent!void sce) {
17244 				string v;
17245 				foreach(o; dirWidget.options)
17246 					if(o.selected) {
17247 						v = o.label;
17248 						break;
17249 					}
17250 				if(v.length) {
17251 					if(v == "$HOME")
17252 						currentDirectory = getUserHomeDir();
17253 					else if(v == "$PWD")
17254 						currentDirectory = ".";
17255 					else
17256 						currentDirectory = currentDirectory ~ "/" ~ v;
17257 					loadFiles(currentDirectory, currentFilter);
17258 				}
17259 
17260 				dirWidget.focusOn = -1;
17261 				lineEdit.focus();
17262 			});
17263 
17264 			// double click here, on the other hand, selects the file
17265 			// and moves on
17266 			listWidget.addEventListener((scope DoubleClickEvent dev) {
17267 				OK();
17268 			});
17269 		}
17270 
17271 		lineEdit = new LabeledLineEdit("File name:", TextAlignment.Right, this);
17272 		lineEdit.focus();
17273 		lineEdit.addEventListener(delegate(CharEvent event) {
17274 			if(event.character == '\t' || event.character == '\n')
17275 				event.preventDefault();
17276 		});
17277 
17278 		listWidget.addEventListener(EventType.change, () {
17279 			foreach(o; listWidget.options)
17280 				if(o.selected)
17281 					lineEdit.content = o.label;
17282 		});
17283 
17284 		currentDirectory = initialDirectory is null ? "." : initialDirectory;
17285 
17286 		auto prefilledPath = FilePath(expandTilde(prefilledName)).makeAbsolute(FilePath(currentDirectory));
17287 		currentDirectory = prefilledPath.directoryName;
17288 		prefilledName = prefilledPath.filename;
17289 		loadFiles(currentDirectory, currentFilter);
17290 
17291 		filesOfType.addEventListener(delegate (FreeEntrySelection.SelectionChangedEvent ce) {
17292 			currentFilter = FileNameFilter.fromString(ce.stringValue);
17293 			currentNonTabFilter = currentFilter;
17294 			loadFiles(currentDirectory, currentFilter);
17295 			// lineEdit.focus(); // this causes a recursive crash.....
17296 		});
17297 
17298 		filesOfType.addEventListener(delegate(KeyDownEvent event) {
17299 			if(event.key == Key.Enter) {
17300 				currentFilter = FileNameFilter.fromString(filesOfType.content);
17301 				currentNonTabFilter = currentFilter;
17302 				loadFiles(currentDirectory, currentFilter);
17303 				event.stopPropagation();
17304 				// FIXME: refocus on the line edit
17305 			}
17306 		});
17307 
17308 		lineEdit.addEventListener((KeyDownEvent event) {
17309 			if(event.key == Key.Tab && !event.ctrlKey && !event.shiftKey) {
17310 
17311 				auto path = FilePath(expandTilde(lineEdit.content)).makeAbsolute(FilePath(currentDirectory));
17312 				currentDirectory = path.directoryName;
17313 				auto current = path.filename;
17314 
17315 				auto newFilter = current;
17316 				if(current.length && current[0] != '*' && current[$-1] != '*')
17317 					newFilter ~= "*";
17318 				else if(newFilter.length == 0)
17319 					newFilter = "*";
17320 
17321 				auto newFilterObj = FileNameFilter("Custom filter", [newFilter]);
17322 
17323 				CommonPrefixInfo commonPrefix = loadFiles(currentDirectory, newFilterObj);
17324 				if(commonPrefix.fileCount == 1) {
17325 					// exactly one file, let's see what it is
17326 					auto specificFile = FilePath(commonPrefix.exactMatch).makeAbsolute(FilePath(currentDirectory));
17327 					if(getFileType(specificFile.toString) == FileType.dir) {
17328 						// a directory means we should change to it and keep the old filter
17329 						currentDirectory = specificFile.toString();
17330 						lineEdit.content = specificFile.toString() ~ "/";
17331 						loadFiles(currentDirectory, currentFilter);
17332 					} else {
17333 						// any other file should be selected in the list
17334 						currentDirectory = specificFile.directoryName;
17335 						current = specificFile.filename;
17336 						lineEdit.content = current;
17337 						loadFiles(currentDirectory, currentFilter);
17338 					}
17339 				} else if(commonPrefix.fileCount > 1) {
17340 					currentFilter = newFilterObj;
17341 					filesOfType.content = currentFilter.toString();
17342 					lineEdit.content = commonPrefix.commonPrefix;
17343 				} else {
17344 					// if there were no files, we don't really want to change the filter..
17345 					//sdpyPrintDebugString("no files");
17346 				}
17347 
17348 				// FIXME: if that is a directory, add the slash? or even go inside?
17349 
17350 				event.preventDefault();
17351 			}
17352 			else if(event.key == Key.Left && event.altKey) {
17353 				this.back();
17354 				event.preventDefault();
17355 			}
17356 			else if(event.key == Key.Right && event.altKey) {
17357 				this.forward();
17358 				event.preventDefault();
17359 			}
17360 		});
17361 
17362 
17363 		lineEdit.content = prefilledName;
17364 
17365 		auto hl = new HorizontalLayout(60, this);
17366 		auto cancelButton = new Button("Cancel", hl);
17367 		auto okButton = new Button(isOpenDialogInsteadOfSave ? "Open" : "Save"/*"OK"*/, hl);
17368 
17369 		cancelButton.addEventListener(EventType.triggered, &Cancel);
17370 		okButton.addEventListener(EventType.triggered, &OK);
17371 
17372 		this.addEventListener((KeyDownEvent event) {
17373 			if(event.key == Key.Enter || event.key == Key.PadEnter) {
17374 				event.preventDefault();
17375 				OK();
17376 			}
17377 			else if(event.key == Key.Escape)
17378 				Cancel();
17379 			else if(event.key == Key.F5)
17380 				refresh();
17381 			else if(event.key == Key.Up && event.altKey)
17382 				up(); // ditto
17383 			else if(event.key == Key.Left && event.altKey)
17384 				back(); // FIXME: it sends the key to the line edit too
17385 			else if(event.key == Key.Right && event.altKey)
17386 				forward(); // ditto
17387 			else if(event.key == Key.Up)
17388 				listWidget.setSelection(listWidget.getSelection() - 1);
17389 			else if(event.key == Key.Down)
17390 				listWidget.setSelection(listWidget.getSelection() + 1);
17391 		});
17392 
17393 		// FIXME: set the list view's focusOn to -1 on most interactions so it doesn't keep a thing highlighted
17394 		// FIXME: button to create new directory
17395 		// FIXME: show dirs in the files list too? idk.
17396 
17397 		// FIXME: support ~ as alias for home in the input
17398 		// FIXME: tab complete ought to be able to change+complete dir too
17399 	}
17400 
17401 	override void OK() {
17402 		if(lineEdit.content.length) {
17403 			auto c = expandTilde(lineEdit.content);
17404 
17405 			FilePath accepted = FilePath(c).makeAbsolute(FilePath(currentDirectory));
17406 
17407 			auto ft = getFileType(accepted.toString);
17408 
17409 			if(ft == FileType.error && isOpenDialogInsteadOfSave) {
17410 				// FIXME: tell the user why
17411 				messageBox("Cannot open file: " ~ accepted.toString ~ "\nTry another or cancel.");
17412 				lineEdit.focus();
17413 				return;
17414 
17415 			}
17416 
17417 			// FIXME: symlinks to dirs should prolly also get this behavior
17418 			if(ft == FileType.dir) {
17419 				currentDirectory = accepted.toString;
17420 
17421 				currentFilter = currentNonTabFilter;
17422 				filesOfType.content = currentFilter.toString();
17423 
17424 				loadFiles(currentDirectory, currentFilter);
17425 				lineEdit.content = "";
17426 
17427 				lineEdit.focus();
17428 
17429 				return;
17430 			}
17431 
17432 			if(onOK)
17433 				onOK(accepted.toString);
17434 		}
17435 		close();
17436 	}
17437 
17438 	override void Cancel() {
17439 		if(onCancel)
17440 			onCancel();
17441 		close();
17442 	}
17443 }
17444 
17445 private enum FileType {
17446 	error,
17447 	dir,
17448 	other
17449 }
17450 
17451 private FileType getFileType(string name) {
17452 	version(Windows) {
17453 		auto ws = WCharzBuffer(name);
17454 		auto ret = GetFileAttributesW(ws.ptr);
17455 		if(ret == INVALID_FILE_ATTRIBUTES)
17456 			return FileType.error;
17457 		return ((ret & FILE_ATTRIBUTE_DIRECTORY) != 0) ? FileType.dir : FileType.other;
17458 	} else version(Posix) {
17459 		import core.sys.posix.sys.stat;
17460 		stat_t buf;
17461 		auto ret = stat((name ~ '\0').ptr, &buf);
17462 		if(ret == -1)
17463 			return FileType.error;
17464 		return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other;
17465 	} else assert(0, "Not implemented");
17466 }
17467 
17468 /*
17469 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes
17470 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx
17471 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx
17472 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx
17473 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx
17474 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box
17475 http://www.sbin.org/doc/Xlib/chapt_03.html
17476 
17477 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx
17478 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx
17479 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx
17480 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx
17481 */
17482 
17483 
17484 // These are all for setMenuAndToolbarFromAnnotatedCode
17485 /// This item in the menu will be preceded by a separator line
17486 /// Group: generating_from_code
17487 struct separator {}
17488 deprecated("It was misspelled, use separator instead") alias seperator = separator;
17489 /// Program-wide keyboard shortcut to trigger the action
17490 /// Group: generating_from_code
17491 struct accelerator { string keyString; } // FIXME: allow multiple aliases here
17492 /// tells which menu the action will be on
17493 /// Group: generating_from_code
17494 struct menu { string name; }
17495 /// Describes which toolbar section the action appears on
17496 /// Group: generating_from_code
17497 struct toolbar { string groupName; }
17498 ///
17499 /// Group: generating_from_code
17500 struct icon { ushort id; }
17501 ///
17502 /// Group: generating_from_code
17503 struct label { string label; }
17504 ///
17505 /// Group: generating_from_code
17506 struct hotkey { dchar ch; }
17507 ///
17508 /// Group: generating_from_code
17509 struct tip { string tip; }
17510 ///
17511 /// Group: generating_from_code
17512 enum context_menu = menu.init;
17513 
17514 
17515 /++
17516 	Observes and allows inspection of an object via automatic gui
17517 +/
17518 /// Group: generating_from_code
17519 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) {
17520 	return new ObjectInspectionWindowImpl!(T)(t);
17521 }
17522 
17523 class ObjectInspectionWindow : Window {
17524 	this(int a, int b, string c) {
17525 		super(a, b, c);
17526 	}
17527 
17528 	abstract void readUpdatesFromObject();
17529 }
17530 
17531 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow {
17532 	T t;
17533 	this(T t) {
17534 		this.t = t;
17535 
17536 		super(300, 400, "ObjectInspectionWindow - " ~ T.stringof);
17537 
17538 		foreach(memberName; __traits(derivedMembers, T)) {{
17539 			alias member = I!(__traits(getMember, t, memberName))[0];
17540 			alias type = typeof(member);
17541 			static if(is(type == int)) {
17542 				auto le = new LabeledLineEdit(memberName ~ ": ", this);
17543 				//le.addEventListener("char", (Event ev) {
17544 					//if((ev.character < '0' || ev.character > '9') && ev.character != '-')
17545 						//ev.preventDefault();
17546 				//});
17547 				le.addEventListener(EventType.change, (Event ev) {
17548 					__traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue);
17549 				});
17550 
17551 				updateMemberDelegates[memberName] = () {
17552 					le.content = toInternal!string(__traits(getMember, t, memberName));
17553 				};
17554 			}
17555 		}}
17556 	}
17557 
17558 	void delegate()[string] updateMemberDelegates;
17559 
17560 	override void readUpdatesFromObject() {
17561 		foreach(k, v; updateMemberDelegates)
17562 			v();
17563 	}
17564 }
17565 
17566 /++
17567 	Creates a dialog based on a data structure.
17568 
17569 	---
17570 	dialog(window, (YourStructure value) {
17571 		// the user filled in the struct and clicked OK,
17572 		// you can check the members now
17573 	});
17574 	---
17575 
17576 	Params:
17577 		initialData = the initial value to show in the dialog. It will not modify this unless
17578 		it is a class then it might, no promises.
17579 
17580 	History:
17581 		The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5)
17582 
17583 		The overloads with `parent` were added September 29, 2024. The ones without it are likely to
17584 		be deprecated soon.
17585 +/
17586 /// Group: generating_from_code
17587 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
17588 	dialog(null, T.init, onOK, onCancel, title);
17589 }
17590 /// ditto
17591 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
17592 	dialog(null, T.init, onOK, onCancel, title);
17593 }
17594 /// ditto
17595 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
17596 	dialog(parent, T.init, onOK, onCancel, title);
17597 }
17598 /// ditto
17599 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
17600 	dialog(parent, initialData, onOK, onCancel, title);
17601 }
17602 /// ditto
17603 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
17604 	auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title);
17605 	dg.show();
17606 }
17607 
17608 private static template I(T...) { alias I = T; }
17609 
17610 
17611 private string beautify(string name, char space = ' ', bool allLowerCase = false) {
17612 	if(name == "id")
17613 		return allLowerCase ? name : "ID";
17614 
17615 	char[160] buffer;
17616 	int bufferIndex = 0;
17617 	bool shouldCap = true;
17618 	bool shouldSpace;
17619 	bool lastWasCap;
17620 	foreach(idx, char ch; name) {
17621 		if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
17622 
17623 		if((ch >= 'A' && ch <= 'Z') || ch == '_') {
17624 			if(lastWasCap) {
17625 				// two caps in a row, don't change. Prolly acronym.
17626 			} else {
17627 				if(idx)
17628 					shouldSpace = true; // new word, add space
17629 			}
17630 
17631 			lastWasCap = true;
17632 		} else {
17633 			lastWasCap = false;
17634 		}
17635 
17636 		if(shouldSpace) {
17637 			buffer[bufferIndex++] = space;
17638 			if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
17639 			shouldSpace = false;
17640 		}
17641 		if(shouldCap) {
17642 			if(ch >= 'a' && ch <= 'z')
17643 				ch -= 32;
17644 			shouldCap = false;
17645 		}
17646 		if(allLowerCase && ch >= 'A' && ch <= 'Z')
17647 			ch += 32;
17648 		buffer[bufferIndex++] = ch;
17649 	}
17650 	return buffer[0 .. bufferIndex].idup;
17651 }
17652 
17653 /++
17654 	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.
17655 +/
17656 class AutomaticDialog(T) : Dialog {
17657 	T t;
17658 
17659 	void delegate(T) onOK;
17660 	void delegate() onCancel;
17661 
17662 	override int paddingTop() { return defaultLineHeight; }
17663 	override int paddingBottom() { return defaultLineHeight; }
17664 	override int paddingRight() { return defaultLineHeight; }
17665 	override int paddingLeft() { return defaultLineHeight; }
17666 
17667 	this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) {
17668 		assert(onOK !is null);
17669 
17670 		t = initialData;
17671 
17672 		static if(is(T == class)) {
17673 			if(t is null)
17674 				t = new T();
17675 		}
17676 		this.onOK = onOK;
17677 		this.onCancel = onCancel;
17678 		super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title);
17679 
17680 		static if(is(T == class))
17681 			this.addDataControllerWidget(t);
17682 		else
17683 			this.addDataControllerWidget(&t);
17684 
17685 		auto hl = new HorizontalLayout(this);
17686 		auto stretch = new HorizontalSpacer(hl); // to right align
17687 		auto ok = new CommandButton("OK", hl);
17688 		auto cancel = new CommandButton("Cancel", hl);
17689 		ok.addEventListener(EventType.triggered, &OK);
17690 		cancel.addEventListener(EventType.triggered, &Cancel);
17691 
17692 		this.addEventListener((KeyDownEvent ev) {
17693 			if(ev.key == Key.Enter || ev.key == Key.PadEnter) {
17694 				ok.focus();
17695 				OK();
17696 				ev.preventDefault();
17697 			}
17698 			if(ev.key == Key.Escape) {
17699 				Cancel();
17700 				ev.preventDefault();
17701 			}
17702 		});
17703 
17704 		this.addEventListener((scope ClosedEvent ce) {
17705 			if(onCancel)
17706 				onCancel();
17707 		});
17708 
17709 		//this.children[0].focus();
17710 	}
17711 
17712 	override void OK() {
17713 		onOK(t);
17714 		close();
17715 	}
17716 
17717 	override void Cancel() {
17718 		if(onCancel)
17719 			onCancel();
17720 		close();
17721 	}
17722 }
17723 
17724 private template baseClassCount(Class) {
17725 	private int helper() {
17726 		int count = 0;
17727 		static if(is(Class bases == super)) {
17728 			foreach(base; bases)
17729 				static if(is(base == class))
17730 					count += 1 + baseClassCount!base;
17731 		}
17732 		return count;
17733 	}
17734 
17735 	enum int baseClassCount = helper();
17736 }
17737 
17738 private long stringToLong(string s) {
17739 	long ret;
17740 	if(s.length == 0)
17741 		return ret;
17742 	bool negative = s[0] == '-';
17743 	if(negative)
17744 		s = s[1 .. $];
17745 	foreach(ch; s) {
17746 		if(ch >= '0' && ch <= '9') {
17747 			ret *= 10;
17748 			ret += ch - '0';
17749 		}
17750 	}
17751 	if(negative)
17752 		ret = -ret;
17753 	return ret;
17754 }
17755 
17756 
17757 interface ReflectableProperties {
17758 	/++
17759 		Iterates the event's properties as strings. Note that keys may be repeated and a get property request may
17760 		call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by
17761 		json in the current implementation.
17762 
17763 		This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have
17764 		properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself
17765 		as of the June 2, 2021 release.
17766 
17767 		History:
17768 			Added June 2, 2021.
17769 
17770 		See_Also: [getPropertyAsString], [setPropertyFromString]
17771 	+/
17772 	void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow;
17773 	/++
17774 		Requests a property to be delivered to you as a string, through your `sink` delegate.
17775 
17776 		If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should
17777 		be interpreted as json, otherwise, it is just a plain string.
17778 
17779 		The sink should always be called exactly once for each call (it is basically a return value, but it might
17780 		use a local buffer it maintains instead of allocating a return value).
17781 
17782 		History:
17783 			Added June 2, 2021.
17784 
17785 		See_Also: [getPropertiesList], [setPropertyFromString]
17786 	+/
17787 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink);
17788 	/++
17789 		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.
17790 
17791 		History:
17792 			Added June 2, 2021.
17793 
17794 		See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult]
17795 	+/
17796 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson);
17797 
17798 	/// [setPropertyFromString] possible return values
17799 	enum SetPropertyResult {
17800 		success = 0, /// the property has been successfully set to the request value
17801 		notPermitted = -1, /// the property exists but it cannot be changed at this time
17802 		notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist)
17803 		noSuchProperty = -3, /// there is no property by that name
17804 		wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value
17805 		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)
17806 	}
17807 
17808 	/++
17809 		You can mix this in to get an implementation in child classes. This does [setPropertyFromString].
17810 
17811 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
17812 
17813 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
17814 		rarely need to use these building blocks directly.
17815 	+/
17816 	mixin template RegisterSetters() {
17817 		override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
17818 			switch(name) {
17819 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
17820 					case memberName:
17821 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
17822 							if(value != "true" && value != "false")
17823 								return SetPropertyResult.wrongFormat;
17824 							__traits(getMember, this, memberName) = value == "true" ? true : false;
17825 							return SetPropertyResult.success;
17826 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
17827 							import core.stdc.stdlib;
17828 							char[128] zero = 0;
17829 							if(buffer.length + 1 >= zero.length)
17830 								return SetPropertyResult.wrongFormat;
17831 							zero[0 .. buffer.length] = buffer[];
17832 							__traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10);
17833 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
17834 							import core.stdc.stdlib;
17835 							char[128] zero = 0;
17836 							if(buffer.length + 1 >= zero.length)
17837 								return SetPropertyResult.wrongFormat;
17838 							zero[0 .. buffer.length] = buffer[];
17839 							__traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10);
17840 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
17841 							__traits(getMember, this, memberName) = value.idup;
17842 						} else {
17843 							return SetPropertyResult.notImplemented;
17844 						}
17845 
17846 				}
17847 				default:
17848 					return super.setPropertyFromString(name, value, valueIsJson);
17849 			}
17850 		}
17851 	}
17852 
17853 	/++
17854 		You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList].
17855 
17856 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
17857 
17858 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
17859 		rarely need to use these building blocks directly.
17860 	+/
17861 	mixin template RegisterGetters() {
17862 		override void getPropertiesList(scope void delegate(string name) sink) const {
17863 			super.getPropertiesList(sink);
17864 
17865 			foreach(memberName; __traits(derivedMembers, typeof(this))) {
17866 				sink(memberName);
17867 			}
17868 		}
17869 		override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
17870 			switch(name) {
17871 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
17872 					case memberName:
17873 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
17874 							sink(name, __traits(getMember, this, memberName) ? "true" : "false", true);
17875 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
17876 							import core.stdc.stdio;
17877 							char[32] buffer;
17878 							auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName));
17879 							sink(name, buffer[0 .. len], true);
17880 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
17881 							import core.stdc.stdio;
17882 							char[32] buffer;
17883 							auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName));
17884 							sink(name, buffer[0 .. len], true);
17885 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
17886 							sink(name, __traits(getMember, this, memberName), false);
17887 							//sinkJsonString(memberName, __traits(getMember, this, memberName), sink);
17888 						} else {
17889 							sink(name, null, true);
17890 						}
17891 
17892 					return;
17893 				}
17894 				default:
17895 					return super.getPropertyAsString(name, sink);
17896 			}
17897 		}
17898 	}
17899 }
17900 
17901 private struct Stack(T) {
17902 	this(int maxSize) {
17903 		internalLength = 0;
17904 		arr = initialBuffer[];
17905 	}
17906 
17907 	///.
17908 	void push(T t) {
17909 		if(internalLength >= arr.length) {
17910 			auto oldarr = arr;
17911 			if(arr.length < 4096)
17912 				arr = new T[arr.length * 2];
17913 			else
17914 				arr = new T[arr.length + 4096];
17915 			arr[0 .. oldarr.length] = oldarr[];
17916 		}
17917 
17918 		arr[internalLength] = t;
17919 		internalLength++;
17920 	}
17921 
17922 	///.
17923 	T pop() {
17924 		assert(internalLength);
17925 		internalLength--;
17926 		return arr[internalLength];
17927 	}
17928 
17929 	///.
17930 	T peek() {
17931 		assert(internalLength);
17932 		return arr[internalLength - 1];
17933 	}
17934 
17935 	///.
17936 	@property bool empty() {
17937 		return internalLength ? false : true;
17938 	}
17939 
17940 	///.
17941 	private T[] arr;
17942 	private size_t internalLength;
17943 	private T[64] initialBuffer;
17944 	// 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),
17945 	// using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push()
17946 	// function thanks to this, and push() was actually one of the slowest individual functions in the code!
17947 }
17948 
17949 /// 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.
17950 private struct WidgetStream {
17951 
17952 	///.
17953 	@property Widget front() {
17954 		return current.widget;
17955 	}
17956 
17957 	/// Use Widget.tree instead.
17958 	this(Widget start) {
17959 		current.widget = start;
17960 		current.childPosition = -1;
17961 		isEmpty = false;
17962 		stack = typeof(stack)(0);
17963 	}
17964 
17965 	/*
17966 		Handle it
17967 		handle its children
17968 
17969 	*/
17970 
17971 	///.
17972 	void popFront() {
17973 	    more:
17974 	    	if(isEmpty) return;
17975 
17976 		// FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times)
17977 
17978 		current.childPosition++;
17979 		if(current.childPosition >= current.widget.children.length) {
17980 			if(stack.empty())
17981 				isEmpty = true;
17982 			else {
17983 				current = stack.pop();
17984 				goto more;
17985 			}
17986 		} else {
17987 			stack.push(current);
17988 			current.widget = current.widget.children[current.childPosition];
17989 			current.childPosition = -1;
17990 		}
17991 	}
17992 
17993 	///.
17994 	@property bool empty() {
17995 		return isEmpty;
17996 	}
17997 
17998 	private:
17999 
18000 	struct Current {
18001 		Widget widget;
18002 		int childPosition;
18003 	}
18004 
18005 	Current current;
18006 
18007 	Stack!(Current) stack;
18008 
18009 	bool isEmpty;
18010 }
18011 
18012 
18013 /+
18014 
18015 	I could fix up the hierarchy kinda like this
18016 
18017 	class Widget {
18018 		Widget[] children() { return null; }
18019 	}
18020 	interface WidgetContainer {
18021 		Widget asWidget();
18022 		void addChild(Widget w);
18023 
18024 		// alias asWidget this; // but meh
18025 	}
18026 
18027 	Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead.
18028 
18029 	class Layout : Widget, WidgetContainer {}
18030 
18031 	class Window : WidgetContainer {}
18032 
18033 
18034 	All constructors that previously took Widgets should now take WidgetContainers instead
18035 
18036 
18037 
18038 	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".
18039 +/
18040 
18041 /+
18042 	LAYOUTS 2.0
18043 
18044 	can just be assigned as a function. assigning a new one will cause it to be immediately called.
18045 
18046 	they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one.
18047 
18048 	recomputeChildLayout only really needs a property accessor proxy... just the layout info too.
18049 
18050 	and even Paint can just use computedStyle...
18051 
18052 		background color
18053 		font
18054 		border color and style
18055 
18056 	And actually the style proxy can offer some helper routines to draw these like the draw 3d box
18057 		please note that many widgets and in some modes will completely ignore properties as they will.
18058 		they are just hints you set, not promises.
18059 
18060 
18061 
18062 
18063 
18064 	So generally the existing virtual functions are just the default for the class. But individual objects
18065 	or stylesheets can override this. The virtual ones count as tag-level specificity in css.
18066 +/
18067 
18068 /++
18069 	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.
18070 
18071 	History:
18072 		Added May 24, 2021.
18073 +/
18074 struct WidgetBackground {
18075 	/++
18076 		A background with the given solid color.
18077 	+/
18078 	this(Color color) {
18079 		this.color = color;
18080 	}
18081 
18082 	this(WidgetBackground bg) {
18083 		this = bg;
18084 	}
18085 
18086 	/++
18087 		Creates a widget from the string.
18088 
18089 		Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css.
18090 	+/
18091 	static WidgetBackground fromString(string s) {
18092 		return WidgetBackground(Color.fromString(s));
18093 	}
18094 
18095 	/++
18096 		The background is not necessarily a solid color, but you can always specify a color as a fallback.
18097 
18098 		History:
18099 			Made `public` on December 18, 2022 (dub v10.10).
18100 	+/
18101 	Color color;
18102 }
18103 
18104 /++
18105 	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!)
18106 
18107 	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.
18108 
18109 	You should not inherit from this directly, but instead use [VisualTheme].
18110 
18111 	History:
18112 		Added May 8, 2021
18113 +/
18114 abstract class BaseVisualTheme {
18115 	/// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override.
18116 	abstract void doPaint(Widget widget, WidgetPainter painter);
18117 
18118 	/+
18119 	/// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override.
18120 	abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg);
18121 	+/
18122 
18123 	/++
18124 		Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css,
18125 		where the interpretation of the string varies for each property and may include things like measurement units.
18126 	+/
18127 	abstract string getPropertyString(Widget widget, string propertyName);
18128 
18129 	/++
18130 		Default background color of the window. Widgets also use this to simulate transparency.
18131 
18132 		Probably some shade of grey.
18133 	+/
18134 	abstract Color windowBackgroundColor();
18135 	abstract Color widgetBackgroundColor();
18136 	abstract Color foregroundColor();
18137 	abstract Color lightAccentColor();
18138 	abstract Color darkAccentColor();
18139 
18140 	/++
18141 		Colors used to indicate active selections in lists and text boxes, etc.
18142 	+/
18143 	abstract Color selectionForegroundColor();
18144 	/// ditto
18145 	abstract Color selectionBackgroundColor();
18146 
18147 	deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); }
18148 
18149 	/++
18150 		If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally.
18151 	+/
18152 	abstract OperatingSystemFont defaultFont(int dpi);
18153 
18154 	private OperatingSystemFont[int] defaultFontCache_;
18155 	private OperatingSystemFont defaultFontCached(int dpi) {
18156 		if(dpi !in defaultFontCache_) {
18157 			// FIXME: set this to false if X disconnect or if visual theme changes
18158 			defaultFontCache_[dpi] = defaultFont(dpi);
18159 		}
18160 		return defaultFontCache_[dpi];
18161 	}
18162 }
18163 
18164 /+
18165 	A widget should have:
18166 		classList
18167 		dataset
18168 		attributes
18169 		computedStyles
18170 		state (persistent)
18171 		dynamic state (focused, hover, etc)
18172 +/
18173 
18174 // visualTheme.computedStyle(this).paddingLeft
18175 
18176 
18177 /++
18178 	This is your entry point to create your own visual theme for custom widgets.
18179 
18180 	You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods.
18181 
18182 	Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating.
18183 +/
18184 abstract class VisualTheme(CRTP) : BaseVisualTheme {
18185 	override string getPropertyString(Widget widget, string propertyName) {
18186 		return null;
18187 	}
18188 
18189 	/+
18190 		mixin StyleOverride!Widget
18191 	final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) {
18192 		w.useStyleProperties(dg);
18193 	}
18194 	+/
18195 
18196 	final override void doPaint(Widget widget, WidgetPainter painter) {
18197 		auto derived = cast(CRTP) cast(void*) this;
18198 
18199 		scope void delegate(Widget, WidgetPainter) bestMatch;
18200 		int bestMatchScore;
18201 
18202 		static if(__traits(hasMember, CRTP, "paint"))
18203 		foreach(overload; __traits(getOverloads, CRTP, "paint")) {
18204 			static if(is(typeof(overload) Params == __parameters)) {
18205 				static assert(Params.length == 2);
18206 				static assert(is(Params[0] : Widget));
18207 				static assert(is(Params[1] == WidgetPainter));
18208 				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);
18209 
18210 				alias type = Params[0];
18211 				if(cast(type) widget) {
18212 					auto score = baseClassCount!type;
18213 
18214 					if(score > bestMatchScore) {
18215 						bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload);
18216 						bestMatchScore = score;
18217 					}
18218 				}
18219 			} else static assert(0, "paint should be a method.");
18220 		}
18221 
18222 		if(bestMatch)
18223 			bestMatch(widget, painter);
18224 		else
18225 			widget.paint(painter);
18226 	}
18227 
18228 	deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; }
18229 
18230 	// 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
18231 	// mixin Beautiful95Theme;
18232 	mixin DefaultLightTheme;
18233 
18234 	private static struct Cached {
18235 		// i prolly want to do this
18236 	}
18237 }
18238 
18239 /// ditto
18240 mixin template Beautiful95Theme() {
18241 	override Color windowBackgroundColor() { return Color(212, 212, 212); }
18242 	override Color widgetBackgroundColor() { return Color.white; }
18243 	override Color foregroundColor() { return Color.black; }
18244 	override Color darkAccentColor() { return Color(172, 172, 172); }
18245 	override Color lightAccentColor() { return Color(223, 223, 223); }
18246 	override Color selectionForegroundColor() { return Color.white; }
18247 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
18248 	override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr
18249 }
18250 
18251 /// ditto
18252 mixin template DefaultLightTheme() {
18253 	override Color windowBackgroundColor() { return Color(232, 232, 232); }
18254 	override Color widgetBackgroundColor() { return Color.white; }
18255 	override Color foregroundColor() { return Color.black; }
18256 	override Color darkAccentColor() { return Color(172, 172, 172); }
18257 	override Color lightAccentColor() { return Color(223, 223, 223); }
18258 	override Color selectionForegroundColor() { return Color.white; }
18259 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
18260 	override OperatingSystemFont defaultFont(int dpi) {
18261 		version(Windows)
18262 			return new OperatingSystemFont("Segoe UI");
18263 		else static if(UsingSimpledisplayCocoa) {
18264 			return (new OperatingSystemFont()).loadDefault;
18265 		} else {
18266 			// FIXME: undo xft's scaling so we don't end up double scaled
18267 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
18268 		}
18269 	}
18270 }
18271 
18272 /// ditto
18273 mixin template DefaultDarkTheme() {
18274 	override Color windowBackgroundColor() { return Color(64, 64, 64); }
18275 	override Color widgetBackgroundColor() { return Color.black; }
18276 	override Color foregroundColor() { return Color.white; }
18277 	override Color darkAccentColor() { return Color(20, 20, 20); }
18278 	override Color lightAccentColor() { return Color(80, 80, 80); }
18279 	override Color selectionForegroundColor() { return Color.white; }
18280 	override Color selectionBackgroundColor() { return Color(128, 0, 128); }
18281 	override OperatingSystemFont defaultFont(int dpi) {
18282 		version(Windows)
18283 			return new OperatingSystemFont("Segoe UI", 12);
18284 		else static if(UsingSimpledisplayCocoa) {
18285 			return (new OperatingSystemFont()).loadDefault;
18286 		} else {
18287 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
18288 		}
18289 	}
18290 }
18291 
18292 /// ditto
18293 alias DefaultTheme = DefaultLightTheme;
18294 
18295 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme {
18296 	/+
18297 	OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); }
18298 	Color windowBackgroundColor() { return Color(242, 242, 242); }
18299 	Color darkAccentColor() { return windowBackgroundColor; }
18300 	Color lightAccentColor() { return windowBackgroundColor; }
18301 	+/
18302 }
18303 
18304 /++
18305 	Event fired when an [Observeable] variable changes. You will want to add an event listener referencing
18306 	the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });`
18307 
18308 	History:
18309 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
18310 
18311 		Made `final` on January 3, 2025
18312 +/
18313 final class StateChanged(alias field) : Event {
18314 	enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change";
18315 	override bool cancelable() const { return false; }
18316 	this(Widget target, typeof(field) newValue) {
18317 		this.newValue = newValue;
18318 		super(EventString, target);
18319 	}
18320 
18321 	typeof(field) newValue;
18322 }
18323 
18324 /++
18325 	Convenience function to add a `triggered` event listener.
18326 
18327 	Its implementation is simply `w.addEventListener("triggered", dg);`
18328 
18329 	History:
18330 		Added November 27, 2021 (dub v10.4)
18331 +/
18332 void addWhenTriggered(Widget w, void delegate() dg) {
18333 	w.addEventListener("triggered", dg);
18334 }
18335 
18336 /++
18337 	Observable varables can be added to widgets and when they are changed, it fires
18338 	off a [StateChanged] event so you can react to it.
18339 
18340 	It is implemented as a getter and setter property, along with another helper you
18341 	can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged]
18342 	event through the usual means. Just give the name of the variable. See [StateChanged] for an
18343 	example.
18344 
18345 	History:
18346 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
18347 +/
18348 mixin template Observable(T, string name) {
18349 	private T backing;
18350 
18351 	mixin(q{
18352 		void } ~ name ~ q{_changed (void delegate(T) dg) {
18353 			this.addEventListener((StateChanged!this_thing ev) {
18354 				dg(ev.newValue);
18355 			});
18356 		}
18357 
18358 		@property T } ~ name ~ q{ () {
18359 			return backing;
18360 		}
18361 
18362 		@property void } ~ name ~ q{ (T t) {
18363 			backing = t;
18364 			auto event = new StateChanged!this_thing(this, t);
18365 			event.dispatch();
18366 		}
18367 	});
18368 
18369 	mixin("private alias this_thing = " ~ name ~ ";");
18370 }
18371 
18372 
18373 private bool startsWith(string test, string thing) {
18374 	if(test.length < thing.length)
18375 		return false;
18376 	return test[0 .. thing.length] == thing;
18377 }
18378 
18379 private bool endsWith(string test, string thing) {
18380 	if(test.length < thing.length)
18381 		return false;
18382 	return test[$ - thing.length .. $] == thing;
18383 }
18384 
18385 /++
18386 	Context menus can have `@hotkey`, `@label`, `@tip`, `@separator`, and `@icon`
18387 
18388 	Note they can NOT have accelerators or toolbars; those annotations will be ignored.
18389 
18390 	Mark the functions callable from it with `@context_menu { ... }` Presence of other `@menu(...)` annotations will exclude it from the context menu at this time.
18391 
18392 	See_Also:
18393 		[Widget.setMenuAndToolbarFromAnnotatedCode]
18394 +/
18395 Menu createContextMenuFromAnnotatedCode(TWidget)(TWidget w) if(is(TWidget : Widget)) {
18396 	return createContextMenuFromAnnotatedCode(w, w);
18397 }
18398 
18399 /// ditto
18400 Menu createContextMenuFromAnnotatedCode(T)(Widget w, ref T t) if(!is(T == class) && !is(T == interface)) {
18401 	return createContextMenuFromAnnotatedCode_internal(w, t);
18402 }
18403 /// ditto
18404 Menu createContextMenuFromAnnotatedCode(T)(Widget w, T t) if(is(T == class) || is(T == interface)) {
18405 	return createContextMenuFromAnnotatedCode_internal(w, t);
18406 }
18407 Menu createContextMenuFromAnnotatedCode_internal(T)(Widget w, ref T t) {
18408 	Menu ret = new Menu("", w);
18409 
18410 	foreach(memberName; __traits(derivedMembers, T)) {
18411 		static if(memberName != "this")
18412 		static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
18413 			.menu menu;
18414 			bool separator;
18415 			.hotkey hotkey;
18416 			.icon icon;
18417 			string label;
18418 			string tip;
18419 			foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
18420 				static if(is(typeof(attr) == .menu))
18421 					menu = attr;
18422 				else static if(is(attr == .separator))
18423 					separator = true;
18424 				else static if(is(typeof(attr) == .hotkey))
18425 					hotkey = attr;
18426 				else static if(is(typeof(attr) == .icon))
18427 					icon = attr;
18428 				else static if(is(typeof(attr) == .label))
18429 					label = attr.label;
18430 				else static if(is(typeof(attr) == .tip))
18431 					tip = attr.tip;
18432 			}
18433 
18434 			if(menu is .menu.init) {
18435 				ushort correctIcon = icon.id; // FIXME
18436 				if(label.length == 0)
18437 					label = memberName.toMenuLabel;
18438 
18439 				auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(w.parentWindow, &__traits(getMember, t, memberName));
18440 
18441 				auto action = new Action(label, correctIcon, handler);
18442 
18443 				if(separator)
18444 					ret.addSeparator();
18445 					ret.addItem(new MenuItem(action));
18446 			}
18447 		}
18448 	}
18449 
18450 	return ret;
18451 }
18452 
18453 // still do layout delegation
18454 // and... split off Window from Widget.
18455 
18456 version(minigui_screenshots)
18457 struct Screenshot {
18458 	string name;
18459 }
18460 
18461 version(minigui_screenshots)
18462 static if(__VERSION__ > 2092)
18463 mixin(q{
18464 shared static this() {
18465 	import core.runtime;
18466 
18467 	static UnitTestResult screenshotMagic() {
18468 		string name;
18469 
18470 		import arsd.png;
18471 
18472 		auto results = new Window();
18473 		auto button = new Button("do it", results);
18474 
18475 		Window.newWindowCreated = delegate(Window w) {
18476 			Timer timer;
18477 			timer = new Timer(250, {
18478 				auto img = w.win.takeScreenshot();
18479 				timer.destroy();
18480 
18481 				version(Windows)
18482 					writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img);
18483 				else
18484 					writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img);
18485 
18486 				w.close();
18487 			});
18488 		};
18489 
18490 		button.addWhenTriggered( {
18491 
18492 		foreach(test; __traits(getUnitTests, mixin("arsd.minigui"))) {
18493 			name = null;
18494 			static foreach(attr; __traits(getAttributes, test)) {
18495 				static if(is(typeof(attr) == Screenshot))
18496 					name = attr.name;
18497 			}
18498 			if(name.length) {
18499 				test();
18500 			}
18501 		}
18502 
18503 		});
18504 
18505 		results.loop();
18506 
18507 		return UnitTestResult(0, 0, false, false);
18508 	}
18509 
18510 
18511 	Runtime.extendedModuleUnitTester = &screenshotMagic;
18512 }
18513 });
18514 version(minigui_screenshots) {
18515 	version(unittest)
18516 		void main() {}
18517 	else static assert(0, "dont forget the -unittest flag to dmd");
18518 }
18519 
18520 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut.
18521 // FIXME: make multiple accelerators disambiguate based ona rgs
18522 // FIXME: MainWindow ctor should have same arg order as Window
18523 // FIXME: mainwindow ctor w/ client area size instead of total size.
18524 // 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.
18525 // FIXME: tri-state checkbox
18526 // FIXME: subordinate controls grouping...