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 // FIXME: checkbox menus and submenus and stuff
25 
26 // FOXME: look at Windows rebar control too
27 
28 /*
29 
30 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
31 
32 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it...
33 */
34 
35 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places
36 
37 // FIXME: text label must be copyable to the clipboard, at least as a full chunk.
38 
39 // FIXME: opt-in file picker widget with image support
40 
41 // FIXME: number widget
42 
43 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm
44 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles
45 
46 // osx style menu search.
47 
48 // would be cool for a scroll bar to have marking capabilities
49 // kinda like vim's marks just on clicks etc and visual representation
50 // generically. may be cool to add an up arrow to the bottom too
51 //
52 // leave a shadow of where you last were for going back easily
53 
54 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various
55 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for
56 // the window.
57 
58 // so what about context menus?
59 
60 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw
61 
62 // FIXME: make the scroll thing go to bottom when the content changes.
63 
64 // 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
65 
66 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood.
67 
68 
69 // FIXME: add a command search thingy built in and implement tip.
70 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?!
71 
72 // On Windows:
73 // FIXME: various labels look broken in high contrast mode
74 // FIXME: changing themes while the program is upen doesn't trigger a redraw
75 
76 // add note about manifest to documentation. also icons.
77 
78 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar
79 // FIXME: clear the corner of scrollbars if they pop up
80 
81 // minigui needs to have a stdout redirection for gui mode on windows writeln
82 
83 // I kinda wanna do state reacting. sort of. idk tho
84 
85 // need a viewer widget that works like a web page - arrows scroll down consistently
86 
87 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside.
88 
89 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two.
90 // and help info about menu items.
91 // and search in menus?
92 
93 // FIXME: a scroll area event signaling when a thing comes into view might be good
94 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must
95 
96 // FIXME: unify Windows style line endings
97 
98 /*
99 	TODO:
100 
101 	pie menu
102 
103 	class Form with submit behavior -- see AutomaticDialog
104 
105 	disabled widgets and menu items
106 
107 	event cleanup
108 	tooltips.
109 	api improvements
110 
111 	margins are kinda broken, they don't collapse like they should. at least.
112 
113 	a table form btw would be a horizontal layout of vertical layouts holding each column
114 	that would give the same width things
115 */
116 
117 /*
118 
119 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
120 */
121 
122 /++
123 	minigui is a smallish GUI widget library, aiming to be on par with at least
124 	HTML4 forms and a few other expected gui components. It uses native controls
125 	on Windows and does its own thing on Linux (Mac is not currently supported but
126 	I'm slowly working on it).
127 
128 
129 	$(H3 Conceptual Overviews)
130 
131 	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.
132 
133 	$(H4 Code structure)
134 
135 	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.
136 
137 	---
138 	import arsd.minigui;
139 
140 	void main() {
141 		// first, create a window, the (optional) string here is its title
142 		auto window = new MainWindow("Hello, World!");
143 
144 		// lay out some widgets inside the window to create the ui
145 		auto name = new LabeledLineEdit("What is your name?", window);
146 		auto button = new Button("Say Hello", window);
147 
148 		// prepare event handlers
149 		button.addEventListener(EventType.triggered, () {
150 			window.messageBox("Hello, " ~ name.content ~ "!");
151 		});
152 
153 		// show the window and run the event loop until this window is closed
154 		window.loop();
155 	}
156 	---
157 
158 	To compile, run `opend hello.d`, then run the generated `hello` program.
159 
160 	While the specifics will change, nearly all minigui applications will roughly follow this pattern.
161 
162 	$(TIP
163 		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.
164 
165 		You may call this if you don't have a single main window.
166 
167 		Even a basic minigui window can benefit from these if you don't have a single main window:
168 
169 		---
170 		import arsd.minigui;
171 
172 		void main() {
173 			// create a struct to hold gathered info
174 			struct Hello { string name; }
175 			// let minigui create a dialog box to get that
176 			// info from the user. If you have a main window,
177 			// you'd pass that here, but it is not required
178 			dialog((Hello info) {
179 				// inline handler of the "OK" button
180 				messageBox("Hello, " ~ info.name);
181 			});
182 
183 			// since there is no main window to loop on,
184 			// we instead call the event loop singleton ourselves
185 			EventLoop.get.run;
186 		}
187 		---
188 
189 		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!
190 	)
191 
192 	$(H4 How to lay out widgets)
193 
194 	To better understand the details of layout algorithms and see more available included classes, see [Layout].
195 
196 	$(H5 Default layouts)
197 
198 	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.
199 
200 	$(TIP
201 		minigui's default [VerticalLayout] and [HorizontalLayout] are roughly based on css flexbox with wrap turned off.
202 	)
203 
204 	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.
205 
206 	$(NOTE
207 		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.
208 	)
209 
210 	For example, to display two widgets side-by-side, you can wrap them in a [HorizontalLayout]:
211 
212 	---
213 	import arsd.minigui;
214 	void main() {
215 		auto window = new MainWindow();
216 
217 		// make the layout a child of our window
218 		auto hl = new HorizontalLayout(window);
219 
220 		// then make the widgets children of the layout
221 		auto leftButton = new Button("Left", hl);
222 		auto rightButton = new Button("Right", hl);
223 
224 		window.loop();
225 	}
226 	---
227 
228 	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.
229 
230 	$(H5 Nesting layouts)
231 
232 	Nesting layouts lets you carve up the rectangle in different ways.
233 
234 	$(EMBED_UNITTEST layout-example)
235 
236 	$(H5 Special layouts)
237 
238 	[TabWidget] can show pages of layouts as tabs.
239 
240 	See [ScrollableWidget] but be warned that it is weird. You might want to consider something like [GenericListViewWidget] instead.
241 
242 	$(H5 Other common layout classes)
243 
244 	[HorizontalLayout], [VerticalLayout], [InlineBlockLayout], [GridLayout]
245 
246 	$(H4 How to respond to widget events)
247 
248 	To better understanding the underlying event system, see [Event].
249 
250 	Each widget emits its own events, which propagate up through their parents until they reach their top-level window.
251 
252 	$(H4 How to do overall ui - title, icons, menus, toolbar, hotkeys, statuses, etc.)
253 
254 	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!
255 
256 	See [MainWindow.setMenuAndToolbarFromAnnotatedCode] for an example.
257 
258 	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)).
259 
260 	$(TIP
261 		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.
262 	)
263 
264 	All windows also have titles. You can change this at any time with the `window.title = "string";` property.
265 
266 	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.)
267 
268 	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.
269 
270 	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.
271 
272 	Other parts can be added by you and are under your control. You add them with:
273 
274 	---
275 	window.statusBar.parts ~= StatusBar.Part(optional_size, optional_units);
276 	---
277 
278 	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.
279 
280 	You may prefer to set them all at once, with:
281 
282 	---
283 	window.statusBar.parts.setSizes(1, 1, 1);
284 	---
285 
286 	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.
287 
288 	You should call this right after creating your `MainWindow` as part of your setup code.
289 
290 	Once you make parts, you can explicitly change their content with `window.statusBar.parts[index].content = "some string";`
291 
292 	$(NOTE
293 		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.
294 	)
295 
296 	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!
297 
298 	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.
299 
300 	$(H4 How to do custom styles)
301 
302 	Minigui's custom widgets support styling parameters on the level of individual widgets, or application-wide with [VisualTheme]s.
303 
304 	$(WARNING
305 		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.
306 
307 		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.
308 
309 		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.
310 	)
311 
312 	See [Widget.Style] for more information.
313 
314 	$(H4 Selection of categorized widgets)
315 
316 	$(LIST
317 		* Buttons: [Button]
318 		* Text display widgets: [TextLabel], [TextDisplay]
319 		* Text edit widgets: [LineEdit] (and [LabeledLineEdit]), [PasswordEdit] (and [LabeledPasswordEdit]), [TextEdit]
320 		* Selecting multiple on/off options: [Checkbox]
321 		* Selecting just one from a list of options: [Fieldset], [Radiobox], [DropDownSelection]
322 		* Getting rough numeric input: [HorizontalSlider], [VerticalSlider]
323 		* Displaying data: [ImageBox], [ProgressBar], [TableView]
324 		* Showing a list of editable items: [GenericListViewWidget]
325 		* Helpers for building your own widgets: [OpenGlWidget], [ScrollMessageWidget]
326 	)
327 
328 	And more. See [#members] until I write up more of this later and also be aware of the package [arsd.minigui_addons].
329 
330 	If none of these do what you need, you'll want to write your own. More on that in the following section.
331 
332 	$(H4 custom widgets - how to write your own)
333 
334 	See some example programs: https://github.com/adamdruppe/minigui-samples
335 
336 	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.
337 
338 	To get more specific, let's consider a few illustrative examples, then we'll come back to some principles.
339 
340 	$(H5 Custom Widget Examples)
341 
342 	$(H5 More notes)
343 
344 	See [Widget].
345 
346 	If you override [Widget.recomputeChildLayout], don't forget to call `registerMovement()` at the top of it, then call recomputeChildLayout of all its children too!
347 
348 		If you need a nested OS level window, see [NestedChildWindowWidget]. Use [Widget.scaleWithDpi] to convert logical pixels to physical pixels, as required.
349 
350 		See [Widget.OverrideStyle], [Widget.paintContent], [Widget.dynamicState] for some useful starting points.
351 
352 		You may also want to provide layout and style hints by overriding things like [Widget.flexBasisWidth], [Widget.flexBasisHeight], [Widget.minHeight], yada, yada, yada.
353 
354 		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!)
355 
356 		$(TIP
357 			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.
358 		)
359 
360 	$(H5 Timers and animations)
361 
362 	The [Timer] class is available and you can call `widget.redraw();` to trigger a redraw from a timer handler.
363 
364 	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.
365 
366 	$(H5 Clipboard integrations, drag and drop)
367 
368 	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.
369 
370 	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.
371 
372 	See: [draggable], [DropHandler], [setClipboardText], [setClipboardImage], [getClipboardText], [getClipboardImage], [setPrimarySelection], and others from simpledisplay.
373 
374 	$(H5 Context menus)
375 
376 	Override [Widget.contextMenu] in your subclass.
377 
378 	$(H4 Coming later)
379 
380 	Among the unfinished features: unified selections, translateable strings, external integrations.
381 
382 	$(H2 Running minigui programs)
383 
384 	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.
385 
386 	$(H2 Building minigui programs)
387 
388 	minigui's only required dependencies are [arsd.simpledisplay], [arsd.color], and
389 	[arsd.textlayouter], on which it is built. simpledisplay provides the low-level
390 	interfaces and minigui builds the concept of widgets inside the windows on top of it.
391 
392 	Its #1 goal is to be useful without being large and complicated like GTK and Qt.
393 	It isn't hugely concerned with appearance - on Windows, it just uses the native
394 	controls and native theme, and on Linux, it keeps it simple and I may change that
395 	at any time, though after May 2021, you can customize some things with css-inspired
396 	[Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`,
397 	you can use the custom implementation there too, but... you shouldn't.)
398 
399 	The event model is similar to what you use in the browser with Javascript and the
400 	layout engine tries to automatically fit things in, similar to a css flexbox.
401 
402 	FOR BEST RESULTS: be sure to link with the appropriate subsystem command
403 	`-L/SUBSYSTEM:WINDOWS` and -L/entry:mainCRTStartup`. If using ldc instead
404 	of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w".
405 
406 	Otherwise you'll get a console and possibly other visual bugs. But if you do use
407 	the subsystem:windows, note that Phobos' writeln will crash the program!
408 
409 	HTML_To_Classes:
410 	$(SMALL_TABLE
411 		HTML Code | Minigui Class
412 
413 		`<input type="text">` | [LineEdit]
414 		`<input type="password">` | [PasswordEdit]
415 		`<textarea>` | [TextEdit]
416 		`<select>` | [DropDownSelection]
417 		`<input type="checkbox">` | [Checkbox]
418 		`<input type="radio">` | [Radiobox]
419 		`<button>` | [Button]
420 	)
421 
422 
423 	Stretchiness:
424 		The default is 4. You can use larger numbers for things that should
425 		consume a lot of space, and lower numbers for ones that are better at
426 		smaller sizes.
427 
428 	Overlapped_input:
429 		COMING EVENTUALLY:
430 		minigui will include a little bit of I/O functionality that just works
431 		with the event loop. If you want to get fancy, I suggest spinning up
432 		another thread and posting events back and forth.
433 
434 	$(H2 Add ons)
435 		See the `minigui_addons` directory in the arsd repo for some add on widgets
436 		you can import separately too.
437 
438 	$(H3 XML definitions)
439 		If you use [arsd.minigui_xml], you can create widget trees from XML at runtime.
440 
441 	$(H3 Scriptability)
442 		minigui is compatible with [arsd.script]. If you see `@scriptable` on a method
443 		in this documentation, it means you can call it from the script language.
444 
445 		Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml]
446 		and make [arsd.minigui_xml.makeWidgetFromString] available to your script:
447 
448 		---
449 		import arsd.minigui_xml;
450 		import arsd.script;
451 
452 		var globals = var.emptyObject;
453 		globals.makeWidgetFromString = &makeWidgetFromString;
454 
455 		// this now works
456 		interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals);
457 		---
458 
459 		More to come.
460 
461 	Widget_tree_notes:
462 		minigui doesn't really formalize these distinctions, but in practice, there are multiple types of widgets:
463 
464 		$(LIST
465 			* Containers - a widget that holds other widgets directly, generally [Layout]s. [WidgetContainer] is an attempt to formalize this but is nothing really special.
466 
467 			* 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.
468 
469 			---
470 			auto child = new Widget(mainWindow);
471 			assert(child.parent is mainWindow); // fails, its actual parent is mainWindow's inner container instead.
472 			---
473 
474 			* Limiting containers - a widget that can only hold children of a particular type. See [TabWidget], which can only hold [TabWidgetPage]s.
475 
476 			* Simple controls - a widget that cannot have children, but instead does a specific job.
477 
478 			* 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.
479 		)
480 
481 		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.
482 
483 		Future breaking changes might be related to making this more structured but im not sure it is that important to actually break stuff over.
484 
485 	My_UI_Guidelines:
486 		Note that the Linux custom widgets generally aim to be efficient on remote X network connections.
487 
488 		In a perfect world, you'd achieve all the following goals:
489 
490 		$(LIST
491 			* All operations are present in the menu
492 			* The operations the user wants at the moment are right where they want them
493 			* All operations can be scripted
494 			* The UI does not move any elements without explicit user action
495 			* All numbers can be seen and typed in if wanted, even if the ui usually hides them
496 		)
497 
498 	$(H2 Future Directions)
499 
500 	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.
501 
502 	History:
503 		In January 2025 (dub v12.0), minigui got a few more breaking changes:
504 
505 		$(LIST
506 			* `defaultEventHandler_*` functions take more specific objects. So if you see errors like:
507 
508 			---
509 			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)`?
510 			---
511 
512 			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.
513 
514 			* 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.
515 		)
516 
517 		Minigui had mostly additive changes or bug fixes since its inception until May 2021.
518 
519 		In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd
520 		tag this as version 2.0.
521 
522 		Among the changes:
523 		$(LIST
524 			* 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.
525 
526 			See [Event] for details.
527 
528 			* 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.
529 
530 			See [DoubleClickEvent] for details.
531 
532 			* 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.
533 
534 			See [Widget.Style] for details.
535 
536 			* 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.
537 
538 			* 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.
539 
540 			* [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor.
541 
542 			* 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.
543 
544 			* Various non-breaking additions.
545 		)
546 +/
547 module arsd.minigui;
548 			// * A widget must now opt in to receiving keyboard focus, rather than opting out.
549 
550 /++
551 	This hello world sample will have an oversized button, but that's ok, you see your first window!
552 +/
553 version(Demo)
554 unittest {
555 	import arsd.minigui;
556 
557 	void main() {
558 		auto window = new MainWindow();
559 
560 		// note the parent widget is almost always passed as the last argument to a constructor
561 		auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window);
562 		auto button = new Button("Close", window);
563 		button.addWhenTriggered({
564 			window.close();
565 		});
566 
567 		window.loop();
568 	}
569 
570 	main(); // exclude from docs
571 }
572 
573 /++
574 	$(ID layout-example)
575 
576 	This example shows one way you can partition your window into a header
577 	and sidebar. Here, the header and sidebar have a fixed width, while the
578 	rest of the content sizes with the window.
579 
580 	It might be a new way of thinking about window layout to do things this
581 	way - perhaps [GridLayout] more matches your style of thought - but the
582 	concept here is to partition the window into sub-boxes with a particular
583 	size, then partition those boxes into further boxes.
584 
585 	$(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.)
586 
587 	So to make the header, start with a child layout that has a max height.
588 	It will use that space from the top, then the remaining children will
589 	split the remaining area, meaning you can think of is as just being another
590 	box you can split again. Keep splitting until you have the look you desire.
591 +/
592 // https://github.com/adamdruppe/arsd/issues/310
593 version(minigui_screenshots)
594 @Screenshot("layout")
595 unittest {
596 	import arsd.minigui;
597 
598 	// This helper class is just to help make the layout boxes visible.
599 	// think of it like a <div style="background-color: whatever;"></div> in HTML.
600 	class ColorWidget : Widget {
601 		this(Color color, Widget parent) {
602 			this.color = color;
603 			super(parent);
604 		}
605 		Color color;
606 		class Style : Widget.Style {
607 			override WidgetBackground background() { return WidgetBackground(color); }
608 		}
609 		mixin OverrideStyle!Style;
610 	}
611 
612 	void main() {
613 		auto window = new Window;
614 
615 		// the key is to give it a max height. This is one way to do it:
616 		auto header = new class HorizontalLayout {
617 			this() { super(window); }
618 			override int maxHeight() { return 50; }
619 		};
620 		// this next line is a shortcut way of doing it too, but it only works
621 		// for HorizontalLayout and VerticalLayout, and is less explicit, so it
622 		// is good to know how to make a new class like above anyway.
623 		// auto header = new HorizontalLayout(50, window);
624 
625 		auto bar = new HorizontalLayout(window);
626 
627 		// or since this is so common, VerticalLayout and HorizontalLayout both
628 		// can just take an argument in their constructor for max width/height respectively
629 
630 		// (could have tone this above too, but I wanted to demo both techniques)
631 		auto left = new VerticalLayout(100, bar);
632 
633 		// and this is the main section's container. A plain Widget instance is good enough here.
634 		auto container = new Widget(bar);
635 
636 		// and these just add color to the containers we made above for the screenshot.
637 		// in a real application, you can just add your actual controls instead of these.
638 		auto headerColorBox = new ColorWidget(Color.teal, header);
639 		auto leftColorBox = new ColorWidget(Color.green, left);
640 		auto rightColorBox = new ColorWidget(Color.purple, container);
641 
642 		window.loop();
643 	}
644 
645 	main(); // exclude from docs
646 }
647 
648 
649 import arsd.core;
650 import arsd.textlayouter;
651 
652 alias Timer = arsd.simpledisplay.Timer;
653 public import arsd.simpledisplay;
654 /++
655 	Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports)
656 
657 	History:
658 		Was private until May 15, 2021.
659 +/
660 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle()
661 
662 version(Windows) {
663 	import core.sys.windows.winnls;
664 	import core.sys.windows.windef;
665 	import core.sys.windows.basetyps;
666 	import core.sys.windows.winbase;
667 	import core.sys.windows.winuser;
668 	import core.sys.windows.wingdi;
669 	static import gdi = core.sys.windows.wingdi;
670 }
671 
672 version(Windows) {
673 	// to swap the default
674 	// version(minigui_manifest) {} else version=minigui_no_manifest;
675 
676 	version(minigui_no_manifest) {} else {
677 		version(D_OpenD) {
678 			// OpenD always supports it
679 			version=UseManifestMinigui;
680 		} else {
681 			version(CRuntime_Microsoft) // FIXME: mingw?
682 				version=UseManifestMinigui;
683 		}
684 
685 	}
686 
687 
688 	version(UseManifestMinigui) {
689 		// assume we want commctrl6 whenever possible since there's really no reason not to
690 		// and this avoids some of the manifest hassle
691 		pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"");
692 	}
693 }
694 
695 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default.
696 private bool lastDefaultPrevented;
697 
698 /// Methods marked with this are available from scripts if added to the [arsd.script] engine.
699 alias scriptable = arsd_jsvar_compatible;
700 
701 version(Windows) {
702 	// use native widgets when available unless specifically asked otherwise
703 	version(custom_widgets) {
704 		enum bool UsingCustomWidgets = true;
705 		enum bool UsingWin32Widgets = false;
706 	} else {
707 		version = win32_widgets;
708 		enum bool UsingCustomWidgets = false;
709 		enum bool UsingWin32Widgets = true;
710 	}
711 	// and native theming when needed
712 	//version = win32_theming;
713 } else {
714 	enum bool UsingCustomWidgets = true;
715 	enum bool UsingWin32Widgets = false;
716 	version=custom_widgets;
717 }
718 
719 
720 
721 /*
722 
723 	The main goals of minigui.d are to:
724 		1) Provide basic widgets that just work in a lightweight lib.
725 		   I basically want things comparable to a plain HTML form,
726 		   plus the easy and obvious things you expect from Windows
727 		   apps like a menu.
728 		2) Use native things when possible for best functionality with
729 		   least library weight.
730 		3) Give building blocks to provide easy extension for your
731 		   custom widgets, or hooking into additional native widgets
732 		   I didn't wrap.
733 		4) Provide interfaces for easy interaction between third
734 		   party minigui extensions. (event model, perhaps
735 		   signals/slots, drop-in ease of use bits.)
736 		5) Zero non-system dependencies, including Phobos as much as
737 		   I reasonably can. It must only import arsd.color and
738 		   my simpledisplay.d. If you need more, it will have to be
739 		   an extension module.
740 		6) An easy layout system that generally works.
741 
742 	A stretch goal is to make it easy to make gui forms with code,
743 	some kind of resource file (xml?) and even a wysiwyg designer.
744 
745 	Another stretch goal is to make it easy to hook data into the gui,
746 	including from reflection. So like auto-generate a form from a
747 	function signature or struct definition, or show a list from an
748 	array that automatically updates as the array is changed. Then,
749 	your program focuses on the data more than the gui interaction.
750 
751 
752 
753 	STILL NEEDED:
754 		* combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect)
755 		* slider
756 		* listbox
757 		* spinner
758 		* label?
759 		* rich text
760 */
761 
762 
763 /+
764 	enum LayoutMethods {
765 		 verticalFlex,
766 		 horizontalFlex,
767 		 inlineBlock, // left to right, no stretch, goes to next line as needed
768 		 static, // just set to x, y
769 		 verticalNoStretch, // browser style default
770 
771 		 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line
772 
773 		 grid, // magic
774 	}
775 +/
776 
777 /++
778 	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.
779 
780 
781 	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.
782 
783 	---
784 	class MinimalWidget : Widget {
785 		this(Widget parent) {
786 			super(parent);
787 		}
788 	}
789 	---
790 
791 	$(SIDEBAR
792 		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.
793 	)
794 
795 	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.
796 
797 	Among the things you'll most likely want to change in your custom widget:
798 
799 	$(LIST
800 		* 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.)
801 
802 		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.
803 
804 		Do this $(I after) calling the `super` constructor.
805 
806 		* 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.
807 
808 		Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision.
809 
810 		* 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.
811 
812 		* 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.
813 	)
814 
815 	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.
816 
817 	It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example.
818 
819 	Your own custom-drawn and native system controls can exist side-by-side.
820 
821 	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.
822 +/
823 class Widget : ReflectableProperties {
824 
825 	private int toolbarIconSize() {
826 		return scaleWithDpi(24);
827 	}
828 
829 
830 	/++
831 		Returns the current size of the widget.
832 
833 		History:
834 			Added January 3, 2025
835 	+/
836 	final Size size() const {
837 		return Size(width, height);
838 	}
839 
840 	private bool willDraw() {
841 		return true;
842 	}
843 
844 	/+
845 	/++
846 		Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need.
847 
848 		History:
849 			Added September 15, 2021
850 			implemented.... ???
851 	+/
852 	void prepareReflection(this This)() {
853 
854 	}
855 	+/
856 
857 	private bool _enabled = true;
858 
859 	/++
860 		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.
861 
862 		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.
863 
864 		History:
865 			Added November 23, 2021 (dub v10.4)
866 
867 			Warning: the specific behavior of disabling with parents may change in the future.
868 		Bugs:
869 			Currently only implemented for widgets backed by native Windows controls.
870 
871 		See_Also: [disabledReason], [disabledBy]
872 	+/
873 	@property bool enabled() {
874 		return disabledBy() is null;
875 	}
876 
877 	/// ditto
878 	@property void enabled(bool yes) {
879 		_enabled = yes;
880 		version(win32_widgets) {
881 			if(hwnd)
882 				EnableWindow(hwnd, yes);
883 		}
884 		setDynamicState(DynamicState.disabled, yes);
885 		redraw();
886 	}
887 
888 	private string disabledReason_;
889 
890 	/++
891 		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.
892 
893 		Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though.
894 
895 		History:
896 			Added November 23, 2021 (dub v10.4)
897 		See_Also: [enabled], [disabledBy]
898 	+/
899 	@property string disabledReason() {
900 		auto w = disabledBy();
901 		return (w is null) ? null : w.disabledReason_;
902 	}
903 
904 	/// ditto
905 	@property void disabledReason(string reason) {
906 		disabledReason_ = reason;
907 	}
908 
909 	/++
910 		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.
911 
912 		History:
913 			Added November 25, 2021 (dub v10.4)
914 		See_Also: [enabled], [disabledReason]
915 	+/
916 	Widget disabledBy() {
917 		Widget p = this;
918 		while(p) {
919 			if(!p._enabled)
920 				return p;
921 			p = p.parent;
922 		}
923 		return null;
924 	}
925 
926 	/// Implementations of [ReflectableProperties] interface. See the interface for details.
927 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
928 		if(valueIsJson)
929 			return SetPropertyResult.wrongFormat;
930 		switch(name) {
931 			case "name":
932 				this.name = value.idup;
933 				return SetPropertyResult.success;
934 			case "statusTip":
935 				this.statusTip = value.idup;
936 				return SetPropertyResult.success;
937 			default:
938 				return SetPropertyResult.noSuchProperty;
939 		}
940 	}
941 	/// ditto
942 	void getPropertiesList(scope void delegate(string name) sink) const {
943 		sink("name");
944 		sink("statusTip");
945 	}
946 	/// ditto
947 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
948 		switch(name) {
949 			case "name":
950 				sink(name, this.name, false);
951 				return;
952 			case "statusTip":
953 				sink(name, this.statusTip, false);
954 				return;
955 			default:
956 				sink(name, null, true);
957 		}
958 	}
959 
960 	/++
961 		Scales the given value to the system-reported DPI for the monitor on which the widget resides.
962 
963 		History:
964 			Added November 25, 2021 (dub v10.5)
965 			`Point` overload added January 12, 2022 (dub v10.6)
966 	+/
967 	int scaleWithDpi(int value, int assumedDpi = 96) {
968 		// avoid potential overflow with common special values
969 		if(value == int.max)
970 			return int.max;
971 		if(value == int.min)
972 			return int.min;
973 		if(value == 0)
974 			return 0;
975 		return value * currentDpi(assumedDpi) / assumedDpi;
976 	}
977 
978 	/// ditto
979 	Point scaleWithDpi(Point value, int assumedDpi = 96) {
980 		return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi));
981 	}
982 
983 	/++
984 		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.
985 
986 		Not entirely stable.
987 
988 		History:
989 			Added August 25, 2023 (dub v11.1)
990 	+/
991 	final int currentDpi(int assumedDpi = 96) {
992 		// assert(parentWindow !is null);
993 		// assert(parentWindow.win !is null);
994 		auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi;
995 		//divide = 138; // to test 1.5x
996 		// 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.
997 		// this also covers the case when actualDpi returns 0.
998 		if(divide < 96)
999 			divide = 96;
1000 		return divide;
1001 	}
1002 
1003 	// avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable
1004 	// I'll think up something better eventually
1005 
1006 	// FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight.
1007 	protected final int defaultLineHeight() {
1008 		auto cs = getComputedStyle();
1009 		if(cs.font && !cs.font.isNull)
1010 			return castFnumToCnum(cs.font.height() * 5 / 4);
1011 		else
1012 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4);
1013 	}
1014 
1015 	/++
1016 
1017 		History:
1018 			Added August 25, 2023 (dub v11.1)
1019 	+/
1020 	protected final int defaultTextHeight(int numberOfLines = 1) {
1021 		auto cs = getComputedStyle();
1022 		if(cs.font && !cs.font.isNull)
1023 			return castFnumToCnum(cs.font.height() * numberOfLines);
1024 		else
1025 			return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines;
1026 	}
1027 
1028 	protected final int defaultTextWidth(const(char)[] text) {
1029 		auto cs = getComputedStyle();
1030 		if(cs.font && !cs.font.isNull)
1031 			return castFnumToCnum(cs.font.stringWidth(text));
1032 		else
1033 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2);
1034 	}
1035 
1036 	/++
1037 		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.
1038 
1039 		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.
1040 
1041 		History:
1042 			Added May 22, 2021
1043 	+/
1044 	protected bool encapsulatedChildren() {
1045 		return false;
1046 	}
1047 
1048 	private void privateDpiChanged() {
1049 		dpiChanged();
1050 		foreach(child; children)
1051 			child.privateDpiChanged();
1052 	}
1053 
1054 	/++
1055 		Virtual hook to update any caches or fonts you need on the event of a dpi scaling change.
1056 
1057 		History:
1058 			Added January 12, 2022 (dub v10.6)
1059 	+/
1060 	protected void dpiChanged() {
1061 
1062 	}
1063 
1064 	// Default layout properties {
1065 
1066 		int minWidth() { return 0; }
1067 		int minHeight() {
1068 			// default widgets have a vertical layout, therefore the minimum height is the sum of the contents
1069 			int sum = this.paddingTop + this.paddingBottom;
1070 			foreach(child; children) {
1071 				if(child.hidden)
1072 					continue;
1073 				sum += child.minHeight();
1074 				sum += child.marginTop();
1075 				sum += child.marginBottom();
1076 			}
1077 
1078 			return sum;
1079 		}
1080 		int maxWidth() { return int.max; }
1081 		int maxHeight() { return int.max; }
1082 		int widthStretchiness() { return 4; }
1083 		int heightStretchiness() { return 4; }
1084 
1085 		/++
1086 			Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items.
1087 
1088 			History:
1089 				Added June 15, 2021 (dub v10.1)
1090 		+/
1091 		int widthShrinkiness() { return 0; }
1092 		/// ditto
1093 		int heightShrinkiness() { return 0; }
1094 
1095 		/++
1096 			The initial size of the widget for layout calculations. Default is 0.
1097 
1098 			See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis]
1099 
1100 			History:
1101 				Added June 15, 2021 (dub v10.1)
1102 		+/
1103 		int flexBasisWidth() { return 0; }
1104 		/// ditto
1105 		int flexBasisHeight() { return 0; }
1106 
1107 		/++
1108 			Not stable.
1109 
1110 			Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored.
1111 
1112 			So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6.
1113 
1114 			History:
1115 				Added January 5, 2023
1116 		+/
1117 		Rectangle defaultMargin;
1118 		/// ditto
1119 		Rectangle defaultPadding;
1120 
1121 		int marginLeft() { return scaleWithDpi(defaultMargin.left); }
1122 		int marginRight() { return scaleWithDpi(defaultMargin.right); }
1123 		int marginTop() { return scaleWithDpi(defaultMargin.top); }
1124 		int marginBottom() { return scaleWithDpi(defaultMargin.bottom); }
1125 		int paddingLeft() { return scaleWithDpi(defaultPadding.left); }
1126 		int paddingRight() { return scaleWithDpi(defaultPadding.right); }
1127 		int paddingTop() { return scaleWithDpi(defaultPadding.top); }
1128 		int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); }
1129 		//LinePreference linePreference() { return LinePreference.PreferOwnLine; }
1130 
1131 		private bool recomputeChildLayoutRequired = true;
1132 		private static class RecomputeEvent {}
1133 		private __gshared rce = new RecomputeEvent();
1134 		protected final void queueRecomputeChildLayout() {
1135 			recomputeChildLayoutRequired = true;
1136 
1137 			if(this.parentWindow) {
1138 				auto sw = this.parentWindow.win;
1139 				assert(sw !is null);
1140 				if(!sw.eventQueued!RecomputeEvent) {
1141 					sw.postEvent(rce);
1142 					// writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
1143 				}
1144 			}
1145 
1146 		}
1147 
1148 		protected final void recomputeChildLayoutEntry() {
1149 			if(recomputeChildLayoutRequired) {
1150 				recomputeChildLayout();
1151 				recomputeChildLayoutRequired = false;
1152 				redraw();
1153 			} else {
1154 				// I still need to check the tree just in case one of them was queued up
1155 				// and the event came up here instead of there.
1156 				foreach(child; children)
1157 					child.recomputeChildLayoutEntry();
1158 			}
1159 		}
1160 
1161 		// this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon
1162 		void recomputeChildLayout() {
1163 			.recomputeChildLayout!"height"(this);
1164 		}
1165 
1166 	// }
1167 
1168 
1169 	/++
1170 		Returns the style's tag name string this object uses.
1171 
1172 		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.
1173 
1174 		This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS.
1175 
1176 		History:
1177 			Added May 10, 2021
1178 	+/
1179 	string styleTagName() const {
1180 		string n = typeid(this).name;
1181 		foreach_reverse(idx, ch; n)
1182 			if(ch == '.') {
1183 				n = n[idx + 1 .. $];
1184 				break;
1185 			}
1186 		return n;
1187 	}
1188 
1189 	/// API for the [styleClassList]
1190 	static struct ClassList {
1191 		private Widget widget;
1192 
1193 		///
1194 		void add(string s) {
1195 			widget.styleClassList_ ~= s;
1196 		}
1197 
1198 		///
1199 		void remove(string s) {
1200 			foreach(idx, s1; widget.styleClassList_)
1201 				if(s1 == s) {
1202 					widget.styleClassList_[idx] = widget.styleClassList_[$-1];
1203 					widget.styleClassList_ = widget.styleClassList_[0 .. $-1];
1204 					widget.styleClassList_.assumeSafeAppend();
1205 					return;
1206 				}
1207 		}
1208 
1209 		/// Returns true if it was added, false if it was removed.
1210 		bool toggle(string s) {
1211 			if(contains(s)) {
1212 				remove(s);
1213 				return false;
1214 			} else {
1215 				add(s);
1216 				return true;
1217 			}
1218 		}
1219 
1220 		///
1221 		bool contains(string s) const {
1222 			foreach(s1; widget.styleClassList_)
1223 				if(s1 == s)
1224 					return true;
1225 			return false;
1226 
1227 		}
1228 	}
1229 
1230 	private string[] styleClassList_;
1231 
1232 	/++
1233 		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.
1234 
1235 		It has no inherent meaning, it is really just a place to put some metadata tags on individual objects.
1236 
1237 		History:
1238 			Added May 10, 2021
1239 	+/
1240 	inout(ClassList) styleClassList() inout {
1241 		return cast(inout(ClassList)) ClassList(cast() this);
1242 	}
1243 
1244 	/++
1245 		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.
1246 
1247 		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.
1248 
1249 		The upper 32 bits are available for your own extensions.
1250 
1251 		History:
1252 			Added May 10, 2021
1253 
1254 		Examples:
1255 
1256 		---
1257 		addEventListener((MouseUpEvent ev) {
1258 			if(ev.button == MouseButton.left) {
1259 				// the first arg is the state to modify, the second arg is what to set it to
1260 				setDynamicState(DynamicState.depressed, false);
1261 			}
1262 		});
1263 		---
1264 
1265 	+/
1266 	enum DynamicState : ulong {
1267 		focus = (1 << 0), /// the widget currently has the keyboard focus
1268 		hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated)
1269 		valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if no validation has been performed!)
1270 		invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if no validation has been performed!)
1271 		checked = (1 << 4), /// the widget is toggleable and currently toggled on
1272 		selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked.
1273 		disabled = (1 << 6), /// the widget is currently unable to perform its designated task
1274 		indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked
1275 		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.
1276 
1277 		USER_BEGIN = (1UL << 32),
1278 	}
1279 
1280 	// I want to add the primary and cancel styles to buttons at least at some point somehow.
1281 
1282 	/// ditto
1283 	@property ulong dynamicState() { return dynamicState_; }
1284 	/// ditto
1285 	@property ulong dynamicState(ulong newValue) {
1286 		if(dynamicState != newValue) {
1287 			auto old = dynamicState_;
1288 			dynamicState_ = newValue;
1289 
1290 			useStyleProperties((scope Widget.Style s) {
1291 				if(s.variesWithState(old ^ newValue))
1292 					redraw();
1293 			});
1294 		}
1295 		return dynamicState_;
1296 	}
1297 
1298 	/// ditto
1299 	void setDynamicState(ulong flags, bool state) {
1300 		auto ds = dynamicState_;
1301 		if(state)
1302 			ds |= flags;
1303 		else
1304 			ds &= ~flags;
1305 
1306 		dynamicState = ds;
1307 	}
1308 
1309 	private ulong dynamicState_;
1310 
1311 	deprecated("Use dynamic styles instead now") {
1312 		Color backgroundColor() { return backgroundColor_; }
1313 		void backgroundColor(Color c){ this.backgroundColor_ = c; }
1314 
1315 		MouseCursor cursor() { return GenericCursor.Default; }
1316 	} private Color backgroundColor_ = Color.transparent;
1317 
1318 
1319 	/++
1320 		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).
1321 
1322 		It is here so there can be a specificity switch.
1323 
1324 		See [OverrideStyle] for a helper function to use your own.
1325 
1326 		History:
1327 			Added May 11, 2021
1328 	+/
1329 	static class Style/* : StyleProperties*/ {
1330 		public Widget widget; // public because the mixin template needs access to it
1331 
1332 		/++
1333 			You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style.
1334 
1335 			History:
1336 				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.
1337 		+/
1338 		bool variesWithState(ulong dynamicStateFlags) {
1339 			version(win32_widgets) {
1340 				if(widget.hwnd)
1341 					return false;
1342 			}
1343 			return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false);
1344 		}
1345 
1346 		///
1347 		Color foregroundColor() {
1348 			return WidgetPainter.visualTheme.foregroundColor;
1349 		}
1350 
1351 		///
1352 		WidgetBackground background() {
1353 			// the default is a "transparent" background, which means
1354 			// it goes as far up as it can to get the color
1355 			if (widget.backgroundColor_ != Color.transparent)
1356 				return WidgetBackground(widget.backgroundColor_);
1357 			if (widget.parent)
1358 				return widget.parent.getComputedStyle.background;
1359 			return WidgetBackground(widget.backgroundColor_);
1360 		}
1361 
1362 		private static OperatingSystemFont fontCached_;
1363 		private OperatingSystemFont fontCached() {
1364 			if(fontCached_ is null)
1365 				fontCached_ = font();
1366 			return fontCached_;
1367 		}
1368 
1369 		/++
1370 			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.
1371 		+/
1372 		OperatingSystemFont font() {
1373 			return null;
1374 		}
1375 
1376 		/++
1377 			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.
1378 
1379 			You can return a member of [GenericCursor] or your own [MouseCursor] instance.
1380 
1381 			History:
1382 				Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021
1383 		+/
1384 		MouseCursor cursor() {
1385 			return GenericCursor.Default;
1386 		}
1387 
1388 		FrameStyle borderStyle() {
1389 			return FrameStyle.none;
1390 		}
1391 
1392 		/++
1393 		+/
1394 		Color borderColor() {
1395 			return Color.transparent;
1396 		}
1397 
1398 		FrameStyle outlineStyle() {
1399 			if(widget.dynamicState & DynamicState.focus)
1400 				return FrameStyle.dotted;
1401 			else
1402 				return FrameStyle.none;
1403 		}
1404 
1405 		Color outlineColor() {
1406 			return foregroundColor;
1407 		}
1408 	}
1409 
1410 	/++
1411 		This mixin overrides the [useStyleProperties] method to direct it toward your own style class.
1412 		The basic usage is simple:
1413 
1414 		---
1415 		static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */
1416 			// override style hints as-needed here
1417 		}
1418 		OverrideStyle!Style; // add the method
1419 		---
1420 
1421 		$(TIP
1422 			While the class is not forced to be `static`, for best results, it should be. A non-static class
1423 			can not be inherited by other objects whereas the static one can. A property on the base class,
1424 			called [Widget.Style.widget|widget], is available for you to access its properties.
1425 		)
1426 
1427 		This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must
1428 		repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches
1429 		and that's a little tedious to repeat in your child classes too when you only care about changing the type.
1430 
1431 
1432 		It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget.
1433 		You may also just override `variesWithState` when you use this flag.
1434 
1435 		---
1436 		mixin OverrideStyle!(
1437 			DynamicState.focus, YourFocusedStyle,
1438 			DynamicState.hover, YourHoverStyle,
1439 			YourDefaultStyle
1440 		)
1441 		---
1442 
1443 		It checks if `dynamicState` matches the state and if so, returns the object given.
1444 
1445 		If there is no state mask given, the next one matches everything. The first match given is used.
1446 
1447 		However, since in most cases you'll want check state inside your individual methods, you probably won't
1448 		find much use for this whole-class swap out.
1449 
1450 		History:
1451 			Added May 16, 2021
1452 	+/
1453 	static protected mixin template OverrideStyle(S...) {
1454 		static import amg = arsd.minigui;
1455 		override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) {
1456 			ulong mask = 0;
1457 			foreach(idx, thing; S) {
1458 				static if(is(typeof(thing) : ulong)) {
1459 					mask = thing;
1460 				} else {
1461 					if(!(idx & 1) || (this.dynamicState & mask) == mask) {
1462 						//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.");
1463 						scope amg.Widget.Style s = new thing();
1464 						s.widget = this;
1465 						dg(s);
1466 						return;
1467 					}
1468 				}
1469 			}
1470 		}
1471 	}
1472 	/++
1473 		You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose.
1474 	+/
1475 	void useStyleProperties(scope void delegate(scope Style props) dg) {
1476 		scope Style s = new Style();
1477 		s.widget = this;
1478 		dg(s);
1479 	}
1480 
1481 
1482 	protected void sendResizeEvent() {
1483 		this.emit!ResizeEvent();
1484 	}
1485 
1486 	/++
1487 		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.
1488 
1489 		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);`
1490 
1491 		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.
1492 
1493 		See_Also:
1494 			[createContextMenuFromAnnotatedCode]
1495 	+/
1496 	Menu contextMenu(int x, int y) { return null; }
1497 
1498 	/++
1499 		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, you can pass one as `menuToShow`, but if you don't, it will call [contextMenu], which you can override on a per-widget basis.
1500 
1501 		History:
1502 			The `menuToShow` parameter was added on March 19, 2025.
1503 	+/
1504 	final bool showContextMenu(int x, int y, Menu menuToShow = null) {
1505 		return showContextMenu(x, y, -2, -2, menuToShow);
1506 	}
1507 
1508 	private final bool showContextMenu(int x, int y, int screenX, int screenY, Menu menu = null) {
1509 		if(parentWindow is null || parentWindow.win is null) return false;
1510 
1511 		if(menu is null)
1512 			menu = this.contextMenu(x, y);
1513 
1514 		if(menu is null)
1515 			return false;
1516 
1517 		version(win32_widgets) {
1518 			// FIXME: if it is -1, -1, do it at the current selection location instead
1519 			// tho the corner of the window, which it does now, isn't the literal worst.
1520 
1521 			// i see notepad just seems to put it in the center of the window so idk
1522 
1523 			if(screenX < 0 && screenY < 0) {
1524 				auto p = this.globalCoordinates();
1525 				if(screenX == -2)
1526 					p.x += x;
1527 				if(screenY == -2)
1528 					p.y += y;
1529 
1530 				screenX = p.x;
1531 				screenY = p.y;
1532 			}
1533 
1534 			if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null))
1535 				throw new Exception("TrackContextMenuEx");
1536 		} else version(custom_widgets) {
1537 			menu.popup(this, x, y);
1538 		}
1539 
1540 		return true;
1541 	}
1542 
1543 	/++
1544 		Removes this widget from its parent.
1545 
1546 		History:
1547 			`removeWidget` was made `final` on May 11, 2021.
1548 	+/
1549 	@scriptable
1550 	final void removeWidget() {
1551 		auto p = this.parent;
1552 		if(p) {
1553 			int item;
1554 			for(item = 0; item < p._children.length; item++)
1555 				if(p._children[item] is this)
1556 					break;
1557 			auto idx = item;
1558 			for(; item < p._children.length - 1; item++)
1559 				p._children[item] = p._children[item + 1];
1560 			p._children = p._children[0 .. $-1];
1561 
1562 			this.parent.widgetRemoved(idx, this);
1563 			//this.parent = null;
1564 
1565 			p.queueRecomputeChildLayout();
1566 		}
1567 		version(win32_widgets) {
1568 			removeAllChildren();
1569 			if(hwnd) {
1570 				DestroyWindow(hwnd);
1571 				hwnd = null;
1572 			}
1573 		}
1574 	}
1575 
1576 	/++
1577 		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.
1578 
1579 		History:
1580 			Added September 19, 2021
1581 	+/
1582 	protected void widgetRemoved(size_t oldIndex, Widget oldReference) { }
1583 
1584 	/++
1585 		Removes all child widgets from `this`. You should not use the removed widgets again.
1586 
1587 		Note that on Windows, it also destroys the native handles for the removed children recursively.
1588 
1589 		History:
1590 			Added July 1, 2021 (dub v10.2)
1591 	+/
1592 	void removeAllChildren() {
1593 		version(win32_widgets)
1594 		foreach(child; _children) {
1595 			child.removeAllChildren();
1596 			if(child.hwnd) {
1597 				DestroyWindow(child.hwnd);
1598 				child.hwnd = null;
1599 			}
1600 		}
1601 		auto orig = this._children;
1602 		this._children = null;
1603 		foreach(idx, w; orig)
1604 			this.widgetRemoved(idx, w);
1605 
1606 		queueRecomputeChildLayout();
1607 	}
1608 
1609 	/++
1610 		Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible.
1611 	+/
1612 	@scriptable
1613 	Widget getChildByName(string name) {
1614 		return getByName(name);
1615 	}
1616 	/++
1617 		Finds the nearest descendant with the requested type and [name]. May return `this`.
1618 	+/
1619 	final WidgetClass getByName(WidgetClass = Widget)(string name) {
1620 		if(this.name == name)
1621 			if(auto c = cast(WidgetClass) this)
1622 				return c;
1623 		foreach(child; children) {
1624 			auto w = child.getByName(name);
1625 			if(auto c = cast(WidgetClass) w)
1626 				return c;
1627 		}
1628 		return null;
1629 	}
1630 
1631 	/++
1632 		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.
1633 		Names should be unique in a window.
1634 
1635 		See_Also: [getByName], [getChildByName]
1636 	+/
1637 	@scriptable string name;
1638 
1639 	private EventHandler[][string] bubblingEventHandlers;
1640 	private EventHandler[][string] capturingEventHandlers;
1641 
1642 	/++
1643 		Default event handlers. These are called on the appropriate
1644 		event unless [Event.preventDefault] is called on the event at
1645 		some point through the bubbling process.
1646 
1647 
1648 		If you are implementing your own widget and want to add custom
1649 		events, you should follow the same pattern here: create a virtual
1650 		function named `defaultEventHandler_eventname` with the implementation,
1651 		then, override [setupDefaultEventHandlers] and add a wrapped caller to
1652 		`defaultEventHandlers["eventname"]`. It should be wrapped like so:
1653 		`defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`.
1654 		This ensures virtual dispatch based on the correct subclass.
1655 
1656 		Also, don't forget to call `super.setupDefaultEventHandlers();` too in your
1657 		overridden version.
1658 
1659 		You only need to do that on parent classes adding NEW event types. If you
1660 		just want to change the default behavior of an existing event type in a subclass,
1661 		you override the function (and optionally call `super.method_name`) like normal.
1662 
1663 		History:
1664 			Some of the events changed to take specific subclasses instead of generic `Event`
1665 			on January 3, 2025.
1666 
1667 	+/
1668 	protected EventHandler[string] defaultEventHandlers;
1669 
1670 	/// ditto
1671 	void setupDefaultEventHandlers() {
1672 		defaultEventHandlers["click"] = (Widget t, Event event)      { if(auto e = cast(ClickEvent) event) t.defaultEventHandler_click(e); };
1673 		defaultEventHandlers["dblclick"] = (Widget t, Event event)   { if(auto e = cast(DoubleClickEvent) event) t.defaultEventHandler_dblclick(e); };
1674 		defaultEventHandlers["keydown"] = (Widget t, Event event)    { if(auto e = cast(KeyDownEvent) event) t.defaultEventHandler_keydown(e); };
1675 		defaultEventHandlers["keyup"] = (Widget t, Event event)      { if(auto e = cast(KeyUpEvent) event) t.defaultEventHandler_keyup(e); };
1676 		defaultEventHandlers["mouseover"] = (Widget t, Event event)  { if(auto e = cast(MouseOverEvent) event) t.defaultEventHandler_mouseover(e); };
1677 		defaultEventHandlers["mouseout"] = (Widget t, Event event)   { if(auto e = cast(MouseOutEvent) event) t.defaultEventHandler_mouseout(e); };
1678 		defaultEventHandlers["mousedown"] = (Widget t, Event event)  { if(auto e = cast(MouseDownEvent) event) t.defaultEventHandler_mousedown(e); };
1679 		defaultEventHandlers["mouseup"] = (Widget t, Event event)    { if(auto e = cast(MouseUpEvent) event) t.defaultEventHandler_mouseup(e); };
1680 		defaultEventHandlers["mouseenter"] = (Widget t, Event event) { if(auto e = cast(MouseEnterEvent) event) t.defaultEventHandler_mouseenter(e); };
1681 		defaultEventHandlers["mouseleave"] = (Widget t, Event event) { if(auto e = cast(MouseLeaveEvent) event) t.defaultEventHandler_mouseleave(e); };
1682 		defaultEventHandlers["mousemove"] = (Widget t, Event event)  { if(auto e = cast(MouseMoveEvent) event) t.defaultEventHandler_mousemove(e); };
1683 		defaultEventHandlers["char"] = (Widget t, Event event)       { if(auto e = cast(CharEvent) event) t.defaultEventHandler_char(e); };
1684 		defaultEventHandlers["triggered"] = (Widget t, Event event)  { if(auto e = cast(Event) event) t.defaultEventHandler_triggered(e); };
1685 		defaultEventHandlers["change"] = (Widget t, Event event)     { if(auto e = cast(ChangeEventBase) event) t.defaultEventHandler_change(e); };
1686 		defaultEventHandlers["focus"] = (Widget t, Event event)      { if(auto e = cast(FocusEvent) event) t.defaultEventHandler_focus(e); };
1687 		defaultEventHandlers["blur"] = (Widget t, Event event)       { if(auto e = cast(BlurEvent) event) t.defaultEventHandler_blur(e); };
1688 		defaultEventHandlers["focusin"] = (Widget t, Event event)    { if(auto e = cast(FocusInEvent) event) t.defaultEventHandler_focusin(e); };
1689 		defaultEventHandlers["focusout"] = (Widget t, Event event)   { if(auto e = cast(FocusOutEvent) event) t.defaultEventHandler_focusout(e); };
1690 	}
1691 
1692 	/// ditto
1693 	void defaultEventHandler_click(ClickEvent event) {}
1694 	/// ditto
1695 	void defaultEventHandler_dblclick(DoubleClickEvent event) {}
1696 	/// ditto
1697 	void defaultEventHandler_keydown(KeyDownEvent event) {}
1698 	/// ditto
1699 	void defaultEventHandler_keyup(KeyUpEvent event) {}
1700 	/// ditto
1701 	void defaultEventHandler_mousedown(MouseDownEvent event) {
1702 		if(event.button == MouseButton.left) {
1703 			if(this.tabStop) {
1704 				this.focus();
1705 			}
1706 		} else if(event.button == MouseButton.right) {
1707 			showContextMenu(event.clientX, event.clientY);
1708 		}
1709 	}
1710 	/// ditto
1711 	void defaultEventHandler_mouseover(MouseOverEvent event) {}
1712 	/// ditto
1713 	void defaultEventHandler_mouseout(MouseOutEvent event) {}
1714 	/// ditto
1715 	void defaultEventHandler_mouseup(MouseUpEvent event) {}
1716 	/// ditto
1717 	void defaultEventHandler_mousemove(MouseMoveEvent event) {}
1718 	/// ditto
1719 	void defaultEventHandler_mouseenter(MouseEnterEvent event) {}
1720 	/// ditto
1721 	void defaultEventHandler_mouseleave(MouseLeaveEvent event) {}
1722 	/// ditto
1723 	void defaultEventHandler_char(CharEvent event) {}
1724 	/// ditto
1725 	void defaultEventHandler_triggered(Event event) {}
1726 	/// ditto
1727 	void defaultEventHandler_change(ChangeEventBase event) {}
1728 	/// ditto
1729 	void defaultEventHandler_focus(FocusEvent event) {}
1730 	/// ditto
1731 	void defaultEventHandler_blur(BlurEvent event) {}
1732 	/// ditto
1733 	void defaultEventHandler_focusin(FocusInEvent event) {}
1734 	/// ditto
1735 	void defaultEventHandler_focusout(FocusOutEvent event) {}
1736 
1737 	/++
1738 		[Event]s use a Javascript-esque model. See more details on the [Event] page.
1739 
1740 		[addEventListener] returns an opaque handle that you can later pass to [removeEventListener].
1741 
1742 		addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out
1743 		of participating in handler delegation.
1744 
1745 		$(TIP
1746 			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.
1747 		)
1748 	+/
1749 	EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) {
1750 		return addEventListener(event, (Widget, scope Event e) {
1751 			if(e.srcElement is this)
1752 				handler();
1753 		}, useCapture);
1754 	}
1755 
1756 	/// ditto
1757 	EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1758 		return addEventListener(event, (Widget, Event e) {
1759 			if(e.srcElement is this)
1760 				handler(e);
1761 		}, useCapture);
1762 	}
1763 
1764 	/// ditto
1765 	EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) {
1766 		static if(is(Handler Fn == delegate)) {
1767 		static if(is(Fn Params == __parameters)) {
1768 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1769 				if(e.srcElement !is this)
1770 					return;
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 	@scriptable
1781 	EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) {
1782 		return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture);
1783 	}
1784 
1785 	/// ditto
1786 	EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) {
1787 		static if(is(Handler Fn == delegate)) {
1788 		static if(is(Fn Params == __parameters)) {
1789 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1790 				auto ty = cast(Params[0]) e;
1791 				if(ty !is null)
1792 					handler(ty);
1793 			}, useCapture);
1794 		} else static assert(0);
1795 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1796 	}
1797 
1798 	/// ditto
1799 	EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1800 		return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture);
1801 	}
1802 
1803 	/// ditto
1804 	EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) {
1805 		if(event.length > 2 && event[0..2] == "on")
1806 			event = event[2 .. $];
1807 
1808 		if(useCapture)
1809 			capturingEventHandlers[event] ~= handler;
1810 		else
1811 			bubblingEventHandlers[event] ~= handler;
1812 
1813 		return EventListener(this, event, handler, useCapture);
1814 	}
1815 
1816 	/// ditto
1817 	void removeEventListener(string event, EventHandler handler, bool useCapture = false) {
1818 		if(event.length > 2 && event[0..2] == "on")
1819 			event = event[2 .. $];
1820 
1821 		if(useCapture) {
1822 			if(event in capturingEventHandlers)
1823 			foreach(ref evt; capturingEventHandlers[event])
1824 				if(evt is handler) evt = null;
1825 		} else {
1826 			if(event in bubblingEventHandlers)
1827 			foreach(ref evt; bubblingEventHandlers[event])
1828 				if(evt is handler) evt = null;
1829 		}
1830 	}
1831 
1832 	/// ditto
1833 	void removeEventListener(EventListener listener) {
1834 		removeEventListener(listener.event, listener.handler, listener.useCapture);
1835 	}
1836 
1837 	static if(UsingSimpledisplayX11) {
1838 		void discardXConnectionState() {
1839 			foreach(child; children)
1840 				child.discardXConnectionState();
1841 		}
1842 
1843 		void recreateXConnectionState() {
1844 			foreach(child; children)
1845 				child.recreateXConnectionState();
1846 			redraw();
1847 		}
1848 	}
1849 
1850 	/++
1851 		Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen.
1852 
1853 		History:
1854 			`globalCoordinates` was made `final` on May 11, 2021.
1855 	+/
1856 	Point globalCoordinates() {
1857 		int x = this.x;
1858 		int y = this.y;
1859 		auto p = this.parent;
1860 		while(p) {
1861 			x += p.x;
1862 			y += p.y;
1863 			p = p.parent;
1864 		}
1865 
1866 		static if(UsingSimpledisplayX11) {
1867 			auto dpy = XDisplayConnection.get;
1868 			arsd.simpledisplay.Window dummyw;
1869 			XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw);
1870 		} else version(Windows) {
1871 			POINT pt;
1872 			pt.x = x;
1873 			pt.y = y;
1874 			MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1);
1875 			x = pt.x;
1876 			y = pt.y;
1877 		} else {
1878 			auto rect = this.parentWindow.win.impl.window.frame;
1879 			// FIXME: confirm?
1880 			x += cast(int) rect.origin.x;
1881 			y += cast(int) rect.origin.y;
1882 		}
1883 
1884 		return Point(x, y);
1885 	}
1886 
1887 	version(win32_widgets)
1888 	int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; }
1889 
1890 	version(win32_widgets)
1891 	/// Called when a WM_COMMAND is sent to the associated hwnd.
1892 	void handleWmCommand(ushort cmd, ushort id) {}
1893 
1894 	version(win32_widgets)
1895 	/++
1896 		Called when a WM_NOTIFY is sent to the associated hwnd.
1897 
1898 		History:
1899 	+/
1900 	int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; }
1901 
1902 	version(win32_widgets)
1903 	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); }
1904 
1905 	/++
1906 		This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget.
1907 
1908 		Updates to this variable will only be made visible on the next mouse enter event.
1909 	+/
1910 	@scriptable string statusTip;
1911 	// string toolTip;
1912 	// string helpText;
1913 
1914 	/++
1915 		If true, this widget can be focused via keyboard control with the tab key.
1916 
1917 		If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to).
1918 	+/
1919 	bool tabStop = true;
1920 	/++
1921 		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.)
1922 	+/
1923 	int tabOrder;
1924 
1925 	version(win32_widgets) {
1926 		static Widget[HWND] nativeMapping;
1927 		/// The native handle, if there is one.
1928 		HWND hwnd;
1929 		WNDPROC originalWindowProcedure;
1930 
1931 		SimpleWindow simpleWindowWrappingHwnd;
1932 
1933 		// please note it IGNORES your return value and does NOT forward it to Windows!
1934 		int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) {
1935 			return 0;
1936 		}
1937 	}
1938 	private bool implicitlyCreated;
1939 
1940 	/// 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.
1941 	int x;
1942 	/// ditto
1943 	int y;
1944 	private int _width;
1945 	private int _height;
1946 	private Widget[] _children;
1947 	private Widget _parent;
1948 	private Window _parentWindow;
1949 
1950 	/++
1951 		Returns the window to which this widget is attached.
1952 
1953 		History:
1954 			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.
1955 	+/
1956 	final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; }
1957 	private @property void parentWindow(Window parent) {
1958 		auto old = _parentWindow;
1959 		_parentWindow = parent;
1960 		newParentWindow(old, _parentWindow);
1961 		foreach(child; children)
1962 			child.parentWindow = parent; // please note that this is recursive
1963 	}
1964 
1965 	/++
1966 		Called when the widget has been added to or remove from a parent window.
1967 
1968 		Note that either oldParent and/or newParent may be null any time this is called.
1969 
1970 		History:
1971 			Added September 13, 2024
1972 	+/
1973 	protected void newParentWindow(Window oldParent, Window newParent) {}
1974 
1975 	/++
1976 		Returns the list of the widget's children.
1977 
1978 		History:
1979 			Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private.
1980 
1981 			Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list.
1982 	+/
1983 	final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; }
1984 
1985 	/++
1986 		Returns the widget's parent.
1987 
1988 		History:
1989 			Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted.
1990 
1991 			The parent should only be managed by the [addChild] and [removeWidget] method.
1992 	+/
1993 	final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; }
1994 
1995 	/// The widget's current size.
1996 	final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; }
1997 	/// ditto
1998 	final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; }
1999 
2000 	/// Only the layout manager should be calling these.
2001 	final protected @property int width(int a) @safe { return _width = a; }
2002 	/// ditto
2003 	final protected @property int height(int a) @safe { return _height = a; }
2004 
2005 	/++
2006 		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.
2007 
2008 		It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size.
2009 	+/
2010 	protected void registerMovement() {
2011 		version(win32_widgets) {
2012 			if(hwnd) {
2013 				auto pos = getChildPositionRelativeToParentHwnd(this);
2014 				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
2015 				this.redraw();
2016 			}
2017 		}
2018 		sendResizeEvent();
2019 	}
2020 
2021 	/// Creates the widget and adds it to the parent.
2022 	this(Widget parent) {
2023 		if(parent !is null)
2024 			parent.addChild(this);
2025 		setupDefaultEventHandlers();
2026 	}
2027 
2028 	/// 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.
2029 	@scriptable
2030 	bool isFocused() {
2031 		return parentWindow && parentWindow.focusedWidget is this;
2032 	}
2033 
2034 	private bool showing_ = true;
2035 	///
2036 	bool showing() const { return showing_; }
2037 	///
2038 	bool hidden() const { return !showing_; }
2039 	/++
2040 		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.
2041 
2042 		Note that a widget only ever shows if all its parents are showing too.
2043 	+/
2044 	void showing(bool s, bool recalculate = true) {
2045 		if(s != showing_) {
2046 			showing_ = s;
2047 			// writeln(typeid(this).toString, " ", this.parent ? typeid(this.parent).toString : "null", " ", s);
2048 
2049 			showNativeWindowChildren(s);
2050 
2051 			if(parent && recalculate) {
2052 				parent.queueRecomputeChildLayout();
2053 				parent.redraw();
2054 			}
2055 
2056 			if(s) {
2057 				queueRecomputeChildLayout();
2058 				redraw();
2059 			}
2060 		}
2061 	}
2062 	/// Convenience method for `showing = true`
2063 	@scriptable
2064 	void show() {
2065 		showing = true;
2066 	}
2067 	/// Convenience method for `showing = false`
2068 	@scriptable
2069 	void hide() {
2070 		showing = false;
2071 	}
2072 
2073 	/++
2074 		If you are a native window, show/hide it based on shouldShow and return `true`.
2075 
2076 		Otherwise, do nothing and return false.
2077 	+/
2078 	protected bool showOrHideIfNativeWindow(bool shouldShow) {
2079 		version(win32_widgets) {
2080 			if(hwnd) {
2081 				ShowWindow(hwnd, shouldShow ? SW_SHOW : SW_HIDE);
2082 				return true;
2083 			} else {
2084 				return false;
2085 			}
2086 		} else {
2087 			return false;
2088 		}
2089 	}
2090 
2091 	private void showNativeWindowChildren(bool s) {
2092 		if(!showOrHideIfNativeWindow(s && showing))
2093 			foreach(child; children)
2094 				child.showNativeWindowChildren(s);
2095 	}
2096 
2097 	///
2098 	@scriptable
2099 	void focus() {
2100 		assert(parentWindow !is null);
2101 		if(isFocused())
2102 			return;
2103 
2104 		if(parentWindow.focusedWidget) {
2105 			// FIXME: more details here? like from and to
2106 			auto from = parentWindow.focusedWidget;
2107 			parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false);
2108 			parentWindow.focusedWidget = null;
2109 			from.emit!BlurEvent();
2110 			from.emit!FocusOutEvent();
2111 		}
2112 
2113 
2114 		version(win32_widgets) {
2115 			if(this.hwnd !is null)
2116 				SetFocus(this.hwnd);
2117 		}
2118 		//else static if(UsingSimpledisplayX11)
2119 			//this.parentWindow.win.focus();
2120 
2121 		parentWindow.focusedWidget = this;
2122 		parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true);
2123 		this.emit!FocusEvent();
2124 		this.emit!FocusInEvent();
2125 	}
2126 
2127 	/+
2128 	/++
2129 		Unfocuses the widget. This may reset
2130 	+/
2131 	@scriptable
2132 	void blur() {
2133 
2134 	}
2135 	+/
2136 
2137 
2138 	/++
2139 		This is called when the widget is added to a window. It gives you a chance to set up event hooks.
2140 
2141 		Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods.
2142 	+/
2143 	void attachedToWindow(Window w) {}
2144 	/++
2145 		Callback when the widget is added to another widget.
2146 
2147 		Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful.
2148 	+/
2149 	void addedTo(Widget w) {}
2150 
2151 	/++
2152 		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.
2153 
2154 		This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget.
2155 	+/
2156 	protected void addChild(Widget w, int position = int.max) {
2157 		assert(w._parent !is this, "Child cannot be added twice to the same parent");
2158 		assert(w !is this, "Child cannot be its own parent!");
2159 		w._parent = this;
2160 		if(position == int.max || position == children.length) {
2161 			_children ~= w;
2162 		} else {
2163 			assert(position < _children.length);
2164 			_children.length = _children.length + 1;
2165 			for(int i = cast(int) _children.length - 1; i > position; i--)
2166 				_children[i] = _children[i - 1];
2167 			_children[position] = w;
2168 		}
2169 
2170 		this.parentWindow = this._parentWindow;
2171 
2172 		w.addedTo(this);
2173 
2174 		bool parentIsNative;
2175 		version(win32_widgets) {
2176 			parentIsNative = hwnd !is null;
2177 		}
2178 		if(!parentIsNative && !showing)
2179 			w.showOrHideIfNativeWindow(false);
2180 
2181 		if(parentWindow !is null) {
2182 			w.attachedToWindow(parentWindow);
2183 			parentWindow.queueRecomputeChildLayout();
2184 			parentWindow.redraw();
2185 		}
2186 	}
2187 
2188 	/++
2189 		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.
2190 	+/
2191 	Widget getChildAtPosition(int x, int y) {
2192 		// it goes backward so the last one to show gets picked first
2193 		// might use z-index later
2194 		foreach_reverse(child; children) {
2195 			if(child.hidden)
2196 				continue;
2197 			if(child.x <= x && child.y <= y
2198 				&& ((x - child.x) < child.width)
2199 				&& ((y - child.y) < child.height))
2200 			{
2201 				return child;
2202 			}
2203 		}
2204 
2205 		return null;
2206 	}
2207 
2208 	/++
2209 		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.
2210 
2211 		History:
2212 			Added July 2, 2021 (v10.2)
2213 	+/
2214 	protected void addScrollPosition(ref int x, ref int y) {}
2215 
2216 	/++
2217 		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.
2218 
2219 		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.
2220 
2221 		[paint] is not called for system widgets as the OS library draws them instead.
2222 
2223 
2224 		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.
2225 
2226 		You should also look at [WidgetPainter.visualTheme] to be theme aware.
2227 
2228 		History:
2229 			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.
2230 	+/
2231 	void paint(WidgetPainter painter) {
2232 		version(win32_widgets)
2233 			if(hwnd) {
2234 				return;
2235 			}
2236 		painter.drawThemed(&paintContent); // note this refers to the following overload
2237 	}
2238 
2239 	/++
2240 		Responsible for drawing the content as the theme engine is responsible for other elements.
2241 
2242 		$(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.)
2243 
2244 		Params:
2245 			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.
2246 
2247 			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.
2248 
2249 			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.
2250 
2251 		Returns:
2252 			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.
2253 
2254 		History:
2255 			Added May 15, 2021
2256 	+/
2257 	Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
2258 		return bounds;
2259 	}
2260 
2261 	deprecated("Change ScreenPainter to WidgetPainter")
2262 	final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
2263 
2264 	/// I don't actually like the name of this
2265 	/// this draws a background on it
2266 	void erase(WidgetPainter painter) {
2267 		version(win32_widgets)
2268 			if(hwnd) return; // Windows will do it. I think.
2269 
2270 		auto c = getComputedStyle().background.color;
2271 		painter.fillColor = c;
2272 		painter.outlineColor = c;
2273 
2274 		version(win32_widgets) {
2275 			HANDLE b, p;
2276 			if(c.a == 0 && parent is parentWindow) {
2277 				// I don't remember why I had this really...
2278 				b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
2279 				p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
2280 			}
2281 		}
2282 		painter.drawRectangle(Point(0, 0), width, height);
2283 		version(win32_widgets) {
2284 			if(c.a == 0 && parent is parentWindow) {
2285 				SelectObject(painter.impl.hdc, p);
2286 				SelectObject(painter.impl.hdc, b);
2287 			}
2288 		}
2289 	}
2290 
2291 	///
2292 	WidgetPainter draw() {
2293 		int x = this.x, y = this.y;
2294 		auto parent = this.parent;
2295 		while(parent) {
2296 			x += parent.x;
2297 			y += parent.y;
2298 			parent = parent.parent;
2299 		}
2300 
2301 		auto painter = parentWindow.win.draw(true);
2302 		painter.originX = x;
2303 		painter.originY = y;
2304 		painter.setClipRectangle(Point(0, 0), width, height);
2305 		return WidgetPainter(painter, this);
2306 	}
2307 
2308 	/// 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.
2309 	protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
2310 		if(hidden)
2311 			return;
2312 
2313 		int paintX = x;
2314 		int paintY = y;
2315 		if(this.useNativeDrawing()) {
2316 			paintX = 0;
2317 			paintY = 0;
2318 			lox = 0;
2319 			loy = 0;
2320 			containment = Rectangle(0, 0, int.max, int.max);
2321 		}
2322 
2323 		painter.originX = lox + paintX;
2324 		painter.originY = loy + paintY;
2325 
2326 		bool actuallyPainted = false;
2327 
2328 		const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height)));
2329 		if(clip == Rectangle.init) {
2330 			// writeln(this, " clipped out");
2331 			return;
2332 		}
2333 
2334 		bool invalidateChildren = invalidate;
2335 
2336 		if(redrawRequested || force) {
2337 			painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
2338 
2339 			painter.drawingUpon = this;
2340 
2341 			erase(painter);
2342 			if(painter.visualTheme)
2343 				painter.visualTheme.doPaint(this, painter);
2344 			else
2345 				paint(painter);
2346 
2347 			if(invalidate) {
2348 				// sdpyPrintDebugString("invalidate " ~ typeid(this).name);
2349 				auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height));
2350 				painter.invalidateRect(region);
2351 				// children are contained inside this, so no need to do extra work
2352 				invalidateChildren = false;
2353 			}
2354 
2355 			redrawRequested = false;
2356 			actuallyPainted = true;
2357 		}
2358 
2359 		foreach(child; children) {
2360 			version(win32_widgets)
2361 				if(child.useNativeDrawing()) continue;
2362 			child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren);
2363 		}
2364 
2365 		version(win32_widgets)
2366 		foreach(child; children) {
2367 			if(child.useNativeDrawing) {
2368 				painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child);
2369 				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
2370 			}
2371 		}
2372 	}
2373 
2374 	protected bool useNativeDrawing() nothrow {
2375 		version(win32_widgets)
2376 			return hwnd !is null;
2377 		else
2378 			return false;
2379 	}
2380 
2381 	private static class RedrawEvent {}
2382 	private __gshared re = new RedrawEvent();
2383 
2384 	private bool redrawRequested;
2385 	///
2386 	final void redraw(string file = __FILE__, size_t line = __LINE__) {
2387 		redrawRequested = true;
2388 
2389 		if(this.parentWindow) {
2390 			auto sw = this.parentWindow.win;
2391 			assert(sw !is null);
2392 			if(!sw.eventQueued!RedrawEvent) {
2393 				sw.postEvent(re);
2394 				//  writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
2395 			}
2396 		}
2397 	}
2398 
2399 	private SimpleWindow drawableWindow;
2400 
2401 	/++
2402 		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.
2403 
2404 		Returns:
2405 			`true` if you should do your default behavior.
2406 
2407 		History:
2408 			Added May 5, 2021
2409 
2410 		Bugs:
2411 			It does not do the static checks on gdc right now.
2412 	+/
2413 	final protected bool emit(EventType, this This, Args...)(Args args) {
2414 		version(GNU) {} else
2415 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2416 		auto e = new EventType(this, args);
2417 		e.dispatch();
2418 		return !e.defaultPrevented;
2419 	}
2420 	/// ditto
2421 	final protected bool emit(string eventString, this This)() {
2422 		auto e = new Event(eventString, this);
2423 		e.dispatch();
2424 		return !e.defaultPrevented;
2425 	}
2426 
2427 	/++
2428 		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.
2429 
2430 		History:
2431 			Added May 5, 2021
2432 	+/
2433 	final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) {
2434 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2435 		return addEventListener(handler);
2436 	}
2437 
2438 	/++
2439 		Gets the computed style properties from the visual theme.
2440 
2441 		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].)
2442 
2443 		History:
2444 			Added May 8, 2021
2445 	+/
2446 	final StyleInformation getComputedStyle() {
2447 		return StyleInformation(this);
2448 	}
2449 
2450 	int focusableWidgets(scope int delegate(Widget) dg) {
2451 		foreach(widget; WidgetStream(this)) {
2452 			if(widget.tabStop && !widget.hidden) {
2453 				int result = dg(widget);
2454 				if (result)
2455 					return result;
2456 			}
2457 		}
2458 		return 0;
2459 	}
2460 
2461 	/++
2462 		Calculates the border box (that is, the full width/height of the widget, from border edge to border edge)
2463 		for the given content box (the area between the padding)
2464 
2465 		History:
2466 			Added January 4, 2023 (dub v11.0)
2467 	+/
2468 	Rectangle borderBoxForContentBox(Rectangle contentBox) {
2469 		auto cs = getComputedStyle();
2470 
2471 		auto borderWidth = getBorderWidth(cs.borderStyle);
2472 
2473 		auto rect = contentBox;
2474 
2475 		rect.left -= borderWidth;
2476 		rect.right += borderWidth;
2477 		rect.top -= borderWidth;
2478 		rect.bottom += borderWidth;
2479 
2480 		auto insideBorderRect = rect;
2481 
2482 		rect.left -= cs.paddingLeft;
2483 		rect.right += cs.paddingRight;
2484 		rect.top -= cs.paddingTop;
2485 		rect.bottom += cs.paddingBottom;
2486 
2487 		return rect;
2488 	}
2489 
2490 
2491 	// FIXME: I kinda want to hide events from implementation widgets
2492 	// so it just catches them all and stops propagation...
2493 	// i guess i can do it with a event listener on star.
2494 
2495 	mixin Emits!KeyDownEvent; ///
2496 	mixin Emits!KeyUpEvent; ///
2497 	mixin Emits!CharEvent; ///
2498 
2499 	mixin Emits!MouseDownEvent; ///
2500 	mixin Emits!MouseUpEvent; ///
2501 	mixin Emits!ClickEvent; ///
2502 	mixin Emits!DoubleClickEvent; ///
2503 	mixin Emits!MouseMoveEvent; ///
2504 	mixin Emits!MouseOverEvent; ///
2505 	mixin Emits!MouseOutEvent; ///
2506 	mixin Emits!MouseEnterEvent; ///
2507 	mixin Emits!MouseLeaveEvent; ///
2508 
2509 	mixin Emits!ResizeEvent; ///
2510 
2511 	mixin Emits!BlurEvent; ///
2512 	mixin Emits!FocusEvent; ///
2513 
2514 	mixin Emits!FocusInEvent; ///
2515 	mixin Emits!FocusOutEvent; ///
2516 }
2517 
2518 /+
2519 /++
2520 	Interface to indicate that the widget has a simple value property.
2521 
2522 	History:
2523 		Added August 26, 2021
2524 +/
2525 interface HasValue!T {
2526 	/// Getter
2527 	@property T value();
2528 	/// Setter
2529 	@property void value(T);
2530 }
2531 
2532 /++
2533 	Interface to indicate that the widget has a range of possible values for its simple value property.
2534 	This would be present on something like a slider or possibly a number picker.
2535 
2536 	History:
2537 		Added September 11, 2021
2538 +/
2539 interface HasRangeOfValues!T : HasValue!T {
2540 	/// The minimum and maximum values in the range, inclusive.
2541 	@property T minValue();
2542 	@property void minValue(T); /// ditto
2543 	@property T maxValue(); /// ditto
2544 	@property void maxValue(T); /// ditto
2545 
2546 	/// The smallest step the user interface allows. User may still type in values without this limitation.
2547 	@property void step(T);
2548 	@property T step(); /// ditto
2549 }
2550 
2551 /++
2552 	Interface to indicate that the widget has a list of possible values the user can choose from.
2553 	This would be present on something like a drop-down selector.
2554 
2555 	The value is NOT necessarily one of the items on the list. Consider the case of a free-entry
2556 	combobox.
2557 
2558 	History:
2559 		Added September 11, 2021
2560 +/
2561 interface HasListOfValues!T : HasValue!T {
2562 	@property T[] values;
2563 	@property void values(T[]);
2564 
2565 	@property int selectedIndex(); // note it may return -1!
2566 	@property void selectedIndex(int);
2567 }
2568 +/
2569 
2570 /++
2571 	History:
2572 		Added September 2021 (dub v10.4)
2573 +/
2574 class GridLayout : Layout {
2575 
2576 	// 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.
2577 
2578 	/++
2579 		If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to.
2580 	+/
2581 	enum Gravity {
2582 		Center    = 0,
2583 		NorthWest = North | West,
2584 		North     = 0b10_00,
2585 		NorthEast = North | East,
2586 		West      = 0b00_10,
2587 		East      = 0b00_01,
2588 		SouthWest = South | West,
2589 		South     = 0b01_00,
2590 		SouthEast = South | East,
2591 	}
2592 
2593 	/++
2594 		The width and height are in some proportional units and can often just be 12.
2595 	+/
2596 	this(int width, int height, Widget parent) {
2597 		this.gridWidth = width;
2598 		this.gridHeight = height;
2599 		super(parent);
2600 	}
2601 
2602 	/++
2603 		Sets the position of the given child.
2604 
2605 		The units of these arguments are in the proportional grid units you set in the constructor.
2606 	+/
2607 	Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) {
2608 		// ensure it is in bounds
2609 		// then ensure no overlaps
2610 
2611 		ChildPosition p = ChildPosition(child, x, y, width, height, gravity);
2612 
2613 		foreach(ref position; positions) {
2614 			if(position.widget is child) {
2615 				position = p;
2616 				goto set;
2617 			}
2618 		}
2619 
2620 		positions ~= p;
2621 
2622 		set:
2623 
2624 		// FIXME: should this batch?
2625 		queueRecomputeChildLayout();
2626 
2627 		return child;
2628 	}
2629 
2630 	override void addChild(Widget w, int position = int.max) {
2631 		super.addChild(w, position);
2632 		//positions ~= ChildPosition(w);
2633 		if(position != int.max) {
2634 			// FIXME: align it so they actually match.
2635 		}
2636 	}
2637 
2638 	override void widgetRemoved(size_t idx, Widget w) {
2639 		// FIXME: keep the positions array aligned
2640 		// positions[idx].widget = null;
2641 	}
2642 
2643 	override void recomputeChildLayout() {
2644 		registerMovement();
2645 		int onGrid = cast(int) positions.length;
2646 		c: foreach(child; children) {
2647 			// just snap it to the grid
2648 			if(onGrid)
2649 			foreach(position; positions)
2650 				if(position.widget is child) {
2651 					child.x = this.width * position.x / this.gridWidth;
2652 					child.y = this.height * position.y / this.gridHeight;
2653 					child.width = this.width * position.width / this.gridWidth;
2654 					child.height = this.height * position.height / this.gridHeight;
2655 
2656 					auto diff = child.width - child.maxWidth();
2657 					// FIXME: gravity?
2658 					if(diff > 0) {
2659 						child.width = child.width - diff;
2660 
2661 						if(position.gravity & Gravity.West) {
2662 							// nothing needed, already aligned
2663 						} else if(position.gravity & Gravity.East) {
2664 							child.x += diff;
2665 						} else {
2666 							child.x += diff / 2;
2667 						}
2668 					}
2669 
2670 					diff = child.height - child.maxHeight();
2671 					// FIXME: gravity?
2672 					if(diff > 0) {
2673 						child.height = child.height - diff;
2674 
2675 						if(position.gravity & Gravity.North) {
2676 							// nothing needed, already aligned
2677 						} else if(position.gravity & Gravity.South) {
2678 							child.y += diff;
2679 						} else {
2680 							child.y += diff / 2;
2681 						}
2682 					}
2683 					child.recomputeChildLayout();
2684 					onGrid--;
2685 					continue c;
2686 				}
2687 			// the position isn't given on the grid array, we'll just fill in from where the explicit ones left off.
2688 		}
2689 	}
2690 
2691 	private struct ChildPosition {
2692 		Widget widget;
2693 		int x;
2694 		int y;
2695 		int width;
2696 		int height;
2697 		Gravity gravity;
2698 	}
2699 	private ChildPosition[] positions;
2700 
2701 	int gridWidth = 12;
2702 	int gridHeight = 12;
2703 }
2704 
2705 ///
2706 abstract class ComboboxBase : Widget {
2707 	// if the user can enter arbitrary data, we want to use  2 == CBS_DROPDOWN
2708 	// or to always show the list, we want CBS_SIMPLE == 1
2709 	version(win32_widgets)
2710 		this(uint style, Widget parent) {
2711 			super(parent);
2712 			createWin32Window(this, "ComboBox"w, null, style);
2713 		}
2714 	else version(custom_widgets)
2715 		this(Widget parent) {
2716 			super(parent);
2717 
2718 			addEventListener((KeyDownEvent event) {
2719 				if(event.key == Key.Up) {
2720 					setSelection(selection_-1);
2721 					event.preventDefault();
2722 				}
2723 				if(event.key == Key.Down) {
2724 					setSelection(selection_+1);
2725 					event.preventDefault();
2726 				}
2727 
2728 			});
2729 
2730 		}
2731 	else static assert(false);
2732 
2733 	protected void scrollSelectionIntoView() {}
2734 
2735 	/++
2736 		Returns the current list of options in the selection.
2737 
2738 		History:
2739 			Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private.
2740 	+/
2741 	final @property string[] options() const {
2742 		return cast(string[]) options_;
2743 	}
2744 
2745 	/++
2746 		Replaces the list of options in the box. Note that calling this will also reset the selection.
2747 
2748 		History:
2749 			Added December, 29 2024
2750 	+/
2751 	final @property void options(string[] options) {
2752 		version(win32_widgets)
2753 			SendMessageW(hwnd, 331 /*CB_RESETCONTENT*/, 0, 0);
2754 		selection_ = -1;
2755 		options_ = null;
2756 		foreach(opt; options)
2757 			addOption(opt);
2758 
2759 		version(custom_widgets)
2760 			redraw();
2761 	}
2762 
2763 	private string[] options_;
2764 	private int selection_ = -1;
2765 
2766 	/++
2767 		Adds an option to the end of options array.
2768 	+/
2769 	void addOption(string s) {
2770 		options_ ~= s;
2771 		version(win32_widgets)
2772 		SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s));
2773 	}
2774 
2775 	/++
2776 		Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected.
2777 	+/
2778 	int getSelection() {
2779 		return selection_;
2780 	}
2781 
2782 	/++
2783 		Returns the current selection as a string.
2784 
2785 		History:
2786 			Added November 17, 2021
2787 	+/
2788 	string getSelectionString() {
2789 		return selection_ == -1 ? null : options[selection_];
2790 	}
2791 
2792 	/++
2793 		Sets the current selection to an index in the options array, or to the given option if present.
2794 		Please note that the string version may do a linear lookup.
2795 
2796 		Returns:
2797 			the index you passed in
2798 
2799 		History:
2800 			The `string` based overload was added on March 1, 2022 (dub v10.7).
2801 
2802 			The return value was `void` prior to March 1, 2022.
2803 	+/
2804 	int setSelection(int idx) {
2805 		if(idx < -1)
2806 			idx = -1;
2807 		if(idx + 1 > options.length)
2808 			idx = cast(int) options.length - 1;
2809 
2810 		selection_ = idx;
2811 
2812 		version(win32_widgets)
2813 		SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0);
2814 
2815 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2816 		t.dispatch();
2817 
2818 		scrollSelectionIntoView();
2819 
2820 		return idx;
2821 	}
2822 
2823 	/// ditto
2824 	int setSelection(string s) {
2825 		if(s !is null)
2826 		foreach(idx, item; options)
2827 			if(item == s) {
2828 				return setSelection(cast(int) idx);
2829 			}
2830 		return setSelection(-1);
2831 	}
2832 
2833 	/++
2834 		This event is fired when the selection changes. Both [Event.stringValue] and
2835 		[Event.intValue] are filled in - `stringValue` is the text in the selection
2836 		and `intValue` is the index of the selection. If the combo box allows multiple
2837 		selection, these values will include only one of the selected items - for those,
2838 		you should loop through the values and check their selected flag instead.
2839 
2840 		(I know that sucks, but it is how it is right now.)
2841 
2842 		History:
2843 			It originally inherited from `ChangeEvent!String`, but now does from [ChangeEventBase] as of January 3, 2025.
2844 			This shouldn't break anything if you used it through either its own name `SelectionChangedEvent` or through the
2845 			base `Event`, only if you specifically used `ChangeEvent!string` - those handlers may now get `null` or fail to
2846 			be called. If you did do this, just change it to generic `Event`, as `stringValue` and `intValue` are already there.
2847 	+/
2848 	static final class SelectionChangedEvent : ChangeEventBase {
2849 		this(Widget target, int iv, string sv) {
2850 			super(target);
2851 			this.iv = iv;
2852 			this.sv = sv;
2853 		}
2854 		immutable int iv;
2855 		immutable string sv;
2856 
2857 		deprecated("Use stringValue or intValue instead") @property string value() {
2858 			return sv;
2859 		}
2860 
2861 		override @property string stringValue() { return sv; }
2862 		override @property int intValue() { return iv; }
2863 	}
2864 
2865 	version(win32_widgets)
2866 	override void handleWmCommand(ushort cmd, ushort id) {
2867 		if(cmd == CBN_SELCHANGE) {
2868 			selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0);
2869 			fireChangeEvent();
2870 		}
2871 	}
2872 
2873 	private void fireChangeEvent() {
2874 		if(selection_ >= options.length)
2875 			selection_ = -1;
2876 
2877 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2878 		t.dispatch();
2879 	}
2880 
2881 	override int minWidth() { return scaleWithDpi(32); }
2882 
2883 	version(win32_widgets) {
2884 		override int minHeight() { return defaultLineHeight + 6; }
2885 		override int maxHeight() { return defaultLineHeight + 6; }
2886 	} else {
2887 		override int minHeight() { return defaultLineHeight + 4; }
2888 		override int maxHeight() { return defaultLineHeight + 4; }
2889 	}
2890 
2891 	version(custom_widgets)
2892 	void popup() {
2893 		CustomComboBoxPopup popup = new CustomComboBoxPopup(this);
2894 	}
2895 
2896 }
2897 
2898 private class CustomComboBoxPopup : Window {
2899 	private ComboboxBase associatedWidget;
2900 	private ListWidget lw;
2901 	private bool cancelled;
2902 
2903 	this(ComboboxBase associatedWidget) {
2904 		this.associatedWidget = associatedWidget;
2905 
2906 		// FIXME: this should scroll if there's too many elements to reasonably fit on screen
2907 
2908 		auto w = associatedWidget.width;
2909 		// FIXME: suggestedDropdownHeight see below
2910 		auto h = cast(int) associatedWidget.options.length * associatedWidget.defaultLineHeight + associatedWidget.scaleWithDpi(8);
2911 
2912 		// FIXME: this sux
2913 		if(h > associatedWidget.parentWindow.height)
2914 			h = associatedWidget.parentWindow.height;
2915 
2916 		auto mh = associatedWidget.scaleWithDpi(16 + 16 + 32); // to make the scrollbar look ok
2917 		if(h < mh)
2918 			h = mh;
2919 
2920 		auto coord = associatedWidget.globalCoordinates();
2921 		auto dropDown = new SimpleWindow(
2922 			w, h,
2923 			null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, associatedWidget.parentWindow ? associatedWidget.parentWindow.win : null);
2924 
2925 		super(dropDown);
2926 
2927 		dropDown.move(coord.x, coord.y + associatedWidget.height);
2928 
2929 		this.lw = new ListWidget(this);
2930 		version(custom_widgets)
2931 			lw.multiSelect = false;
2932 		foreach(option; associatedWidget.options)
2933 			lw.addOption(option);
2934 
2935 		auto originalSelection = associatedWidget.getSelection;
2936 		lw.setSelection(originalSelection);
2937 		lw.scrollSelectionIntoView();
2938 
2939 		/+
2940 		{
2941 			auto cs = getComputedStyle();
2942 			auto painter = dropDown.draw();
2943 			draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color);
2944 			auto p = Point(4, 4);
2945 			painter.outlineColor = cs.foregroundColor;
2946 			foreach(option; associatedWidget.options) {
2947 				painter.drawText(p, option);
2948 				p.y += defaultLineHeight;
2949 			}
2950 		}
2951 
2952 		dropDown.setEventHandlers(
2953 			(MouseEvent event) {
2954 				if(event.type == MouseEventType.buttonReleased) {
2955 					dropDown.close();
2956 					auto element = (event.y - 4) / defaultLineHeight;
2957 					if(element >= 0 && element <= associatedWidget.options.length) {
2958 						associatedWidget.selection_ = element;
2959 
2960 						associatedWidget.fireChangeEvent();
2961 					}
2962 				}
2963 			}
2964 		);
2965 		+/
2966 
2967 		Widget previouslyFocusedWidget;
2968 
2969 		dropDown.visibilityChanged = (bool visible) {
2970 			if(visible) {
2971 				this.redraw();
2972 				captureMouse(this);
2973 
2974 				if(previouslyFocusedWidget is null)
2975 					previouslyFocusedWidget = associatedWidget.parentWindow.focusedWidget;
2976 				associatedWidget.parentWindow.focusedWidget = lw;
2977 			} else {
2978 				//dropDown.releaseInputGrab();
2979 				releaseMouseCapture();
2980 
2981 				if(!cancelled)
2982 					associatedWidget.setSelection(lw.getSelection);
2983 
2984 				associatedWidget.parentWindow.focusedWidget = previouslyFocusedWidget;
2985 			}
2986 		};
2987 
2988 		dropDown.show();
2989 	}
2990 
2991 	private bool shouldCloseIfClicked(Widget w) {
2992 		if(w is this)
2993 			return true;
2994 		version(custom_widgets)
2995 		if(cast(TextListViewWidget.TextListViewItem) w)
2996 			return true;
2997 		return false;
2998 	}
2999 
3000 	override void defaultEventHandler_click(ClickEvent ce) {
3001 		if(ce.button == MouseButton.left && shouldCloseIfClicked(ce.target)) {
3002 			this.win.close();
3003 		}
3004 	}
3005 
3006 	override void defaultEventHandler_char(CharEvent ce) {
3007 		if(ce.character == '\n')
3008 			this.win.close();
3009 	}
3010 
3011 	override void defaultEventHandler_keydown(KeyDownEvent kde) {
3012 		if(kde.key == Key.Escape) {
3013 			cancelled = true;
3014 			this.win.close();
3015 		}/+ else if(kde.key == Key.Up || kde.key == Key.Down)
3016 			{} // intentionally blank, the list view handles these
3017 			// separately from the scroll message widget default handler
3018 		else if(lw && lw.glvw && lw.glvw.smw)
3019 			lw.glvw.smw.defaultKeyboardListener(kde);+/
3020 	}
3021 }
3022 
3023 /++
3024 	A drop-down list where the user must select one of the
3025 	given options. Like `<select>` in HTML.
3026 
3027 	The current selection is given as a string or an index.
3028 	It emits a SelectionChangedEvent when it changes.
3029 +/
3030 class DropDownSelection : ComboboxBase {
3031 	/++
3032 		Creates a drop down selection, optionally passing its initial list of options.
3033 
3034 		History:
3035 			The overload with the `options` parameter was added December 29, 2024.
3036 	+/
3037 	this(Widget parent) {
3038 		version(win32_widgets)
3039 			super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent);
3040 		else version(custom_widgets) {
3041 			super(parent);
3042 
3043 			addEventListener("focus", () { this.redraw; });
3044 			addEventListener("blur", () { this.redraw; });
3045 			addEventListener(EventType.change, () { this.redraw; });
3046 			addEventListener("mousedown", () { this.focus(); this.popup(); });
3047 			addEventListener((KeyDownEvent event) {
3048 				if(event.key == Key.Space)
3049 					popup();
3050 			});
3051 		} else static assert(false);
3052 	}
3053 
3054 	/// ditto
3055 	this(string[] options, Widget parent) {
3056 		this(parent);
3057 		this.options = options;
3058 	}
3059 
3060 	mixin Padding!q{2};
3061 	static class Style : Widget.Style {
3062 		override FrameStyle borderStyle() { return FrameStyle.risen; }
3063 	}
3064 	mixin OverrideStyle!Style;
3065 
3066 	version(custom_widgets)
3067 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
3068 		auto cs = getComputedStyle();
3069 
3070 		painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]);
3071 
3072 		painter.outlineColor = cs.foregroundColor;
3073 		painter.fillColor = cs.foregroundColor;
3074 
3075 		/+
3076 		Point[4] triangle;
3077 		enum padding = 6;
3078 		enum paddingV = 7;
3079 		enum triangleWidth = 10;
3080 		triangle[0] = Point(width - padding - triangleWidth, paddingV);
3081 		triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV);
3082 		triangle[2] = Point(width - padding - 0, paddingV);
3083 		triangle[3] = triangle[0];
3084 		painter.drawPolygon(triangle[]);
3085 		+/
3086 
3087 		auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2);
3088 
3089 		painter.drawPolygon(
3090 			scaleWithDpi(Point(2, 6) + offset),
3091 			scaleWithDpi(Point(7, 11) + offset),
3092 			scaleWithDpi(Point(12, 6) + offset),
3093 			scaleWithDpi(Point(2, 6) + offset)
3094 		);
3095 
3096 
3097 		return bounds;
3098 	}
3099 
3100 	version(win32_widgets)
3101 	override void registerMovement() {
3102 		version(win32_widgets) {
3103 			if(hwnd) {
3104 				auto pos = getChildPositionRelativeToParentHwnd(this);
3105 				// the height given to this from Windows' perspective is supposed
3106 				// to include the drop down's height. so I add to it to give some
3107 				// room for that.
3108 				// FIXME: maybe make the subclass provide a suggestedDropdownHeight thing
3109 				MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true);
3110 			}
3111 		}
3112 		sendResizeEvent();
3113 	}
3114 }
3115 
3116 /++
3117 	A text box with a drop down arrow listing selections.
3118 	The user can choose from the list, or type their own.
3119 +/
3120 class FreeEntrySelection : ComboboxBase {
3121 	this(Widget parent) {
3122 		this(null, parent);
3123 	}
3124 
3125 	this(string[] options, Widget parent) {
3126 		version(win32_widgets)
3127 			super(2 /* CBS_DROPDOWN */, parent);
3128 		else version(custom_widgets) {
3129 			super(parent);
3130 			auto hl = new HorizontalLayout(this);
3131 			lineEdit = new LineEdit(hl);
3132 
3133 			tabStop = false;
3134 
3135 			// lineEdit.addEventListener((FocusEvent fe) {  lineEdit.selectAll(); } );
3136 
3137 			auto btn = new class ArrowButton {
3138 				this() {
3139 					super(ArrowDirection.down, hl);
3140 				}
3141 				override int heightStretchiness() {
3142 					return 1;
3143 				}
3144 				override int heightShrinkiness() {
3145 					return 1;
3146 				}
3147 				override int maxHeight() {
3148 					return lineEdit.maxHeight;
3149 				}
3150 			};
3151 			//btn.addDirectEventListener("focus", &lineEdit.focus);
3152 			btn.addEventListener("triggered", &this.popup);
3153 			addEventListener(EventType.change, (Event event) {
3154 				lineEdit.content = event.stringValue;
3155 				lineEdit.focus();
3156 				redraw();
3157 			});
3158 		}
3159 		else static assert(false);
3160 
3161 		this.options = options;
3162 	}
3163 
3164 	string content() {
3165 		version(win32_widgets)
3166 			assert(0, "not implemented");
3167 		else version(custom_widgets)
3168 			return lineEdit.content;
3169 		else static assert(0);
3170 	}
3171 
3172 	void content(string s) {
3173 		version(win32_widgets)
3174 			assert(0, "not implemented");
3175 		else version(custom_widgets)
3176 			lineEdit.content = s;
3177 		else static assert(0);
3178 	}
3179 
3180 	override string getSelectionString() {
3181 		return content;
3182 	}
3183 
3184 	version(custom_widgets) {
3185 		LineEdit lineEdit;
3186 
3187 		override int widthStretchiness() {
3188 			return lineEdit ? lineEdit.widthStretchiness : super.widthStretchiness;
3189 		}
3190 		override int flexBasisWidth() {
3191 			return lineEdit ? lineEdit.flexBasisWidth : super.flexBasisWidth;
3192 		}
3193 	}
3194 }
3195 
3196 /++
3197 	A combination of free entry with a list below it.
3198 +/
3199 class ComboBox : ComboboxBase {
3200 	this(Widget parent) {
3201 		version(win32_widgets)
3202 			super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent);
3203 		else version(custom_widgets) {
3204 			super(parent);
3205 			lineEdit = new LineEdit(this);
3206 			listWidget = new ListWidget(this);
3207 			listWidget.multiSelect = false;
3208 			listWidget.addEventListener(EventType.change, delegate(Widget, Event) {
3209 				string c = null;
3210 				foreach(option; listWidget.options)
3211 					if(option.selected) {
3212 						c = option.label;
3213 						break;
3214 					}
3215 				lineEdit.content = c;
3216 			});
3217 
3218 			listWidget.tabStop = false;
3219 			this.tabStop = false;
3220 			listWidget.addEventListener("focusin", &lineEdit.focus);
3221 			this.addEventListener("focusin", &lineEdit.focus);
3222 
3223 			addDirectEventListener(EventType.change, {
3224 				listWidget.setSelection(selection_);
3225 				if(selection_ != -1)
3226 					lineEdit.content = options[selection_];
3227 				lineEdit.focus();
3228 				redraw();
3229 			});
3230 
3231 			lineEdit.addEventListener("focusin", &lineEdit.selectAll);
3232 
3233 			listWidget.addDirectEventListener(EventType.change, {
3234 				int set = -1;
3235 				foreach(idx, opt; listWidget.options)
3236 					if(opt.selected) {
3237 						set = cast(int) idx;
3238 						break;
3239 					}
3240 				if(set != selection_)
3241 					this.setSelection(set);
3242 			});
3243 		} else static assert(false);
3244 	}
3245 
3246 	override int minHeight() { return defaultLineHeight * 3; }
3247 	override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; }
3248 	override int heightStretchiness() { return 5; }
3249 
3250 	version(custom_widgets) {
3251 		LineEdit lineEdit;
3252 		ListWidget listWidget;
3253 
3254 		override void addOption(string s) {
3255 			listWidget.addOption(s);
3256 			ComboboxBase.addOption(s);
3257 		}
3258 
3259 		override void scrollSelectionIntoView() {
3260 			listWidget.scrollSelectionIntoView();
3261 		}
3262 	}
3263 }
3264 
3265 /+
3266 class Spinner : Widget {
3267 	version(win32_widgets)
3268 	this(Widget parent) {
3269 		super(parent);
3270 		parentWindow = parent.parentWindow;
3271 		auto hlayout = new HorizontalLayout(this);
3272 		lineEdit = new LineEdit(hlayout);
3273 		upDownControl = new UpDownControl(hlayout);
3274 	}
3275 
3276 	LineEdit lineEdit;
3277 	UpDownControl upDownControl;
3278 }
3279 
3280 class UpDownControl : Widget {
3281 	version(win32_widgets)
3282 	this(Widget parent) {
3283 		super(parent);
3284 		parentWindow = parent.parentWindow;
3285 		createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */);
3286 	}
3287 
3288 	override int minHeight() { return defaultLineHeight; }
3289 	override int maxHeight() { return defaultLineHeight * 3/2; }
3290 
3291 	override int minWidth() { return defaultLineHeight * 3/2; }
3292 	override int maxWidth() { return defaultLineHeight * 3/2; }
3293 }
3294 +/
3295 
3296 /+
3297 class DataView : Widget {
3298 	// this is the omnibus data viewer
3299 	// the internal data layout is something like:
3300 	// string[string][] but also each node can have parents
3301 }
3302 +/
3303 
3304 
3305 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS
3306 
3307 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d
3308 
3309 // FIXME: menus should prolly capture the mouse. ugh i kno.
3310 /*
3311 	TextEdit needs:
3312 
3313 	* caret manipulation
3314 	* selection control
3315 	* convenience functions for appendText, insertText, insertTextAtCaret, etc.
3316 
3317 	For example:
3318 
3319 	connect(paste, &textEdit.insertTextAtCaret);
3320 
3321 	would be nice.
3322 
3323 
3324 
3325 	I kinda want an omnibus dataview that combines list, tree,
3326 	and table - it can be switched dynamically between them.
3327 
3328 	Flattening policy: only show top level, show recursive, show grouped
3329 	List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer)
3330 
3331 	Single select, multi select, organization, drag+drop
3332 */
3333 
3334 //static if(UsingSimpledisplayX11)
3335 version(win32_widgets) {}
3336 else version(custom_widgets) {
3337 	enum scrollClickRepeatInterval = 50;
3338 
3339 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") {
3340 	enum windowBackgroundColor = Color(212, 212, 212); // used to be 192
3341 	enum activeTabColor = lightAccentColor;
3342 	enum hoveringColor = Color(228, 228, 228);
3343 	enum buttonColor = windowBackgroundColor;
3344 	enum depressedButtonColor = darkAccentColor;
3345 	enum activeListXorColor = Color(255, 255, 127);
3346 	enum progressBarColor = Color(0, 0, 128);
3347 	enum activeMenuItemColor = Color(0, 0, 128);
3348 
3349 }}
3350 else static assert(false);
3351 deprecated("Get these properties off the `visualTheme` instead.") {
3352 	// these are used by horizontal rule so not just custom_widgets. for now at least.
3353 	enum darkAccentColor = Color(172, 172, 172);
3354 	enum lightAccentColor = Color(223, 223, 223); // used to be 223
3355 }
3356 
3357 private const(wchar)* toWstringzInternal(in char[] s) {
3358 	wchar[] str;
3359 	str.reserve(s.length + 1);
3360 	foreach(dchar ch; s)
3361 		str ~= ch;
3362 	str ~= '\0';
3363 	return str.ptr;
3364 }
3365 
3366 static if(SimpledisplayTimerAvailable)
3367 void setClickRepeat(Widget w, int interval, int delay = 250) {
3368 	Timer timer;
3369 	int delayRemaining = delay / interval;
3370 	if(delayRemaining <= 1)
3371 		delayRemaining = 2;
3372 
3373 	immutable originalDelayRemaining = delayRemaining;
3374 
3375 	w.addDirectEventListener((scope MouseDownEvent ev) {
3376 		if(ev.srcElement !is w)
3377 			return;
3378 		if(timer !is null) {
3379 			timer.destroy();
3380 			timer = null;
3381 		}
3382 		delayRemaining = originalDelayRemaining;
3383 		timer = new Timer(interval, () {
3384 			if(delayRemaining > 0)
3385 				delayRemaining--;
3386 			else {
3387 				auto ev = new Event("triggered", w);
3388 				ev.sendDirectly();
3389 			}
3390 		});
3391 	});
3392 
3393 	w.addDirectEventListener((scope MouseUpEvent ev) {
3394 		if(ev.srcElement !is w)
3395 			return;
3396 		if(timer !is null) {
3397 			timer.destroy();
3398 			timer = null;
3399 		}
3400 	});
3401 
3402 	w.addDirectEventListener((scope MouseLeaveEvent ev) {
3403 		if(ev.srcElement !is w)
3404 			return;
3405 		if(timer !is null) {
3406 			timer.destroy();
3407 			timer = null;
3408 		}
3409 	});
3410 
3411 }
3412 else
3413 void setClickRepeat(Widget w, int interval, int delay = 250) {}
3414 
3415 enum FrameStyle {
3416 	none, ///
3417 	risen, /// a 3d pop-out effect (think Windows 95 button)
3418 	sunk, /// a 3d sunken effect (think Windows 95 button as you click on it)
3419 	solid, ///
3420 	dotted, ///
3421 	fantasy, /// a style based on a popular fantasy video game
3422 	rounded, /// a rounded rectangle
3423 }
3424 
3425 version(custom_widgets)
3426 deprecated
3427 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) {
3428 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
3429 }
3430 
3431 version(custom_widgets)
3432 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) {
3433 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, background);
3434 }
3435 
3436 version(custom_widgets)
3437 deprecated
3438 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) {
3439 	draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
3440 }
3441 
3442 int getBorderWidth(FrameStyle style) {
3443 	final switch(style) {
3444 		case FrameStyle.sunk, FrameStyle.risen:
3445 			return 2;
3446 		case FrameStyle.none:
3447 			return 0;
3448 		case FrameStyle.solid:
3449 			return 1;
3450 		case FrameStyle.dotted:
3451 			return 1;
3452 		case FrameStyle.fantasy:
3453 			return 3;
3454 		case FrameStyle.rounded:
3455 			return 2;
3456 	}
3457 }
3458 
3459 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) {
3460 	int borderWidth = getBorderWidth(style);
3461 	final switch(style) {
3462 		case FrameStyle.sunk, FrameStyle.risen:
3463 			// outer layer
3464 			painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black;
3465 		break;
3466 		case FrameStyle.none:
3467 			painter.outlineColor = background;
3468 		break;
3469 		case FrameStyle.solid:
3470 		case FrameStyle.rounded:
3471 			painter.pen = Pen(border, 1);
3472 		break;
3473 		case FrameStyle.dotted:
3474 			painter.pen = Pen(border, 1, Pen.Style.Dotted);
3475 		break;
3476 		case FrameStyle.fantasy:
3477 			painter.pen = Pen(border, 3);
3478 		break;
3479 	}
3480 
3481 	painter.fillColor = background;
3482 
3483 	if(style == FrameStyle.rounded) {
3484 		painter.drawRectangleRounded(Point(x, y), Size(width, height), 6);
3485 	} else {
3486 		painter.drawRectangle(Point(x + 0, y + 0), width, height);
3487 
3488 		if(style == FrameStyle.sunk || style == FrameStyle.risen) {
3489 			// 3d effect
3490 			auto vt = WidgetPainter.visualTheme;
3491 
3492 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor;
3493 			painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0));
3494 			painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1));
3495 
3496 			// inner layer
3497 			//right, bottom
3498 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor;
3499 			painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2));
3500 			painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2));
3501 			// left, top
3502 			painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white;
3503 			painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1));
3504 			painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2));
3505 		} else if(style == FrameStyle.fantasy) {
3506 			painter.pen = Pen(Color.white, 1, Pen.Style.Solid);
3507 			painter.fillColor = Color.transparent;
3508 			painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1));
3509 		}
3510 	}
3511 
3512 	return borderWidth;
3513 }
3514 
3515 /++
3516 	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.
3517 
3518 	See_Also:
3519 		[MenuItem]
3520 		[ToolButton]
3521 		[Menu.addItem]
3522 +/
3523 class Action {
3524 	version(win32_widgets) {
3525 		private int id;
3526 		private static int lastId = 9000;
3527 		private static Action[int] mapping;
3528 	}
3529 
3530 	KeyEvent accelerator;
3531 
3532 	// FIXME: disable message
3533 	// and toggle thing?
3534 	// ??? and trigger arguments too ???
3535 
3536 	/++
3537 		Params:
3538 			label = the textual label
3539 			icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons.
3540 			triggered = initial handler, more can be added via the [triggered] member.
3541 	+/
3542 	this(string label, ushort icon = 0, void delegate() triggered = null) {
3543 		this.label = label;
3544 		this.iconId = icon;
3545 		if(triggered !is null)
3546 			this.triggered ~= triggered;
3547 		version(win32_widgets) {
3548 			id = ++lastId;
3549 			mapping[id] = this;
3550 		}
3551 	}
3552 
3553 	private string label;
3554 	private ushort iconId;
3555 	// icon
3556 
3557 	// when it is triggered, the triggered event is fired on the window
3558 	/// The list of handlers when it is triggered.
3559 	void delegate()[] triggered;
3560 }
3561 
3562 /*
3563 	plan:
3564 		keyboard accelerators
3565 
3566 		* menus (and popups and tooltips)
3567 		* status bar
3568 		* toolbars and buttons
3569 
3570 		sortable table view
3571 
3572 		maybe notification area icons
3573 		basic clipboard
3574 
3575 		* radio box
3576 		splitter
3577 		toggle buttons (optionally mutually exclusive, like in Paint)
3578 		label, rich text display, multi line plain text (selectable)
3579 		* fieldset
3580 		* nestable grid layout
3581 		single line text input
3582 		* multi line text input
3583 		slider
3584 		spinner
3585 		list box
3586 		drop down
3587 		combo box
3588 		auto complete box
3589 		* progress bar
3590 
3591 		terminal window/widget (on unix it might even be a pty but really idk)
3592 
3593 		ok button
3594 		cancel button
3595 
3596 		keyboard hotkeys
3597 
3598 		scroll widget
3599 
3600 		event redirections and network transparency
3601 		script integration
3602 */
3603 
3604 
3605 /*
3606 	MENUS
3607 
3608 	auto bar = new MenuBar(window);
3609 	window.menuBar = bar;
3610 
3611 	auto fileMenu = bar.addItem(new Menu("&File"));
3612 	fileMenu.addItem(new MenuItem("&Exit"));
3613 
3614 
3615 	EVENTS
3616 
3617 	For controls, you should usually use "triggered" rather than "click", etc., because
3618 	triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation.
3619 	This is the case on menus and pushbuttons.
3620 
3621 	"click", on the other hand, currently only fires when it is literally clicked by the mouse.
3622 */
3623 
3624 
3625 /*
3626 enum LinePreference {
3627 	AlwaysOnOwnLine, // always on its own line
3628 	PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way
3629 	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
3630 }
3631 */
3632 
3633 /++
3634 	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.
3635 
3636 	---
3637 	class MyWidget : Widget {
3638 		this(Widget parent) { super(parent); }
3639 
3640 		// set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go:
3641 		mixin Padding!q{4};
3642 
3643 		// set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go:
3644 		mixin Margin!q{8};
3645 
3646 		// but if I specify one outside, it overrides the override, so now marginLeft is 2,
3647 		// while Top/Bottom/Right remain 8 from the mixin above.
3648 		override int marginLeft() { return 2; }
3649 	}
3650 	---
3651 
3652 
3653 	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]).
3654 
3655 	Padding is the area inside a widget where its background is drawn, but the content avoids.
3656 
3657 	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!).
3658 
3659 	* 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.
3660 +/
3661 mixin template Padding(string code) {
3662 	override int paddingLeft() { return mixin(code);}
3663 	override int paddingRight() { return mixin(code);}
3664 	override int paddingTop() { return mixin(code);}
3665 	override int paddingBottom() { return mixin(code);}
3666 }
3667 
3668 /// ditto
3669 mixin template Margin(string code) {
3670 	override int marginLeft() { return mixin(code);}
3671 	override int marginRight() { return mixin(code);}
3672 	override int marginTop() { return mixin(code);}
3673 	override int marginBottom() { return mixin(code);}
3674 }
3675 
3676 private
3677 void recomputeChildLayout(string relevantMeasure)(Widget parent) {
3678 	enum calcingV = relevantMeasure == "height";
3679 
3680 	parent.registerMovement();
3681 
3682 	if(parent.children.length == 0)
3683 		return;
3684 
3685 	auto parentStyle = parent.getComputedStyle();
3686 
3687 	enum firstThingy = relevantMeasure == "height" ? "Top" : "Left";
3688 	enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right";
3689 
3690 	enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top";
3691 	enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom";
3692 
3693 	// my own width and height should already be set by the caller of this function...
3694 	int spaceRemaining = mixin("parent." ~ relevantMeasure) -
3695 		mixin("parentStyle.padding"~firstThingy~"()") -
3696 		mixin("parentStyle.padding"~secondThingy~"()");
3697 
3698 	int stretchinessSum;
3699 	int stretchyChildSum;
3700 	int lastMargin = 0;
3701 
3702 	int shrinkinessSum;
3703 	int shrinkyChildSum;
3704 
3705 	// set initial size
3706 	foreach(child; parent.children) {
3707 
3708 		auto childStyle = child.getComputedStyle();
3709 
3710 		if(cast(StaticPosition) child)
3711 			continue;
3712 		if(child.hidden)
3713 			continue;
3714 
3715 		const iw = child.flexBasisWidth();
3716 		const ih = child.flexBasisHeight();
3717 
3718 		static if(calcingV) {
3719 			child.width = parent.width -
3720 				mixin("childStyle.margin"~otherFirstThingy~"()") -
3721 				mixin("childStyle.margin"~otherSecondThingy~"()") -
3722 				mixin("parentStyle.padding"~otherFirstThingy~"()") -
3723 				mixin("parentStyle.padding"~otherSecondThingy~"()");
3724 
3725 			if(child.width < 0)
3726 				child.width = 0;
3727 			if(child.width > childStyle.maxWidth())
3728 				child.width = childStyle.maxWidth();
3729 
3730 			if(iw > 0) {
3731 				auto totalPossible = child.width;
3732 				if(child.width > iw && child.widthStretchiness() == 0)
3733 					child.width = iw;
3734 			}
3735 
3736 			child.height = mymax(childStyle.minHeight(), ih);
3737 		} else {
3738 			// set to take all the space
3739 			child.height = parent.height -
3740 				mixin("childStyle.margin"~firstThingy~"()") -
3741 				mixin("childStyle.margin"~secondThingy~"()") -
3742 				mixin("parentStyle.padding"~firstThingy~"()") -
3743 				mixin("parentStyle.padding"~secondThingy~"()");
3744 
3745 			// then clamp it
3746 			if(child.height < 0)
3747 				child.height = 0;
3748 			if(child.height > childStyle.maxHeight())
3749 				child.height = childStyle.maxHeight();
3750 
3751 			// and if possible, respect the ideal target
3752 			if(ih > 0) {
3753 				auto totalPossible = child.height;
3754 				if(child.height > ih && child.heightStretchiness() == 0)
3755 					child.height = ih;
3756 			}
3757 
3758 			// if we have an ideal, try to respect it, otehrwise, just use the minimum
3759 			child.width = mymax(childStyle.minWidth(), iw);
3760 		}
3761 
3762 		spaceRemaining -= mixin("child." ~ relevantMeasure);
3763 
3764 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3765 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3766 		lastMargin = margin;
3767 		spaceRemaining -= thisMargin + margin;
3768 
3769 		auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3770 		stretchinessSum += s;
3771 		if(s > 0)
3772 			stretchyChildSum++;
3773 
3774 		auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()");
3775 		shrinkinessSum += s2;
3776 		if(s2 > 0)
3777 			shrinkyChildSum++;
3778 	}
3779 
3780 	if(spaceRemaining < 0 && shrinkyChildSum) {
3781 		// shrink to get into the space if it is possible
3782 		auto toRemove = -spaceRemaining;
3783 		auto removalPerItem = toRemove / shrinkinessSum;
3784 		auto remainder = toRemove % shrinkinessSum;
3785 
3786 		// FIXME: wtf why am i shrinking things with no shrinkiness?
3787 
3788 		foreach(child; parent.children) {
3789 			auto childStyle = child.getComputedStyle();
3790 			if(cast(StaticPosition) child)
3791 				continue;
3792 			if(child.hidden)
3793 				continue;
3794 			static if(calcingV) {
3795 				auto minimum = childStyle.minHeight();
3796 				auto stretch = childStyle.heightShrinkiness();
3797 			} else {
3798 				auto minimum = childStyle.minWidth();
3799 				auto stretch = childStyle.widthShrinkiness();
3800 			}
3801 
3802 			if(mixin("child._" ~ relevantMeasure) <= minimum)
3803 				continue;
3804 			// import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch);
3805 
3806 			mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh.
3807 
3808 			spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum;
3809 		}
3810 	}
3811 
3812 	// stretch to fill space
3813 	while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) {
3814 		auto spacePerChild = spaceRemaining / stretchinessSum;
3815 		bool spreadEvenly;
3816 		bool giveToBiggest;
3817 		if(spacePerChild <= 0) {
3818 			spacePerChild = spaceRemaining / stretchyChildSum;
3819 			spreadEvenly = true;
3820 		}
3821 		if(spacePerChild <= 0) {
3822 			giveToBiggest = true;
3823 		}
3824 		int previousSpaceRemaining = spaceRemaining;
3825 		stretchinessSum = 0;
3826 		Widget mostStretchy;
3827 		int mostStretchyS;
3828 		foreach(child; parent.children) {
3829 			auto childStyle = child.getComputedStyle();
3830 			if(cast(StaticPosition) child)
3831 				continue;
3832 			if(child.hidden)
3833 				continue;
3834 			static if(calcingV) {
3835 				auto maximum = childStyle.maxHeight();
3836 			} else {
3837 				auto maximum = childStyle.maxWidth();
3838 			}
3839 
3840 			if(mixin("child." ~ relevantMeasure) >= maximum) {
3841 				auto adj = mixin("child." ~ relevantMeasure) - maximum;
3842 				mixin("child._" ~ relevantMeasure) -= adj;
3843 				spaceRemaining += adj;
3844 				continue;
3845 			}
3846 			auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3847 			if(s <= 0)
3848 				continue;
3849 			auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s);
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 			} else if(mixin("child._" ~ relevantMeasure) < maximum) {
3857 				stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3858 				if(mostStretchy is null || s >= mostStretchyS) {
3859 					mostStretchy = child;
3860 					mostStretchyS = s;
3861 				}
3862 			}
3863 		}
3864 
3865 		if(giveToBiggest && mostStretchy !is null) {
3866 			auto child = mostStretchy;
3867 			auto childStyle = child.getComputedStyle();
3868 			int spaceAdjustment = spaceRemaining;
3869 
3870 			static if(calcingV)
3871 				auto maximum = childStyle.maxHeight();
3872 			else
3873 				auto maximum = childStyle.maxWidth();
3874 
3875 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3876 			spaceRemaining -= spaceAdjustment;
3877 			if(mixin("child._" ~ relevantMeasure) > maximum) {
3878 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3879 				mixin("child._" ~ relevantMeasure) -= diff;
3880 				spaceRemaining += diff;
3881 			}
3882 		}
3883 
3884 		if(spaceRemaining == previousSpaceRemaining) {
3885 			if(mostStretchy !is null) {
3886 				static if(calcingV)
3887 					auto maximum = mostStretchy.maxHeight();
3888 				else
3889 					auto maximum = mostStretchy.maxWidth();
3890 
3891 				mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining;
3892 				if(mixin("mostStretchy._" ~ relevantMeasure) > maximum)
3893 					mixin("mostStretchy._" ~ relevantMeasure) = maximum;
3894 			}
3895 			break; // apparently nothing more we can do
3896 		}
3897 	}
3898 
3899 	foreach(child; parent.children) {
3900 		auto childStyle = child.getComputedStyle();
3901 		if(cast(StaticPosition) child)
3902 			continue;
3903 		if(child.hidden)
3904 			continue;
3905 
3906 		static if(calcingV)
3907 			auto maximum = childStyle.maxHeight();
3908 		else
3909 			auto maximum = childStyle.maxWidth();
3910 		if(mixin("child._" ~ relevantMeasure) > maximum)
3911 			mixin("child._" ~ relevantMeasure) = maximum;
3912 	}
3913 
3914 	// position
3915 	lastMargin = 0;
3916 	int currentPos = mixin("parent.padding"~firstThingy~"()");
3917 	foreach(child; parent.children) {
3918 		auto childStyle = child.getComputedStyle();
3919 		if(cast(StaticPosition) child) {
3920 			child.recomputeChildLayout();
3921 			continue;
3922 		}
3923 		if(child.hidden)
3924 			continue;
3925 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3926 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3927 		currentPos += thisMargin;
3928 		static if(calcingV) {
3929 			child.x = parentStyle.paddingLeft() + childStyle.marginLeft();
3930 			child.y = currentPos;
3931 		} else {
3932 			child.x = currentPos;
3933 			child.y = parentStyle.paddingTop() + childStyle.marginTop();
3934 
3935 		}
3936 		currentPos += mixin("child." ~ relevantMeasure);
3937 		currentPos += margin;
3938 		lastMargin = margin;
3939 
3940 		child.recomputeChildLayout();
3941 	}
3942 }
3943 
3944 int mymax(int a, int b) { return a > b ? a : b; }
3945 int mymax(int a, int b, int c) {
3946 	auto d = mymax(a, b);
3947 	return c > d ? c : d;
3948 }
3949 
3950 // OK so we need to make getting at the native window stuff possible in simpledisplay.d
3951 // and here, it must be integrable with the layout, the event system, and not be painted over.
3952 version(win32_widgets) {
3953 
3954 	// this function just does stuff that a parent window needs for redirection
3955 	int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
3956 		this_.hookedWndProc(msg, wParam, lParam);
3957 
3958 		switch(msg) {
3959 
3960 			case WM_VSCROLL, WM_HSCROLL:
3961 				auto pos = HIWORD(wParam);
3962 				auto m = LOWORD(wParam);
3963 
3964 				auto scrollbarHwnd = cast(HWND) lParam;
3965 
3966 				if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) {
3967 
3968 					//auto smw = cast(ScrollMessageWidget) widgetp.parent;
3969 
3970 					switch(m) {
3971 						/+
3972 						// I don't think those messages are ever actually sent normally by the widget itself,
3973 						// they are more used for the keyboard interface. methinks.
3974 						case SB_BOTTOM:
3975 							// writeln("end");
3976 							auto event = new Event("scrolltoend", *widgetp);
3977 							event.dispatch();
3978 							//if(!event.defaultPrevented)
3979 						break;
3980 						case SB_TOP:
3981 							// writeln("top");
3982 							auto event = new Event("scrolltobeginning", *widgetp);
3983 							event.dispatch();
3984 						break;
3985 						case SB_ENDSCROLL:
3986 							// idk
3987 						break;
3988 						+/
3989 						case SB_LINEDOWN:
3990 							(*widgetp).emitCommand!"scrolltonextline"();
3991 						return 0;
3992 						case SB_LINEUP:
3993 							(*widgetp).emitCommand!"scrolltopreviousline"();
3994 						return 0;
3995 						case SB_PAGEDOWN:
3996 							(*widgetp).emitCommand!"scrolltonextpage"();
3997 						return 0;
3998 						case SB_PAGEUP:
3999 							(*widgetp).emitCommand!"scrolltopreviouspage"();
4000 						return 0;
4001 						case SB_THUMBPOSITION:
4002 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
4003 							ev.dispatch();
4004 						return 0;
4005 						case SB_THUMBTRACK:
4006 							// eh kinda lying but i like the real time update display
4007 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
4008 							ev.dispatch();
4009 
4010 							// the event loop doesn't seem to carry on with a requested redraw..
4011 							// so we request it to get our dirty bit set...
4012 							// then we need to immediately actually redraw it too for instant feedback to user
4013 							SimpleWindow.processAllCustomEvents();
4014 							SimpleWindow.processAllCustomEvents();
4015 							//if(this_.parentWindow)
4016 								//this_.parentWindow.actualRedraw();
4017 
4018 							// and this ensures the WM_PAINT message is sent fairly quickly
4019 							// still seems to lag a little in large windows but meh it basically works.
4020 							if(this_.parentWindow) {
4021 								// FIXME: if painting is slow, this does still lag
4022 								// we probably will want to expose some user hook to ScrollWindowEx
4023 								// or something.
4024 								UpdateWindow(this_.parentWindow.hwnd);
4025 							}
4026 						return 0;
4027 						default:
4028 					}
4029 				}
4030 			break;
4031 
4032 			case WM_CONTEXTMENU:
4033 				auto hwndFrom = cast(HWND) wParam;
4034 
4035 				auto xPos = cast(short) LOWORD(lParam);
4036 				auto yPos = cast(short) HIWORD(lParam);
4037 
4038 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
4039 					POINT p;
4040 					p.x = xPos;
4041 					p.y = yPos;
4042 					ScreenToClient(hwnd, &p);
4043 					auto clientX = cast(ushort) p.x;
4044 					auto clientY = cast(ushort) p.y;
4045 
4046 					auto wap = widgetAtPoint(*widgetp, clientX, clientY);
4047 
4048 					if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) {
4049 						return 0;
4050 					}
4051 				}
4052 			break;
4053 
4054 			case WM_DRAWITEM:
4055 				auto dis = cast(DRAWITEMSTRUCT*) lParam;
4056 				if(auto widgetp = dis.hwndItem in Widget.nativeMapping) {
4057 					return (*widgetp).handleWmDrawItem(dis);
4058 				}
4059 			break;
4060 
4061 			case WM_NOTIFY:
4062 				auto hdr = cast(NMHDR*) lParam;
4063 				auto hwndFrom = hdr.hwndFrom;
4064 				auto code = hdr.code;
4065 
4066 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
4067 					return (*widgetp).handleWmNotify(hdr, code, mustReturn);
4068 				}
4069 			break;
4070 			case WM_COMMAND:
4071 				auto handle = cast(HWND) lParam;
4072 				auto cmd = HIWORD(wParam);
4073 				return processWmCommand(hwnd, handle, cmd, LOWORD(wParam));
4074 
4075 			default:
4076 				// pass it on
4077 		}
4078 		return 0;
4079 	}
4080 
4081 
4082 
4083 	extern(Windows)
4084 	private
4085 	// this is called by native child windows, whereas the other hook is done by simpledisplay windows
4086 	// but can i merge them?!
4087 	LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
4088 		// try { writeln(iMessage); } catch(Exception e) {};
4089 
4090 		if(auto te = hWnd in Widget.nativeMapping) {
4091 			try {
4092 
4093 				te.hookedWndProc(iMessage, wParam, lParam);
4094 
4095 				int mustReturn;
4096 				auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn);
4097 				if(mustReturn)
4098 					return ret;
4099 
4100 				if(iMessage == WM_SETFOCUS) {
4101 					auto lol = *te;
4102 					while(lol !is null && lol.implicitlyCreated)
4103 						lol = lol.parent;
4104 					lol.focus();
4105 					//(*te).parentWindow.focusedWidget = lol;
4106 				}
4107 
4108 
4109 				if(iMessage == WM_CTLCOLOREDIT) {
4110 
4111 				}
4112 				if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) {
4113 					SetBkMode(cast(HDC) wParam, TRANSPARENT);
4114 					return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color...
4115 						//GetStockObject(NULL_BRUSH);
4116 				}
4117 
4118 				auto pos = getChildPositionRelativeToParentOrigin(*te);
4119 				lastDefaultPrevented = false;
4120 				// try { writeln(typeid(*te)); } catch(Exception e) {}
4121 				if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented)
4122 					return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam);
4123 				else {
4124 					// it was something we recognized, should only call the window procedure if the default was not prevented
4125 				}
4126 			} catch(Exception e) {
4127 				assert(0, e.toString());
4128 			}
4129 			return 0;
4130 		}
4131 		assert(0, "shouldn't be receiving messages for this window....");
4132 		//assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen
4133 	}
4134 
4135 	extern(Windows)
4136 	private
4137 	// see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/
4138 	LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
4139 		if(iMessage == WM_ERASEBKGND) {
4140 			auto dc = GetDC(hWnd);
4141 			auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE));
4142 			auto p = SelectObject(dc, GetStockObject(NULL_PEN));
4143 			RECT r;
4144 			GetWindowRect(hWnd, &r);
4145 			// since the pen is null, to fill the whole space, we need the +1 on both.
4146 			gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1);
4147 			SelectObject(dc, p);
4148 			SelectObject(dc, b);
4149 			ReleaseDC(hWnd, dc);
4150 			InvalidateRect(hWnd, null, false); // redraw the border
4151 			return 1;
4152 		}
4153 		return HookedWndProc(hWnd, iMessage, wParam, lParam);
4154 	}
4155 
4156 	/++
4157 		Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create
4158 		needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest
4159 		of minigui's expectations.
4160 
4161 		This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window
4162 		member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor.
4163 
4164 		It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`.
4165 
4166 		To check if you can use this, use `static if(UsingWin32Widgets)`.
4167 	+/
4168 	void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) {
4169 		assert(p.parentWindow !is null);
4170 		assert(p.parentWindow.win.impl.hwnd !is null);
4171 
4172 		auto bsgroupbox = style == BS_GROUPBOX;
4173 
4174 		HWND phwnd;
4175 
4176 		auto wtf = p.parent;
4177 		while(wtf) {
4178 			if(wtf.hwnd !is null) {
4179 				phwnd = wtf.hwnd;
4180 				break;
4181 			}
4182 			wtf = wtf.parent;
4183 		}
4184 
4185 		if(phwnd is null)
4186 			phwnd = p.parentWindow.win.impl.hwnd;
4187 
4188 		assert(phwnd !is null);
4189 
4190 		WCharzBuffer wt = WCharzBuffer(windowText);
4191 
4192 		style |= WS_VISIBLE | WS_CHILD;
4193 		//if(className != WC_TABCONTROL)
4194 			style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
4195 		p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style,
4196 				CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
4197 				phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null);
4198 
4199 		assert(p.hwnd !is null);
4200 
4201 
4202 		static HFONT font;
4203 		if(font is null) {
4204 			NONCLIENTMETRICS params;
4205 			params.cbSize = params.sizeof;
4206 			if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, &params, 0)) {
4207 				font = CreateFontIndirect(&params.lfMessageFont);
4208 			}
4209 		}
4210 
4211 		if(font)
4212 			SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true);
4213 
4214 		p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd);
4215 		p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
4216 		Widget.nativeMapping[p.hwnd] = p;
4217 
4218 		if(bsgroupbox)
4219 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK);
4220 		else
4221 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4222 
4223 		EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
4224 
4225 		p.registerMovement();
4226 	}
4227 }
4228 
4229 version(win32_widgets)
4230 private
4231 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) {
4232 	if(hwnd is null || hwnd in Widget.nativeMapping)
4233 		return true;
4234 	auto parent = cast(Widget) cast(void*) lparam;
4235 	Widget p = new Widget(null);
4236 	p._parent = parent;
4237 	p.parentWindow = parent.parentWindow;
4238 	p.hwnd = hwnd;
4239 	p.implicitlyCreated = true;
4240 	Widget.nativeMapping[p.hwnd] = p;
4241 	p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4242 	return true;
4243 }
4244 
4245 /++
4246 	Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness.
4247 +/
4248 struct WidgetPainter {
4249 	this(ScreenPainter screenPainter, Widget drawingUpon) {
4250 		this.drawingUpon = drawingUpon;
4251 		this.screenPainter = screenPainter;
4252 
4253 		this.widgetClipRectangle = screenPainter.currentClipRectangle;
4254 
4255 		// this.screenPainter.impl.enableXftDraw();
4256 		if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi))
4257 			this.screenPainter.setFont(font);
4258 	}
4259 
4260 	/++
4261 		EXPERIMENTAL. subject to change.
4262 
4263 		When you draw a cursor, you can draw this to notify your window of where it is,
4264 		for IME systems to use.
4265 	+/
4266 	void notifyCursorPosition(int x, int y, int width, int height) {
4267 		if(auto a = drawingUpon.parentWindow)
4268 		if(auto w = a.inputProxy) {
4269 			w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height);
4270 		}
4271 	}
4272 
4273 	private Rectangle widgetClipRectangle;
4274 
4275 	private Rectangle setClipRectangleForWidget(Point upperLeft, int width, int height) {
4276 		widgetClipRectangle = Rectangle(upperLeft, Size(width, height));
4277 
4278 		return screenPainter.setClipRectangle(widgetClipRectangle);
4279 	}
4280 
4281 	/++
4282 		Sets the clip rectangle to the given settings. It will automatically calculate the intersection
4283 		of your widget's content boundaries and your requested clip rectangle.
4284 
4285 		History:
4286 			Before February 26, 2025, you could sometimes exceed widget boundaries, as this forwarded
4287 			directly to the underlying `ScreenPainter`. It now wraps it to calculate the intersection.
4288 	+/
4289 	Rectangle setClipRectangle(Rectangle rectangle) {
4290 		return screenPainter.setClipRectangle(rectangle.intersectionOf(widgetClipRectangle));
4291 	}
4292 	/// ditto
4293 	Rectangle setClipRectangle(Point upperLeft, int width, int height) {
4294 		return setClipRectangle(Rectangle(upperLeft, Size(width, height)));
4295 	}
4296 	/// ditto
4297 	Rectangle setClipRectangle(Point upperLeft, Size size) {
4298 		return setClipRectangle(Rectangle(upperLeft, size));
4299 	}
4300 
4301 	///
4302 	ScreenPainter screenPainter;
4303 	/// Forward to the screen painter for all other methods, see [arsd.simpledisplay.ScreenPainter] for more information
4304 	alias screenPainter this;
4305 
4306 	private Widget drawingUpon;
4307 
4308 	/++
4309 		This is the list of rectangles that actually need to be redrawn.
4310 
4311 		Not actually implemented yet.
4312 	+/
4313 	Rectangle[] invalidatedRectangles;
4314 
4315 	private static BaseVisualTheme _visualTheme;
4316 
4317 	/++
4318 		Functions to access the visual theme and helpers to easily use it.
4319 
4320 		These are aware of the current widget's computed style out of the theme.
4321 	+/
4322 	static @property BaseVisualTheme visualTheme() {
4323 		if(_visualTheme is null)
4324 			_visualTheme = new DefaultVisualTheme();
4325 		return _visualTheme;
4326 	}
4327 
4328 	/// ditto
4329 	static @property void visualTheme(BaseVisualTheme theme) {
4330 		_visualTheme = theme;
4331 
4332 		// FIXME: notify all windows about the new theme, they should recompute layout and redraw.
4333 	}
4334 
4335 	/// ditto
4336 	Color themeForeground() {
4337 		return drawingUpon.getComputedStyle().foregroundColor();
4338 	}
4339 
4340 	/// ditto
4341 	Color themeBackground() {
4342 		return drawingUpon.getComputedStyle().background.color;
4343 	}
4344 
4345 	int isDarkTheme() {
4346 		return 0; // unspecified, yes, no as enum. FIXME
4347 	}
4348 
4349 	/++
4350 		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.
4351 
4352 		It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding.
4353 
4354 		If you change teh clip rectangle, you should change it back before you return.
4355 
4356 
4357 		The sequence it uses is:
4358 			background
4359 			content (delegated to you)
4360 			border
4361 			focused outline
4362 			selected overlay
4363 
4364 		Example code:
4365 
4366 		---
4367 		void paint(WidgetPainter painter) {
4368 			painter.drawThemed((bounds) {
4369 				return bounds; // if the selection overlay should be contained, you can return it here.
4370 			});
4371 		}
4372 		---
4373 	+/
4374 	void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) {
4375 		drawThemed((WidgetPainter painter, const Rectangle bounds) {
4376 			return drawBody(bounds);
4377 		});
4378 	}
4379 	// this overload is actually mroe for setting the delegate to a virtual function
4380 	void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) {
4381 		Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height);
4382 
4383 		auto cs = drawingUpon.getComputedStyle();
4384 
4385 		auto bg = cs.background.color;
4386 
4387 		auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor);
4388 
4389 		rect.left += borderWidth;
4390 		rect.right -= borderWidth;
4391 		rect.top += borderWidth;
4392 		rect.bottom -= borderWidth;
4393 
4394 		auto insideBorderRect = rect;
4395 
4396 		rect.left += cs.paddingLeft;
4397 		rect.right -= cs.paddingRight;
4398 		rect.top += cs.paddingTop;
4399 		rect.bottom -= cs.paddingBottom;
4400 
4401 		this.outlineColor = this.themeForeground;
4402 		this.fillColor = bg;
4403 
4404 		auto widgetFont = cs.fontCached;
4405 		if(widgetFont !is null)
4406 			this.setFont(widgetFont);
4407 
4408 		rect = drawBody(this, rect);
4409 
4410 		if(widgetFont !is null) {
4411 			if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi))
4412 				this.setFont(vtFont);
4413 			else
4414 				this.setFont(null);
4415 		}
4416 
4417 		if(auto os = cs.outlineStyle()) {
4418 			this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid);
4419 			this.fillColor = Color.transparent;
4420 			this.drawRectangle(insideBorderRect);
4421 		}
4422 	}
4423 
4424 	/++
4425 		First, draw the background.
4426 		Then draw your content.
4427 		Next, draw the border.
4428 		And the focused indicator.
4429 		And the is-selected box.
4430 
4431 		If it is focused i can draw the outline too...
4432 
4433 		If selected i can even do the xor action but that's at the end.
4434 	+/
4435 	void drawThemeBackground() {
4436 
4437 	}
4438 
4439 	void drawThemeBorder() {
4440 
4441 	}
4442 
4443 	// all this stuff is a dangerous experiment....
4444 	static class ScriptableVersion {
4445 		ScreenPainterImplementation* p;
4446 		int originX, originY;
4447 
4448 		@scriptable:
4449 		void drawRectangle(int x, int y, int width, int height) {
4450 			p.drawRectangle(x + originX, y + originY, width, height);
4451 		}
4452 		void drawLine(int x1, int y1, int x2, int y2) {
4453 			p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY);
4454 		}
4455 		void drawText(int x, int y, string text) {
4456 			p.drawText(x + originX, y + originY, 100000, 100000, text, 0);
4457 		}
4458 		void setOutlineColor(int r, int g, int b) {
4459 			p.pen = Pen(Color(r,g,b), 1);
4460 		}
4461 		void setFillColor(int r, int g, int b) {
4462 			p.fillColor = Color(r,g,b);
4463 		}
4464 	}
4465 
4466 	ScriptableVersion toArsdJsvar() {
4467 		auto sv = new ScriptableVersion;
4468 		sv.p = this.screenPainter.impl;
4469 		sv.originX = this.screenPainter.originX;
4470 		sv.originY = this.screenPainter.originY;
4471 		return sv;
4472 	}
4473 
4474 	static WidgetPainter fromJsVar(T)(T t) {
4475 		return WidgetPainter.init;
4476 	}
4477 	// done..........
4478 }
4479 
4480 
4481 struct Style {
4482 	static struct helper(string m, T) {
4483 		enum method = m;
4484 		T v;
4485 
4486 		mixin template MethodOverride(typeof(this) v) {
4487 			mixin("override typeof(v.v) "~v.method~"() { return v.v; }");
4488 		}
4489 	}
4490 
4491 	static auto opDispatch(string method, T)(T value) {
4492 		return helper!(method, T)(value);
4493 	}
4494 }
4495 
4496 /++
4497 	Implementation detail of the [ControlledBy] UDA.
4498 
4499 	History:
4500 		Added Oct 28, 2020
4501 +/
4502 struct ControlledBy_(T, Args...) {
4503 	Args args;
4504 
4505 	static if(Args.length)
4506 	this(Args args) {
4507 		this.args = args;
4508 	}
4509 
4510 	private T construct(Widget parent) {
4511 		return new T(args, parent);
4512 	}
4513 }
4514 
4515 /++
4516 	User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them.
4517 
4518 	History:
4519 		Added Oct 28, 2020
4520 +/
4521 auto ControlledBy(T, Args...)(Args args) {
4522 	return ControlledBy_!(T, Args)(args);
4523 }
4524 
4525 struct ContainerMeta {
4526 	string name;
4527 	ContainerMeta[] children;
4528 	Widget function(Widget parent) factory;
4529 
4530 	Widget instantiate(Widget parent) {
4531 		auto n = factory(parent);
4532 		n.name = name;
4533 		foreach(child; children)
4534 			child.instantiate(n);
4535 		return n;
4536 	}
4537 }
4538 
4539 /++
4540 	This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See
4541 	http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information.
4542 
4543 	Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level
4544 	structures. It works fine on structs declared inside functions though.
4545 
4546 	See: https://issues.dlang.org/show_bug.cgi?id=21984
4547 +/
4548 template Container(CArgs...) {
4549 	static if(CArgs.length && is(CArgs[0] : Widget)) {
4550 		private alias Super = CArgs[0];
4551 		private alias CArgs2 = CArgs[1 .. $];
4552 	} else {
4553 		private alias Super = Layout;
4554 		private alias CArgs2 = CArgs;
4555 	}
4556 
4557 	class Container : Super {
4558 		this(Widget parent) { super(parent); }
4559 
4560 		// just to partially support old gdc versions
4561 		version(GNU) {
4562 			static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); }
4563 			static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); }
4564 			static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); }
4565 			static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time");
4566 		} else mixin(q{
4567 			static foreach(Arg; CArgs2) {
4568 				mixin Arg.MethodOverride!(Arg);
4569 			}
4570 		});
4571 
4572 		static ContainerMeta opCall(string name, ContainerMeta[] children...) {
4573 			return ContainerMeta(
4574 				name,
4575 				children.dup,
4576 				function (Widget parent) { return new typeof(this)(parent); }
4577 			);
4578 		}
4579 
4580 		static ContainerMeta opCall(ContainerMeta[] children...) {
4581 			return opCall(null, children);
4582 		}
4583 	}
4584 }
4585 
4586 /++
4587 	The data controller widget is created by reflecting over the given
4588 	data type. You can use [ControlledBy] as a UDA on a struct or
4589 	just let it create things automatically.
4590 
4591 	Unlike [dialog], this uses real-time updating of the data and
4592 	you add it to another window yourself.
4593 
4594 	---
4595 		struct Test {
4596 			int x;
4597 			int y;
4598 		}
4599 
4600 		auto window = new Window();
4601 		auto dcw = new DataControllerWidget!Test(new Test, window);
4602 	---
4603 
4604 	The way it works is any public members are given a widget based
4605 	on their data type, and public methods trigger an action button
4606 	if no relevant parameters or a dialog action if it does have
4607 	parameters, similar to the [menu] facility.
4608 
4609 	If you change data programmatically, without going through the
4610 	DataControllerWidget methods, you will have to tell it something
4611 	has changed and it needs to redraw. This is done with the `invalidate`
4612 	method.
4613 
4614 	History:
4615 		Added Oct 28, 2020
4616 +/
4617 /// Group: generating_from_code
4618 class DataControllerWidget(T) : WidgetContainer {
4619 	static if(is(T == class) || is(T == interface) || is(T : const E[], E))
4620 		private alias Tref = T;
4621 	else
4622 		private alias Tref = T*;
4623 
4624 	Tref datum;
4625 
4626 	/++
4627 		See_also: [addDataControllerWidget]
4628 	+/
4629 	this(Tref datum, Widget parent) {
4630 		this.datum = datum;
4631 
4632 		Widget cp = this;
4633 
4634 		super(parent);
4635 
4636 		foreach(attr; __traits(getAttributes, T))
4637 			static if(is(typeof(attr) == ContainerMeta)) {
4638 				cp = attr.instantiate(this);
4639 			}
4640 
4641 		auto def = this.getByName("default");
4642 		if(def !is null)
4643 			cp = def;
4644 
4645 		Widget helper(string name) {
4646 			auto maybe = this.getByName(name);
4647 			if(maybe is null)
4648 				return cp;
4649 			return maybe;
4650 
4651 		}
4652 
4653 		foreach(member; __traits(allMembers, T))
4654 		static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011
4655 		static if(is(typeof(__traits(getMember, this.datum, member))))
4656 		static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") {
4657 			void delegate() updateWidgetFromData;
4658 			void delegate() updateDataFromWidget;
4659 
4660 			static if(is(typeof(__traits(getMember, this.datum, member)) == function)) {
4661 				auto w = widgetFor!(__traits(getMember, T, member), void)(null, helper(member), updateWidgetFromData, updateDataFromWidget);
4662 				w.addEventListener("triggered", delegate() {
4663 					makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))();
4664 					notifyDataUpdated();
4665 				});
4666 			} else {
4667 				auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), updateWidgetFromData, updateDataFromWidget);
4668 				if(updateWidgetFromData)
4669 					updaters ~= updateWidgetFromData;
4670 				if(updateDataFromWidget)
4671 				w.addEventListener(EventType.change, (Event ev) {
4672 					updateDataFromWidget();
4673 				});
4674 			}
4675 			/+
4676 			static if(is(typeof(w.isChecked) == bool)) {
4677 					__traits(getMember, this.datum, member) = w.isChecked;
4678 			} else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) {
4679 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } );
4680 			} else static if(is(typeof(w.value) == int)) {
4681 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4682 			} else static if(is(typeof(w) == DropDownSelection)) {
4683 				// special case for this to kinda support enums and such. could be better though
4684 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4685 			} else {
4686 				//static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof);
4687 			}
4688 			+/
4689 		}
4690 	}
4691 
4692 	/++
4693 		If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages.
4694 
4695 		History:
4696 			Added May 28, 2021
4697 	+/
4698 	void notifyDataUpdated() {
4699 		foreach(updater; updaters)
4700 			updater();
4701 
4702 		this.emit!(ChangeEvent!void)(delegate{});
4703 	}
4704 
4705 	private Widget[string] memberWidgets;
4706 	private void delegate()[] updaters;
4707 
4708 	mixin Emits!(ChangeEvent!void);
4709 }
4710 
4711 private int saturatedSum(int[] values...) {
4712 	int sum;
4713 	foreach(value; values) {
4714 		if(value == int.max)
4715 			return int.max;
4716 		sum += value;
4717 	}
4718 	return sum;
4719 }
4720 
4721 void genericSetValue(T, W)(T* where, W what) {
4722 	version(D_OpenD) {
4723 		static if(is(T == int[])) {
4724 			// pragma(msg, "FIXME");
4725 		} else
4726 		static if(is(W : T)) {
4727 			*where = what;
4728 		} else
4729 		{
4730 			import arsd.conv;
4731 			*where = to!T(what);
4732 		}
4733 	} else {
4734 		// slow, less feature fallback branch cuz i hate dub
4735 		import std.conv;
4736 		*where = to!T(what);
4737 	}
4738 }
4739 
4740 /++
4741 	Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent.
4742 
4743 	The `updateWidgetFromData` delegate can be called if you change `*valptr` to reflect those changes in the widget.
4744 
4745 	Note that this creates the widget but does not attach any event handlers to it. You might set a change event to call this.
4746 +/
4747 private static auto widgetFor(alias tt, P)(P* valptr, Widget parent, out void delegate() updateWidgetFromData, out void delegate() updateDataFromWidget) {
4748 
4749 	string displayName = __traits(identifier, tt).beautify;
4750 
4751 	static if(controlledByCount!tt == 1) {
4752 		foreach(i, attr; __traits(getAttributes, tt)) {
4753 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) {
4754 				auto w = attr.construct(parent);
4755 				static if(__traits(compiles, w.setPosition(*valptr))) {
4756 					updateWidgetFromData = () { w.setPosition(*valptr); };
4757 					updateDataFromWidget = () { *valptr = w.position; };
4758 				}
4759 				else static if(__traits(compiles, w.setValue(*valptr))) {
4760 					updateWidgetFromData = () { w.setValue(*valptr); };
4761 					updateDataFromWidget = () { *valptr = w.value; };
4762 				}
4763 
4764 				if(updateWidgetFromData)
4765 					updateWidgetFromData();
4766 				return w;
4767 			}
4768 		}
4769 	} else static if(controlledByCount!tt == 0) {
4770 
4771 		version(D_OpenD)
4772 			import arsd.conv;
4773 		else
4774 			import std.conv;
4775 
4776 		static if(choicesCount!tt == 1) {
4777 			auto choices = ChoicesFor!tt;
4778 
4779 			static if(is(typeof(tt) == E[], E)) {
4780 				// can select multiple...
4781 				auto list = new Fieldset(displayName, parent);
4782 
4783 				Checkbox[] boxes;
4784 
4785 				foreach(option; choices.options()) {
4786 					boxes ~= new Checkbox(option, list);
4787 				}
4788 
4789 				updateWidgetFromData = () {
4790 					foreach(box; boxes) {
4791 						box.isChecked = box.label == *valptr;
4792 					}
4793 				};
4794 
4795 				updateDataFromWidget = () {
4796 					(*valptr) = [];
4797 					foreach(idx, box; boxes) {
4798 						// FIXME: what if it is not an int[]?
4799 						if(box.isChecked)
4800 							(*valptr) ~= cast(int) idx;
4801 					}
4802 				};
4803 
4804 				return list;
4805 			} else {
4806 				auto dds = new DropDownSelection(parent);
4807 
4808 				// FIXME: label
4809 
4810 				foreach(option; choices.options()) {
4811 					// FIXME: options need not be strings
4812 					dds.addOption(option);
4813 				}
4814 
4815 				// FIXME: value need not be ints...
4816 				updateWidgetFromData = () {
4817 					dds.setSelection(*valptr);
4818 				};
4819 				updateDataFromWidget = () {
4820 					if(dds.getSelection != -1)
4821 						*valptr = cast(P) dds.getSelection;
4822 				};
4823 				updateWidgetFromData();
4824 
4825 				return dds;
4826 			}
4827 
4828 	/+ // FIXME consider these things:
4829 	bool allowCustom = false;
4830 	/// only relevant if attached to an array
4831 	bool allowReordering = true;
4832 	/// ditto
4833 	bool allowDuplicates = true;
4834 	/// makes no sense on a set
4835 	bool requireAll = false;
4836 	+/
4837 		} else static if(choicesCount!tt == 0) {
4838 
4839 			static if(is(typeof(tt) == enum)) {
4840 				// FIXME: label
4841 				auto dds = new DropDownSelection(parent);
4842 				foreach(idx, option; __traits(allMembers, typeof(tt))) {
4843 					dds.addOption(option);
4844 				}
4845 				updateWidgetFromData = () {
4846 					foreach(idx, option; __traits(allMembers, typeof(tt))) {
4847 						if(__traits(getMember, typeof(tt), option) == *valptr)
4848 							dds.setSelection(cast(int) idx);
4849 					}
4850 				};
4851 				updateDataFromWidget = () {
4852 					if(dds.getSelection != -1)
4853 						*valptr = cast(P) dds.getSelection;
4854 				};
4855 				updateWidgetFromData();
4856 				return dds;
4857 			} else static if(is(typeof(tt) == bool)) {
4858 				auto box = new Checkbox(displayName, parent);
4859 				updateWidgetFromData = () { box.isChecked = *valptr; };
4860 				updateDataFromWidget = () { *valptr = box.isChecked; };
4861 				updateWidgetFromData();
4862 				return box;
4863 			} else static if(is(typeof(tt) : const long)) {
4864 				auto le = new LabeledLineEdit(displayName, parent);
4865 				updateWidgetFromData = () { le.content = toInternal!string(*valptr); };
4866 				updateDataFromWidget = () { *valptr = to!P(le.content); };
4867 				updateWidgetFromData();
4868 				return le;
4869 			} else static if(is(typeof(tt) : const double)) {
4870 				auto le = new LabeledLineEdit(displayName, parent);
4871 				version(D_OpenD)
4872 					import arsd.conv;
4873 				else
4874 					import std.conv;
4875 				updateWidgetFromData = () { le.content = to!string(*valptr); };
4876 				updateDataFromWidget = () { *valptr = to!P(le.content); };
4877 				updateWidgetFromData();
4878 				return le;
4879 			} else static if(is(typeof(tt) : const string)) {
4880 				auto le = new LabeledLineEdit(displayName, parent);
4881 				updateWidgetFromData = () { le.content = *valptr; };
4882 				updateDataFromWidget = () { *valptr = to!P(le.content); };
4883 				updateWidgetFromData();
4884 				return le;
4885 			} else static if(is(typeof(tt) == E[], E)) {
4886 				auto w = new ArrayEditingWidget!E(parent);
4887 				// FIXME updateWidgetFromData
4888 				return w;
4889 			} else static if(is(typeof(tt) == function)) {
4890 				auto w = new Button(displayName, parent);
4891 				return w;
4892 			} else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) {
4893 				// FIXME: updaters
4894 				return parent.addDataControllerWidget(tt);
4895 			} else static assert(0, typeof(tt).stringof);
4896 		} else static assert(0, "multiple choices not supported");
4897 	} else static assert(0, "multiple controllers not yet supported");
4898 }
4899 
4900 class ArrayEditingWidget(T) : ArrayEditingWidgetBase {
4901 	this(Widget parent) {
4902 		super(parent);
4903 	}
4904 }
4905 
4906 class ArrayEditingWidgetBase : Widget {
4907 	this(Widget parent) {
4908 		super(parent);
4909 
4910 		// FIXME: a trash can to move items into to delete them?
4911 		static class MyListViewItem : GenericListViewItem {
4912 			this(Widget parent) {
4913 				super(parent);
4914 
4915 				/+
4916 					drag handle
4917 						left click lets you move the whole selection. if the current element is not selected, it changes the selection to it.
4918 						right click here gives you the movement controls too
4919 					index/key view zone
4920 						left click here selects/unselects
4921 					element view/edit zone
4922 					delete button
4923 				+/
4924 
4925 				// FIXME: make sure the index is viewable
4926 
4927 				auto hl = new HorizontalLayout(this);
4928 
4929 				button = new CommandButton("d", hl);
4930 
4931 				label = new TextLabel("unloaded", TextAlignment.Left, hl);
4932 				// if member editable, have edit view... get from the subclass.
4933 
4934 				// or a "..." menu?
4935 				button = new CommandButton("Up", hl); // shift+click is move to top
4936 				button = new CommandButton("Down", hl); // shift+click is move to bottom
4937 				button = new CommandButton("Move to", hl); // move before, after, or swap
4938 				button = new CommandButton("Delete", hl);
4939 
4940 				button.addEventListener("triggered", delegate(){
4941 					//messageBox(text("clicked ", currentIndexLoaded()));
4942 				});
4943 			}
4944 			override void showItem(int idx) {
4945 				label.label = "Item ";// ~ to!string(idx);
4946 			}
4947 
4948 			TextLabel label;
4949 			Button button;
4950 		}
4951 
4952 		auto outer_this = this;
4953 
4954 		// FIXME: make sure item count is easy to see
4955 
4956 		glvw = new class GenericListViewWidget {
4957 			this() {
4958 				super(outer_this);
4959 			}
4960 			override GenericListViewItem itemFactory(Widget parent) {
4961 				return new MyListViewItem(parent);
4962 			}
4963 			override Size itemSize() {
4964 				return Size(0, scaleWithDpi(80));
4965 			}
4966 
4967 			override Menu contextMenu(int x, int y) {
4968 				return createContextMenuFromAnnotatedCode(this);
4969 			}
4970 
4971 			@context_menu {
4972 				void Select_All() {
4973 
4974 				}
4975 
4976 				void Undo() {
4977 
4978 				}
4979 
4980 				void Redo() {
4981 
4982 				}
4983 
4984 				void Cut() {
4985 
4986 				}
4987 
4988 				void Copy() {
4989 
4990 				}
4991 
4992 				void Paste() {
4993 
4994 				}
4995 
4996 				void Delete() {
4997 
4998 				}
4999 
5000 				void Find() {
5001 
5002 				}
5003 			}
5004 		};
5005 
5006 		glvw.setItemCount(400);
5007 
5008 		auto hl = new HorizontalLayout(this);
5009 		add = new FreeEntrySelection(hl);
5010 		addButton = new Button("Add", hl);
5011 	}
5012 
5013 	GenericListViewWidget glvw;
5014 	ComboboxBase add;
5015 	Button addButton;
5016 	/+
5017 		Controls:
5018 			clear (select all / delete)
5019 			reset (confirmation blocked button, maybe only on the whole form? or hit undo so many times to get back there)
5020 			add item
5021 				palette of options to add to the array (add prolly a combo box)
5022 			rearrange - move up/down, drag and drop a selection? right click can always do, left click only drags when on a selection handle.
5023 			edit/input/view items (GLVW? or it could be a table view in a way.)
5024 			undo/redo
5025 			select whole elements (even if a struct)
5026 			cut/copy/paste elements
5027 
5028 			could have an element picker, a details pane, and an add bare?
5029 
5030 
5031 			put a handle on the elements for left click dragging. allow right click drag anywhere but pretty big wiggle until it enables.
5032 			left click and drag should never work for plain text, i more want to change selection there and there no room to put a handle on it.
5033 			the handle should let dragging w/o changing the selection, or if part of the selection, drag the whole selection i think.
5034 			make it textured and use the grabby hand mouse cursor.
5035 	+/
5036 }
5037 
5038 /++
5039 	A button that pops up a menu on click for working on a particular item or selection.
5040 
5041 	History:
5042 		Added March 23, 2025
5043 +/
5044 class MenuPopupButton : Button {
5045 	/++
5046 		You might consider using [createContextMenuFromAnnotatedCode] to populate the `menu` argument.
5047 
5048 		You also may want to set the [prepare] delegate after construction.
5049 	+/
5050 	this(Menu menu, Widget parent) {
5051 		assert(menu !is null);
5052 
5053 		this.menu = menu;
5054 		super("...", parent);
5055 	}
5056 
5057 	private Menu menu;
5058 	/++
5059 		If set, this delegate is called before popping up the window. This gives you a chance
5060 		to prepare your dynamic data structures for the element(s) selected.
5061 
5062 		For example, if your `MenuPopupButton` is attached to a [GenericListViewItem], you can call
5063 		[GenericListViewItem.currentIndexLoaded] in here and set it to a variable in the object you
5064 		called [createContextMenuFromAnnotatedCode] to apply the operation to the right object.
5065 
5066 		(The api could probably be simpler...)
5067 	+/
5068 	void delegate() prepare;
5069 
5070 	override void defaultEventHandler_triggered(scope Event e) {
5071 		if(prepare)
5072 			prepare();
5073 		showContextMenu(this.x, this.y + this.height, -2, -2, menu);
5074 	}
5075 
5076 	override int maxHeight() {
5077 		return defaultLineHeight;
5078 	}
5079 
5080 	override int maxWidth() {
5081 		return defaultLineHeight;
5082 	}
5083 }
5084 
5085 /++
5086 	A button that pops up an information box, similar to a tooltip, but explicitly triggered.
5087 
5088 	FIXME: i want to be able to easily embed these in other things too.
5089 +/
5090 class TipPopupButton : Button {
5091 	/++
5092 	+/
5093 	this(Widget delegate(Widget p) factory, Widget parent) {
5094 		this.factory = factory;
5095 		super("?", parent);
5096 	}
5097 	/// ditto
5098 	this(string tip, Widget parent) {
5099 		this((parent) {
5100 			auto td = new TextDisplayTooltip(tip, parent);
5101 			return td;
5102 		}, parent);
5103 	}
5104 
5105 	private Widget delegate(Widget p) factory;
5106 
5107 	override void defaultEventHandler_triggered(scope Event e) {
5108 		auto window = new TooltipWindow(factory, this);
5109 		window.popup(this);
5110 	}
5111 
5112 	private static class TextDisplayTooltip : TextDisplay {
5113 		this(string txt, Widget parent) {
5114 			super(txt, parent);
5115 		}
5116 
5117 		// override int minHeight() { return defaultLineHeight; }
5118 		// override int flexBasisHeight() { return defaultLineHeight; }
5119 
5120 		static class Style : TextDisplay.Style {
5121 			override WidgetBackground background() {
5122 				return WidgetBackground(Color.yellow);
5123 			}
5124 
5125 			override FrameStyle borderStyle() {
5126 				return FrameStyle.solid;
5127 			}
5128 
5129 			override Color borderColor() {
5130 				return Color.black;
5131 			}
5132 		}
5133 
5134 		mixin OverrideStyle!Style;
5135 	}
5136 }
5137 
5138 /++
5139 	History:
5140 		Added March 23, 2025
5141 +/
5142 class TooltipWindow : Window {
5143 
5144 	private Widget previouslyFocusedWidget;
5145 	private Widget* previouslyFocusedWidgetBelongsIn;
5146 
5147 	void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
5148 		if(offsetY == int.min)
5149 			offsetY = 0;
5150 
5151 		int w = child.flexBasisWidth();
5152 		int h = child.flexBasisHeight() + this.paddingTop + this.paddingBottom + /* horiz scroll bar - FIXME */ 16 + 2 /* for border */;
5153 
5154 		auto coord = parent.globalCoordinates();
5155 		dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
5156 
5157 		this.width = w;
5158 		this.height = h;
5159 
5160 		this.recomputeChildLayout();
5161 
5162 		static if(UsingSimpledisplayX11)
5163 			XSync(XDisplayConnection.get, 0);
5164 
5165 		dropDown.visibilityChanged = (bool visible) {
5166 			if(visible) {
5167 				this.redraw();
5168 				//dropDown.grabInput();
5169 				captureMouse(this);
5170 
5171 				if(previouslyFocusedWidget is null)
5172 					previouslyFocusedWidget = parent.parentWindow.focusedWidget;
5173 				parent.parentWindow.focusedWidget = this;
5174 			} else {
5175 				releaseMouseCapture();
5176 				//dropDown.releaseInputGrab();
5177 
5178 				parent.parentWindow.focusedWidget = previouslyFocusedWidget;
5179 
5180 				static if(UsingSimpledisplayX11)
5181 					flushGui();
5182 			}
5183 		};
5184 
5185 		dropDown.show();
5186 
5187 		clickListener = this.addEventListener((scope ClickEvent ev) {
5188 			if(ev.target is this) {
5189 				unpopup();
5190 			}
5191 		}, true /* again for asap action */);
5192 	}
5193 
5194 	private EventListener clickListener;
5195 
5196 	void unpopup() {
5197 		mouseLastOver = mouseLastDownOn = null;
5198 		dropDown.hide();
5199 		clickListener.disconnect();
5200 	}
5201 
5202 	override void defaultEventHandler_char(CharEvent ce) {
5203 		if(ce.character == '\033')
5204 			unpopup();
5205 	}
5206 
5207 	private SimpleWindow dropDown;
5208 	private Widget child;
5209 
5210 	///
5211 	this(Widget delegate(Widget p) factory, Widget parent) {
5212 		assert(parent);
5213 		assert(parent.parentWindow);
5214 		assert(parent.parentWindow.win);
5215 		dropDown = new SimpleWindow(
5216 			250, 40,
5217 			null, OpenGlOptions.no, Resizability.fixedSize,
5218 			WindowTypes.tooltip,
5219 			WindowFlags.dontAutoShow,
5220 			parent ? parent.parentWindow.win : null
5221 		);
5222 
5223 		super(dropDown);
5224 
5225 		child = factory(this);
5226 	}
5227 }
5228 
5229 private template controlledByCount(alias tt) {
5230 	static int helper() {
5231 		int count;
5232 		foreach(i, attr; __traits(getAttributes, tt))
5233 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...))
5234 				count++;
5235 		return count;
5236 	}
5237 
5238 	enum controlledByCount = helper;
5239 }
5240 
5241 private template choicesCount(alias tt) {
5242 	static int helper() {
5243 		int count;
5244 		foreach(i, attr; __traits(getAttributes, tt))
5245 			static if(is(typeof(attr) == Choices!T, T))
5246 				count++;
5247 		return count;
5248 	}
5249 
5250 	enum choicesCount = helper;
5251 }
5252 
5253 private template ChoicesFor(alias tt) {
5254 	static int helper() {
5255 		int count;
5256 		foreach(i, attr; __traits(getAttributes, tt))
5257 			static if(is(typeof(attr) == Choices!T, T))
5258 				return i;
5259 		return -1;
5260 	}
5261 
5262 	static immutable ChoicesFor = __traits(getAttributes, tt)[helper()]; // FIXME: change static to enum and get illegal instruction from dmd backend
5263 }
5264 
5265 /++
5266 	Intended for UFCS action like `window.addDataControllerWidget(new MyObject());`
5267 
5268 	If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method.
5269 
5270 	History:
5271 		The `redrawOnChange` parameter was added on May 28, 2021.
5272 +/
5273 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) {
5274 	auto dcw = new DataControllerWidget!T(t, parent);
5275 	initializeDataControllerWidget(dcw, redrawOnChange);
5276 	return dcw;
5277 }
5278 
5279 /// ditto
5280 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) {
5281 	auto dcw = new DataControllerWidget!T(t, parent);
5282 	initializeDataControllerWidget(dcw, redrawOnChange);
5283 	return dcw;
5284 }
5285 
5286 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) {
5287 	if(redrawOnChange !is null)
5288 		w.addEventListener("change", delegate() { redrawOnChange.redraw(); });
5289 }
5290 
5291 /++
5292 	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.
5293 
5294 	History:
5295 		Finalized on June 3, 2021 for the dub v10.0 release
5296 +/
5297 struct StyleInformation {
5298 	private Widget w;
5299 	private BaseVisualTheme visualTheme;
5300 
5301 	private this(Widget w) {
5302 		this.w = w;
5303 		this.visualTheme = WidgetPainter.visualTheme;
5304 	}
5305 
5306 	/++
5307 		Forwards to [Widget.Style]
5308 
5309 		Bugs:
5310 			It is supposed to fall back to the [VisualTheme] if
5311 			the style doesn't override the default, but that is
5312 			not generally implemented. Many of them may end up
5313 			being explicit overloads instead of the generic
5314 			opDispatch fallback, like [font] is now.
5315 	+/
5316 	public @property opDispatch(string name)() {
5317 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
5318 		w.useStyleProperties((scope Widget.Style props) {
5319 		//visualTheme.useStyleProperties(w, (props) {
5320 			prop = __traits(getMember, props, name);
5321 		});
5322 		return prop;
5323 	}
5324 
5325 	/++
5326 		Returns the cached font object associated with the widget,
5327 		if overridden by the [Widget.Style|Style], or the [VisualTheme] if not.
5328 
5329 		History:
5330 			Prior to March 21, 2022 (dub v10.7), `font` went through
5331 			[opDispatch], which did not use the cache. You can now call it
5332 			repeatedly without guilt.
5333 	+/
5334 	public @property OperatingSystemFont font() {
5335 		OperatingSystemFont prop;
5336 		w.useStyleProperties((scope Widget.Style props) {
5337 			prop = props.fontCached;
5338 		});
5339 		if(prop is null) {
5340 			prop = visualTheme.defaultFontCached(w.currentDpi);
5341 		}
5342 		return prop;
5343 	}
5344 
5345 	@property {
5346 		// Layout helpers. Currently just forwarding since I haven't made up my mind on a better way.
5347 		/** */ int paddingLeft() { return w.paddingLeft(); }
5348 		/** */ int paddingRight() { return w.paddingRight(); }
5349 		/** */ int paddingTop() { return w.paddingTop(); }
5350 		/** */ int paddingBottom() { return w.paddingBottom(); }
5351 
5352 		/** */ int marginLeft() { return w.marginLeft(); }
5353 		/** */ int marginRight() { return w.marginRight(); }
5354 		/** */ int marginTop() { return w.marginTop(); }
5355 		/** */ int marginBottom() { return w.marginBottom(); }
5356 
5357 		/** */ int maxHeight() { return w.maxHeight(); }
5358 		/** */ int minHeight() { return w.minHeight(); }
5359 
5360 		/** */ int maxWidth() { return w.maxWidth(); }
5361 		/** */ int minWidth() { return w.minWidth(); }
5362 
5363 		/** */ int flexBasisWidth() { return w.flexBasisWidth(); }
5364 		/** */ int flexBasisHeight() { return w.flexBasisHeight(); }
5365 
5366 		/** */ int heightStretchiness() { return w.heightStretchiness(); }
5367 		/** */ int widthStretchiness() { return w.widthStretchiness(); }
5368 
5369 		/** */ int heightShrinkiness() { return w.heightShrinkiness(); }
5370 		/** */ int widthShrinkiness() { return w.widthShrinkiness(); }
5371 
5372 		// Global helpers some of these are unstable.
5373 		static:
5374 		/** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
5375 		/** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
5376 		/** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
5377 		/** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
5378 		/** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); }
5379 		/** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
5380 
5381 		/** */ Color activeTabColor() { return lightAccentColor; }
5382 		/** */ Color buttonColor() { return windowBackgroundColor; }
5383 		/** */ Color depressedButtonColor() { return darkAccentColor; }
5384 		/** the background color of the widget when mouse hovering over it, if it responds to mouse hovers */ Color hoveringColor() { return lightAccentColor; }
5385 		deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() {
5386 			auto c = WidgetPainter.visualTheme.selectionColor();
5387 			return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
5388 		}
5389 		/** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
5390 		/** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
5391 	}
5392 
5393 
5394 
5395 	/+
5396 
5397 	private static auto extractStyleProperty(string name)(Widget w) {
5398 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
5399 		w.useStyleProperties((props) {
5400 			prop = __traits(getMember, props, name);
5401 		});
5402 		return prop;
5403 	}
5404 
5405 	// FIXME: clear this upon a X server disconnect
5406 	private static OperatingSystemFont[string] fontCache;
5407 
5408 	T getProperty(T)(string name, lazy T default_) {
5409 		if(visualTheme !is null) {
5410 			auto str = visualTheme.getPropertyString(w, name);
5411 			if(str is null)
5412 				return default_;
5413 			static if(is(T == Color))
5414 				return Color.fromString(str);
5415 			else static if(is(T == Measurement))
5416 				return Measurement(cast(int) toInternal!int(str));
5417 			else static if(is(T == WidgetBackground))
5418 				return WidgetBackground.fromString(str);
5419 			else static if(is(T == OperatingSystemFont)) {
5420 				if(auto f = str in fontCache)
5421 					return *f;
5422 				else
5423 					return fontCache[str] = new OperatingSystemFont(str);
5424 			} else static if(is(T == FrameStyle)) {
5425 				switch(str) {
5426 					default:
5427 						return FrameStyle.none;
5428 					foreach(style; __traits(allMembers, FrameStyle))
5429 					case style:
5430 						return __traits(getMember, FrameStyle, style);
5431 				}
5432 			} else static assert(0);
5433 		} else
5434 			return default_;
5435 	}
5436 
5437 	static struct Measurement {
5438 		int value;
5439 		alias value this;
5440 	}
5441 
5442 	@property:
5443 
5444 	int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); }
5445 	int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); }
5446 	int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); }
5447 	int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); }
5448 
5449 	int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); }
5450 	int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); }
5451 	int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); }
5452 	int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); }
5453 
5454 	int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); }
5455 	int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); }
5456 
5457 	int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); }
5458 	int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); }
5459 
5460 
5461 	WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); }
5462 	Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); }
5463 
5464 	OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); }
5465 
5466 	FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); }
5467 	Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); }
5468 
5469 	FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); }
5470 	Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); }
5471 
5472 
5473 	Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
5474 	Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
5475 	Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
5476 	Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
5477 
5478 	Color activeTabColor() { return lightAccentColor; }
5479 	Color buttonColor() { return windowBackgroundColor; }
5480 	Color depressedButtonColor() { return darkAccentColor; }
5481 	Color hoveringColor() { return Color(228, 228, 228); }
5482 	Color activeListXorColor() {
5483 		auto c = WidgetPainter.visualTheme.selectionColor();
5484 		return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
5485 	}
5486 	Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); }
5487 	Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); }
5488 	+/
5489 }
5490 
5491 
5492 
5493 // pragma(msg, __traits(classInstanceSize, Widget));
5494 
5495 /*private*/ template EventString(E) {
5496 	static if(is(typeof(E.EventString)))
5497 		enum EventString = E.EventString;
5498 	else
5499 		enum EventString = E.mangleof; // FIXME fqn? or something more user friendly
5500 }
5501 
5502 /*private*/ template EventStringIdentifier(E) {
5503 	string helper() {
5504 		auto es = EventString!E;
5505 		char[] id = new char[](es.length * 2);
5506 		size_t idx;
5507 		foreach(char ch; es) {
5508 			id[idx++] = cast(char)('a' + (ch >> 4));
5509 			id[idx++] = cast(char)('a' + (ch & 0x0f));
5510 		}
5511 		return cast(string) id;
5512 	}
5513 
5514 	enum EventStringIdentifier = helper();
5515 }
5516 
5517 
5518 template classStaticallyEmits(This, EventType) {
5519 	static if(is(This Base == super))
5520 		static if(is(Base : Widget))
5521 			enum baseEmits = classStaticallyEmits!(Base, EventType);
5522 		else
5523 			enum baseEmits = false;
5524 	else
5525 		enum baseEmits = false;
5526 
5527 	enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]);
5528 
5529 	enum classStaticallyEmits = thisEmits || baseEmits;
5530 }
5531 
5532 /++
5533 	A helper to make widgets out of other native windows.
5534 
5535 	History:
5536 		Factored out of OpenGlWidget on November 5, 2021
5537 +/
5538 class NestedChildWindowWidget : Widget {
5539 	SimpleWindow win;
5540 
5541 	/++
5542 		Used on X to send focus to the appropriate child window when requested by the window manager.
5543 
5544 		Normally returns its own nested window. Can also return another child or null to revert to the parent
5545 		if you override it in a child class.
5546 
5547 		History:
5548 			Added April 2, 2022 (dub v10.8)
5549 	+/
5550 	SimpleWindow focusableWindow() {
5551 		return win;
5552 	}
5553 
5554 	///
5555 	// win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
5556 	this(SimpleWindow win, Widget parent) {
5557 		this.parentWindow = parent.parentWindow;
5558 		this.win = win;
5559 
5560 		super(parent);
5561 		windowsetup(win);
5562 	}
5563 
5564 	static protected SimpleWindow getParentWindow(Widget parent) {
5565 		assert(parent !is null);
5566 		SimpleWindow pwin = parent.parentWindow.win;
5567 
5568 		version(win32_widgets) {
5569 			HWND phwnd;
5570 			auto wtf = parent;
5571 			while(wtf) {
5572 				if(wtf.hwnd) {
5573 					phwnd = wtf.hwnd;
5574 					break;
5575 				}
5576 				wtf = wtf.parent;
5577 			}
5578 			// kinda a hack here just because the ctor below just needs a SimpleWindow wrapper....
5579 			if(phwnd)
5580 				pwin = new SimpleWindow(phwnd);
5581 		}
5582 
5583 		return pwin;
5584 	}
5585 
5586 	/++
5587 		Called upon the nested window being destroyed.
5588 		Remember the window has already been destroyed at
5589 		this point, so don't use the native handle for anything.
5590 
5591 		History:
5592 			Added April 3, 2022 (dub v10.8)
5593 	+/
5594 	protected void dispose() {
5595 
5596 	}
5597 
5598 	protected void windowsetup(SimpleWindow w) {
5599 		/*
5600 		win.onFocusChange = (bool getting) {
5601 			if(getting)
5602 				this.focus();
5603 		};
5604 		*/
5605 
5606 		/+
5607 		win.onFocusChange = (bool getting) {
5608 			if(getting) {
5609 				this.parentWindow.focusedWidget = this;
5610 				this.emit!FocusEvent();
5611 				this.emit!FocusInEvent();
5612 			} else {
5613 				this.emit!BlurEvent();
5614 				this.emit!FocusOutEvent();
5615 			}
5616 		};
5617 		+/
5618 
5619 		win.onDestroyed = () {
5620 			this.dispose();
5621 		};
5622 
5623 		version(win32_widgets) {
5624 			Widget.nativeMapping[win.hwnd] = this;
5625 			this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
5626 		} else {
5627 			win.setEventHandlers(
5628 				(MouseEvent e) {
5629 					Widget p = this;
5630 					while(p ! is parentWindow) {
5631 						e.x += p.x;
5632 						e.y += p.y;
5633 						p = p.parent;
5634 					}
5635 					parentWindow.dispatchMouseEvent(e);
5636 				},
5637 				(KeyEvent e) {
5638 					//writefln("%s %x   %s", cast(void*) win, cast(uint) e.key, e.key);
5639 					parentWindow.dispatchKeyEvent(e);
5640 				},
5641 				(dchar e) {
5642 					parentWindow.dispatchCharEvent(e);
5643 				},
5644 			);
5645 		}
5646 
5647 	}
5648 
5649 	override bool showOrHideIfNativeWindow(bool shouldShow) {
5650 		auto cur = hidden;
5651 		win.hidden = !shouldShow;
5652 		if(cur != shouldShow && shouldShow)
5653 			redraw();
5654 		return true;
5655 	}
5656 
5657 	/// OpenGL widgets cannot have child widgets. Do not call this.
5658 	/* @disable */ final override void addChild(Widget, int) {
5659 		throw new Error("cannot add children to OpenGL widgets");
5660 	}
5661 
5662 	/// When an opengl widget is laid out, it will adjust the glViewport for you automatically.
5663 	/// Keep in mind that events like mouse coordinates are still relative to your size.
5664 	override void registerMovement() {
5665 		// writefln("%d %d %d %d", x,y,width,height);
5666 		version(win32_widgets)
5667 			auto pos = getChildPositionRelativeToParentHwnd(this);
5668 		else
5669 			auto pos = getChildPositionRelativeToParentOrigin(this);
5670 		win.moveResize(pos[0], pos[1], width, height);
5671 
5672 		registerMovementAdditionalWork();
5673 		sendResizeEvent();
5674 	}
5675 
5676 	abstract void registerMovementAdditionalWork();
5677 }
5678 
5679 /++
5680 	Nests an opengl capable window inside this window as a widget.
5681 
5682 	You may also just want to create an additional [SimpleWindow] with
5683 	[OpenGlOptions.yes] yourself.
5684 
5685 	An OpenGL widget cannot have child widgets. It will throw if you try.
5686 +/
5687 static if(OpenGlEnabled)
5688 class OpenGlWidget : NestedChildWindowWidget {
5689 
5690 	override void registerMovementAdditionalWork() {
5691 		win.setAsCurrentOpenGlContext();
5692 	}
5693 
5694 	///
5695 	this(Widget parent) {
5696 		auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
5697 		super(win, parent);
5698 	}
5699 
5700 	override void paint(WidgetPainter painter) {
5701 		win.setAsCurrentOpenGlContext();
5702 		glViewport(0, 0, this.width, this.height);
5703 		win.redrawOpenGlSceneNow();
5704 	}
5705 
5706 	void redrawOpenGlScene(void delegate() dg) {
5707 		win.redrawOpenGlScene = dg;
5708 	}
5709 }
5710 
5711 /++
5712 	This demo shows how to draw text in an opengl scene.
5713 +/
5714 unittest {
5715 	import arsd.minigui;
5716 	import arsd.ttf;
5717 
5718 	void main() {
5719 		auto window = new Window();
5720 
5721 		auto widget = new OpenGlWidget(window);
5722 
5723 		// old means non-shader code so compatible with glBegin etc.
5724 		// tbh I haven't implemented new one in font yet...
5725 		// anyway, declaring here, will construct soon.
5726 		OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont;
5727 
5728 		// this is a little bit awkward, calling some methods through
5729 		// the underlying SimpleWindow `win` method, and you can't do this
5730 		// on a nanovega widget due to conflicts so I should probably fix
5731 		// the api to be a bit easier. But here it will work.
5732 		//
5733 		// Alternatively, you could load the font on the first draw, inside
5734 		// the redrawOpenGlScene, and keep a flag so you don't do it every
5735 		// time. That'd be a bit easier since the lib sets up the context
5736 		// by then guaranteed.
5737 		//
5738 		// But still, I wanna show this.
5739 		widget.win.visibleForTheFirstTime = delegate {
5740 			// must set the opengl context
5741 			widget.win.setAsCurrentOpenGlContext();
5742 
5743 			// if you were doing a OpenGL 3+ shader, this
5744 			// gets especially important to do in order. With
5745 			// old-style opengl, I think you can even do it
5746 			// in main(), but meh, let's show it more correctly.
5747 
5748 			// Anyway, now it is time to load the font from the
5749 			// OS (you can alternatively load one from a .ttf file
5750 			// you bundle with the application), then load the
5751 			// font into texture for drawing.
5752 
5753 			auto osfont = new OperatingSystemFont("DejaVu Sans", 18);
5754 
5755 			assert(!osfont.isNull()); // make sure it actually loaded
5756 
5757 			// using typeof to avoid repeating the long name lol
5758 			glfont = new typeof(glfont)(
5759 				// get the raw data from the font for loading in here
5760 				// since it doesn't use the OS function to draw the
5761 				// text, we gotta treat it more as a file than as
5762 				// a drawing api.
5763 				osfont.getTtfBytes(),
5764 				18, // need to respecify size since opengl world is different coordinate system
5765 
5766 				// these last two numbers are why it is called
5767 				// "Limited" font. It only loads the characters
5768 				// in the given range, since the texture atlas
5769 				// it references is all a big image generated ahead
5770 				// of time. You could maybe do the whole thing but
5771 				// idk how much memory that is.
5772 				//
5773 				// But here, 0-128 represents the ASCII range, so
5774 				// good enough for most English things, numeric labels,
5775 				// etc.
5776 				0,
5777 				128
5778 			);
5779 		};
5780 
5781 		widget.redrawOpenGlScene = () {
5782 			// now we can use the glfont's drawString function
5783 
5784 			// first some opengl setup. You can do this in one place
5785 			// on window first visible too in many cases, just showing
5786 			// here cuz it is easier for me.
5787 
5788 			// gonna need some alpha blending or it just looks awful
5789 			glEnable(GL_BLEND);
5790 			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
5791 			glClearColor(0,0,0,0);
5792 			glDepthFunc(GL_LEQUAL);
5793 
5794 			// Also need to enable 2d textures, since it draws the
5795 			// font characters as images baked in
5796 			glMatrixMode(GL_MODELVIEW);
5797 			glLoadIdentity();
5798 			glDisable(GL_DEPTH_TEST);
5799 			glEnable(GL_TEXTURE_2D);
5800 
5801 			// the orthographic matrix is best for 2d things like text
5802 			// so let's set that up. This matrix makes the coordinates
5803 			// in the opengl scene be one-to-one with the actual pixels
5804 			// on screen. (Not necessarily best, you may wish to scale
5805 			// things, but it does help keep fonts looking normal.)
5806 			glMatrixMode(GL_PROJECTION);
5807 			glLoadIdentity();
5808 			glOrtho(0, widget.width, widget.height, 0, 0, 1);
5809 
5810 			// you can do other glScale, glRotate, glTranslate, etc
5811 			// to the matrix here of course if you want.
5812 
5813 			// note the x,y coordinates here are for the text baseline
5814 			// NOT the upper-left corner. The baseline is like the line
5815 			// in the notebook you write on. Most the letters are actually
5816 			// above it, but some, like p and q, dip a bit below it.
5817 			//
5818 			// So if you're used to the upper left coordinate like the
5819 			// rest of simpledisplay/minigui usually do, do the
5820 			// y + glfont.ascent to bring it down a little. So this
5821 			// example puts the string in the upper left of the window.
5822 			glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green);
5823 
5824 			// re color btw: the function sets a solid color internally,
5825 			// but you actually COULD do your own thing for rainbow effects
5826 			// and the sort if you wanted too, by pulling its guts out.
5827 			// Just view its source for an idea of how it actually draws:
5828 			// http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332
5829 
5830 			// it gets a bit complicated with the character positioning,
5831 			// but the opengl parts are fairly simple: bind a texture,
5832 			// set the color, draw a quad for each letter.
5833 
5834 
5835 			// the last optional argument there btw is a bounding box
5836 			// it will/ use to word wrap and return an object you can
5837 			// use to implement scrolling or pagination; it tells how
5838 			// much of the string didn't fit in the box. But for simple
5839 			// labels we can just ignore that.
5840 
5841 
5842 			// I'd suggest drawing text as the last step, after you
5843 			// do your other drawing. You might use the push/pop matrix
5844 			// stuff to keep your place. You, in theory, should be able
5845 			// to do text in a 3d space but I've never actually tried
5846 			// that....
5847 		};
5848 
5849 		window.loop();
5850 	}
5851 }
5852 
5853 version(custom_widgets)
5854 private class TextListViewWidget : GenericListViewWidget {
5855 	static class TextListViewItem : GenericListViewItem {
5856 		ListWidget controller;
5857 		this(ListWidget controller, Widget parent) {
5858 			this.controller = controller;
5859 			this.tabStop = false;
5860 			super(parent);
5861 		}
5862 
5863 		ListWidget.Option* showing;
5864 
5865 		override void showItem(int idx) {
5866 			showing = idx < controller.options.length ? &controller.options[idx] : null;
5867 			redraw(); // is this necessary? the generic thing might call it...
5868 		}
5869 
5870 		override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
5871 			if(showing is null)
5872 				return bounds;
5873 			painter.drawText(bounds.upperLeft, showing.label);
5874 			return bounds;
5875 		}
5876 
5877 		static class Style : Widget.Style {
5878 			override WidgetBackground background() {
5879 				// FIXME: change it if it is focused or not
5880 				// 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
5881 				auto tlvi = cast(TextListViewItem) widget;
5882 				if(tlvi && tlvi.showing && tlvi && tlvi.showing.selected)
5883 					return WidgetBackground(true /*widget.parent.isFocused*/ ? WidgetPainter.visualTheme.selectionBackgroundColor : Color(128, 128, 128)); // FIXME: don't hardcode
5884 				return super.background();
5885 			}
5886 
5887 			override Color foregroundColor() {
5888 				auto tlvi = cast(TextListViewItem) widget;
5889 				return tlvi && tlvi.showing && tlvi && tlvi.showing.selected ? WidgetPainter.visualTheme.selectionForegroundColor : super.foregroundColor();
5890 			}
5891 
5892 			override FrameStyle outlineStyle() {
5893 				// FIXME: change it if it is focused or not
5894 				auto tlvi = cast(TextListViewItem) widget;
5895 				return (tlvi && tlvi.currentIndexLoaded() == tlvi.controller.focusOn) ? FrameStyle.dotted : super.outlineStyle();
5896 			}
5897 		}
5898 		mixin OverrideStyle!Style;
5899 
5900 		mixin Padding!q{2};
5901 
5902 		override void defaultEventHandler_click(ClickEvent event) {
5903 			if(event.button == MouseButton.left) {
5904 				controller.setSelection(currentIndexLoaded());
5905 				controller.focusOn = currentIndexLoaded();
5906 			}
5907 		}
5908 
5909 	}
5910 
5911 	ListWidget controller;
5912 
5913 	this(ListWidget parent) {
5914 		this.controller = parent;
5915 		this.tabStop = false; // this is only used as a child of the ListWidget
5916 		super(parent);
5917 
5918 		smw.movementPerButtonClick(1, itemSize().height);
5919 	}
5920 
5921 	override Size itemSize() {
5922 		return Size(0, defaultLineHeight + scaleWithDpi(4 /* the top and bottom padding */));
5923 	}
5924 
5925 	override GenericListViewItem itemFactory(Widget parent) {
5926 		return new TextListViewItem(controller, parent);
5927 	}
5928 
5929 	static class Style : Widget.Style {
5930 		override FrameStyle borderStyle() {
5931 			return FrameStyle.sunk;
5932 		}
5933 
5934 		override WidgetBackground background() {
5935 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
5936 		}
5937 	}
5938 	mixin OverrideStyle!Style;
5939 }
5940 
5941 /++
5942 	A list widget contains a list of strings that the user can examine and select.
5943 
5944 
5945 	In the future, items in the list may be possible to be more than just strings.
5946 
5947 	See_Also:
5948 		[TableView]
5949 +/
5950 class ListWidget : Widget {
5951 	/// 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.
5952 	mixin Emits!(ChangeEvent!void);
5953 
5954 	version(custom_widgets)
5955 		TextListViewWidget glvw;
5956 
5957 	static struct Option {
5958 		string label;
5959 		bool selected;
5960 		void* tag;
5961 	}
5962 	private Option[] options;
5963 
5964 	/++
5965 		Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete.
5966 	+/
5967 	void setSelection(int y) {
5968 		if(!multiSelect)
5969 			foreach(ref opt; options)
5970 				opt.selected = false;
5971 		if(y >= 0 && y < options.length)
5972 			options[y].selected = !options[y].selected;
5973 
5974 		version(custom_widgets)
5975 			focusOn = y;
5976 
5977 		this.emit!(ChangeEvent!void)(delegate {});
5978 
5979 		version(custom_widgets)
5980 			redraw();
5981 	}
5982 
5983 	/++
5984 		Gets the index of the selected item. In case of multi select, the index of the first selected item is returned.
5985 		Returns -1 if nothing is selected.
5986 	+/
5987 	int getSelection()
5988 	{
5989 		foreach(i, opt; options) {
5990 			if (opt.selected)
5991 				return cast(int) i;
5992 		}
5993 		return -1;
5994 	}
5995 
5996 	version(custom_widgets)
5997 	private int focusOn;
5998 
5999 	this(Widget parent) {
6000 		super(parent);
6001 
6002 		version(custom_widgets)
6003 			glvw = new TextListViewWidget(this);
6004 
6005 		version(win32_widgets)
6006 			createWin32Window(this, WC_LISTBOX, "",
6007 				0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0);
6008 	}
6009 
6010 	version(win32_widgets)
6011 	override void handleWmCommand(ushort code, ushort id) {
6012 		switch(code) {
6013 			case LBN_SELCHANGE:
6014 				auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0);
6015 				setSelection(cast(int) sel);
6016 			break;
6017 			default:
6018 		}
6019 	}
6020 
6021 
6022 	void addOption(string text, void* tag = null) {
6023 		options ~= Option(text, false, tag);
6024 		version(win32_widgets) {
6025 			WCharzBuffer buffer = WCharzBuffer(text);
6026 			SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr);
6027 		}
6028 		version(custom_widgets) {
6029 			glvw.setItemCount(cast(int) options.length);
6030 			//setContentSize(width, cast(int) (options.length * defaultLineHeight));
6031 			redraw();
6032 		}
6033 	}
6034 
6035 	void clear() {
6036 		options = null;
6037 		version(win32_widgets) {
6038 			while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0)
6039 				{}
6040 
6041 		} else version(custom_widgets) {
6042 			focusOn = -1;
6043 			glvw.setItemCount(0);
6044 			redraw();
6045 		}
6046 	}
6047 
6048 	version(custom_widgets)
6049 	override void defaultEventHandler_keydown(KeyDownEvent kde) {
6050 		void changedFocusOn() {
6051 			scrollFocusIntoView();
6052 			if(multiSelect)
6053 				redraw();
6054 			else
6055 				setSelection(focusOn);
6056 		}
6057 		switch(kde.key) {
6058 			case Key.Up:
6059 				if(focusOn) {
6060 					focusOn--;
6061 					changedFocusOn();
6062 				}
6063 			break;
6064 			case Key.Down:
6065 				if(focusOn + 1 < options.length) {
6066 					focusOn++;
6067 					changedFocusOn();
6068 				}
6069 			break;
6070 			case Key.Home:
6071 				if(focusOn) {
6072 					focusOn = 0;
6073 					changedFocusOn();
6074 				}
6075 			break;
6076 			case Key.End:
6077 				if(options.length && focusOn + 1 != options.length) {
6078 					focusOn = cast(int) options.length - 1;
6079 					changedFocusOn();
6080 				}
6081 			break;
6082 			case Key.PageUp:
6083 				auto n = glvw.numberOfCurrentlyFullyVisibleItems;
6084 				focusOn -= n;
6085 				if(focusOn < 0)
6086 					focusOn = 0;
6087 				changedFocusOn();
6088 			break;
6089 			case Key.PageDown:
6090 				if(options.length == 0)
6091 					break;
6092 				auto n = glvw.numberOfCurrentlyFullyVisibleItems;
6093 				focusOn += n;
6094 				if(focusOn >= options.length)
6095 					focusOn = cast(int) options.length - 1;
6096 				changedFocusOn();
6097 			break;
6098 
6099 			default:
6100 		}
6101 	}
6102 
6103 	version(custom_widgets)
6104 	override void defaultEventHandler_char(CharEvent ce) {
6105 		if(ce.character == '\n' || ce.character == ' ') {
6106 			setSelection(focusOn);
6107 		} else {
6108 			// search for the item that best matches and jump to it
6109 			// FIXME this sucks in tons of ways. the normal thing toolkits
6110 			// do here is to search for a substring on a timer, but i'd kinda
6111 			// rather make an actual little dialog with some options. still meh for now.
6112 			dchar search = ce.character;
6113 			if(search >= 'A' && search <= 'Z')
6114 				search += 32;
6115 			foreach(idx, option; options) {
6116 				auto ch = option.label.length ? option.label[0] : 0;
6117 				if(ch >= 'A' && ch <= 'Z')
6118 					ch += 32;
6119 				if(ch == search) {
6120 					setSelection(cast(int) idx);
6121 					scrollSelectionIntoView();
6122 					break;
6123 				}
6124 			}
6125 
6126 		}
6127 	}
6128 
6129 	version(win32_widgets)
6130 		enum multiSelect = false; /// not implemented yet
6131 	else
6132 		bool multiSelect;
6133 
6134 	override int heightStretchiness() { return 6; }
6135 
6136 	version(custom_widgets)
6137 	void scrollFocusIntoView() {
6138 		glvw.ensureItemVisibleInScroll(focusOn);
6139 	}
6140 
6141 	void scrollSelectionIntoView() {
6142 		// FIXME: implement on Windows
6143 
6144 		version(custom_widgets)
6145 			glvw.ensureItemVisibleInScroll(getSelection());
6146 	}
6147 
6148 	/*
6149 	version(custom_widgets)
6150 	override void defaultEventHandler_focusout(Event foe) {
6151 		glvw.redraw();
6152 	}
6153 
6154 	version(custom_widgets)
6155 	override void defaultEventHandler_focusin(Event foe) {
6156 		glvw.redraw();
6157 	}
6158 	*/
6159 
6160 }
6161 
6162 
6163 
6164 /// For [ScrollableWidget], determines when to show the scroll bar to the user.
6165 /// NEVER USED
6166 enum ScrollBarShowPolicy {
6167 	automatic, /// automatically show the scroll bar if it is necessary
6168 	never, /// never show the scroll bar (scrolling must be done programmatically)
6169 	always /// always show the scroll bar, even if it is disabled
6170 }
6171 
6172 /++
6173 	A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner.
6174 
6175 	It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code.
6176 +/
6177 // FIXME ScrollBarShowPolicy
6178 // FIXME: use the ScrollMessageWidget in here now that it exists
6179 deprecated("Use ScrollMessageWidget or ScrollableContainerWidget instead") // ugh compiler won't let me do it
6180 class ScrollableWidget : Widget {
6181 	// FIXME: make line size configurable
6182 	// FIXME: add keyboard controls
6183 	version(win32_widgets) {
6184 		override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) {
6185 			if(msg == WM_VSCROLL || msg == WM_HSCROLL) {
6186 				auto pos = HIWORD(wParam);
6187 				auto m = LOWORD(wParam);
6188 
6189 				// FIXME: I can reintroduce the
6190 				// scroll bars now by using this
6191 				// in the top-level window handler
6192 				// to forward comamnds
6193 				auto scrollbarHwnd = lParam;
6194 				switch(m) {
6195 					case SB_BOTTOM:
6196 						if(msg == WM_HSCROLL)
6197 							horizontalScrollTo(contentWidth_);
6198 						else
6199 							verticalScrollTo(contentHeight_);
6200 					break;
6201 					case SB_TOP:
6202 						if(msg == WM_HSCROLL)
6203 							horizontalScrollTo(0);
6204 						else
6205 							verticalScrollTo(0);
6206 					break;
6207 					case SB_ENDSCROLL:
6208 						// idk
6209 					break;
6210 					case SB_LINEDOWN:
6211 						if(msg == WM_HSCROLL)
6212 							horizontalScroll(scaleWithDpi(16));
6213 						else
6214 							verticalScroll(scaleWithDpi(16));
6215 					break;
6216 					case SB_LINEUP:
6217 						if(msg == WM_HSCROLL)
6218 							horizontalScroll(scaleWithDpi(-16));
6219 						else
6220 							verticalScroll(scaleWithDpi(-16));
6221 					break;
6222 					case SB_PAGEDOWN:
6223 						if(msg == WM_HSCROLL)
6224 							horizontalScroll(scaleWithDpi(100));
6225 						else
6226 							verticalScroll(scaleWithDpi(100));
6227 					break;
6228 					case SB_PAGEUP:
6229 						if(msg == WM_HSCROLL)
6230 							horizontalScroll(scaleWithDpi(-100));
6231 						else
6232 							verticalScroll(scaleWithDpi(-100));
6233 					break;
6234 					case SB_THUMBPOSITION:
6235 					case SB_THUMBTRACK:
6236 						if(msg == WM_HSCROLL)
6237 							horizontalScrollTo(pos);
6238 						else
6239 							verticalScrollTo(pos);
6240 
6241 						if(m == SB_THUMBTRACK) {
6242 							// the event loop doesn't seem to carry on with a requested redraw..
6243 							// so we request it to get our dirty bit set...
6244 							redraw();
6245 
6246 							// then we need to immediately actually redraw it too for instant feedback to user
6247 
6248 							SimpleWindow.processAllCustomEvents();
6249 							//if(parentWindow)
6250 								//parentWindow.actualRedraw();
6251 						}
6252 					break;
6253 					default:
6254 				}
6255 			}
6256 			return super.hookedWndProc(msg, wParam, lParam);
6257 		}
6258 	}
6259 	///
6260 	this(Widget parent) {
6261 		this.parentWindow = parent.parentWindow;
6262 
6263 		version(win32_widgets) {
6264 			createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "",
6265 				0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0);
6266 			super(parent);
6267 		} else version(custom_widgets) {
6268 			outerContainer = new InternalScrollableContainerWidget(this, parent);
6269 			super(outerContainer);
6270 		} else static assert(0);
6271 	}
6272 
6273 	version(custom_widgets)
6274 		InternalScrollableContainerWidget outerContainer;
6275 
6276 	override void defaultEventHandler_click(ClickEvent event) {
6277 		if(event.button == MouseButton.wheelUp)
6278 			verticalScroll(scaleWithDpi(-16));
6279 		if(event.button == MouseButton.wheelDown)
6280 			verticalScroll(scaleWithDpi(16));
6281 		if(event.button == MouseButton.wheelLeft)
6282 			horizontalScroll(scaleWithDpi(-16));
6283 		if(event.button == MouseButton.wheelRight)
6284 			horizontalScroll(scaleWithDpi(16));
6285 		super.defaultEventHandler_click(event);
6286 	}
6287 
6288 	override void defaultEventHandler_keydown(KeyDownEvent event) {
6289 		switch(event.key) {
6290 			case Key.Left:
6291 				horizontalScroll(scaleWithDpi(-16));
6292 			break;
6293 			case Key.Right:
6294 				horizontalScroll(scaleWithDpi(16));
6295 			break;
6296 			case Key.Up:
6297 				verticalScroll(scaleWithDpi(-16));
6298 			break;
6299 			case Key.Down:
6300 				verticalScroll(scaleWithDpi(16));
6301 			break;
6302 			case Key.Home:
6303 				verticalScrollTo(0);
6304 			break;
6305 			case Key.End:
6306 				verticalScrollTo(contentHeight);
6307 			break;
6308 			case Key.PageUp:
6309 				verticalScroll(scaleWithDpi(-160));
6310 			break;
6311 			case Key.PageDown:
6312 				verticalScroll(scaleWithDpi(160));
6313 			break;
6314 			default:
6315 		}
6316 		super.defaultEventHandler_keydown(event);
6317 	}
6318 
6319 
6320 	version(win32_widgets)
6321 	override void recomputeChildLayout() {
6322 		super.recomputeChildLayout();
6323 		SCROLLINFO info;
6324 		info.cbSize = info.sizeof;
6325 		info.nPage = viewportHeight;
6326 		info.fMask = SIF_PAGE | SIF_RANGE;
6327 		info.nMin = 0;
6328 		info.nMax = contentHeight_;
6329 		SetScrollInfo(hwnd, SB_VERT, &info, true);
6330 
6331 		info.cbSize = info.sizeof;
6332 		info.nPage = viewportWidth;
6333 		info.fMask = SIF_PAGE | SIF_RANGE;
6334 		info.nMin = 0;
6335 		info.nMax = contentWidth_;
6336 		SetScrollInfo(hwnd, SB_HORZ, &info, true);
6337 	}
6338 
6339 	/*
6340 		Scrolling
6341 		------------
6342 
6343 		You are assigned a width and a height by the layout engine, which
6344 		is your viewport box. However, you may draw more than that by setting
6345 		a contentWidth and contentHeight.
6346 
6347 		If these can be contained by the viewport, no scrollbar is displayed.
6348 		If they cannot fit though, it will automatically show scroll as necessary.
6349 
6350 		If contentWidth == 0, no horizontal scrolling is performed. If contentHeight
6351 		is zero, no vertical scrolling is performed.
6352 
6353 		If scrolling is necessary, the lib will automatically work with the bars.
6354 		When you redraw, the origin and clipping info in the painter is set so if
6355 		you just draw everything, it will work, but you can be more efficient by checking
6356 		the viewportWidth, viewportHeight, and scrollOrigin members.
6357 	*/
6358 
6359 	///
6360 	final @property int viewportWidth() {
6361 		return width - (showingVerticalScroll ? scaleWithDpi(16) : 0);
6362 	}
6363 	///
6364 	final @property int viewportHeight() {
6365 		return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0);
6366 	}
6367 
6368 	// FIXME property
6369 	Point scrollOrigin_;
6370 
6371 	///
6372 	final const(Point) scrollOrigin() {
6373 		return scrollOrigin_;
6374 	}
6375 
6376 	// the user sets these two
6377 	private int contentWidth_ = 0;
6378 	private int contentHeight_ = 0;
6379 
6380 	///
6381 	int contentWidth() { return contentWidth_; }
6382 	///
6383 	int contentHeight() { return contentHeight_; }
6384 
6385 	///
6386 	void setContentSize(int width, int height) {
6387 		contentWidth_ = width;
6388 		contentHeight_ = height;
6389 
6390 		version(custom_widgets) {
6391 			if(showingVerticalScroll || showingHorizontalScroll) {
6392 				outerContainer.queueRecomputeChildLayout();
6393 			}
6394 
6395 			if(showingVerticalScroll())
6396 				outerContainer.verticalScrollBar.redraw();
6397 			if(showingHorizontalScroll())
6398 				outerContainer.horizontalScrollBar.redraw();
6399 		} else version(win32_widgets) {
6400 			queueRecomputeChildLayout();
6401 		} else static assert(0);
6402 	}
6403 
6404 	///
6405 	void verticalScroll(int delta) {
6406 		verticalScrollTo(scrollOrigin.y + delta);
6407 	}
6408 	///
6409 	void verticalScrollTo(int pos) {
6410 		scrollOrigin_.y = pos;
6411 		if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight))
6412 			scrollOrigin_.y = contentHeight - viewportHeight;
6413 
6414 		if(scrollOrigin_.y < 0)
6415 			scrollOrigin_.y = 0;
6416 
6417 		version(win32_widgets) {
6418 			SCROLLINFO info;
6419 			info.cbSize = info.sizeof;
6420 			info.fMask = SIF_POS;
6421 			info.nPos = scrollOrigin_.y;
6422 			SetScrollInfo(hwnd, SB_VERT, &info, true);
6423 		} else version(custom_widgets) {
6424 			outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y);
6425 		} else static assert(0);
6426 
6427 		redraw();
6428 	}
6429 
6430 	///
6431 	void horizontalScroll(int delta) {
6432 		horizontalScrollTo(scrollOrigin.x + delta);
6433 	}
6434 	///
6435 	void horizontalScrollTo(int pos) {
6436 		scrollOrigin_.x = pos;
6437 		if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth))
6438 			scrollOrigin_.x = contentWidth - viewportWidth;
6439 
6440 		if(scrollOrigin_.x < 0)
6441 			scrollOrigin_.x = 0;
6442 
6443 		version(win32_widgets) {
6444 			SCROLLINFO info;
6445 			info.cbSize = info.sizeof;
6446 			info.fMask = SIF_POS;
6447 			info.nPos = scrollOrigin_.x;
6448 			SetScrollInfo(hwnd, SB_HORZ, &info, true);
6449 		} else version(custom_widgets) {
6450 			outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x);
6451 		} else static assert(0);
6452 
6453 		redraw();
6454 	}
6455 	///
6456 	void scrollTo(Point p) {
6457 		verticalScrollTo(p.y);
6458 		horizontalScrollTo(p.x);
6459 	}
6460 
6461 	///
6462 	void ensureVisibleInScroll(Point p) {
6463 		auto rect = viewportRectangle();
6464 		if(rect.contains(p))
6465 			return;
6466 		if(p.x < rect.left)
6467 			horizontalScroll(p.x - rect.left);
6468 		else if(p.x > rect.right)
6469 			horizontalScroll(p.x - rect.right);
6470 
6471 		if(p.y < rect.top)
6472 			verticalScroll(p.y - rect.top);
6473 		else if(p.y > rect.bottom)
6474 			verticalScroll(p.y - rect.bottom);
6475 	}
6476 
6477 	///
6478 	void ensureVisibleInScroll(Rectangle rect) {
6479 		ensureVisibleInScroll(rect.upperLeft);
6480 		ensureVisibleInScroll(rect.lowerRight);
6481 	}
6482 
6483 	///
6484 	Rectangle viewportRectangle() {
6485 		return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight));
6486 	}
6487 
6488 	///
6489 	bool showingHorizontalScroll() {
6490 		return contentWidth > width;
6491 	}
6492 	///
6493 	bool showingVerticalScroll() {
6494 		return contentHeight > height;
6495 	}
6496 
6497 	/// This is called before the ordinary paint delegate,
6498 	/// giving you a chance to draw the window frame, etc,
6499 	/// before the scroll clip takes effect
6500 	void paintFrameAndBackground(WidgetPainter painter) {
6501 		version(win32_widgets) {
6502 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
6503 			auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
6504 			// since the pen is null, to fill the whole space, we need the +1 on both.
6505 			gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
6506 			SelectObject(painter.impl.hdc, p);
6507 			SelectObject(painter.impl.hdc, b);
6508 		}
6509 
6510 	}
6511 
6512 	// make space for the scroll bar, and that's it.
6513 	final override int paddingRight() { return scaleWithDpi(16); }
6514 	final override int paddingBottom() { return scaleWithDpi(16); }
6515 
6516 	/*
6517 		END SCROLLING
6518 	*/
6519 
6520 	override WidgetPainter draw() {
6521 		int x = this.x, y = this.y;
6522 		auto parent = this.parent;
6523 		while(parent) {
6524 			x += parent.x;
6525 			y += parent.y;
6526 			parent = parent.parent;
6527 		}
6528 
6529 		//version(win32_widgets) {
6530 			//auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
6531 		//} else {
6532 			auto painter = parentWindow.win.draw(true);
6533 		//}
6534 		painter.originX = x;
6535 		painter.originY = y;
6536 
6537 		painter.originX = painter.originX - scrollOrigin.x;
6538 		painter.originY = painter.originY - scrollOrigin.y;
6539 		painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight());
6540 
6541 		return WidgetPainter(painter, this);
6542 	}
6543 
6544 	override void addScrollPosition(ref int x, ref int y) {
6545 		x += scrollOrigin.x;
6546 		y += scrollOrigin.y;
6547 	}
6548 
6549 	mixin ScrollableChildren;
6550 }
6551 
6552 // you need to have a Point scrollOrigin in the class somewhere
6553 // and a paintFrameAndBackground
6554 private mixin template ScrollableChildren() {
6555 	static assert(!__traits(isSame, this.addScrollPosition, Widget.addScrollPosition), "Your widget should provide `Point scrollOrigin()` and `override void addScrollPosition`");
6556 
6557 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
6558 		if(hidden)
6559 			return;
6560 
6561 		//version(win32_widgets)
6562 			//painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
6563 
6564 		painter.originX = lox + x;
6565 		painter.originY = loy + y;
6566 
6567 		bool actuallyPainted = false;
6568 
6569 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height)));
6570 		if(clip == Rectangle.init)
6571 			return;
6572 
6573 		if(force || redrawRequested) {
6574 			//painter.setClipRectangle(scrollOrigin, width, height);
6575 			painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
6576 			paintFrameAndBackground(painter);
6577 		}
6578 
6579 		/+
6580 		version(win32_widgets) {
6581 			if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW);
6582 		}
6583 		+/
6584 
6585 		painter.originX = painter.originX - scrollOrigin.x;
6586 		painter.originY = painter.originY - scrollOrigin.y;
6587 		if(force || redrawRequested) {
6588 			painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4);
6589 			//painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4);
6590 
6591 			//erase(painter); // we paintFrameAndBackground above so no need
6592 			if(painter.visualTheme)
6593 				painter.visualTheme.doPaint(this, painter);
6594 			else
6595 				paint(painter);
6596 
6597 			if(invalidate) {
6598 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
6599 				// children are contained inside this, so no need to do extra work
6600 				invalidate = false;
6601 			}
6602 
6603 
6604 			actuallyPainted = true;
6605 			redrawRequested = false;
6606 		}
6607 
6608 		foreach(child; children) {
6609 			if(cast(FixedPosition) child)
6610 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
6611 			else
6612 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
6613 		}
6614 	}
6615 }
6616 
6617 private class InternalScrollableContainerInsideWidget : ContainerWidget {
6618 	ScrollableContainerWidget scw;
6619 
6620 	this(ScrollableContainerWidget parent) {
6621 		scw = parent;
6622 		super(parent);
6623 	}
6624 
6625 	version(custom_widgets)
6626 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
6627 		if(hidden)
6628 			return;
6629 
6630 		bool actuallyPainted = false;
6631 
6632 		auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_);
6633 
6634 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_)));
6635 		if(clip == Rectangle.init)
6636 			return;
6637 
6638 		painter.originX = lox + x - scrollOrigin.x;
6639 		painter.originY = loy + y - scrollOrigin.y;
6640 		if(force || redrawRequested) {
6641 			painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
6642 
6643 			erase(painter);
6644 			if(painter.visualTheme)
6645 				painter.visualTheme.doPaint(this, painter);
6646 			else
6647 				paint(painter);
6648 
6649 			if(invalidate) {
6650 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
6651 				// children are contained inside this, so no need to do extra work
6652 				invalidate = false;
6653 			}
6654 
6655 			actuallyPainted = true;
6656 			redrawRequested = false;
6657 		}
6658 		foreach(child; children) {
6659 			if(cast(FixedPosition) child)
6660 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
6661 			else
6662 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
6663 		}
6664 	}
6665 
6666 	version(custom_widgets)
6667 	override protected void addScrollPosition(ref int x, ref int y) {
6668 		x += scw.scrollX_;
6669 		y += scw.scrollY_;
6670 	}
6671 }
6672 
6673 /++
6674 	A widget meant to contain other widgets that may need to scroll.
6675 
6676 	Currently buggy.
6677 
6678 	History:
6679 		Added July 1, 2021 (dub v10.2)
6680 
6681 		On January 3, 2022, I tried to use it in a few other cases
6682 		and found it only worked well in the original test case. Since
6683 		it still sucks, I think I'm going to rewrite it again.
6684 +/
6685 class ScrollableContainerWidget : ContainerWidget {
6686 	///
6687 	this(Widget parent) {
6688 		super(parent);
6689 
6690 		container = new InternalScrollableContainerInsideWidget(this);
6691 		hsb = new HorizontalScrollbar(this);
6692 		vsb = new VerticalScrollbar(this);
6693 
6694 		tabStop = false;
6695 		container.tabStop = false;
6696 		magic = true;
6697 
6698 
6699 		vsb.addEventListener("scrolltonextline", () {
6700 			scrollBy(0, scaleWithDpi(16));
6701 		});
6702 		vsb.addEventListener("scrolltopreviousline", () {
6703 			scrollBy(0,scaleWithDpi( -16));
6704 		});
6705 		vsb.addEventListener("scrolltonextpage", () {
6706 			scrollBy(0, container.height);
6707 		});
6708 		vsb.addEventListener("scrolltopreviouspage", () {
6709 			scrollBy(0, -container.height);
6710 		});
6711 		vsb.addEventListener((scope ScrollToPositionEvent spe) {
6712 			scrollTo(scrollX_, spe.value);
6713 		});
6714 
6715 		this.addEventListener(delegate (scope ClickEvent e) {
6716 			if(e.button == MouseButton.wheelUp) {
6717 				if(!e.defaultPrevented)
6718 					scrollBy(0, scaleWithDpi(-16));
6719 				e.stopPropagation();
6720 			} else if(e.button == MouseButton.wheelDown) {
6721 				if(!e.defaultPrevented)
6722 					scrollBy(0, scaleWithDpi(16));
6723 				e.stopPropagation();
6724 			} else if(e.button == MouseButton.wheelLeft) {
6725 				if(!e.defaultPrevented)
6726 					scrollBy(scaleWithDpi(-16), 0);
6727 				e.stopPropagation();
6728 			} else if(e.button == MouseButton.wheelRight) {
6729 				if(!e.defaultPrevented)
6730 					scrollBy(scaleWithDpi(16), 0);
6731 				e.stopPropagation();
6732 			}
6733 		});
6734 	}
6735 
6736 	/+
6737 	override void defaultEventHandler_click(ClickEvent e) {
6738 	}
6739 	+/
6740 
6741 	override void removeAllChildren() {
6742 		container.removeAllChildren();
6743 	}
6744 
6745 	void scrollTo(int x, int y) {
6746 		scrollBy(x - scrollX_, y - scrollY_);
6747 	}
6748 
6749 	void scrollBy(int x, int y) {
6750 		auto ox = scrollX_;
6751 		auto oy = scrollY_;
6752 
6753 		auto nx = ox + x;
6754 		auto ny = oy + y;
6755 
6756 		if(nx < 0)
6757 			nx = 0;
6758 		if(ny < 0)
6759 			ny = 0;
6760 
6761 		auto maxX = hsb.max - container.width;
6762 		if(maxX < 0) maxX = 0;
6763 		auto maxY = vsb.max - container.height;
6764 		if(maxY < 0) maxY = 0;
6765 
6766 		if(nx > maxX)
6767 			nx = maxX;
6768 		if(ny > maxY)
6769 			ny = maxY;
6770 
6771 		auto dx = nx - ox;
6772 		auto dy = ny - oy;
6773 
6774 		if(dx || dy) {
6775 			version(win32_widgets)
6776 				ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
6777 			else {
6778 				redraw();
6779 			}
6780 
6781 			hsb.setPosition = nx;
6782 			vsb.setPosition = ny;
6783 
6784 			scrollX_ = nx;
6785 			scrollY_ = ny;
6786 		}
6787 	}
6788 
6789 	private int scrollX_;
6790 	private int scrollY_;
6791 
6792 	void setTotalArea(int width, int height) {
6793 		hsb.setMax(width);
6794 		vsb.setMax(height);
6795 	}
6796 
6797 	///
6798 	void setViewableArea(int width, int height) {
6799 		hsb.setViewableArea(width);
6800 		vsb.setViewableArea(height);
6801 	}
6802 
6803 	private bool magic;
6804 	override void addChild(Widget w, int position = int.max) {
6805 		if(magic)
6806 			container.addChild(w, position);
6807 		else
6808 			super.addChild(w, position);
6809 	}
6810 
6811 	override void recomputeChildLayout() {
6812 		if(hsb is null || vsb is null || container is null) return;
6813 
6814 		/+
6815 		writeln(x, " ", y , " ", width, " ", height);
6816 		writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight());
6817 		+/
6818 
6819 		registerMovement();
6820 
6821 		hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane?
6822 		hsb.x = 0;
6823 		hsb.y = this.height - hsb.height;
6824 		hsb.width = this.width - scaleWithDpi(16);
6825 		hsb.recomputeChildLayout();
6826 
6827 		vsb.width = scaleWithDpi(16); // FIXME?
6828 		vsb.x = this.width - vsb.width;
6829 		vsb.y = 0;
6830 		vsb.height = this.height - scaleWithDpi(16);
6831 		vsb.recomputeChildLayout();
6832 
6833 		container.x = 0;
6834 		container.y = 0;
6835 		container.width = this.width - vsb.width;
6836 		container.height = this.height - hsb.height;
6837 		container.recomputeChildLayout();
6838 
6839 		scrollX_ = 0;
6840 		scrollY_ = 0;
6841 
6842 		hsb.setPosition(0);
6843 		vsb.setPosition(0);
6844 
6845 		int mw, mh;
6846 		Widget c = container;
6847 		// FIXME: hack here to handle a layout inside...
6848 		if(c.children.length == 1 && cast(Layout) c.children[0])
6849 			c = c.children[0];
6850 		foreach(child; c.children) {
6851 			auto w = child.x + child.width;
6852 			auto h = child.y + child.height;
6853 
6854 			if(w > mw) mw = w;
6855 			if(h > mh) mh = h;
6856 		}
6857 
6858 		setTotalArea(mw, mh);
6859 		setViewableArea(width, height);
6860 	}
6861 
6862 	override int minHeight() { return scaleWithDpi(64); }
6863 
6864 	HorizontalScrollbar hsb;
6865 	VerticalScrollbar vsb;
6866 	ContainerWidget container;
6867 }
6868 
6869 
6870 version(custom_widgets)
6871 deprecated
6872 private class InternalScrollableContainerWidget : Widget {
6873 
6874 	ScrollableWidget sw;
6875 
6876 	VerticalScrollbar verticalScrollBar;
6877 	HorizontalScrollbar horizontalScrollBar;
6878 
6879 	this(ScrollableWidget sw, Widget parent) {
6880 		this.sw = sw;
6881 
6882 		this.tabStop = false;
6883 
6884 		super(parent);
6885 
6886 		horizontalScrollBar = new HorizontalScrollbar(this);
6887 		verticalScrollBar = new VerticalScrollbar(this);
6888 
6889 		horizontalScrollBar.showing_ = false;
6890 		verticalScrollBar.showing_ = false;
6891 
6892 		horizontalScrollBar.addEventListener("scrolltonextline", {
6893 			horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
6894 			sw.horizontalScrollTo(horizontalScrollBar.position);
6895 		});
6896 		horizontalScrollBar.addEventListener("scrolltopreviousline", {
6897 			horizontalScrollBar.setPosition(horizontalScrollBar.position - 1);
6898 			sw.horizontalScrollTo(horizontalScrollBar.position);
6899 		});
6900 		verticalScrollBar.addEventListener("scrolltonextline", {
6901 			verticalScrollBar.setPosition(verticalScrollBar.position + 1);
6902 			sw.verticalScrollTo(verticalScrollBar.position);
6903 		});
6904 		verticalScrollBar.addEventListener("scrolltopreviousline", {
6905 			verticalScrollBar.setPosition(verticalScrollBar.position - 1);
6906 			sw.verticalScrollTo(verticalScrollBar.position);
6907 		});
6908 		horizontalScrollBar.addEventListener("scrolltonextpage", {
6909 			horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_);
6910 			sw.horizontalScrollTo(horizontalScrollBar.position);
6911 		});
6912 		horizontalScrollBar.addEventListener("scrolltopreviouspage", {
6913 			horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_);
6914 			sw.horizontalScrollTo(horizontalScrollBar.position);
6915 		});
6916 		verticalScrollBar.addEventListener("scrolltonextpage", {
6917 			verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_);
6918 			sw.verticalScrollTo(verticalScrollBar.position);
6919 		});
6920 		verticalScrollBar.addEventListener("scrolltopreviouspage", {
6921 			verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_);
6922 			sw.verticalScrollTo(verticalScrollBar.position);
6923 		});
6924 		horizontalScrollBar.addEventListener("scrolltoposition", (Event event) {
6925 			horizontalScrollBar.setPosition(event.intValue);
6926 			sw.horizontalScrollTo(horizontalScrollBar.position);
6927 		});
6928 		verticalScrollBar.addEventListener("scrolltoposition", (Event event) {
6929 			verticalScrollBar.setPosition(event.intValue);
6930 			sw.verticalScrollTo(verticalScrollBar.position);
6931 		});
6932 		horizontalScrollBar.addEventListener("scrolltrack", (Event event) {
6933 			horizontalScrollBar.setPosition(event.intValue);
6934 			sw.horizontalScrollTo(horizontalScrollBar.position);
6935 		});
6936 		verticalScrollBar.addEventListener("scrolltrack", (Event event) {
6937 			verticalScrollBar.setPosition(event.intValue);
6938 		});
6939 	}
6940 
6941 	// this is supposed to be basically invisible...
6942 	override int minWidth() { return sw.minWidth; }
6943 	override int minHeight() { return sw.minHeight; }
6944 	override int maxWidth() { return sw.maxWidth; }
6945 	override int maxHeight() { return sw.maxHeight; }
6946 	override int widthStretchiness() { return sw.widthStretchiness; }
6947 	override int heightStretchiness() { return sw.heightStretchiness; }
6948 	override int marginLeft() { return sw.marginLeft; }
6949 	override int marginRight() { return sw.marginRight; }
6950 	override int marginTop() { return sw.marginTop; }
6951 	override int marginBottom() { return sw.marginBottom; }
6952 	override int paddingLeft() { return sw.paddingLeft; }
6953 	override int paddingRight() { return sw.paddingRight; }
6954 	override int paddingTop() { return sw.paddingTop; }
6955 	override int paddingBottom() { return sw.paddingBottom; }
6956 	override void focus() { sw.focus(); }
6957 
6958 
6959 	override void recomputeChildLayout() {
6960 		// The stupid thing needs to calculate if a scroll bar is needed...
6961 		recomputeChildLayoutHelper();
6962 		// then running it again will position things correctly if the bar is NOT needed
6963 		recomputeChildLayoutHelper();
6964 
6965 		// this sucks but meh it barely works
6966 	}
6967 
6968 	private void recomputeChildLayoutHelper() {
6969 		if(sw is null) return;
6970 
6971 		bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll;
6972 		if(horizontalScrollBar && verticalScrollBar) {
6973 			horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0);
6974 			horizontalScrollBar.height = horizontalScrollBar.minHeight();
6975 			horizontalScrollBar.x = 0;
6976 			horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight();
6977 
6978 			verticalScrollBar.width = verticalScrollBar.minWidth();
6979 			verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2;
6980 			verticalScrollBar.x = this.width - verticalScrollBar.minWidth();
6981 			verticalScrollBar.y = 0 + 2;
6982 
6983 			sw.x = 0;
6984 			sw.y = 0;
6985 			sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0);
6986 			sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0);
6987 
6988 			if(sw.contentWidth_ <= this.width)
6989 				sw.scrollOrigin_.x = 0;
6990 			if(sw.contentHeight_ <= this.height)
6991 				sw.scrollOrigin_.y = 0;
6992 
6993 			horizontalScrollBar.recomputeChildLayout();
6994 			verticalScrollBar.recomputeChildLayout();
6995 			sw.recomputeChildLayout();
6996 		}
6997 
6998 		if(sw.contentWidth_ <= this.width)
6999 			sw.scrollOrigin_.x = 0;
7000 		if(sw.contentHeight_ <= this.height)
7001 			sw.scrollOrigin_.y = 0;
7002 
7003 		if(sw.showingHorizontalScroll())
7004 			horizontalScrollBar.showing(true, false);
7005 		else
7006 			horizontalScrollBar.showing(false, false);
7007 		if(sw.showingVerticalScroll())
7008 			verticalScrollBar.showing(true, false);
7009 		else
7010 			verticalScrollBar.showing(false, false);
7011 
7012 		verticalScrollBar.setViewableArea(sw.viewportHeight());
7013 		verticalScrollBar.setMax(sw.contentHeight);
7014 		verticalScrollBar.setPosition(sw.scrollOrigin.y);
7015 
7016 		horizontalScrollBar.setViewableArea(sw.viewportWidth());
7017 		horizontalScrollBar.setMax(sw.contentWidth);
7018 		horizontalScrollBar.setPosition(sw.scrollOrigin.x);
7019 	}
7020 }
7021 
7022 /*
7023 class ScrollableClientWidget : Widget {
7024 	this(Widget parent) {
7025 		super(parent);
7026 	}
7027 	override void paint(WidgetPainter p) {
7028 		parent.paint(p);
7029 	}
7030 }
7031 */
7032 
7033 /++
7034 	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.
7035 +/
7036 abstract class Slider : Widget {
7037 	this(int min, int max, int step, Widget parent) {
7038 		min_ = min;
7039 		max_ = max;
7040 		step_ = step;
7041 		page_ = step;
7042 		super(parent);
7043 	}
7044 
7045 	private int min_;
7046 	private int max_;
7047 	private int step_;
7048 	private int position_;
7049 	private int page_;
7050 
7051 	// selection start and selection end
7052 	// tics
7053 	// tooltip?
7054 	// some way to see and just type the value
7055 	// win32 buddy controls are labels
7056 
7057 	///
7058 	void setMin(int a) {
7059 		min_ = a;
7060 		version(custom_widgets)
7061 			redraw();
7062 		version(win32_widgets)
7063 			SendMessage(hwnd, TBM_SETRANGEMIN, true, a);
7064 	}
7065 	///
7066 	int min() {
7067 		return min_;
7068 	}
7069 	///
7070 	void setMax(int a) {
7071 		max_ = a;
7072 		version(custom_widgets)
7073 			redraw();
7074 		version(win32_widgets)
7075 			SendMessage(hwnd, TBM_SETRANGEMAX, true, a);
7076 	}
7077 	///
7078 	int max() {
7079 		return max_;
7080 	}
7081 	///
7082 	void setPosition(int a) {
7083 		if(a > max)
7084 			a = max;
7085 		if(a < min)
7086 			a = min;
7087 		position_ = a;
7088 		version(custom_widgets)
7089 			setPositionCustom(a);
7090 
7091 		version(win32_widgets)
7092 			setPositionWindows(a);
7093 	}
7094 	version(win32_widgets) {
7095 		protected abstract void setPositionWindows(int a);
7096 	}
7097 
7098 	protected abstract int win32direction();
7099 
7100 	/++
7101 		Alias for [position] for better compatibility with generic code.
7102 
7103 		History:
7104 			Added October 5, 2021
7105 	+/
7106 	@property int value() {
7107 		return position;
7108 	}
7109 
7110 	///
7111 	int position() {
7112 		return position_;
7113 	}
7114 	///
7115 	void setStep(int a) {
7116 		step_ = a;
7117 		version(win32_widgets)
7118 			SendMessage(hwnd, TBM_SETLINESIZE, 0, a);
7119 	}
7120 	///
7121 	int step() {
7122 		return step_;
7123 	}
7124 	///
7125 	void setPageSize(int a) {
7126 		page_ = a;
7127 		version(win32_widgets)
7128 			SendMessage(hwnd, TBM_SETPAGESIZE, 0, a);
7129 	}
7130 	///
7131 	int pageSize() {
7132 		return page_;
7133 	}
7134 
7135 	private void notify() {
7136 		auto event = new ChangeEvent!int(this, &this.position);
7137 		event.dispatch();
7138 	}
7139 
7140 	version(win32_widgets)
7141 	void win32Setup(int style) {
7142 		createWin32Window(this, TRACKBAR_CLASS, "",
7143 			0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0);
7144 
7145 		// the trackbar sends the same messages as scroll, which
7146 		// our other layer sends as these... just gonna translate
7147 		// here
7148 		this.addDirectEventListener("scrolltoposition", (Event event) {
7149 			event.stopPropagation();
7150 			this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue);
7151 			notify();
7152 		});
7153 		this.addDirectEventListener("scrolltonextline", (Event event) {
7154 			event.stopPropagation();
7155 			this.setPosition(this.position + this.step_ * this.win32direction);
7156 			notify();
7157 		});
7158 		this.addDirectEventListener("scrolltopreviousline", (Event event) {
7159 			event.stopPropagation();
7160 			this.setPosition(this.position - this.step_ * this.win32direction);
7161 			notify();
7162 		});
7163 		this.addDirectEventListener("scrolltonextpage", (Event event) {
7164 			event.stopPropagation();
7165 			this.setPosition(this.position + this.page_ * this.win32direction);
7166 			notify();
7167 		});
7168 		this.addDirectEventListener("scrolltopreviouspage", (Event event) {
7169 			event.stopPropagation();
7170 			this.setPosition(this.position - this.page_ * this.win32direction);
7171 			notify();
7172 		});
7173 
7174 		setMin(min_);
7175 		setMax(max_);
7176 		setStep(step_);
7177 		setPageSize(page_);
7178 	}
7179 
7180 	version(custom_widgets) {
7181 		protected MouseTrackingWidget thumb;
7182 
7183 		protected abstract void setPositionCustom(int a);
7184 
7185 		override void defaultEventHandler_keydown(KeyDownEvent event) {
7186 			switch(event.key) {
7187 				case Key.Up:
7188 				case Key.Right:
7189 					setPosition(position() - step() * win32direction);
7190 					changed();
7191 				break;
7192 				case Key.Down:
7193 				case Key.Left:
7194 					setPosition(position() + step() * win32direction);
7195 					changed();
7196 				break;
7197 				case Key.Home:
7198 					setPosition(win32direction > 0 ? min() : max());
7199 					changed();
7200 				break;
7201 				case Key.End:
7202 					setPosition(win32direction > 0 ? max() : min());
7203 					changed();
7204 				break;
7205 				case Key.PageUp:
7206 					setPosition(position() - pageSize() * win32direction);
7207 					changed();
7208 				break;
7209 				case Key.PageDown:
7210 					setPosition(position() + pageSize() * win32direction);
7211 					changed();
7212 				break;
7213 				default:
7214 			}
7215 			super.defaultEventHandler_keydown(event);
7216 		}
7217 
7218 		protected void changed() {
7219 			auto ev = new ChangeEvent!int(this, &position);
7220 			ev.dispatch();
7221 		}
7222 	}
7223 }
7224 
7225 /++
7226 
7227 +/
7228 class VerticalSlider : Slider {
7229 	this(int min, int max, int step, Widget parent) {
7230 		version(custom_widgets)
7231 			initialize();
7232 
7233 		super(min, max, step, parent);
7234 
7235 		version(win32_widgets)
7236 			win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */);
7237 	}
7238 
7239 	protected override int win32direction() {
7240 		return -1;
7241 	}
7242 
7243 	version(win32_widgets)
7244 	protected override void setPositionWindows(int a) {
7245 		// the windows thing makes the top 0 and i don't like that.
7246 		SendMessage(hwnd, TBM_SETPOS, true, max - a);
7247 	}
7248 
7249 	version(custom_widgets)
7250 	private void initialize() {
7251 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this);
7252 
7253 		thumb.tabStop = false;
7254 
7255 		thumb.thumbWidth = width;
7256 		thumb.thumbHeight = scaleWithDpi(16);
7257 
7258 		thumb.addEventListener(EventType.change, () {
7259 			auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16));
7260 			sx = max - sx;
7261 			//informProgramThatUserChangedPosition(sx);
7262 
7263 			position_ = sx;
7264 
7265 			changed();
7266 		});
7267 	}
7268 
7269 	version(custom_widgets)
7270 	override void recomputeChildLayout() {
7271 		thumb.thumbWidth = this.width;
7272 		super.recomputeChildLayout();
7273 		setPositionCustom(position_);
7274 	}
7275 
7276 	version(custom_widgets)
7277 	protected override void setPositionCustom(int a) {
7278 		if(max())
7279 			thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max();
7280 		redraw();
7281 	}
7282 }
7283 
7284 /++
7285 
7286 +/
7287 class HorizontalSlider : Slider {
7288 	this(int min, int max, int step, Widget parent) {
7289 		version(custom_widgets)
7290 			initialize();
7291 
7292 		super(min, max, step, parent);
7293 
7294 		version(win32_widgets)
7295 			win32Setup(TBS_HORZ);
7296 	}
7297 
7298 	version(win32_widgets)
7299 	protected override void setPositionWindows(int a) {
7300 		SendMessage(hwnd, TBM_SETPOS, true, a);
7301 	}
7302 
7303 	protected override int win32direction() {
7304 		return 1;
7305 	}
7306 
7307 	version(custom_widgets)
7308 	private void initialize() {
7309 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this);
7310 
7311 		thumb.tabStop = false;
7312 
7313 		thumb.thumbWidth = scaleWithDpi(16);
7314 		thumb.thumbHeight = height;
7315 
7316 		thumb.addEventListener(EventType.change, () {
7317 			auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16));
7318 			//informProgramThatUserChangedPosition(sx);
7319 
7320 			position_ = sx;
7321 
7322 			changed();
7323 		});
7324 	}
7325 
7326 	version(custom_widgets)
7327 	override void recomputeChildLayout() {
7328 		thumb.thumbHeight = this.height;
7329 		super.recomputeChildLayout();
7330 		setPositionCustom(position_);
7331 	}
7332 
7333 	version(custom_widgets)
7334 	protected override void setPositionCustom(int a) {
7335 		if(max())
7336 			thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max();
7337 		redraw();
7338 	}
7339 }
7340 
7341 
7342 ///
7343 abstract class ScrollbarBase : Widget {
7344 	///
7345 	this(Widget parent) {
7346 		super(parent);
7347 		tabStop = false;
7348 		step_ = scaleWithDpi(16);
7349 	}
7350 
7351 	private int viewableArea_;
7352 	private int max_;
7353 	private int step_;// = 16;
7354 	private int position_;
7355 
7356 	///
7357 	bool atEnd() {
7358 		return position_ + viewableArea_ >= max_;
7359 	}
7360 
7361 	///
7362 	bool atStart() {
7363 		return position_ == 0;
7364 	}
7365 
7366 	///
7367 	void setViewableArea(int a) {
7368 		viewableArea_ = a;
7369 		version(custom_widgets)
7370 			redraw();
7371 	}
7372 	///
7373 	void setMax(int a) {
7374 		max_ = a;
7375 		version(custom_widgets)
7376 			redraw();
7377 	}
7378 	///
7379 	int max() {
7380 		return max_;
7381 	}
7382 	///
7383 	void setPosition(int a) {
7384 		auto logicalMax = max_ - viewableArea_;
7385 		if(a == int.max)
7386 			a = logicalMax;
7387 
7388 		if(a > logicalMax)
7389 			a = logicalMax;
7390 		if(a < 0)
7391 			a = 0;
7392 
7393 		position_ = a;
7394 
7395 		version(custom_widgets)
7396 			redraw();
7397 	}
7398 	///
7399 	int position() {
7400 		return position_;
7401 	}
7402 	///
7403 	void setStep(int a) {
7404 		step_ = a;
7405 	}
7406 	///
7407 	int step() {
7408 		return step_;
7409 	}
7410 
7411 	// FIXME: remove this.... maybe
7412 	/+
7413 	protected void informProgramThatUserChangedPosition(int n) {
7414 		position_ = n;
7415 		auto evt = new Event(EventType.change, this);
7416 		evt.intValue = n;
7417 		evt.dispatch();
7418 	}
7419 	+/
7420 
7421 	version(custom_widgets) {
7422 		enum MIN_THUMB_SIZE = 8;
7423 
7424 		abstract protected int getBarDim();
7425 		int thumbSize() {
7426 			if(viewableArea_ >= max_ || max_ == 0)
7427 				return getBarDim();
7428 
7429 			int res = viewableArea_ * getBarDim() / max_;
7430 
7431 			if(res < scaleWithDpi(MIN_THUMB_SIZE))
7432 				res = scaleWithDpi(MIN_THUMB_SIZE);
7433 
7434 			return res;
7435 		}
7436 
7437 		int thumbPosition() {
7438 			/*
7439 				viewableArea_ is the viewport height/width
7440 				position_ is where we are
7441 			*/
7442 			//if(position_ + viewableArea_ >= max_)
7443 				//return getBarDim - thumbSize;
7444 
7445 			auto maximumPossibleValue = getBarDim() - thumbSize;
7446 			auto maximiumLogicalValue = max_ - viewableArea_;
7447 
7448 			auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0;
7449 
7450 			return p;
7451 		}
7452 	}
7453 }
7454 
7455 //public import mgt;
7456 
7457 /++
7458 	A mouse tracking widget is one that follows the mouse when dragged inside it.
7459 
7460 	Concrete subclasses may include a scrollbar thumb and a volume control.
7461 +/
7462 //version(custom_widgets)
7463 class MouseTrackingWidget : Widget {
7464 
7465 	///
7466 	int positionX() { return positionX_; }
7467 	///
7468 	int positionY() { return positionY_; }
7469 
7470 	///
7471 	void positionX(int p) { positionX_ = p; }
7472 	///
7473 	void positionY(int p) { positionY_ = p; }
7474 
7475 	private int positionX_;
7476 	private int positionY_;
7477 
7478 	///
7479 	enum Orientation {
7480 		horizontal, ///
7481 		vertical, ///
7482 		twoDimensional, ///
7483 	}
7484 
7485 	private int thumbWidth_;
7486 	private int thumbHeight_;
7487 
7488 	///
7489 	int thumbWidth() { return thumbWidth_; }
7490 	///
7491 	int thumbHeight() { return thumbHeight_; }
7492 	///
7493 	int thumbWidth(int a) { return thumbWidth_ = a; }
7494 	///
7495 	int thumbHeight(int a) { return thumbHeight_ = a; }
7496 
7497 	private bool dragging;
7498 	private bool hovering;
7499 	private int startMouseX, startMouseY;
7500 
7501 	///
7502 	this(Orientation orientation, Widget parent) {
7503 		super(parent);
7504 
7505 		//assert(parentWindow !is null);
7506 
7507 		addEventListener((MouseDownEvent event) {
7508 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
7509 				dragging = true;
7510 				startMouseX = event.clientX - positionX;
7511 				startMouseY = event.clientY - positionY;
7512 				parentWindow.captureMouse(this);
7513 			} else {
7514 				if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
7515 					positionX = event.clientX - thumbWidth / 2;
7516 				if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
7517 					positionY = event.clientY - thumbHeight / 2;
7518 
7519 				if(positionX + thumbWidth > this.width)
7520 					positionX = this.width - thumbWidth;
7521 				if(positionY + thumbHeight > this.height)
7522 					positionY = this.height - thumbHeight;
7523 
7524 				if(positionX < 0)
7525 					positionX = 0;
7526 				if(positionY < 0)
7527 					positionY = 0;
7528 
7529 
7530 				// this.emit!(ChangeEvent!void)();
7531 				auto evt = new Event(EventType.change, this);
7532 				evt.sendDirectly();
7533 
7534 				redraw();
7535 
7536 			}
7537 		});
7538 
7539 		addEventListener(EventType.mouseup, (Event event) {
7540 			dragging = false;
7541 			parentWindow.releaseMouseCapture();
7542 		});
7543 
7544 		addEventListener(EventType.mouseout, (Event event) {
7545 			if(!hovering)
7546 				return;
7547 			hovering = false;
7548 			redraw();
7549 		});
7550 
7551 		int lpx, lpy;
7552 
7553 		addEventListener((MouseMoveEvent event) {
7554 			auto oh = hovering;
7555 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
7556 				hovering = true;
7557 			} else {
7558 				hovering = false;
7559 			}
7560 			if(!dragging) {
7561 				if(hovering != oh)
7562 					redraw();
7563 				return;
7564 			}
7565 
7566 			if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
7567 				positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it
7568 			if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
7569 				positionY = event.clientY - startMouseY;
7570 
7571 			if(positionX + thumbWidth > this.width)
7572 				positionX = this.width - thumbWidth;
7573 			if(positionY + thumbHeight > this.height)
7574 				positionY = this.height - thumbHeight;
7575 
7576 			if(positionX < 0)
7577 				positionX = 0;
7578 			if(positionY < 0)
7579 				positionY = 0;
7580 
7581 			if(positionX != lpx || positionY != lpy) {
7582 				lpx = positionX;
7583 				lpy = positionY;
7584 
7585 				auto evt = new Event(EventType.change, this);
7586 				evt.sendDirectly();
7587 			}
7588 
7589 			redraw();
7590 		});
7591 	}
7592 
7593 	version(custom_widgets)
7594 	override void paint(WidgetPainter painter) {
7595 		auto cs = getComputedStyle();
7596 		auto c = darken(cs.windowBackgroundColor, 0.2);
7597 		painter.outlineColor = c;
7598 		painter.fillColor = c;
7599 		painter.drawRectangle(Point(0, 0), this.width, this.height);
7600 
7601 		auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor;
7602 		draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color);
7603 	}
7604 }
7605 
7606 //version(custom_widgets)
7607 //private
7608 class HorizontalScrollbar : ScrollbarBase {
7609 
7610 	version(custom_widgets) {
7611 		private MouseTrackingWidget thumb;
7612 
7613 		override int getBarDim() {
7614 			return thumb.width;
7615 		}
7616 	}
7617 
7618 	override void setViewableArea(int a) {
7619 		super.setViewableArea(a);
7620 
7621 		version(win32_widgets) {
7622 			SCROLLINFO info;
7623 			info.cbSize = info.sizeof;
7624 			info.nPage = a + 1;
7625 			info.fMask = SIF_PAGE;
7626 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7627 		} else version(custom_widgets) {
7628 			thumb.positionX = thumbPosition;
7629 			thumb.thumbWidth = thumbSize;
7630 			thumb.redraw();
7631 		} else static assert(0);
7632 
7633 	}
7634 
7635 	override void setMax(int a) {
7636 		super.setMax(a);
7637 		version(win32_widgets) {
7638 			SCROLLINFO info;
7639 			info.cbSize = info.sizeof;
7640 			info.nMin = 0;
7641 			info.nMax = max;
7642 			info.fMask = SIF_RANGE;
7643 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7644 		} else version(custom_widgets) {
7645 			thumb.positionX = thumbPosition;
7646 			thumb.thumbWidth = thumbSize;
7647 			thumb.redraw();
7648 		}
7649 	}
7650 
7651 	override void setPosition(int a) {
7652 		super.setPosition(a);
7653 		version(win32_widgets) {
7654 			SCROLLINFO info;
7655 			info.cbSize = info.sizeof;
7656 			info.fMask = SIF_POS;
7657 			info.nPos = position;
7658 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7659 		} else version(custom_widgets) {
7660 			thumb.positionX = thumbPosition();
7661 			thumb.thumbWidth = thumbSize;
7662 			thumb.redraw();
7663 		} else static assert(0);
7664 	}
7665 
7666 	this(Widget parent) {
7667 		super(parent);
7668 
7669 		version(win32_widgets) {
7670 			createWin32Window(this, "Scrollbar"w, "",
7671 				0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0);
7672 		} else version(custom_widgets) {
7673 			auto vl = new HorizontalLayout(this);
7674 			auto leftButton = new ArrowButton(ArrowDirection.left, vl);
7675 			leftButton.setClickRepeat(scrollClickRepeatInterval);
7676 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl);
7677 			auto rightButton = new ArrowButton(ArrowDirection.right, vl);
7678 			rightButton.setClickRepeat(scrollClickRepeatInterval);
7679 
7680 			leftButton.tabStop = false;
7681 			rightButton.tabStop = false;
7682 			thumb.tabStop = false;
7683 
7684 			leftButton.addEventListener(EventType.triggered, () {
7685 				this.emitCommand!"scrolltopreviousline"();
7686 				//informProgramThatUserChangedPosition(position - step());
7687 			});
7688 			rightButton.addEventListener(EventType.triggered, () {
7689 				this.emitCommand!"scrolltonextline"();
7690 				//informProgramThatUserChangedPosition(position + step());
7691 			});
7692 
7693 			thumb.thumbWidth = this.minWidth;
7694 			thumb.thumbHeight = scaleWithDpi(16);
7695 
7696 			thumb.addEventListener(EventType.change, () {
7697 				auto maximumPossibleValue = thumb.width - thumb.thumbWidth;
7698 				auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0;
7699 
7700 				//informProgramThatUserChangedPosition(sx);
7701 
7702 				auto ev = new ScrollToPositionEvent(this, sx);
7703 				ev.dispatch();
7704 			});
7705 		}
7706 	}
7707 
7708 	version(custom_widgets)
7709 	override void dpiChanged() {
7710 		thumb.thumbHeight = scaleWithDpi(16);
7711 	}
7712 
7713 	override int minHeight() { return scaleWithDpi(16); }
7714 	override int maxHeight() { return scaleWithDpi(16); }
7715 	override int minWidth() { return scaleWithDpi(48); }
7716 }
7717 
7718 final class ScrollToPositionEvent : Event {
7719 	enum EventString = "scrolltoposition";
7720 
7721 	this(Widget target, int value) {
7722 		this.value = value;
7723 		super(EventString, target);
7724 	}
7725 
7726 	immutable int value;
7727 
7728 	override @property int intValue() {
7729 		return value;
7730 	}
7731 }
7732 
7733 //version(custom_widgets)
7734 //private
7735 class VerticalScrollbar : ScrollbarBase {
7736 
7737 	version(custom_widgets) {
7738 		override int getBarDim() {
7739 			return thumb.height;
7740 		}
7741 
7742 		private MouseTrackingWidget thumb;
7743 	}
7744 
7745 	override void setViewableArea(int a) {
7746 		super.setViewableArea(a);
7747 
7748 		version(win32_widgets) {
7749 			SCROLLINFO info;
7750 			info.cbSize = info.sizeof;
7751 			info.nPage = a + 1;
7752 			info.fMask = SIF_PAGE;
7753 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7754 		} else version(custom_widgets) {
7755 			thumb.positionY = thumbPosition;
7756 			thumb.thumbHeight = thumbSize;
7757 			thumb.redraw();
7758 		} else static assert(0);
7759 
7760 	}
7761 
7762 	override void setMax(int a) {
7763 		super.setMax(a);
7764 		version(win32_widgets) {
7765 			SCROLLINFO info;
7766 			info.cbSize = info.sizeof;
7767 			info.nMin = 0;
7768 			info.nMax = max;
7769 			info.fMask = SIF_RANGE;
7770 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7771 		} else version(custom_widgets) {
7772 			thumb.positionY = thumbPosition;
7773 			thumb.thumbHeight = thumbSize;
7774 			thumb.redraw();
7775 		}
7776 	}
7777 
7778 	override void setPosition(int a) {
7779 		super.setPosition(a);
7780 		version(win32_widgets) {
7781 			SCROLLINFO info;
7782 			info.cbSize = info.sizeof;
7783 			info.fMask = SIF_POS;
7784 			info.nPos = position;
7785 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7786 		} else version(custom_widgets) {
7787 			thumb.positionY = thumbPosition;
7788 			thumb.thumbHeight = thumbSize;
7789 			thumb.redraw();
7790 		} else static assert(0);
7791 	}
7792 
7793 	this(Widget parent) {
7794 		super(parent);
7795 
7796 		version(win32_widgets) {
7797 			createWin32Window(this, "Scrollbar"w, "",
7798 				0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0);
7799 		} else version(custom_widgets) {
7800 			auto vl = new VerticalLayout(this);
7801 			auto upButton = new ArrowButton(ArrowDirection.up, vl);
7802 			upButton.setClickRepeat(scrollClickRepeatInterval);
7803 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl);
7804 			auto downButton = new ArrowButton(ArrowDirection.down, vl);
7805 			downButton.setClickRepeat(scrollClickRepeatInterval);
7806 
7807 			upButton.addEventListener(EventType.triggered, () {
7808 				this.emitCommand!"scrolltopreviousline"();
7809 				//informProgramThatUserChangedPosition(position - step());
7810 			});
7811 			downButton.addEventListener(EventType.triggered, () {
7812 				this.emitCommand!"scrolltonextline"();
7813 				//informProgramThatUserChangedPosition(position + step());
7814 			});
7815 
7816 			thumb.thumbWidth = this.minWidth;
7817 			thumb.thumbHeight = scaleWithDpi(16);
7818 
7819 			thumb.addEventListener(EventType.change, () {
7820 				auto maximumPossibleValue = thumb.height - thumb.thumbHeight;
7821 				auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0;
7822 
7823 				auto ev = new ScrollToPositionEvent(this, sy);
7824 				ev.dispatch();
7825 
7826 				//informProgramThatUserChangedPosition(sy);
7827 			});
7828 
7829 			upButton.tabStop = false;
7830 			downButton.tabStop = false;
7831 			thumb.tabStop = false;
7832 		}
7833 	}
7834 
7835 	version(custom_widgets)
7836 	override void dpiChanged() {
7837 		thumb.thumbWidth = scaleWithDpi(16);
7838 	}
7839 
7840 	override int minWidth() { return scaleWithDpi(16); }
7841 	override int maxWidth() { return scaleWithDpi(16); }
7842 	override int minHeight() { return scaleWithDpi(48); }
7843 }
7844 
7845 
7846 /++
7847 	EXPERIMENTAL
7848 
7849 	A widget specialized for being a container for other widgets.
7850 
7851 	History:
7852 		Added May 29, 2021. Not stabilized at this time.
7853 +/
7854 class WidgetContainer : Widget {
7855 	this(Widget parent) {
7856 		tabStop = false;
7857 		super(parent);
7858 	}
7859 
7860 	override int maxHeight() {
7861 		if(this.children.length == 1) {
7862 			return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom);
7863 		} else {
7864 			return int.max;
7865 		}
7866 	}
7867 
7868 	override int maxWidth() {
7869 		if(this.children.length == 1) {
7870 			return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight);
7871 		} else {
7872 			return int.max;
7873 		}
7874 	}
7875 
7876 	/+
7877 
7878 	override int minHeight() {
7879 		int largest = 0;
7880 		int margins = 0;
7881 		int lastMargin = 0;
7882 		foreach(child; children) {
7883 			auto mh = child.minHeight();
7884 			if(mh > largest)
7885 				largest = mh;
7886 			margins += mymax(lastMargin, child.marginTop());
7887 			lastMargin = child.marginBottom();
7888 		}
7889 		return largest + margins;
7890 	}
7891 
7892 	override int maxHeight() {
7893 		int largest = 0;
7894 		int margins = 0;
7895 		int lastMargin = 0;
7896 		foreach(child; children) {
7897 			auto mh = child.maxHeight();
7898 			if(mh == int.max)
7899 				return int.max;
7900 			if(mh > largest)
7901 				largest = mh;
7902 			margins += mymax(lastMargin, child.marginTop());
7903 			lastMargin = child.marginBottom();
7904 		}
7905 		return largest + margins;
7906 	}
7907 
7908 	override int minWidth() {
7909 		int min;
7910 		foreach(child; children) {
7911 			auto cm = child.minWidth;
7912 			if(cm > min)
7913 				min = cm;
7914 		}
7915 		return min + paddingLeft + paddingRight;
7916 	}
7917 
7918 	override int minHeight() {
7919 		int min;
7920 		foreach(child; children) {
7921 			auto cm = child.minHeight;
7922 			if(cm > min)
7923 				min = cm;
7924 		}
7925 		return min + paddingTop + paddingBottom;
7926 	}
7927 
7928 	override int maxHeight() {
7929 		int largest = 0;
7930 		int margins = 0;
7931 		int lastMargin = 0;
7932 		foreach(child; children) {
7933 			auto mh = child.maxHeight();
7934 			if(mh == int.max)
7935 				return int.max;
7936 			if(mh > largest)
7937 				largest = mh;
7938 			margins += mymax(lastMargin, child.marginTop());
7939 			lastMargin = child.marginBottom();
7940 		}
7941 		return largest + margins;
7942 	}
7943 
7944 	override int heightStretchiness() {
7945 		int max;
7946 		foreach(child; children) {
7947 			auto c = child.heightStretchiness;
7948 			if(c > max)
7949 				max = c;
7950 		}
7951 		return max;
7952 	}
7953 
7954 	override int marginTop() {
7955 		if(this.children.length)
7956 			return this.children[0].marginTop;
7957 		return 0;
7958 	}
7959 	+/
7960 }
7961 
7962 ///
7963 abstract class Layout : Widget {
7964 	this(Widget parent) {
7965 		tabStop = false;
7966 		super(parent);
7967 	}
7968 }
7969 
7970 /++
7971 	Makes all children minimum width and height, placing them down
7972 	left to right, top to bottom.
7973 
7974 	Useful if you want to make a list of buttons that automatically
7975 	wrap to a new line when necessary.
7976 +/
7977 class InlineBlockLayout : Layout {
7978 	///
7979 	this(Widget parent) { super(parent); }
7980 
7981 	override void recomputeChildLayout() {
7982 		registerMovement();
7983 
7984 		int x = this.paddingLeft, y = this.paddingTop;
7985 
7986 		int lineHeight;
7987 		int previousMargin = 0;
7988 		int previousMarginBottom = 0;
7989 
7990 		foreach(child; children) {
7991 			if(child.hidden)
7992 				continue;
7993 			if(cast(FixedPosition) child) {
7994 				child.recomputeChildLayout();
7995 				continue;
7996 			}
7997 			child.width = child.flexBasisWidth();
7998 			if(child.width == 0)
7999 				child.width = child.minWidth();
8000 			if(child.width == 0)
8001 				child.width = 32;
8002 
8003 			child.height = child.flexBasisHeight();
8004 			if(child.height == 0)
8005 				child.height = child.minHeight();
8006 			if(child.height == 0)
8007 				child.height = 32;
8008 
8009 			if(x + child.width + paddingRight > this.width) {
8010 				x = this.paddingLeft;
8011 				y += lineHeight;
8012 				lineHeight = 0;
8013 				previousMargin = 0;
8014 				previousMarginBottom = 0;
8015 			}
8016 
8017 			auto margin = child.marginLeft;
8018 			if(previousMargin > margin)
8019 				margin = previousMargin;
8020 
8021 			x += margin;
8022 
8023 			child.x = x;
8024 			child.y = y;
8025 
8026 			int marginTopApplied;
8027 			if(child.marginTop > previousMarginBottom) {
8028 				child.y += child.marginTop;
8029 				marginTopApplied = child.marginTop;
8030 			}
8031 
8032 			x += child.width;
8033 			previousMargin = child.marginRight;
8034 
8035 			if(child.marginBottom > previousMarginBottom)
8036 				previousMarginBottom = child.marginBottom;
8037 
8038 			auto h = child.height + previousMarginBottom + marginTopApplied;
8039 			if(h > lineHeight)
8040 				lineHeight = h;
8041 
8042 			child.recomputeChildLayout();
8043 		}
8044 
8045 	}
8046 
8047 	override int minWidth() {
8048 		int min;
8049 		foreach(child; children) {
8050 			auto cm = child.minWidth;
8051 			if(cm > min)
8052 				min = cm;
8053 		}
8054 		return min + paddingLeft + paddingRight;
8055 	}
8056 
8057 	override int minHeight() {
8058 		int min;
8059 		foreach(child; children) {
8060 			auto cm = child.minHeight;
8061 			if(cm > min)
8062 				min = cm;
8063 		}
8064 		return min + paddingTop + paddingBottom;
8065 	}
8066 }
8067 
8068 /++
8069 	A TabMessageWidget is a clickable row of tabs followed by a content area, very similar
8070 	to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas
8071 	the [TabWidget] will automatically change pages of child widgets.
8072 
8073 	This allows you to react to it however you see fit rather than having to
8074 	be tied to just the new sets of child widgets.
8075 
8076 	It sends the message in the form of `this.emitCommand!"changetab"();`.
8077 
8078 	History:
8079 		Added December 24, 2021 (dub v10.5)
8080 +/
8081 class TabMessageWidget : Widget {
8082 
8083 	protected void tabIndexClicked(int item) {
8084 		this.emitCommand!"changetab"();
8085 	}
8086 
8087 	/++
8088 		Adds the a new tab to the control with the given title.
8089 
8090 		Returns:
8091 			The index of the newly added tab. You will need to know
8092 			this index to refer to it later and to know which tab to
8093 			change to when you get a changetab message.
8094 	+/
8095 	int addTab(string title, int pos = int.max) {
8096 		version(win32_widgets) {
8097 			TCITEM item;
8098 			item.mask = TCIF_TEXT;
8099 			WCharzBuffer buf = WCharzBuffer(title);
8100 			item.pszText = buf.ptr;
8101 			return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item);
8102 		} else version(custom_widgets) {
8103 			if(pos >= tabs.length) {
8104 				tabs ~= title;
8105 				redraw();
8106 				return cast(int) tabs.length - 1;
8107 			} else if(pos <= 0) {
8108 				tabs = title ~ tabs;
8109 				redraw();
8110 				return 0;
8111 			} else {
8112 				tabs = tabs[0 .. pos] ~ title ~ title[pos .. $];
8113 				redraw();
8114 				return pos;
8115 			}
8116 		}
8117 	}
8118 
8119 	override void addChild(Widget child, int pos = int.max) {
8120 		if(container)
8121 			container.addChild(child, pos);
8122 		else
8123 			super.addChild(child, pos);
8124 	}
8125 
8126 	protected Widget makeContainer() {
8127 		return new Widget(this);
8128 	}
8129 
8130 	private Widget container;
8131 
8132 	override void recomputeChildLayout() {
8133 		version(win32_widgets) {
8134 			this.registerMovement();
8135 
8136 			RECT rect;
8137 			GetWindowRect(hwnd, &rect);
8138 
8139 			auto left = rect.left;
8140 			auto top = rect.top;
8141 
8142 			TabCtrl_AdjustRect(hwnd, false, &rect);
8143 			foreach(child; children) {
8144 				if(!child.showing) continue;
8145 				child.x = rect.left - left;
8146 				child.y = rect.top - top;
8147 				child.width = rect.right - rect.left;
8148 				child.height = rect.bottom - rect.top;
8149 				child.recomputeChildLayout();
8150 			}
8151 		} else version(custom_widgets) {
8152 			this.registerMovement();
8153 			foreach(child; children) {
8154 				if(!child.showing) continue;
8155 				child.x = 2;
8156 				child.y = tabBarHeight + 2; // for the border
8157 				child.width = width - 4; // for the border
8158 				child.height = height - tabBarHeight - 2 - 2; // for the border
8159 				child.recomputeChildLayout();
8160 			}
8161 		} else static assert(0);
8162 	}
8163 
8164 	this(Widget parent) {
8165 		super(parent);
8166 
8167 		tabStop = false;
8168 
8169 		version(win32_widgets) {
8170 			createWin32Window(this, WC_TABCONTROL, "", 0);
8171 		} else version(custom_widgets) {
8172 			addEventListener((ClickEvent event) {
8173 				if(event.target !is this)
8174 					return;
8175 				if(event.clientY >= 0 && event.clientY < tabBarHeight) {
8176 					auto t = (event.clientX / tabWidth);
8177 					if(t >= 0 && t < tabs.length) {
8178 						currentTab_ = t;
8179 						tabIndexClicked(t);
8180 						redraw();
8181 					}
8182 				}
8183 			});
8184 		} else static assert(0);
8185 
8186 		this.container = makeContainer();
8187 	}
8188 
8189 	override int marginTop() { return 4; }
8190 	override int paddingBottom() { return 4; }
8191 
8192 	override int minHeight() {
8193 		int max = 0;
8194 		foreach(child; children)
8195 			max = mymax(child.minHeight, max);
8196 
8197 
8198 		version(win32_widgets) {
8199 			RECT rect;
8200 			rect.right = this.width;
8201 			rect.bottom = max;
8202 			TabCtrl_AdjustRect(hwnd, true, &rect);
8203 
8204 			max = rect.bottom;
8205 		} else {
8206 			max += defaultLineHeight + 4;
8207 		}
8208 
8209 
8210 		return max;
8211 	}
8212 
8213 	version(win32_widgets)
8214 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
8215 		switch(code) {
8216 			case TCN_SELCHANGE:
8217 				auto sel = TabCtrl_GetCurSel(hwnd);
8218 				tabIndexClicked(sel);
8219 			break;
8220 			default:
8221 		}
8222 		return 0;
8223 	}
8224 
8225 	version(custom_widgets) {
8226 		private int currentTab_;
8227 		private int tabBarHeight() { return defaultLineHeight; }
8228 		int tabWidth() { return scaleWithDpi(80); }
8229 
8230 		string[] tabs;
8231 	}
8232 
8233 	version(win32_widgets)
8234 	override void paint(WidgetPainter painter) {}
8235 
8236 	version(custom_widgets)
8237 	override void paint(WidgetPainter painter) {
8238 		auto cs = getComputedStyle();
8239 
8240 		draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color);
8241 
8242 		int posX = 0;
8243 		foreach(idx, title; tabs) {
8244 			auto isCurrent = idx == getCurrentTab();
8245 
8246 			painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight);
8247 
8248 			draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1));
8249 			painter.outlineColor = cs.foregroundColor;
8250 			painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter);
8251 
8252 			if(isCurrent) {
8253 				painter.outlineColor = cs.windowBackgroundColor;
8254 				painter.fillColor = Color.transparent;
8255 				painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1));
8256 				painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2));
8257 
8258 				painter.outlineColor = Color.white;
8259 				painter.drawPixel(Point(posX + 1, tabBarHeight - 1));
8260 				painter.drawPixel(Point(posX + 1, tabBarHeight - 2));
8261 				painter.outlineColor = cs.activeTabColor;
8262 				painter.drawPixel(Point(posX, tabBarHeight - 1));
8263 			}
8264 
8265 			posX += tabWidth - 2;
8266 		}
8267 	}
8268 
8269 	///
8270 	@scriptable
8271 	void setCurrentTab(int item) {
8272 		version(win32_widgets)
8273 			TabCtrl_SetCurSel(hwnd, item);
8274 		else version(custom_widgets)
8275 			currentTab_ = item;
8276 		else static assert(0);
8277 
8278 		tabIndexClicked(item);
8279 	}
8280 
8281 	///
8282 	@scriptable
8283 	int getCurrentTab() {
8284 		version(win32_widgets)
8285 			return TabCtrl_GetCurSel(hwnd);
8286 		else version(custom_widgets)
8287 			return currentTab_; // FIXME
8288 		else static assert(0);
8289 	}
8290 
8291 	///
8292 	@scriptable
8293 	void removeTab(int item) {
8294 		if(item && item == getCurrentTab())
8295 			setCurrentTab(item - 1);
8296 
8297 		version(win32_widgets) {
8298 			TabCtrl_DeleteItem(hwnd, item);
8299 		}
8300 
8301 		for(int a = item; a < children.length - 1; a++)
8302 			this._children[a] = this._children[a + 1];
8303 		this._children = this._children[0 .. $-1];
8304 	}
8305 
8306 }
8307 
8308 
8309 /++
8310 	A tab widget is a set of clickable tab buttons followed by a content area.
8311 
8312 
8313 	Tabs can change existing content or can be new pages.
8314 
8315 	When the user picks a different tab, a `change` message is generated.
8316 +/
8317 class TabWidget : TabMessageWidget {
8318 	this(Widget parent) {
8319 		super(parent);
8320 	}
8321 
8322 	override protected Widget makeContainer() {
8323 		return null;
8324 	}
8325 
8326 	override void addChild(Widget child, int pos = int.max) {
8327 		if(auto twp = cast(TabWidgetPage) child) {
8328 			Widget.addChild(child, pos);
8329 			if(pos == int.max)
8330 				pos = cast(int) this.children.length - 1;
8331 
8332 			super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop...
8333 
8334 			if(pos != getCurrentTab) {
8335 				child.showing = false;
8336 			}
8337 		} else {
8338 			assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)");
8339 		}
8340 	}
8341 
8342 	// FIXME: add tab icons at some point, Windows supports them
8343 	/++
8344 		Adds a page and its associated tab with the given label to the widget.
8345 
8346 		Returns:
8347 			The added page object, to which you can add other widgets.
8348 	+/
8349 	@scriptable
8350 	TabWidgetPage addPage(string title) {
8351 		return new TabWidgetPage(title, this);
8352 	}
8353 
8354 	/++
8355 		Gets the page at the given tab index, or `null` if the index is bad.
8356 
8357 		History:
8358 			Added December 24, 2021.
8359 	+/
8360 	TabWidgetPage getPage(int index) {
8361 		if(index < this.children.length)
8362 			return null;
8363 		return cast(TabWidgetPage) this.children[index];
8364 	}
8365 
8366 	/++
8367 		While you can still use the addTab from the parent class,
8368 		*strongly* recommend you use [addPage] insteaad.
8369 
8370 		History:
8371 			Added December 24, 2021 to fulful the interface
8372 			requirement that came from adding [TabMessageWidget].
8373 
8374 			You should not use it though since the [addPage] function
8375 			is much easier to use here.
8376 	+/
8377 	override int addTab(string title, int pos = int.max) {
8378 		auto p = addPage(title);
8379 		foreach(idx, child; this.children)
8380 			if(child is p)
8381 				return cast(int) idx;
8382 		return -1;
8383 	}
8384 
8385 	protected override void tabIndexClicked(int item) {
8386 		super.tabIndexClicked(item);
8387 		foreach(idx, child; children) {
8388 			child.showing(false, false); // batch the recalculates for the end
8389 		}
8390 
8391 		foreach(idx, child; children) {
8392 			if(idx == item) {
8393 				child.showing(true, false);
8394 				if(parentWindow) {
8395 					auto f = parentWindow.getFirstFocusable(child);
8396 					if(f)
8397 						f.focus();
8398 				}
8399 				recomputeChildLayout();
8400 			}
8401 		}
8402 
8403 		version(win32_widgets) {
8404 			InvalidateRect(hwnd, null, true);
8405 		} else version(custom_widgets) {
8406 			this.redraw();
8407 		}
8408 	}
8409 
8410 }
8411 
8412 /++
8413 	A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget".
8414 
8415 	You add [TabWidgetPage]s to it.
8416 +/
8417 class PageWidget : Widget {
8418 	this(Widget parent) {
8419 		super(parent);
8420 	}
8421 
8422 	override int minHeight() {
8423 		int max = 0;
8424 		foreach(child; children)
8425 			max = mymax(child.minHeight, max);
8426 
8427 		return max;
8428 	}
8429 
8430 
8431 	override void addChild(Widget child, int pos = int.max) {
8432 		if(auto twp = cast(TabWidgetPage) child) {
8433 			super.addChild(child, pos);
8434 			if(pos == int.max)
8435 				pos = cast(int) this.children.length - 1;
8436 
8437 			if(pos != getCurrentTab) {
8438 				child.showing = false;
8439 			}
8440 		} else {
8441 			assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)");
8442 		}
8443 	}
8444 
8445 	override void recomputeChildLayout() {
8446 		this.registerMovement();
8447 		foreach(child; children) {
8448 			child.x = 0;
8449 			child.y = 0;
8450 			child.width = width;
8451 			child.height = height;
8452 			child.recomputeChildLayout();
8453 		}
8454 	}
8455 
8456 	private int currentTab_;
8457 
8458 	///
8459 	@scriptable
8460 	void setCurrentTab(int item) {
8461 		currentTab_ = item;
8462 
8463 		showOnly(item);
8464 	}
8465 
8466 	///
8467 	@scriptable
8468 	int getCurrentTab() {
8469 		return currentTab_;
8470 	}
8471 
8472 	///
8473 	@scriptable
8474 	void removeTab(int item) {
8475 		if(item && item == getCurrentTab())
8476 			setCurrentTab(item - 1);
8477 
8478 		for(int a = item; a < children.length - 1; a++)
8479 			this._children[a] = this._children[a + 1];
8480 		this._children = this._children[0 .. $-1];
8481 	}
8482 
8483 	///
8484 	@scriptable
8485 	TabWidgetPage addPage(string title) {
8486 		return new TabWidgetPage(title, this);
8487 	}
8488 
8489 	private void showOnly(int item) {
8490 		foreach(idx, child; children)
8491 			if(idx == item) {
8492 				child.show();
8493 				child.queueRecomputeChildLayout();
8494 			} else {
8495 				child.hide();
8496 			}
8497 	}
8498 }
8499 
8500 /++
8501 
8502 +/
8503 class TabWidgetPage : Widget {
8504 	this(string title, Widget parent) {
8505 		this.title_ = title;
8506 		this.tabStop = false;
8507 		super(parent);
8508 
8509 		///*
8510 		version(win32_widgets) {
8511 			createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0);
8512 		}
8513 		//*/
8514 	}
8515 
8516 	private string title_;
8517 
8518 	/++
8519 		History:
8520 			Prior to April 6, 2025, it was a public field. It was changed to properties so it can queue redraws;
8521 	+/
8522 	string title() {
8523 		return title_;
8524 	}
8525 
8526 	/// ditto
8527 	void title(string t) {
8528 		title_ = t;
8529 		version(custom_widgets) {
8530 			if(auto tw = cast(TabWidget) parent) {
8531 				foreach(idx, child; tw.children)
8532 					if(child is this)
8533 						tw.tabs[idx] = t;
8534 				tw.redraw();
8535 			}
8536 		}
8537 	}
8538 
8539 	override int minHeight() {
8540 		int sum = 0;
8541 		foreach(child; children)
8542 			sum += child.minHeight();
8543 		return sum;
8544 	}
8545 }
8546 
8547 version(none)
8548 /++
8549 	A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button.
8550 
8551 	I think I need to modify the layout algorithms to support this.
8552 +/
8553 class CollapsableSidebar : Widget {
8554 
8555 }
8556 
8557 /// Stacks the widgets vertically, taking all the available width for each child.
8558 class VerticalLayout : Layout {
8559 	// most of this is intentionally blank - widget's default is vertical layout right now
8560 	///
8561 	this(Widget parent) { super(parent); }
8562 
8563 	/++
8564 		Sets a max width for the layout so you don't have to subclass. The max width
8565 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
8566 
8567 		History:
8568 			Added November 29, 2021 (dub v10.5)
8569 	+/
8570 	this(int maxWidth, Widget parent) {
8571 		this.mw = maxWidth;
8572 		super(parent);
8573 	}
8574 
8575 	private int mw = int.max;
8576 
8577 	override int maxWidth() { return scaleWithDpi(mw); }
8578 }
8579 
8580 /// Stacks the widgets horizontally, taking all the available height for each child.
8581 class HorizontalLayout : Layout {
8582 	///
8583 	this(Widget parent) { super(parent); }
8584 
8585 	/++
8586 		Sets a max height for the layout so you don't have to subclass. The max height
8587 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
8588 
8589 		History:
8590 			Added November 29, 2021 (dub v10.5)
8591 	+/
8592 	this(int maxHeight, Widget parent) {
8593 		this.mh = maxHeight;
8594 		super(parent);
8595 	}
8596 
8597 	private int mh = 0;
8598 
8599 
8600 
8601 	override void recomputeChildLayout() {
8602 		.recomputeChildLayout!"width"(this);
8603 	}
8604 
8605 	override int minHeight() {
8606 		int largest = 0;
8607 		int margins = 0;
8608 		int lastMargin = 0;
8609 		foreach(child; children) {
8610 			auto mh = child.minHeight();
8611 			if(mh > largest)
8612 				largest = mh;
8613 			margins += mymax(lastMargin, child.marginTop());
8614 			lastMargin = child.marginBottom();
8615 		}
8616 		return largest + margins;
8617 	}
8618 
8619 	override int maxHeight() {
8620 		if(mh != 0)
8621 			return mymax(minHeight, scaleWithDpi(mh));
8622 
8623 		int largest = 0;
8624 		int margins = 0;
8625 		int lastMargin = 0;
8626 		foreach(child; children) {
8627 			auto mh = child.maxHeight();
8628 			if(mh == int.max)
8629 				return int.max;
8630 			if(mh > largest)
8631 				largest = mh;
8632 			margins += mymax(lastMargin, child.marginTop());
8633 			lastMargin = child.marginBottom();
8634 		}
8635 		return largest + margins;
8636 	}
8637 
8638 	override int heightStretchiness() {
8639 		int max;
8640 		foreach(child; children) {
8641 			auto c = child.heightStretchiness;
8642 			if(c > max)
8643 				max = c;
8644 		}
8645 		return max;
8646 	}
8647 }
8648 
8649 version(win32_widgets)
8650 private
8651 extern(Windows)
8652 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow {
8653 	Widget* pwin = hwnd in Widget.nativeMapping;
8654 	if(pwin is null)
8655 		return DefWindowProc(hwnd, message, wparam, lparam);
8656 	SimpleWindow win = pwin.simpleWindowWrappingHwnd;
8657 	if(win is null)
8658 		return DefWindowProc(hwnd, message, wparam, lparam);
8659 
8660 	switch(message) {
8661 		case WM_SIZE:
8662 			auto width = LOWORD(lparam);
8663 			auto height = HIWORD(lparam);
8664 
8665 			auto hdc = GetDC(hwnd);
8666 			auto hdcBmp = CreateCompatibleDC(hdc);
8667 
8668 			// FIXME: could this be more efficient? it never relinquishes a large bitmap
8669 			if(width > win.bmpWidth || height > win.bmpHeight) {
8670 				auto oldBuffer = win.buffer;
8671 				win.buffer = CreateCompatibleBitmap(hdc, width, height);
8672 
8673 				if(oldBuffer)
8674 					DeleteObject(oldBuffer);
8675 
8676 				win.bmpWidth = width;
8677 				win.bmpHeight = height;
8678 			}
8679 
8680 			// just always erase it upon resizing so minigui can draw over with a clean slate
8681 			auto oldBmp = SelectObject(hdcBmp, win.buffer);
8682 
8683 			auto brush = GetSysColorBrush(COLOR_3DFACE);
8684 			RECT r;
8685 			r.left = 0;
8686 			r.top = 0;
8687 			r.right = width;
8688 			r.bottom = height;
8689 			FillRect(hdcBmp, &r, brush);
8690 
8691 			SelectObject(hdcBmp, oldBmp);
8692 			DeleteDC(hdcBmp);
8693 			ReleaseDC(hwnd, hdc);
8694 		break;
8695 		case WM_PAINT:
8696 			if(win.buffer is null)
8697 				goto default;
8698 
8699 			BITMAP bm;
8700 			PAINTSTRUCT ps;
8701 
8702 			HDC hdc = BeginPaint(hwnd, &ps);
8703 
8704 			HDC hdcMem = CreateCompatibleDC(hdc);
8705 			HBITMAP hbmOld = SelectObject(hdcMem, win.buffer);
8706 
8707 			GetObject(win.buffer, bm.sizeof, &bm);
8708 
8709 			BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
8710 
8711 			SelectObject(hdcMem, hbmOld);
8712 			DeleteDC(hdcMem);
8713 			EndPaint(hwnd, &ps);
8714 		break;
8715 		default:
8716 			return DefWindowProc(hwnd, message, wparam, lparam);
8717 	}
8718 
8719 	return 0;
8720 }
8721 
8722 private wstring Win32Class(wstring name)() {
8723 	static bool classRegistered;
8724 	if(!classRegistered) {
8725 		HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null);
8726 		WNDCLASSEX wc;
8727 		wc.cbSize = wc.sizeof;
8728 		wc.hInstance = hInstance;
8729 		wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH);
8730 		wc.lpfnWndProc = &DoubleBufferWndProc;
8731 		wc.lpszClassName = name.ptr;
8732 		if(!RegisterClassExW(&wc))
8733 			throw new Exception("RegisterClass ");// ~ to!string(GetLastError()));
8734 		classRegistered = true;
8735 	}
8736 
8737 		return name;
8738 }
8739 
8740 /+
8741 version(win32_widgets)
8742 extern(Windows)
8743 private
8744 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
8745 	switch(iMessage) {
8746 		case WM_PAINT:
8747 			if(auto te = hWnd in Widget.nativeMapping) {
8748 				try {
8749 					//te.redraw();
8750 					writeln(te, " drawing");
8751 				} catch(Exception) {}
8752 			}
8753 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
8754 		default:
8755 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
8756 	}
8757 }
8758 +/
8759 
8760 
8761 /++
8762 	A widget specifically designed to hold other widgets.
8763 
8764 	History:
8765 		Added July 1, 2021
8766 +/
8767 class ContainerWidget : Widget {
8768 	this(Widget parent) {
8769 		super(parent);
8770 		this.tabStop = false;
8771 
8772 		version(win32_widgets) {
8773 			createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0);
8774 		}
8775 	}
8776 }
8777 
8778 /++
8779 	A widget that takes your widget, puts scroll bars around it, and sends
8780 	messages to it when the user scrolls. Unlike [ScrollableWidget], it makes
8781 	no effort to automatically scroll or clip its child widgets - it just sends
8782 	the messages.
8783 
8784 
8785 	A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed.
8786 	The scroll coordinates are all given in a unit you interpret as you wish. One
8787 	of these units is moved on each press of the arrow buttons and represents the
8788 	smallest amount the user can scroll. The intention is for this to be one line,
8789 	one item in a list, one row in a table, etc. Whatever makes sense for your widget
8790 	in each direction that the user might be interested in.
8791 
8792 	You can set a "page size" with the [step] property. (Yes, I regret the name...)
8793 	This is the amount it jumps when the user pressed page up and page down, or clicks
8794 	in the exposed part of the scroll bar.
8795 
8796 	You should add child content to the ScrollMessageWidget. However, it is important to
8797 	note that the coordinates are always independent of the scroll position! It is YOUR
8798 	responsibility to do any necessary transforms, clipping, etc., while drawing the
8799 	content and interpreting mouse events if they are supposed to change with the scroll.
8800 	This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries
8801 	to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives
8802 	you more control (which can be considerably more efficient and adapted to your actual data)
8803 	at the expense of you also needing to be aware of its reality.
8804 
8805 	Please note that it does NOT react to mouse wheel events or various keyboard events as of
8806 	version 10.3. Maybe this will change in the future.... but for now you must call
8807 	[addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself.
8808 +/
8809 class ScrollMessageWidget : Widget {
8810 	this(Widget parent) {
8811 		super(parent);
8812 
8813 		container = new Widget(this);
8814 		hsb = new HorizontalScrollbar(this);
8815 		vsb = new VerticalScrollbar(this);
8816 
8817 		hsb.addEventListener("scrolltonextline", {
8818 			hsb.setPosition(hsb.position + movementPerButtonClickH_);
8819 			notify();
8820 		});
8821 		hsb.addEventListener("scrolltopreviousline", {
8822 			hsb.setPosition(hsb.position - movementPerButtonClickH_);
8823 			notify();
8824 		});
8825 		vsb.addEventListener("scrolltonextline", {
8826 			vsb.setPosition(vsb.position + movementPerButtonClickV_);
8827 			notify();
8828 		});
8829 		vsb.addEventListener("scrolltopreviousline", {
8830 			vsb.setPosition(vsb.position - movementPerButtonClickV_);
8831 			notify();
8832 		});
8833 		hsb.addEventListener("scrolltonextpage", {
8834 			hsb.setPosition(hsb.position + hsb.step_);
8835 			notify();
8836 		});
8837 		hsb.addEventListener("scrolltopreviouspage", {
8838 			hsb.setPosition(hsb.position - hsb.step_);
8839 			notify();
8840 		});
8841 		vsb.addEventListener("scrolltonextpage", {
8842 			vsb.setPosition(vsb.position + vsb.step_);
8843 			notify();
8844 		});
8845 		vsb.addEventListener("scrolltopreviouspage", {
8846 			vsb.setPosition(vsb.position - vsb.step_);
8847 			notify();
8848 		});
8849 		hsb.addEventListener("scrolltoposition", (Event event) {
8850 			hsb.setPosition(event.intValue);
8851 			notify();
8852 		});
8853 		vsb.addEventListener("scrolltoposition", (Event event) {
8854 			vsb.setPosition(event.intValue);
8855 			notify();
8856 		});
8857 
8858 
8859 		tabStop = false;
8860 		container.tabStop = false;
8861 		magic = true;
8862 	}
8863 
8864 	private int movementPerButtonClickH_ = 1;
8865 	private int movementPerButtonClickV_ = 1;
8866 	public void movementPerButtonClick(int h, int v) {
8867 		movementPerButtonClickH_ = h;
8868 		movementPerButtonClickV_ = v;
8869 	}
8870 
8871 	/++
8872 		Add default event listeners for keyboard and mouse wheel scrolling shortcuts.
8873 
8874 
8875 		The defaults for [addDefaultWheelListeners] are:
8876 
8877 			$(LIST
8878 				* Mouse wheel scrolls vertically
8879 				* Alt key + mouse wheel scrolls horiontally
8880 				* Shift + mouse wheel scrolls faster.
8881 				* Any mouse click or wheel event will focus the inner widget if it has `tabStop = true`
8882 			)
8883 
8884 		The defaults for [addDefaultKeyboardListeners] are:
8885 
8886 			$(LIST
8887 				* Arrow keys scroll by the given amounts
8888 				* Shift+arrow keys scroll by the given amounts times the given shiftMultiplier
8889 				* Page up and down scroll by the vertical viewable area
8890 				* Home and end scroll to the start and end of the verticle viewable area.
8891 				* Alt + page up / page down / home / end will horizonally scroll instead of vertical.
8892 			)
8893 
8894 		My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line.
8895 
8896 		Params:
8897 			horizontalArrowScrollAmount =
8898 			verticalArrowScrollAmount =
8899 			verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel
8900 			horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel
8901 			shiftMultiplier = multiplies the scroll amount by this when shift is held
8902 	+/
8903 	void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) {
8904 		defaultKeyboardListener_verticalArrowScrollAmount = verticalArrowScrollAmount;
8905 		defaultKeyboardListener_horizontalArrowScrollAmount = horizontalArrowScrollAmount;
8906 		defaultKeyboardListener_shiftMultiplier = shiftMultiplier;
8907 
8908 		container.addEventListener(&defaultKeyboardListener);
8909 	}
8910 
8911 	/// ditto
8912 	void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) {
8913 		auto _this = this;
8914 		container.addEventListener((scope ClickEvent ce) {
8915 
8916 			//if(ce.target && ce.target.tabStop)
8917 				//ce.target.focus();
8918 
8919 			// ctrl is reserved for the application
8920 			if(ce.ctrlKey)
8921 				return;
8922 
8923 			if(horizontalWheelScrollAmount == 0 && ce.altKey)
8924 				return;
8925 
8926 			if(shiftMultiplier == 0 && ce.shiftKey)
8927 				return;
8928 
8929 			if(ce.button == MouseButton.wheelDown) {
8930 				if(ce.altKey)
8931 					_this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8932 				else
8933 					_this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8934 			} else if(ce.button == MouseButton.wheelUp) {
8935 				if(ce.altKey)
8936 					_this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8937 				else
8938 					_this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8939 			}
8940 			else
8941 			if(ce.button == MouseButton.wheelRight) {
8942 				if(ce.altKey)
8943 					_this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8944 				else
8945 					_this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8946 			} else if(ce.button == MouseButton.wheelLeft) {
8947 				if(ce.altKey)
8948 					_this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8949 				else
8950 					_this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8951 			}
8952 
8953 		});
8954 	}
8955 
8956 	int defaultKeyboardListener_verticalArrowScrollAmount = 1;
8957 	int defaultKeyboardListener_horizontalArrowScrollAmount = 1;
8958 	int defaultKeyboardListener_shiftMultiplier = 3;
8959 
8960 	void defaultKeyboardListener(scope KeyDownEvent ke) {
8961 		switch(ke.key) {
8962 			case Key.Left:
8963 				this.scrollLeft(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8964 			break;
8965 			case Key.Right:
8966 				this.scrollRight(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8967 			break;
8968 			case Key.Up:
8969 				this.scrollUp(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8970 			break;
8971 			case Key.Down:
8972 				this.scrollDown(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8973 			break;
8974 			case Key.PageUp:
8975 				if(ke.altKey)
8976 					this.scrollLeft(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8977 				else
8978 					this.scrollUp(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8979 			break;
8980 			case Key.PageDown:
8981 				if(ke.altKey)
8982 					this.scrollRight(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8983 				else
8984 					this.scrollDown(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8985 			break;
8986 			case Key.Home:
8987 				if(ke.altKey)
8988 					this.scrollLeft(short.max * 16);
8989 				else
8990 					this.scrollUp(short.max * 16);
8991 			break;
8992 			case Key.End:
8993 				if(ke.altKey)
8994 					this.scrollRight(short.max * 16);
8995 				else
8996 					this.scrollDown(short.max * 16);
8997 			break;
8998 
8999 			default:
9000 				// ignore, not for us.
9001 		}
9002 	}
9003 
9004 	/++
9005 		Scrolls the given amount.
9006 
9007 		History:
9008 			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.
9009 	+/
9010 	void scrollUp(int amount = 1) {
9011 		vsb.setPosition(vsb.position.NonOverflowingInt - amount);
9012 		notify();
9013 	}
9014 	/// ditto
9015 	void scrollDown(int amount = 1) {
9016 		vsb.setPosition(vsb.position.NonOverflowingInt + amount);
9017 		notify();
9018 	}
9019 	/// ditto
9020 	void scrollLeft(int amount = 1) {
9021 		hsb.setPosition(hsb.position.NonOverflowingInt - amount);
9022 		notify();
9023 	}
9024 	/// ditto
9025 	void scrollRight(int amount = 1) {
9026 		hsb.setPosition(hsb.position.NonOverflowingInt + amount);
9027 		notify();
9028 	}
9029 
9030 	///
9031 	VerticalScrollbar verticalScrollBar() { return vsb; }
9032 	///
9033 	HorizontalScrollbar horizontalScrollBar() { return hsb; }
9034 
9035 	void notify() {
9036 		static bool insideNotify;
9037 
9038 		if(insideNotify)
9039 			return; // avoid the recursive call, even if it isn't strictly correct
9040 
9041 		insideNotify = true;
9042 		scope(exit) insideNotify = false;
9043 
9044 		this.emit!ScrollEvent();
9045 	}
9046 
9047 	mixin Emits!ScrollEvent;
9048 
9049 	///
9050 	Point position() {
9051 		return Point(hsb.position, vsb.position);
9052 	}
9053 
9054 	///
9055 	void setPosition(int x, int y) {
9056 		hsb.setPosition(x);
9057 		vsb.setPosition(y);
9058 	}
9059 
9060 	///
9061 	void setPageSize(int unitsX, int unitsY) {
9062 		hsb.setStep(unitsX);
9063 		vsb.setStep(unitsY);
9064 	}
9065 
9066 	/// Always call this BEFORE setViewableArea
9067 	void setTotalArea(int width, int height) {
9068 		hsb.setMax(width);
9069 		vsb.setMax(height);
9070 	}
9071 
9072 	/++
9073 		Always set the viewable area AFTER setitng the total area if you are going to change both.
9074 		NEVER call this from inside a scroll event. This includes through recomputeChildLayout.
9075 		If you need to do that, use [queueRecomputeChildLayout].
9076 	+/
9077 	void setViewableArea(int width, int height) {
9078 
9079 		// actually there IS A need to dothis cuz the max might have changed since then
9080 		//if(width == hsb.viewableArea_ && height == vsb.viewableArea_)
9081 			//return; // no need to do what is already done
9082 		hsb.setViewableArea(width);
9083 		vsb.setViewableArea(height);
9084 
9085 		bool needsNotify = false;
9086 
9087 		// FIXME: if at any point the rhs is outside the scrollbar, we need
9088 		// to reset to 0. but it should remember the old position in case the
9089 		// window resizes again, so it can kinda return ot where it was.
9090 		//
9091 		// so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0)
9092 		if(width >= hsb.max) {
9093 			// there's plenty of room to display it all so we need to reset to zero
9094 			// FIXME: adjust so it matches the note above
9095 			hsb.setPosition(0);
9096 			needsNotify = true;
9097 		}
9098 		if(height >= vsb.max) {
9099 			// there's plenty of room to display it all so we need to reset to zero
9100 			// FIXME: adjust so it matches the note above
9101 			vsb.setPosition(0);
9102 			needsNotify = true;
9103 		}
9104 		if(needsNotify)
9105 			notify();
9106 	}
9107 
9108 	private bool magic;
9109 	override void addChild(Widget w, int position = int.max) {
9110 		if(magic)
9111 			container.addChild(w, position);
9112 		else
9113 			super.addChild(w, position);
9114 	}
9115 
9116 	override void recomputeChildLayout() {
9117 		if(hsb is null || vsb is null || container is null) return;
9118 
9119 		registerMovement();
9120 
9121 		enum BUTTON_SIZE = 16;
9122 
9123 		hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane?
9124 		hsb.x = 0;
9125 		hsb.y = this.height - hsb.height;
9126 
9127 		vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME?
9128 		vsb.x = this.width - vsb.width;
9129 		vsb.y = 0;
9130 
9131 		auto vsb_width = vsb.showing ? vsb.width : 0;
9132 		auto hsb_height = hsb.showing ? hsb.height : 0;
9133 
9134 		hsb.width = this.width - vsb_width;
9135 		vsb.height = this.height - hsb_height;
9136 
9137 		hsb.recomputeChildLayout();
9138 		vsb.recomputeChildLayout();
9139 
9140 		if(this.header is null) {
9141 			container.x = 0;
9142 			container.y = 0;
9143 			container.width = this.width - vsb_width;
9144 			container.height = this.height - hsb_height;
9145 			container.recomputeChildLayout();
9146 		} else {
9147 			header.x = 0;
9148 			header.y = 0;
9149 			header.width = this.width - vsb_width;
9150 			header.height = scaleWithDpi(BUTTON_SIZE); // size of the button
9151 			header.recomputeChildLayout();
9152 
9153 			container.x = 0;
9154 			container.y = scaleWithDpi(BUTTON_SIZE);
9155 			container.width = this.width - vsb_width;
9156 			container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE);
9157 			container.recomputeChildLayout();
9158 		}
9159 	}
9160 
9161 	private HorizontalScrollbar hsb;
9162 	private VerticalScrollbar vsb;
9163 	Widget container;
9164 	private Widget header;
9165 
9166 	/++
9167 		Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button.
9168 
9169 		History:
9170 			Added September 27, 2021 (dub v10.3)
9171 	+/
9172 	Widget getHeader() {
9173 		if(this.header is null) {
9174 			magic = false;
9175 			scope(exit) magic = true;
9176 			this.header = new Widget(this);
9177 			queueRecomputeChildLayout();
9178 		}
9179 		return this.header;
9180 	}
9181 
9182 	/++
9183 		Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary.
9184 
9185 		History:
9186 			Added January 3, 2023 (dub v11.0)
9187 	+/
9188 	void scrollIntoView(Rectangle rect) {
9189 		Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_));
9190 
9191 		// import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1)));
9192 
9193 		// the lower right is exclusive normally
9194 		auto test = rect.lowerRight;
9195 		if(test.x > 0) test.x--;
9196 		if(test.y > 0) test.y--;
9197 
9198 		if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) {
9199 			// try to scroll only one dimension at a time if we can
9200 			if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y)))
9201 				setPosition(rect.upperLeft.x, position.y);
9202 			if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y)))
9203 				setPosition(position.x, rect.upperLeft.y);
9204 		}
9205 
9206 	}
9207 
9208 	override int minHeight() {
9209 		int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0));
9210 		if(header !is null)
9211 			min += header.minHeight;
9212 		if(horizontalScrollBar.showing)
9213 			min += horizontalScrollBar.minHeight;
9214 		return min;
9215 	}
9216 
9217 	override int maxHeight() {
9218 		int max = container ? container.maxHeight : int.max;
9219 		if(max == int.max)
9220 			return max;
9221 		if(horizontalScrollBar.showing)
9222 			max += horizontalScrollBar.minHeight;
9223 		return max;
9224 	}
9225 
9226 	static class Style : Widget.Style {
9227 		override WidgetBackground background() {
9228 			return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
9229 		}
9230 	}
9231 	mixin OverrideStyle!Style;
9232 }
9233 
9234 /++
9235 	$(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")
9236 	$(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.)
9237 +/
9238 version(minigui_screenshots)
9239 @Screenshot("ScrollMessageWidget")
9240 unittest {
9241 	auto window = new Window("ScrollMessageWidget");
9242 
9243 	auto smw = new ScrollMessageWidget(window);
9244 	smw.addDefaultKeyboardListeners();
9245 	smw.addDefaultWheelListeners();
9246 
9247 	window.loop();
9248 }
9249 
9250 /++
9251 	Bypasses automatic layout for its children, using manual positioning and sizing only.
9252 	While you need to manually position them, you must ensure they are inside the StaticLayout's
9253 	bounding box to avoid undefined behavior.
9254 
9255 	You should almost never use this.
9256 +/
9257 class StaticLayout : Layout {
9258 	///
9259 	this(Widget parent) { super(parent); }
9260 	override void recomputeChildLayout() {
9261 		registerMovement();
9262 		foreach(child; children)
9263 			child.recomputeChildLayout();
9264 	}
9265 }
9266 
9267 /++
9268 	Bypasses automatic positioning when being laid out. It is your responsibility to make
9269 	room for this widget in the parent layout.
9270 
9271 	Its children are laid out normally, unless there is exactly one, in which case it takes
9272 	on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you
9273 	can do that with `padding`).
9274 +/
9275 class StaticPosition : Layout {
9276 	///
9277 	this(Widget parent) { super(parent); }
9278 
9279 	override void recomputeChildLayout() {
9280 		registerMovement();
9281 		if(this.children.length == 1) {
9282 			auto child = children[0];
9283 			child.x = 0;
9284 			child.y = 0;
9285 			child.width = this.width;
9286 			child.height = this.height;
9287 			child.recomputeChildLayout();
9288 		} else
9289 		foreach(child; children)
9290 			child.recomputeChildLayout();
9291 	}
9292 
9293 	alias width = typeof(super).width;
9294 	alias height = typeof(super).height;
9295 
9296 	@property int width(int w) @nogc pure @safe nothrow {
9297 		return this._width = w;
9298 	}
9299 
9300 	@property int height(int w) @nogc pure @safe nothrow {
9301 		return this._height = w;
9302 	}
9303 
9304 }
9305 
9306 /++
9307 	FixedPosition is like [StaticPosition], but its coordinates
9308 	are always relative to the viewport, meaning they do not scroll with
9309 	the parent content.
9310 +/
9311 class FixedPosition : StaticPosition {
9312 	///
9313 	this(Widget parent) { super(parent); }
9314 }
9315 
9316 version(win32_widgets)
9317 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) {
9318 	if(true) {
9319 		// cmd == 0 = menu, cmd == 1 = accelerator
9320 		if(auto item = idm in Action.mapping) {
9321 			foreach(handler; (*item).triggered)
9322 				handler();
9323 		/*
9324 			auto event = new Event("triggered", *item);
9325 			event.button = idm;
9326 			event.dispatch();
9327 		*/
9328 			return 0;
9329 		}
9330 	}
9331 	if(handle)
9332 	if(auto widgetp = handle in Widget.nativeMapping) {
9333 		(*widgetp).handleWmCommand(cmd, idm);
9334 		return 0;
9335 	}
9336 	return 1;
9337 }
9338 
9339 
9340 ///
9341 class Window : Widget {
9342 	Widget[] mouseCapturedBy;
9343 	void captureMouse(Widget byWhom) {
9344 		assert(byWhom !is null);
9345 		if(mouseCapturedBy.length > 0) {
9346 			auto cc = mouseCapturedBy[$-1];
9347 			if(cc is byWhom)
9348 				return; // or should it throw?
9349 			auto par = byWhom;
9350 			while(par) {
9351 				if(cc is par)
9352 					goto allowed;
9353 				par = par.parent;
9354 			}
9355 
9356 			throw new Exception("mouse is already captured by other widget");
9357 		}
9358 		allowed:
9359 		mouseCapturedBy ~= byWhom;
9360 		if(mouseCapturedBy.length == 1)
9361 			win.grabInput(false, true, false);
9362 		//void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) {
9363 	}
9364 	void releaseMouseCapture() {
9365 		if(mouseCapturedBy.length == 0)
9366 			return; // or should it throw?
9367 		mouseCapturedBy = mouseCapturedBy[0 .. $-1];
9368 		mouseCapturedBy.assumeSafeAppend();
9369 		if(mouseCapturedBy.length == 0)
9370 			win.releaseInputGrab();
9371 	}
9372 
9373 
9374 	/++
9375 
9376 	+/
9377 	MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
9378 		return .messageBox(this, title, message, style, icon);
9379 	}
9380 
9381 	/// ditto
9382 	int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
9383 		return messageBox(null, message, style, icon);
9384 	}
9385 
9386 
9387 	/++
9388 		Sets the window icon which is often seen in title bars and taskbars.
9389 
9390 		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.
9391 
9392 		History:
9393 			Added April 5, 2022 (dub v10.8)
9394 	+/
9395 	@property void icon(MemoryImage icon) {
9396 		if(win && icon)
9397 			win.icon = icon;
9398 	}
9399 
9400 	// 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
9401 	// this does NOT change the icon on the window! That's what the other overload is for
9402 	static @property .icon icon(GenericIcons i) {
9403 		return .icon(i);
9404 	}
9405 
9406 	///
9407 	@scriptable
9408 	@property bool focused() {
9409 		return win.focused;
9410 	}
9411 
9412 	static class Style : Widget.Style {
9413 		override WidgetBackground background() {
9414 			version(custom_widgets)
9415 				return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
9416 			else version(win32_widgets)
9417 				return WidgetBackground(Color.transparent);
9418 			else static assert(0);
9419 		}
9420 	}
9421 	mixin OverrideStyle!Style;
9422 
9423 	/++
9424 		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.
9425 	+/
9426 	deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() {
9427 		return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback();
9428 	}
9429 
9430 	private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() {
9431 		OperatingSystemFont font;
9432 		if(auto vt = WidgetPainter.visualTheme) {
9433 			font = vt.defaultFontCached(96); // FIXME
9434 		}
9435 
9436 		if(font is null) {
9437 			static int defaultHeightCache;
9438 			if(defaultHeightCache == 0) {
9439 				font = new OperatingSystemFont;
9440 				font.loadDefault;
9441 				defaultHeightCache = castFnumToCnum(font.height());// * 5 / 4;
9442 			}
9443 			return defaultHeightCache;
9444 		}
9445 
9446 		return castFnumToCnum(font.height());// * 5 / 4;
9447 	}
9448 
9449 	Widget focusedWidget;
9450 
9451 	private SimpleWindow win_;
9452 
9453 	@property {
9454 		/++
9455 			Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers.
9456 
9457 			History:
9458 				Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property.
9459 		+/
9460 		public SimpleWindow win() {
9461 			return win_;
9462 		}
9463 		///
9464 		protected void win(SimpleWindow w) {
9465 			win_ = w;
9466 		}
9467 	}
9468 
9469 	/// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing.
9470 	this(Widget p) {
9471 		tabStop = false;
9472 		super(p);
9473 	}
9474 
9475 	private void actualRedraw() {
9476 		if(recomputeChildLayoutRequired)
9477 			recomputeChildLayoutEntry();
9478 		if(!showing) return;
9479 
9480 		assert(parentWindow !is null);
9481 
9482 		auto w = drawableWindow;
9483 		if(w is null)
9484 			w = parentWindow.win;
9485 
9486 		if(w.closed())
9487 			return;
9488 
9489 		auto ugh = this.parent;
9490 		int lox, loy;
9491 		while(ugh) {
9492 			lox += ugh.x;
9493 			loy += ugh.y;
9494 			ugh = ugh.parent;
9495 		}
9496 		auto painter = w.draw(true);
9497 		privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw());
9498 	}
9499 
9500 
9501 	private bool skipNextChar = false;
9502 
9503 	/++
9504 		Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers.
9505 
9506 		This constructor is intended primarily for internal use and may be changed to `protected` later.
9507 	+/
9508 	this(SimpleWindow win) {
9509 
9510 		static if(UsingSimpledisplayX11) {
9511 			win.discardAdditionalConnectionState = &discardXConnectionState;
9512 			win.recreateAdditionalConnectionState = &recreateXConnectionState;
9513 		}
9514 
9515 		tabStop = false;
9516 		super(null);
9517 		this.win = win;
9518 
9519 		win.addEventListener((Widget.RedrawEvent) {
9520 			if(win.eventQueued!RecomputeEvent) {
9521 				// writeln("skipping");
9522 				return; // let the recompute event do the actual redraw
9523 			}
9524 			this.actualRedraw();
9525 		});
9526 
9527 		win.addEventListener((Widget.RecomputeEvent) {
9528 			recomputeChildLayoutEntry();
9529 			if(win.eventQueued!RedrawEvent)
9530 				return; // let the queued one do it
9531 			else {
9532 				// writeln("drawing");
9533 				this.actualRedraw(); // if not queued, it needs to be done now anyway
9534 			}
9535 		});
9536 
9537 		this.width = win.width;
9538 		this.height = win.height;
9539 		this.parentWindow = this;
9540 
9541 		win.closeQuery = () {
9542 			if(this.emit!ClosingEvent())
9543 				win.close();
9544 		};
9545 		win.onClosing = () {
9546 			this.emit!ClosedEvent();
9547 		};
9548 
9549 		win.windowResized = (int w, int h) {
9550 			this.width = w;
9551 			this.height = h;
9552 			queueRecomputeChildLayout();
9553 			// this causes a HUGE performance problem for no apparent benefit, hence the commenting
9554 			//version(win32_widgets)
9555 				//InvalidateRect(hwnd, null, true);
9556 			redraw();
9557 		};
9558 
9559 		win.onFocusChange = (bool getting) {
9560 			// sdpyPrintDebugString("onFocusChange ", getting, " ", this.toString);
9561 			if(this.focusedWidget) {
9562 				if(getting) {
9563 					this.focusedWidget.emit!FocusEvent();
9564 					this.focusedWidget.emit!FocusInEvent();
9565 				} else {
9566 					this.focusedWidget.emit!BlurEvent();
9567 					this.focusedWidget.emit!FocusOutEvent();
9568 				}
9569 			}
9570 
9571 			if(getting) {
9572 				this.emit!FocusEvent();
9573 				this.emit!FocusInEvent();
9574 			} else {
9575 				this.emit!BlurEvent();
9576 				this.emit!FocusOutEvent();
9577 			}
9578 		};
9579 
9580 		win.onDpiChanged = {
9581 			this.queueRecomputeChildLayout();
9582 			auto event = new DpiChangedEvent(this);
9583 			event.sendDirectly();
9584 
9585 			privateDpiChanged();
9586 		};
9587 
9588 		win.setEventHandlers(
9589 			(MouseEvent e) {
9590 				dispatchMouseEvent(e);
9591 			},
9592 			(KeyEvent e) {
9593 				//writefln("%x   %s", cast(uint) e.key, e.key);
9594 				dispatchKeyEvent(e);
9595 			},
9596 			(dchar e) {
9597 				if(e == 13) e = 10; // hack?
9598 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
9599 				dispatchCharEvent(e);
9600 			},
9601 		);
9602 
9603 		addEventListener("char", (Widget, Event ev) {
9604 			if(skipNextChar) {
9605 				ev.preventDefault();
9606 				skipNextChar = false;
9607 			}
9608 		});
9609 
9610 		version(win32_widgets)
9611 		win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
9612 			if(hwnd !is this.win.impl.hwnd)
9613 				return 1; // we don't care... pass it on
9614 			auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn);
9615 			if(mustReturn)
9616 				return ret;
9617 			return 1; // pass it on
9618 		};
9619 
9620 		if(Window.newWindowCreated)
9621 			Window.newWindowCreated(this);
9622 	}
9623 
9624 	version(custom_widgets)
9625 	override void defaultEventHandler_click(ClickEvent event) {
9626 		if(!event.isMouseWheel()) {
9627 			if(event.target && event.target.tabStop)
9628 				event.target.focus();
9629 		}
9630 	}
9631 
9632 	private static void delegate(Window) newWindowCreated;
9633 
9634 	version(win32_widgets)
9635 	override void paint(WidgetPainter painter) {
9636 		/*
9637 		RECT rect;
9638 		rect.right = this.width;
9639 		rect.bottom = this.height;
9640 		DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null);
9641 		*/
9642 		// 3dface is used as window backgrounds by Windows too, so that's why I'm using it here
9643 		auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
9644 		auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
9645 		// since the pen is null, to fill the whole space, we need the +1 on both.
9646 		gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
9647 		SelectObject(painter.impl.hdc, p);
9648 		SelectObject(painter.impl.hdc, b);
9649 	}
9650 	version(custom_widgets)
9651 	override void paint(WidgetPainter painter) {
9652 		auto cs = getComputedStyle();
9653 		painter.fillColor = cs.windowBackgroundColor;
9654 		painter.outlineColor = cs.windowBackgroundColor;
9655 		painter.drawRectangle(Point(0, 0), this.width, this.height);
9656 	}
9657 
9658 
9659 	override void defaultEventHandler_keydown(KeyDownEvent event) {
9660 		Widget _this = event.target;
9661 
9662 		if(event.key == Key.Tab) {
9663 			/* Window tab ordering is a recursive thingy with each group */
9664 
9665 			// FIXME inefficient
9666 			Widget[] helper(Widget p) {
9667 				if(p.hidden)
9668 					return null;
9669 				Widget[] childOrdering;
9670 
9671 				auto children = p.children.dup;
9672 
9673 				while(true) {
9674 					// UIs should be generally small, so gonna brute force it a little
9675 					// note that it must be a stable sort here; if all are index 0, it should be in order of declaration
9676 
9677 					Widget smallestTab;
9678 					foreach(ref c; children) {
9679 						if(c is null) continue;
9680 						if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) {
9681 							smallestTab = c;
9682 							c = null;
9683 						}
9684 					}
9685 					if(smallestTab !is null) {
9686 						if(smallestTab.tabStop && !smallestTab.hidden)
9687 							childOrdering ~= smallestTab;
9688 						if(!smallestTab.hidden)
9689 							childOrdering ~= helper(smallestTab);
9690 					} else
9691 						break;
9692 
9693 				}
9694 
9695 				return childOrdering;
9696 			}
9697 
9698 			Widget[] tabOrdering = helper(this);
9699 
9700 			Widget recipient;
9701 
9702 			if(tabOrdering.length) {
9703 				bool seenThis = false;
9704 				Widget previous;
9705 				foreach(idx, child; tabOrdering) {
9706 					if(child is focusedWidget) {
9707 
9708 						if(event.shiftKey) {
9709 							if(idx == 0)
9710 								recipient = tabOrdering[$-1];
9711 							else
9712 								recipient = tabOrdering[idx - 1];
9713 							break;
9714 						}
9715 
9716 						seenThis = true;
9717 						if(idx + 1 == tabOrdering.length) {
9718 							// we're at the end, either move to the next group
9719 							// or start back over
9720 							recipient = tabOrdering[0];
9721 						}
9722 						continue;
9723 					}
9724 					if(seenThis) {
9725 						recipient = child;
9726 						break;
9727 					}
9728 					previous = child;
9729 				}
9730 			}
9731 
9732 			if(recipient !is null) {
9733 				//  writeln(typeid(recipient));
9734 				recipient.focus();
9735 
9736 				skipNextChar = true;
9737 			}
9738 		}
9739 
9740 		debug if(event.key == Key.F12) {
9741 			if(devTools) {
9742 				devTools.close();
9743 				devTools = null;
9744 			} else {
9745 				devTools = new DevToolWindow(this);
9746 				devTools.show();
9747 			}
9748 		}
9749 	}
9750 
9751 	debug DevToolWindow devTools;
9752 
9753 
9754 	/++
9755 		Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display.
9756 
9757 		History:
9758 			Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead.
9759 
9760 			The width and height arguments were added to the overload that takes `string` first on June 21, 2021.
9761 	+/
9762 	this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
9763 		if(title is null) {
9764 			import core.runtime;
9765 			if(Runtime.args.length)
9766 				title = Runtime.args[0];
9767 		}
9768 		win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent);
9769 
9770 		static if(UsingSimpledisplayX11)
9771 		if(windowFlags & WindowFlags.managesChildWindowFocus) {
9772 		///+
9773 		// for input proxy
9774 		auto display = XDisplayConnection.get;
9775 		auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0);
9776 		XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask);
9777 		XMapWindow(display, inputProxy);
9778 		// writefln("input proxy: 0x%0x", inputProxy);
9779 		this.inputProxy = new SimpleWindow(inputProxy);
9780 
9781 		/+
9782 		this.inputProxy.onFocusChange = (bool getting) {
9783 			sdpyPrintDebugString("input proxy focus change ", getting);
9784 		};
9785 		+/
9786 
9787 		XEvent lastEvent;
9788 		this.inputProxy.handleNativeEvent = (XEvent ev) {
9789 			lastEvent = ev;
9790 			return 1;
9791 		};
9792 		this.inputProxy.setEventHandlers(
9793 			(MouseEvent e) {
9794 				dispatchMouseEvent(e);
9795 			},
9796 			(KeyEvent e) {
9797 				//writefln("%x   %s", cast(uint) e.key, e.key);
9798 				if(dispatchKeyEvent(e)) {
9799 					// FIXME: i should trap error
9800 					if(auto nw = cast(NestedChildWindowWidget) focusedWidget) {
9801 						auto thing = nw.focusableWindow();
9802 						if(thing && thing.window) {
9803 							lastEvent.xkey.window = thing.window;
9804 							// writeln("sending event ", lastEvent.xkey);
9805 							trapXErrors( {
9806 								XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent);
9807 							});
9808 						}
9809 					}
9810 				}
9811 			},
9812 			(dchar e) {
9813 				if(e == 13) e = 10; // hack?
9814 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
9815 				dispatchCharEvent(e);
9816 			},
9817 		);
9818 
9819 		this.inputProxy.populateXic();
9820 		// done
9821 		//+/
9822 		}
9823 
9824 
9825 
9826 		win.setRequestedInputFocus = &this.setRequestedInputFocus;
9827 
9828 		this(win);
9829 	}
9830 
9831 	SimpleWindow inputProxy;
9832 
9833 	private SimpleWindow setRequestedInputFocus() {
9834 		return inputProxy;
9835 	}
9836 
9837 	/// ditto
9838 	this(string title, int width = 500, int height = 500) {
9839 		this(width, height, title);
9840 	}
9841 
9842 	///
9843 	@property string title() { return parentWindow.win.title; }
9844 	///
9845 	@property void title(string title) { parentWindow.win.title = title; }
9846 
9847 	///
9848 	@scriptable
9849 	void close() {
9850 		win.close();
9851 		// I synchronize here upon window closing to ensure all child windows
9852 		// get updated too before the event loop. This avoids some random X errors.
9853 		static if(UsingSimpledisplayX11) {
9854 			runInGuiThread( {
9855 				XSync(XDisplayConnection.get, false);
9856 			});
9857 		}
9858 	}
9859 
9860 	bool dispatchKeyEvent(KeyEvent ev) {
9861 		auto wid = focusedWidget;
9862 		if(wid is null)
9863 			wid = this;
9864 		KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid);
9865 		event.originalKeyEvent = ev;
9866 		event.key = ev.key;
9867 		event.state = ev.modifierState;
9868 		event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
9869 		event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
9870 		event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
9871 		event.dispatch();
9872 
9873 		return !event.propagationStopped;
9874 	}
9875 
9876 	// returns true if propagation should continue into nested things.... prolly not a great thing to do.
9877 	bool dispatchCharEvent(dchar ch) {
9878 		if(focusedWidget) {
9879 			auto event = new CharEvent(focusedWidget, ch);
9880 			event.dispatch();
9881 			return !event.propagationStopped;
9882 		}
9883 		return true;
9884 	}
9885 
9886 	Widget mouseLastOver;
9887 	Widget mouseLastDownOn;
9888 	bool lastWasDoubleClick;
9889 	bool dispatchMouseEvent(MouseEvent ev) {
9890 		auto eleR = widgetAtPoint(this, ev.x, ev.y);
9891 		auto ele = eleR.widget;
9892 
9893 		auto captureEle = ele;
9894 
9895 		auto mouseCapturedBy = this.mouseCapturedBy.length ? this.mouseCapturedBy[$-1] : null;
9896 		if(mouseCapturedBy !is null) {
9897 			if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele))
9898 				captureEle = mouseCapturedBy;
9899 		}
9900 
9901 		// a hack to get it relative to the widget.
9902 		eleR.x = ev.x;
9903 		eleR.y = ev.y;
9904 		auto pain = captureEle;
9905 
9906 		auto vpx = eleR.x;
9907 		auto vpy = eleR.y;
9908 
9909 		while(pain) {
9910 			eleR.x -= pain.x;
9911 			eleR.y -= pain.y;
9912 			pain.addScrollPosition(eleR.x, eleR.y);
9913 
9914 			vpx -= pain.x;
9915 			vpy -= pain.y;
9916 
9917 			pain = pain.parent;
9918 		}
9919 
9920 		void populateMouseEventBase(MouseEventBase event) {
9921 			event.button = ev.button;
9922 			event.buttonLinear = ev.buttonLinear;
9923 			event.state = ev.modifierState;
9924 			event.clientX = eleR.x;
9925 			event.clientY = eleR.y;
9926 
9927 			event.viewportX = vpx;
9928 			event.viewportY = vpy;
9929 
9930 			event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
9931 			event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
9932 			event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
9933 		}
9934 
9935 		if(ev.type == MouseEventType.buttonPressed) {
9936 			{
9937 				auto event = new MouseDownEvent(captureEle);
9938 				populateMouseEventBase(event);
9939 				event.dispatch();
9940 			}
9941 
9942 			if(!ev.isMouseWheel && mouseLastDownOn is ele && ev.doubleClick) {
9943 				auto event = new DoubleClickEvent(captureEle);
9944 				populateMouseEventBase(event);
9945 				event.dispatch();
9946 				lastWasDoubleClick = ev.doubleClick;
9947 			} else {
9948 				lastWasDoubleClick = false;
9949 			}
9950 
9951 			mouseLastDownOn = ele;
9952 		} else if(ev.type == MouseEventType.buttonReleased) {
9953 			{
9954 				auto event = new MouseUpEvent(captureEle);
9955 				populateMouseEventBase(event);
9956 				event.dispatch();
9957 			}
9958 			if(!lastWasDoubleClick && mouseLastDownOn is ele) {
9959 				auto event = new ClickEvent(captureEle);
9960 				populateMouseEventBase(event);
9961 				event.dispatch();
9962 			}
9963 		} else if(ev.type == MouseEventType.motion) {
9964 			// motion
9965 			{
9966 				auto event = new MouseMoveEvent(captureEle);
9967 				populateMouseEventBase(event); // fills in button which is meaningless but meh
9968 				event.dispatch();
9969 			}
9970 
9971 			if(mouseLastOver !is ele) {
9972 				if(ele !is null) {
9973 					if(!isAParentOf(ele, mouseLastOver)) {
9974 						ele.setDynamicState(DynamicState.hover, true);
9975 						auto event = new MouseEnterEvent(ele);
9976 						event.relatedTarget = mouseLastOver;
9977 						event.sendDirectly();
9978 
9979 						ele.useStyleProperties((scope Widget.Style s) {
9980 							ele.parentWindow.win.cursor = s.cursor;
9981 						});
9982 					}
9983 				}
9984 
9985 				if(mouseLastOver !is null) {
9986 					if(!isAParentOf(mouseLastOver, ele)) {
9987 						mouseLastOver.setDynamicState(DynamicState.hover, false);
9988 						auto event = new MouseLeaveEvent(mouseLastOver);
9989 						event.relatedTarget = ele;
9990 						event.sendDirectly();
9991 					}
9992 				}
9993 
9994 				if(ele !is null) {
9995 					auto event = new MouseOverEvent(ele);
9996 					event.relatedTarget = mouseLastOver;
9997 					event.dispatch();
9998 				}
9999 
10000 				if(mouseLastOver !is null) {
10001 					auto event = new MouseOutEvent(mouseLastOver);
10002 					event.relatedTarget = ele;
10003 					event.dispatch();
10004 				}
10005 
10006 				mouseLastOver = ele;
10007 			}
10008 		}
10009 
10010 		return true; // FIXME: the event default prevented?
10011 	}
10012 
10013 	/++
10014 		Shows the window and runs the application event loop.
10015 
10016 		Blocks until this window is closed.
10017 
10018 		Bugs:
10019 
10020 		$(PITFALL
10021 			You should always have one event loop live for your application.
10022 			If you make two windows in sequence, the second call to loop (or
10023 			simpledisplay's [SimpleWindow.eventLoop], upon which this is built)
10024 			might fail:
10025 
10026 			---
10027 			// don't do this!
10028 			auto window = new Window();
10029 			window.loop();
10030 
10031 			// or new Window or new MainWindow, all the same
10032 			auto window2 = new SimpleWindow();
10033 			window2.eventLoop(0); // problematic! might crash
10034 			---
10035 
10036 			simpledisplay's current implementation assumes that final cleanup is
10037 			done when the event loop refcount reaches zero. So after the first
10038 			eventLoop returns, when there isn't already another one active, it assumes
10039 			the program will exit soon and cleans up.
10040 
10041 			This is arguably a bug that it doesn't reinitialize, and I'll probably change
10042 			it eventually, but in the mean time, there's an easy solution:
10043 
10044 			---
10045 			// do this
10046 			EventLoop mainEventLoop = EventLoop.get; // just add this line
10047 
10048 			auto window = new Window();
10049 			window.loop();
10050 
10051 			// or any other type of Window etc.
10052 			auto window2 = new Window();
10053 			window2.loop(); // perfectly fine since mainEventLoop still alive
10054 			---
10055 
10056 			By adding a top-level reference to the event loop, it ensures the final cleanup
10057 			is not performed until it goes out of scope too, letting the individual window loops
10058 			work without trouble despite the bug.
10059 		)
10060 
10061 		History:
10062 			The [BlockingMode] parameter was added on December 8, 2021.
10063 			The default behavior is to block until the application quits
10064 			(so all windows have been closed), unless another minigui or
10065 			simpledisplay event loop is already running, in which case it
10066 			will block until this window closes specifically.
10067 	+/
10068 	@scriptable
10069 	void loop(BlockingMode bm = BlockingMode.automatic) {
10070 		if(win.closed)
10071 			return; // otherwise show will throw
10072 		show();
10073 		win.eventLoopWithBlockingMode(bm, 0);
10074 	}
10075 
10076 	private bool firstShow = true;
10077 
10078 	@scriptable
10079 	override void show() {
10080 		bool rd = false;
10081 		if(firstShow) {
10082 			firstShow = false;
10083 			queueRecomputeChildLayout();
10084 			// unless the programmer already called focus on something, pick something ourselves
10085 			auto f = focusedWidget is null ? getFirstFocusable(this) : focusedWidget; // FIXME: autofocus?
10086 			if(f)
10087 				f.focus();
10088 			redraw();
10089 		}
10090 		win.show();
10091 		super.show();
10092 	}
10093 	@scriptable
10094 	override void hide() {
10095 		win.hide();
10096 		super.hide();
10097 	}
10098 
10099 	static Widget getFirstFocusable(Widget start) {
10100 		if(start is null)
10101 			return null;
10102 
10103 		foreach(widget; &start.focusableWidgets) {
10104 			return widget;
10105 		}
10106 
10107 		return null;
10108 	}
10109 
10110 	static Widget getLastFocusable(Widget start) {
10111 		if(start is null)
10112 			return null;
10113 
10114 		Widget last;
10115 		foreach(widget; &start.focusableWidgets) {
10116 			last = widget;
10117 		}
10118 
10119 		return last;
10120 	}
10121 
10122 
10123 	mixin Emits!ClosingEvent;
10124 	mixin Emits!ClosedEvent;
10125 }
10126 
10127 /++
10128 	History:
10129 		Added January 12, 2022
10130 
10131 		Made `final` on January 3, 2025
10132 +/
10133 final class DpiChangedEvent : Event {
10134 	enum EventString = "dpichanged";
10135 
10136 	this(Widget target) {
10137 		super(EventString, target);
10138 	}
10139 }
10140 
10141 debug private class DevToolWindow : Window {
10142 	Window p;
10143 
10144 	TextEdit parentList;
10145 	TextEdit logWindow;
10146 	TextLabel clickX, clickY;
10147 
10148 	this(Window p) {
10149 		this.p = p;
10150 		super(400, 300, "Developer Toolbox");
10151 
10152 		logWindow = new TextEdit(this);
10153 		parentList = new TextEdit(this);
10154 
10155 		auto hl = new HorizontalLayout(this);
10156 		clickX = new TextLabel("", TextAlignment.Right, hl);
10157 		clickY = new TextLabel("", TextAlignment.Right, hl);
10158 
10159 		parentListeners ~= p.addEventListener("*", (Event ev) {
10160 			log(typeid(ev.source).name, " emitted ", typeid(ev).name);
10161 		});
10162 
10163 		parentListeners ~= p.addEventListener((ClickEvent ev) {
10164 			auto s = ev.srcElement;
10165 
10166 			string list;
10167 
10168 			void addInfo(Widget s) {
10169 				list ~= s.toString();
10170 				list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight);
10171 				list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight);
10172 				list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness);
10173 				list ~= "\n\theight: " ~ toInternal!string(s.height);
10174 				list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth);
10175 				list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth);
10176 				list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness);
10177 				list ~= "\n\twidth: " ~ toInternal!string(s.width);
10178 				list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop);
10179 				list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom);
10180 			}
10181 
10182 			addInfo(s);
10183 
10184 			s = s.parent;
10185 			while(s) {
10186 				list ~= "\n";
10187 				addInfo(s);
10188 				s = s.parent;
10189 			}
10190 			parentList.content = list;
10191 
10192 			clickX.label = toInternal!string(ev.clientX);
10193 			clickY.label = toInternal!string(ev.clientY);
10194 		});
10195 	}
10196 
10197 	EventListener[] parentListeners;
10198 
10199 	override void close() {
10200 		assert(p !is null);
10201 		foreach(p; parentListeners)
10202 			p.disconnect();
10203 		parentListeners = null;
10204 		p.devTools = null;
10205 		p = null;
10206 		super.close();
10207 	}
10208 
10209 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
10210 		if(ev.key == Key.F12) {
10211 			this.close();
10212 			if(p)
10213 				p.devTools = null;
10214 		} else {
10215 			super.defaultEventHandler_keydown(ev);
10216 		}
10217 	}
10218 
10219 	void log(T...)(T t) {
10220 		string str;
10221 		import std.conv;
10222 		foreach(i; t)
10223 			str ~= to!string(i);
10224 		str ~= "\n";
10225 		logWindow.addText(str);
10226 		logWindow.scrollToBottom();
10227 
10228 		//version(custom_widgets)
10229 		//logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox());
10230 	}
10231 }
10232 
10233 /++
10234 	A dialog is a transient window that intends to get information from
10235 	the user before being dismissed.
10236 +/
10237 class Dialog : Window {
10238 	///
10239 	this(Window parent, int width, int height, string title = null) {
10240 		super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win);
10241 
10242 		// this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
10243 	}
10244 
10245 	///
10246 	this(Window parent, string title, int width, int height) {
10247 		this(parent, width, height, title);
10248 	}
10249 
10250 	deprecated("Pass an explicit parent window, even if it is `null`")
10251 	this(int width, int height, string title = null) {
10252 		this(null, width, height, title);
10253 	}
10254 
10255 	///
10256 	void OK() {
10257 
10258 	}
10259 
10260 	///
10261 	void Cancel() {
10262 		this.close();
10263 	}
10264 }
10265 
10266 /++
10267 	A custom widget similar to the HTML5 <details> tag.
10268 +/
10269 version(none)
10270 class DetailsView : Widget {
10271 
10272 }
10273 
10274 // FIXME: maybe i should expose the other list views Windows offers too
10275 
10276 /++
10277 	A TableView is a widget made to display a table of data strings.
10278 
10279 
10280 	Future_Directions:
10281 		Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers.
10282 
10283 		I will add a selection changed event at some point, as well as item clicked events.
10284 	History:
10285 		Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet.
10286 	See_Also:
10287 		[ListWidget] which displays a list of strings without additional columns.
10288 +/
10289 class TableView : Widget {
10290 	/++
10291 
10292 	+/
10293 	this(Widget parent) {
10294 		super(parent);
10295 
10296 		version(win32_widgets) {
10297 			// LVS_EX_LABELTIP might be worth too
10298 			// LVS_OWNERDRAWFIXED
10299 			createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//, LVS_EX_TRACKSELECT); // ex style for for LVN_HOTTRACK
10300 		} else version(custom_widgets) {
10301 			auto smw = new ScrollMessageWidget(this);
10302 			smw.addDefaultKeyboardListeners();
10303 			smw.addDefaultWheelListeners(1, scaleWithDpi(16));
10304 			tvwi = new TableViewWidgetInner(this, smw);
10305 		}
10306 	}
10307 
10308 	// FIXME: auto-size columns on double click of header thing like in Windows
10309 	// it need only make the currently displayed things fit well.
10310 
10311 
10312 	private ColumnInfo[] columns;
10313 	private int itemCount;
10314 
10315 	version(custom_widgets) private {
10316 		TableViewWidgetInner tvwi;
10317 	}
10318 
10319 	/// Passed to [setColumnInfo]
10320 	static struct ColumnInfo {
10321 		const(char)[] name; /// the name displayed in the header
10322 		/++
10323 			The default width, in pixels. As a special case, you can set this to -1
10324 			if you want the system to try to automatically size the width to fit visible
10325 			content. If it can't, it will try to pick a sensible default size.
10326 
10327 			Any other negative value is not allowed and may lead to unpredictable results.
10328 
10329 			History:
10330 				The -1 behavior was specified on December 3, 2021. It actually worked before
10331 				anyway on Win32 but now it is a formal feature with partial Linux support.
10332 
10333 			Bugs:
10334 				It doesn't actually attempt to calculate a best-fit width on Linux as of
10335 				December 3, 2021. I do plan to fix this in the future, but Windows is the
10336 				priority right now. At least it doesn't break things when you use it now.
10337 		+/
10338 		int width;
10339 
10340 		/++
10341 			Alignment of the text in the cell. Applies to the header as well as all data in this
10342 			column.
10343 
10344 			Bugs:
10345 				On Windows, the first column ignores this member and is always left aligned.
10346 				You can work around this by inserting a dummy first column with width = 0
10347 				then putting your actual data in the second column, which does respect the
10348 				alignment.
10349 
10350 				This is a quirk of the operating system's implementation going back a very
10351 				long time and is unlikely to ever be fixed.
10352 		+/
10353 		TextAlignment alignment;
10354 
10355 		/++
10356 			After all the pixel widths have been assigned, any left over
10357 			space is divided up among all columns and distributed to according
10358 			to the widthPercent field.
10359 
10360 
10361 			For example, if you have two fields, both with width 50 and one with
10362 			widthPercent of 25 and the other with widthPercent of 75, and the
10363 			container is 200 pixels wide, first both get their width of 50.
10364 			then the 100 remaining pixels are split up, so the one gets a total
10365 			of 75 pixels and the other gets a total of 125.
10366 
10367 			This is automatically applied as the window is resized.
10368 
10369 			If there is not enough space - that is, when a horizontal scrollbar
10370 			needs to appear - there are 0 pixels divided up, and thus everyone
10371 			gets 0. This can cause a column to shrink out of proportion when
10372 			passing the scroll threshold.
10373 
10374 			It is important to still set a fixed width (that is, to populate the
10375 			`width` field) even if you use the percents because that will be the
10376 			default minimum in the event of a scroll bar appearing.
10377 
10378 			The percents total in the column can never exceed 100 or be less than 0.
10379 			Doing this will trigger an assert error.
10380 
10381 			Implementation note:
10382 
10383 			Please note that percentages are only recalculated 1) upon original
10384 			construction and 2) upon resizing the control. If the user adjusts the
10385 			width of a column, the percentage items will not be updated.
10386 
10387 			On the other hand, if the user adjusts the width of a percentage column
10388 			then resizes the window, it is recalculated, meaning their hand adjustment
10389 			is discarded. This specific behavior may change in the future as it is
10390 			arguably a bug, but I'm not certain yet.
10391 
10392 			History:
10393 				Added November 10, 2021 (dub v10.4)
10394 		+/
10395 		int widthPercent;
10396 
10397 
10398 		private int calculatedWidth;
10399 	}
10400 	/++
10401 		Sets the number of columns along with information about the headers.
10402 
10403 		Please note: on Windows, the first column ignores your alignment preference
10404 		and is always left aligned.
10405 	+/
10406 	void setColumnInfo(ColumnInfo[] columns...) {
10407 
10408 		foreach(ref c; columns) {
10409 			c.name = c.name.idup;
10410 		}
10411 		this.columns = columns.dup;
10412 
10413 		updateCalculatedWidth(false);
10414 
10415 		version(custom_widgets) {
10416 			tvwi.header.updateHeaders();
10417 			tvwi.updateScrolls();
10418 		} else version(win32_widgets)
10419 		foreach(i, column; this.columns) {
10420 			LVCOLUMN lvColumn;
10421 			lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
10422 			lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth;
10423 
10424 			auto bfr = WCharzBuffer(column.name);
10425 			lvColumn.pszText = bfr.ptr;
10426 
10427 			if(column.alignment & TextAlignment.Center)
10428 				lvColumn.fmt = LVCFMT_CENTER;
10429 			else if(column.alignment & TextAlignment.Right)
10430 				lvColumn.fmt = LVCFMT_RIGHT;
10431 			else
10432 				lvColumn.fmt = LVCFMT_LEFT;
10433 
10434 			if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1)
10435 				throw new WindowsApiException("Insert Column Fail", GetLastError());
10436 		}
10437 	}
10438 
10439 	version(custom_widgets)
10440 	private int getColumnSizeForContent(size_t columnIndex) {
10441 		// FIXME: idk where the problem is but with a 2x scale the horizontal scroll is insuffiicent. i think the SMW is doing it wrong.
10442 		// might also want a user-defined max size too
10443 		int padding = scaleWithDpi(6);
10444 		int m = this.defaultTextWidth(this.columns[columnIndex].name) + padding;
10445 
10446 		if(getData !is null)
10447 		foreach(row; 0 .. itemCount)
10448 			getData(row, cast(int) columnIndex, (txt) {
10449 				m = mymax(m, this.defaultTextWidth(txt) + padding);
10450 			});
10451 
10452 		if(m < 32)
10453 			m = 32;
10454 
10455 		return m;
10456 	}
10457 
10458 	/++
10459 		History:
10460 			Added February 26, 2025
10461 	+/
10462 	void autoSizeColumnsToContent() {
10463 		version(custom_widgets) {
10464 			foreach(idx, ref c; columns) {
10465 				c.width = getColumnSizeForContent(idx);
10466 			}
10467 			updateCalculatedWidth(false);
10468 			tvwi.updateScrolls();
10469 		} else version(win32_widgets) {
10470 			foreach(i, c; columns)
10471 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, LVSCW_AUTOSIZE); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
10472 		}
10473 	}
10474 
10475 	/++
10476 		History:
10477 			Added March 1, 2025
10478 	+/
10479 	bool supportsPerCellAlignment() {
10480 		version(custom_widgets)
10481 			return true;
10482 		else version(win32_widgets)
10483 			return false;
10484 		return false;
10485 	}
10486 
10487 	private int getActualSetSize(size_t i, bool askWindows) {
10488 		version(win32_widgets)
10489 			if(askWindows)
10490 				return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0);
10491 		auto w = columns[i].width;
10492 		if(w == -1)
10493 			return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME
10494 		return w;
10495 	}
10496 
10497 	private void updateCalculatedWidth(bool informWindows) {
10498 		int padding;
10499 		version(win32_widgets)
10500 			padding = 4;
10501 		int remaining = this.width;
10502 		foreach(i, column; columns)
10503 			remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding;
10504 		remaining -= padding;
10505 		if(remaining < 0)
10506 			remaining = 0;
10507 
10508 		int percentTotal;
10509 		foreach(i, ref column; columns) {
10510 			percentTotal += column.widthPercent;
10511 
10512 			auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100;
10513 
10514 			column.calculatedWidth = c;
10515 
10516 			version(win32_widgets)
10517 			if(informWindows)
10518 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
10519 		}
10520 
10521 		assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100.");
10522 		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).");
10523 
10524 
10525 	}
10526 
10527 	override void registerMovement() {
10528 		super.registerMovement();
10529 
10530 		updateCalculatedWidth(true);
10531 	}
10532 
10533 	/++
10534 		History:
10535 			Added September 27, 2025
10536 	+/
10537 	void scrollIntoView(int row, int column) {
10538 		version(custom_widgets) {
10539 			tvwi.smw.vsb.setPosition(row);
10540 			int w;
10541 			foreach(col; this.columns[0 .. column])
10542 				w += col.calculatedWidth;
10543 			tvwi.smw.hsb.setPosition(w);
10544 			tvwi.smw.notify();
10545 
10546 		} else version(win32_widgets) {
10547 			SendMessage(hwnd, LVM_ENSUREVISIBLE, row, true);
10548 		}
10549 	}
10550 
10551 	/++
10552 		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.
10553 	+/
10554 	void setItemCount(int count) {
10555 		this.itemCount = count;
10556 		version(custom_widgets) {
10557 			tvwi.updateScrolls();
10558 			redraw();
10559 		} else version(win32_widgets) {
10560 			SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0);
10561 		}
10562 	}
10563 
10564 	/++
10565 		Clears all items;
10566 	+/
10567 	void clear() {
10568 		this.itemCount = 0;
10569 		this.columns = null;
10570 		version(custom_widgets) {
10571 			tvwi.header.updateHeaders();
10572 			tvwi.updateScrolls();
10573 			redraw();
10574 		} else version(win32_widgets) {
10575 			SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0);
10576 		}
10577 	}
10578 
10579 	/+
10580 	version(win32_widgets)
10581 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis)
10582 		auto itemId = dis.itemID;
10583 		auto hdc = dis.hDC;
10584 		auto rect = dis.rcItem;
10585 		switch(dis.itemAction) {
10586 			case ODA_DRAWENTIRE:
10587 
10588 				// FIXME: do other items
10589 				// FIXME: do the focus rectangle i guess
10590 				// FIXME: alignment
10591 				// FIXME: column width
10592 				// FIXME: padding left
10593 				// FIXME: check dpi scaling
10594 				// FIXME: don't owner draw unless it is necessary.
10595 
10596 				auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi
10597 				RECT itemRect;
10598 				itemRect.top = 1; // subitem idx, 1-based
10599 				itemRect.left = LVIR_BOUNDS;
10600 
10601 				SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect);
10602 				itemRect.left += padding;
10603 
10604 				getData(itemId, 0, (in char[] data) {
10605 					auto wdata = WCharzBuffer(data);
10606 					DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS);
10607 
10608 				});
10609 			goto case;
10610 			case ODA_FOCUS:
10611 				if(dis.itemState & ODS_FOCUS)
10612 					DrawFocusRect(hdc, &rect);
10613 			break;
10614 			case ODA_SELECT:
10615 				// itemState & ODS_SELECTED
10616 			break;
10617 			default:
10618 		}
10619 		return 1;
10620 	}
10621 	+/
10622 
10623 	version(win32_widgets) {
10624 		CellStyle last;
10625 		COLORREF defaultColor;
10626 		COLORREF defaultBackground;
10627 	}
10628 
10629 	version(win32_widgets)
10630 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
10631 		switch(code) {
10632 			case NM_CUSTOMDRAW:
10633 				auto s = cast(NMLVCUSTOMDRAW*) hdr;
10634 				switch(s.nmcd.dwDrawStage) {
10635 					case CDDS_PREPAINT:
10636 						if(getCellStyle is null)
10637 							return 0;
10638 
10639 						mustReturn = true;
10640 						return CDRF_NOTIFYITEMDRAW;
10641 					case CDDS_ITEMPREPAINT:
10642 						mustReturn = true;
10643 						return CDRF_NOTIFYSUBITEMDRAW;
10644 					case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
10645 						mustReturn = true;
10646 
10647 						if(getCellStyle is null) // this SHOULD never happen...
10648 							return 0;
10649 
10650 						if(s.iSubItem == 0) {
10651 							// Windows resets it per row so we'll use item 0 as a chance
10652 							// to capture these for later
10653 							defaultColor = s.clrText;
10654 							defaultBackground = s.clrTextBk;
10655 						}
10656 
10657 						auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem);
10658 						// if no special style and no reset needed...
10659 						if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init))
10660 							return 0; // allow default processing to continue
10661 
10662 						last = style;
10663 
10664 						// might still need to reset or use the preference.
10665 
10666 						if(style.flags & CellStyle.Flags.textColorSet)
10667 							s.clrText = style.textColor.asWindowsColorRef;
10668 						else
10669 							s.clrText = defaultColor; // reset in case it was set from last iteration not a fan
10670 						if(style.flags & CellStyle.Flags.backgroundColorSet)
10671 							s.clrTextBk = style.backgroundColor.asWindowsColorRef;
10672 						else
10673 							s.clrTextBk = defaultBackground; // need to reset it... not a fan of this
10674 
10675 						return CDRF_NEWFONT;
10676 					default:
10677 						return 0;
10678 
10679 				}
10680 			case NM_RETURN: // no need since i subclass keydown
10681 			break;
10682 			case LVN_COLUMNCLICK:
10683 				auto info = cast(LPNMLISTVIEW) hdr;
10684 				// FIXME can i get the button?
10685 				this.emit!HeaderClickedEvent(info.iSubItem, MouseButton.left);
10686 			break;
10687 			case (LVN_FIRST-21) /* LVN_HOTTRACK */:
10688 				// requires LVS_EX_TRACKSELECT
10689 				// sdpyPrintDebugString("here");
10690 				mustReturn = 1; // override Windows' auto selection
10691 			break;
10692 			case NM_CLICK:
10693 				NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr;
10694 				this.emit!CellClickedEvent(info.iItem, info.iSubItem, MouseButton.left, MouseButtonLinear.left, info.ptAction.x, info.ptAction.y, !!(info.uKeyFlags & LVKF_ALT), !!(info.uKeyFlags & LVKF_CONTROL), !!(info.uKeyFlags & LVKF_SHIFT), false);
10695 			break;
10696 			case NM_DBLCLK:
10697 				NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr;
10698 				this.emit!CellClickedEvent(info.iItem, info.iSubItem, MouseButton.left, MouseButtonLinear.left, info.ptAction.x, info.ptAction.y, !!(info.uKeyFlags & LVKF_ALT), !!(info.uKeyFlags & LVKF_CONTROL), !!(info.uKeyFlags & LVKF_SHIFT), true);
10699 			break;
10700 			case NM_RCLICK:
10701 				NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr;
10702 				this.emit!CellClickedEvent(info.iItem, info.iSubItem, MouseButton.right, MouseButtonLinear.left, info.ptAction.x, info.ptAction.y, !!(info.uKeyFlags & LVKF_ALT), !!(info.uKeyFlags & LVKF_CONTROL), !!(info.uKeyFlags & LVKF_SHIFT), false);
10703 			break;
10704 			case NM_RDBLCLK:
10705 				NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr;
10706 				this.emit!CellClickedEvent(info.iItem, info.iSubItem, MouseButton.right, MouseButtonLinear.left, info.ptAction.x, info.ptAction.y, !!(info.uKeyFlags & LVKF_ALT), !!(info.uKeyFlags & LVKF_CONTROL), !!(info.uKeyFlags & LVKF_SHIFT), true);
10707 			break;
10708 			case LVN_GETDISPINFO:
10709 				LV_DISPINFO* info = cast(LV_DISPINFO*) hdr;
10710 				if(info.item.mask & LVIF_TEXT) {
10711 					if(getData) {
10712 						getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) {
10713 							auto bfr = WCharzBuffer(dataReceived);
10714 							auto len = info.item.cchTextMax;
10715 							if(bfr.length < len)
10716 								len = cast(typeof(len)) bfr.length;
10717 							info.item.pszText[0 .. len] = bfr.ptr[0 .. len];
10718 							info.item.pszText[len] = 0;
10719 						});
10720 					} else {
10721 						info.item.pszText[0] = 0;
10722 					}
10723 					//info.item.iItem
10724 					//if(info.item.iSubItem)
10725 				}
10726 			break;
10727 			default:
10728 		}
10729 		return 0;
10730 	}
10731 
10732 	// FIXME: this throws off mouse calculations, it should only happen when we're at the top level or something idk
10733 	override bool encapsulatedChildren() {
10734 		return true;
10735 	}
10736 
10737 	/++
10738 		Informs the control that content has changed.
10739 
10740 		History:
10741 			Added November 10, 2021 (dub v10.4)
10742 	+/
10743 	void update() {
10744 		version(custom_widgets)
10745 			redraw();
10746 		else {
10747 			SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0));
10748 			UpdateWindow(hwnd);
10749 		}
10750 
10751 
10752 	}
10753 
10754 	/++
10755 		Called by the system to request the text content of an individual cell. You
10756 		should pass the text into the provided `sink` delegate. This function will be
10757 		called for each visible cell as-needed when drawing.
10758 	+/
10759 	void delegate(int row, int column, scope void delegate(in char[]) sink) getData;
10760 
10761 	/++
10762 		Available per-cell style customization options. Use one of the constructors
10763 		provided to set the values conveniently, or default construct it and set individual
10764 		values yourself. Just remember to set the `flags` so your values are actually used.
10765 		If the flag isn't set, the field is ignored and the system default is used instead.
10766 
10767 		This is returned by the [getCellStyle] delegate.
10768 
10769 		Examples:
10770 			---
10771 			// assumes you have a variables called `my_data` which is an array of arrays of numbers
10772 			auto table = new TableView(window);
10773 			// snip: you would set up columns here
10774 
10775 			// this is how you provide data to the table view class
10776 			table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) {
10777 				import std.conv;
10778 				sink(to!string(my_data[row][column]));
10779 			};
10780 
10781 			// and this is how you customize the colors
10782 			table.getCellStyle = delegate(int row, int column) {
10783 				return (my_data[row][column] < 0) ?
10784 					TableView.CellStyle(Color.red); // make negative numbers red
10785 					: TableView.CellStyle.init; // leave the rest alone
10786 			};
10787 			// snip: you would call table.setItemCount here then continue with the rest of your window setup work
10788 			---
10789 
10790 		History:
10791 			Added November 27, 2021 (dub v10.4)
10792 	+/
10793 	struct CellStyle {
10794 		/// 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.
10795 		this(Color textColor) {
10796 			this.textColor = textColor;
10797 			this.flags |= Flags.textColorSet;
10798 		}
10799 		/// Sets a custom text and background color.
10800 		this(Color textColor, Color backgroundColor) {
10801 			this.textColor = textColor;
10802 			this.backgroundColor = backgroundColor;
10803 			this.flags |= Flags.textColorSet | Flags.backgroundColorSet;
10804 		}
10805 		/++
10806 			Alignment is only supported on some platforms.
10807 		+/
10808 		this(TextAlignment alignment) {
10809 			this.alignment = alignment;
10810 			this.flags |= Flags.alignmentSet;
10811 		}
10812 		/// ditto
10813 		this(TextAlignment alignment, Color textColor) {
10814 			this.alignment = alignment;
10815 			this.textColor = textColor;
10816 			this.flags |= Flags.alignmentSet | Flags.textColorSet;
10817 		}
10818 		/// ditto
10819 		this(TextAlignment alignment, Color textColor, Color backgroundColor) {
10820 			this.alignment = alignment;
10821 			this.textColor = textColor;
10822 			this.backgroundColor = backgroundColor;
10823 			this.flags |= Flags.alignmentSet | Flags.textColorSet | Flags.backgroundColorSet;
10824 		}
10825 
10826 		TextAlignment alignment;
10827 		Color textColor;
10828 		Color backgroundColor;
10829 		int flags; /// bitmask of [Flags]
10830 		/// available options to combine into [flags]
10831 		enum Flags {
10832 			textColorSet = 1 << 0,
10833 			backgroundColorSet = 1 << 1,
10834 			alignmentSet = 1 << 2,
10835 		}
10836 	}
10837 	/++
10838 		Companion delegate to [getData] that allows you to custom style each
10839 		cell of the table.
10840 
10841 		Returns:
10842 			A [CellStyle] structure that describes the desired style for the
10843 			given cell. `return CellStyle.init` if you want the default style.
10844 
10845 		History:
10846 			Added November 27, 2021 (dub v10.4)
10847 	+/
10848 	CellStyle delegate(int row, int column) getCellStyle;
10849 
10850 	// i want to be able to do things like draw little colored things to show red for negative numbers
10851 	// or background color indicators or even in-cell charts
10852 	// void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell;
10853 
10854 	/++
10855 		When the user clicks on a header, this event is emitted. It has a member to identify which header (by index) was clicked.
10856 	+/
10857 	mixin Emits!HeaderClickedEvent;
10858 
10859 	/++
10860 		History:
10861 			Added March 2, 2025
10862 	+/
10863 	mixin Emits!CellClickedEvent;
10864 }
10865 
10866 /++
10867 	This is emitted by the [TableView] when a user clicks on a column header.
10868 
10869 	Its member `columnIndex` has the zero-based index of the column that was clicked.
10870 
10871 	The default behavior of this event is to do nothing, so `preventDefault` has no effect.
10872 
10873 	History:
10874 		Added November 27, 2021 (dub v10.4)
10875 
10876 		Made `final` on January 3, 2025
10877 +/
10878 final class HeaderClickedEvent : Event {
10879 	enum EventString = "HeaderClicked";
10880 	this(Widget target, int columnIndex, MouseButton button) {
10881 		this.columnIndex = columnIndex;
10882 		this.button = button;
10883 		super(EventString, target);
10884 	}
10885 
10886 	/// The index of the column
10887 	int columnIndex;
10888 
10889 	/++
10890 		History:
10891 			Added September 27, 2025
10892 		Bugs:
10893 			Not implemented on Windows, always sets mouse button left.
10894 	+/
10895 	MouseButton button;
10896 
10897 	///
10898 	override @property int intValue() {
10899 		return columnIndex;
10900 	}
10901 }
10902 
10903 /++
10904 	History:
10905 		Added March 2, 2025
10906 +/
10907 final class CellClickedEvent : MouseEventBase {
10908 	enum EventString = "CellClicked";
10909 	this(Widget target, int rowIndex, int columnIndex, MouseButton button, MouseButtonLinear mouseButtonLinear, int x, int y, bool altKey, bool ctrlKey, bool shiftKey, bool isDoubleClick) {
10910 		this.rowIndex = rowIndex;
10911 		this.columnIndex = columnIndex;
10912 		this.button = button;
10913 		this.buttonLinear = mouseButtonLinear;
10914 		this.isDoubleClick = isDoubleClick;
10915 		this.clientX = x;
10916 		this.clientY = y;
10917 
10918 		this.altKey = altKey;
10919 		this.ctrlKey = ctrlKey;
10920 		this.shiftKey = shiftKey;
10921 
10922 		// import std.stdio; std.stdio.writeln(rowIndex, "x", columnIndex, " @ ", x, ",", y, " ", button, " ", isDoubleClick, " ", altKey, " ", ctrlKey, " ", shiftKey);
10923 
10924 		// FIXME: x, y, state, altButton etc?
10925 		super(EventString, target);
10926 	}
10927 
10928 	/++
10929 		See also: [button] inherited from the base class.
10930 
10931 		clientX and clientY are irrespective of scrolling - FIXME is that sane?
10932 	+/
10933 	int columnIndex;
10934 
10935 	/// ditto
10936 	int rowIndex;
10937 
10938 	/// ditto
10939 	bool isDoubleClick;
10940 
10941 	/+
10942 	// i could do intValue as a linear index if we know the width
10943 	// and a stringValue with the string in the cell. but idk if worth.
10944 	override @property int intValue() {
10945 		return columnIndex;
10946 	}
10947 	+/
10948 
10949 }
10950 
10951 version(custom_widgets)
10952 private class TableViewWidgetInner : Widget {
10953 
10954 // wrap this thing in a ScrollMessageWidget
10955 
10956 	TableView tvw;
10957 	ScrollMessageWidget smw;
10958 	HeaderWidget header;
10959 
10960 	this(TableView tvw, ScrollMessageWidget smw) {
10961 		this.tvw = tvw;
10962 		this.smw = smw;
10963 		super(smw);
10964 
10965 		this.tabStop = true;
10966 
10967 		header = new HeaderWidget(this, smw.getHeader());
10968 
10969 		smw.addEventListener("scroll", () {
10970 			this.redraw();
10971 			header.redraw();
10972 		});
10973 
10974 
10975 		// I need headers outside the scroll area but rendered on the same line as the up arrow
10976 		// FIXME: add a fixed header to the SMW
10977 	}
10978 
10979 	enum padding = 3;
10980 
10981 	void updateScrolls() {
10982 		int w;
10983 		foreach(idx, column; tvw.columns) {
10984 			w += column.calculatedWidth;
10985 		}
10986 		smw.setTotalArea(w, tvw.itemCount);
10987 		columnsWidth = w;
10988 	}
10989 
10990 	private int columnsWidth;
10991 
10992 	private int lh() { return scaleWithDpi(16); } // FIXME lineHeight
10993 
10994 	override void registerMovement() {
10995 		super.registerMovement();
10996 		// FIXME: actual column width. it might need to be done per-pixel instead of per-column
10997 		smw.setViewableArea(this.width, this.height / lh);
10998 	}
10999 
11000 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
11001 		int x;
11002 		int y;
11003 
11004 		int row = smw.position.y;
11005 
11006 		foreach(lol; 0 .. this.height / lh) {
11007 			if(row >= tvw.itemCount)
11008 				break;
11009 			x = 0;
11010 			foreach(columnNumber, column; tvw.columns) {
11011 				auto x2 = x + column.calculatedWidth;
11012 				auto smwx = smw.position.x;
11013 
11014 				if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) {
11015 					auto startX = x;
11016 					auto endX = x + column.calculatedWidth;
11017 					switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) {
11018 						case TextAlignment.Left: startX += padding; break;
11019 						case TextAlignment.Center: startX += padding; endX -= padding; break;
11020 						case TextAlignment.Right: endX -= padding; break;
11021 						default: /* broken */ break;
11022 					}
11023 					if(column.width != 0) // no point drawing an invisible column
11024 					tvw.getData(row, cast(int) columnNumber, (in char[] info) {
11025 						auto endClip = endX - smw.position.x;
11026 						if(endClip > this.width - padding)
11027 							endClip = this.width - padding;
11028 						auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endClip, y + lh)));
11029 
11030 						void dotext(WidgetPainter painter, TextAlignment alignment) {
11031 							painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x - padding, y + lh), alignment);
11032 						}
11033 
11034 						if(tvw.getCellStyle !is null) {
11035 							auto style = tvw.getCellStyle(row, cast(int) columnNumber);
11036 
11037 							if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) {
11038 								auto tempPainter = painter;
11039 								tempPainter.fillColor = style.backgroundColor;
11040 								tempPainter.outlineColor = style.backgroundColor;
11041 
11042 								tempPainter.drawRectangle(Point(startX - smw.position.x, y),
11043 									Point(endX - smw.position.x, y + lh));
11044 							}
11045 							auto tempPainter = painter;
11046 							if(style.flags & TableView.CellStyle.Flags.textColorSet)
11047 								tempPainter.outlineColor = style.textColor;
11048 
11049 							auto alignment = column.alignment;
11050 							if(style.flags & TableView.CellStyle.Flags.alignmentSet)
11051 								alignment = style.alignment;
11052 							dotext(tempPainter, alignment);
11053 						} else {
11054 							dotext(painter, column.alignment);
11055 						}
11056 					});
11057 				}
11058 
11059 				x += column.calculatedWidth;
11060 			}
11061 			row++;
11062 			y += lh;
11063 		}
11064 		return bounds;
11065 	}
11066 
11067 	static class Style : Widget.Style {
11068 		override WidgetBackground background() {
11069 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
11070 		}
11071 	}
11072 	mixin OverrideStyle!Style;
11073 
11074 	private static class HeaderWidget : Widget {
11075 		/+
11076 			maybe i should do a splitter thing on top of the other widgets
11077 			so the splitter itself isn't really drawn but still replies to mouse events?
11078 		+/
11079 		this(TableViewWidgetInner tvw, Widget parent) {
11080 			super(parent);
11081 			this.tvw = tvw;
11082 
11083 			this.remainder = new Button("", this);
11084 
11085 			this.addEventListener((scope ClickEvent ev) {
11086 				int header = -1;
11087 				foreach(idx, child; this.children[1 .. $]) {
11088 					if(child is ev.target) {
11089 						header = cast(int) idx;
11090 						break;
11091 					}
11092 				}
11093 
11094 				if(header != -1) {
11095 					auto hce = new HeaderClickedEvent(tvw.tvw, header, cast(MouseButton) ev.button);
11096 					hce.dispatch();
11097 				}
11098 
11099 			});
11100 		}
11101 
11102 		override int minHeight() {
11103 			return defaultLineHeight + 4; // same as Button
11104 		}
11105 
11106 		void updateHeaders() {
11107 			foreach(child; children[1 .. $])
11108 				child.removeWidget();
11109 
11110 			foreach(column; tvw.tvw.columns) {
11111 				// the cast is ok because I dup it above, just the type is never changed.
11112 				// all this is private so it should never get messed up.
11113 				new Button(ImageLabel(cast(string) column.name, column.alignment), this);
11114 			}
11115 		}
11116 
11117 		Button remainder;
11118 		TableViewWidgetInner tvw;
11119 
11120 		override void recomputeChildLayout() {
11121 			registerMovement();
11122 			int pos;
11123 			foreach(idx, child; children[1 .. $]) {
11124 				if(idx >= tvw.tvw.columns.length)
11125 					continue;
11126 				child.x = pos;
11127 				child.y = 0;
11128 				child.width = tvw.tvw.columns[idx].calculatedWidth;
11129 				child.height = scaleWithDpi(16);// this.height;
11130 				pos += child.width;
11131 
11132 				child.recomputeChildLayout();
11133 			}
11134 
11135 			if(remainder is null)
11136 				return;
11137 
11138 			remainder.x = pos;
11139 			remainder.y = 0;
11140 			if(pos < this.width)
11141 				remainder.width = this.width - pos;// + 4;
11142 			else
11143 				remainder.width = 0;
11144 			remainder.height = scaleWithDpi(16);
11145 
11146 			remainder.recomputeChildLayout();
11147 		}
11148 
11149 		// for the scrollable children mixin
11150 		Point scrollOrigin() {
11151 			return Point(tvw.smw.position.x, 0);
11152 		}
11153 		void paintFrameAndBackground(WidgetPainter painter) { }
11154 
11155 		// for mouse event dispatching
11156 		override protected void addScrollPosition(ref int x, ref int y) {
11157 			x += scrollOrigin.x;
11158 			y += scrollOrigin.y;
11159 		}
11160 
11161 		mixin ScrollableChildren;
11162 	}
11163 
11164 	private void emitCellClickedEvent(scope MouseEventBase event, bool isDoubleClick) {
11165 		int mx = event.clientX + smw.position.x;
11166 		int my = event.clientY;
11167 
11168 		Widget par = this;
11169 		while(par && !par.encapsulatedChildren) {
11170 			my -= par.y; // to undo the encapsulatedChildren adjustClientCoordinates effect
11171 			par = par.parent;
11172 		}
11173 		if(par is null)
11174 			my = event.clientY; // encapsulatedChildren not present?
11175 
11176 		int row = my / lh + smw.position.y; // scrolling here is done per-item, not per pixel
11177 		if(row > tvw.itemCount)
11178 			row = -1;
11179 
11180 		int column = -1;
11181 		if(row != -1) {
11182 			int pos;
11183 			foreach(idx, col; tvw.columns) {
11184 				pos += col.calculatedWidth;
11185 				if(mx < pos) {
11186 					column = cast(int) idx;
11187 					break;
11188 				}
11189 			}
11190 		}
11191 
11192 		// wtf are these casts about?
11193 		tvw.emit!CellClickedEvent(row, column, cast(MouseButton) event.button, cast(MouseButtonLinear) event.buttonLinear, event.clientX, event.clientY, event.altKey, event.ctrlKey, event.shiftKey, isDoubleClick);
11194 	}
11195 
11196 	override void defaultEventHandler_click(scope ClickEvent ce) {
11197 		// FIXME: should i filter mouse wheel events? Windows doesn't send them but i can.
11198 		emitCellClickedEvent(ce, false);
11199 	}
11200 
11201 	override void defaultEventHandler_dblclick(scope DoubleClickEvent ce) {
11202 		emitCellClickedEvent(ce, true);
11203 	}
11204 }
11205 
11206 /+
11207 
11208 // given struct / array / number / string / etc, make it viewable and editable
11209 class DataViewerWidget : Widget {
11210 
11211 }
11212 +/
11213 
11214 /++
11215 	A line edit box with an associated label.
11216 
11217 	History:
11218 		On May 17, 2021, the default internal layout was changed from horizontal to vertical.
11219 
11220 		```
11221 		Old: ________
11222 
11223 		New:
11224 		____________
11225 		```
11226 
11227 		To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);`
11228 
11229 		You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a
11230 		horizontal label but left aligned. You may also consider a [GridLayout].
11231 +/
11232 alias LabeledLineEdit = Labeled!LineEdit;
11233 
11234 private int widthThatWouldFitChildLabels(Widget w) {
11235 	if(w is null)
11236 		return 0;
11237 
11238 	int max;
11239 
11240 	if(auto label = cast(TextLabel) w) {
11241 		return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight();
11242 	} else {
11243 		foreach(child; w.children) {
11244 			max = mymax(max, widthThatWouldFitChildLabels(child));
11245 		}
11246 	}
11247 
11248 	return max;
11249 }
11250 
11251 /++
11252 	History:
11253 		Added May 19, 2021
11254 +/
11255 class Labeled(T) : Widget {
11256 	///
11257 	this(string label, Widget parent) {
11258 		super(parent);
11259 		initialize!VerticalLayout(label, TextAlignment.Left, parent);
11260 	}
11261 
11262 	/++
11263 		History:
11264 			The alignment parameter was added May 17, 2021
11265 	+/
11266 	this(string label, TextAlignment alignment, Widget parent) {
11267 		super(parent);
11268 		initialize!HorizontalLayout(label, alignment, parent);
11269 	}
11270 
11271 	private void initialize(L)(string label, TextAlignment alignment, Widget parent) {
11272 		tabStop = false;
11273 		horizontal = is(L == HorizontalLayout);
11274 		auto hl = new L(this);
11275 		if(horizontal) {
11276 			static class SpecialTextLabel : TextLabel {
11277 				Widget outerParent;
11278 
11279 				this(string label, TextAlignment alignment, Widget outerParent, Widget parent) {
11280 					this.outerParent = outerParent;
11281 					super(label, alignment, parent);
11282 				}
11283 
11284 				override int flexBasisWidth() {
11285 					return widthThatWouldFitChildLabels(outerParent);
11286 				}
11287 				/+
11288 				override int widthShrinkiness() { return 0; }
11289 				override int widthStretchiness() { return 1; }
11290 				+/
11291 
11292 				override int paddingRight() { return 6; }
11293 				override int paddingLeft() { return 9; }
11294 
11295 				override int paddingTop() { return 3; }
11296 			}
11297 			this.label = new SpecialTextLabel(label, alignment, parent, hl);
11298 		} else
11299 			this.label = new TextLabel(label, alignment, hl);
11300 		this.lineEdit = new T(hl);
11301 
11302 		this.label.labelFor = this.lineEdit;
11303 	}
11304 
11305 	private bool horizontal;
11306 
11307 	TextLabel label; ///
11308 	T lineEdit; ///
11309 
11310 	override int flexBasisWidth() { return 250; }
11311 	override int widthShrinkiness() { return 1; }
11312 
11313 	override int minHeight() {
11314 		return this.children[0].minHeight;
11315 	}
11316 	override int maxHeight() { return minHeight(); }
11317 	override int marginTop() { return 4; }
11318 	override int marginBottom() { return 4; }
11319 
11320 	// FIXME: i should prolly call it value as well as content tbh
11321 
11322 	///
11323 	@property string content() {
11324 		return lineEdit.content;
11325 	}
11326 	///
11327 	@property void content(string c) {
11328 		return lineEdit.content(c);
11329 	}
11330 
11331 	///
11332 	void selectAll() {
11333 		lineEdit.selectAll();
11334 	}
11335 
11336 	override void focus() {
11337 		lineEdit.focus();
11338 	}
11339 }
11340 
11341 /++
11342 	A labeled password edit.
11343 
11344 	History:
11345 		Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021
11346 
11347 		The default parameters for the constructors were also removed on May 19, 2021
11348 +/
11349 alias LabeledPasswordEdit = Labeled!PasswordEdit;
11350 
11351 private string toMenuLabel(string s) {
11352 	string n;
11353 	n.reserve(s.length);
11354 	foreach(c; s)
11355 		if(c == '_')
11356 			n ~= ' ';
11357 		else
11358 			n ~= c;
11359 	return n;
11360 }
11361 
11362 private void autoExceptionHandler(Exception e) {
11363 	messageBox(e.msg);
11364 }
11365 
11366 void callAsIfClickedFromMenu(alias fn)(auto ref __traits(parent, fn) _this, Window window) {
11367 	makeAutomaticHandler!(fn)(window, &__traits(child, _this, fn))();
11368 }
11369 
11370 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) {
11371 	static if(is(T : void delegate())) {
11372 		return () {
11373 			try
11374 				t();
11375 			catch(Exception e)
11376 				autoExceptionHandler(e);
11377 		};
11378 	} else static if(is(typeof(fn) Params == __parameters)) {
11379 		static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) {
11380 			return () {
11381 				void onOK(string s) {
11382 					member = s;
11383 					try
11384 						t(Params[0](s));
11385 					catch(Exception e)
11386 						autoExceptionHandler(e);
11387 				}
11388 
11389 				if(
11390 					(type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export")))
11391 					|| type == FileDialogType.Save)
11392 				{
11393 					getSaveFileName(window, &onOK, member, filters, null);
11394 				} else
11395 					getOpenFileName(window, &onOK, member, filters, null);
11396 			};
11397 		} else {
11398 			struct S {
11399 				static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) {
11400 					pragma(msg, "warning: automatic handler of params not yet implemented on your compiler");
11401 				} else mixin(q{
11402 				static foreach(idx, ignore; Params) {
11403 					mixin("@(__traits(getAttributes, Params[idx .. idx + 1])) Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";");
11404 				}
11405 				});
11406 			}
11407 			return () {
11408 				dialog(window, (S s) {
11409 					try {
11410 						static if(is(typeof(t) Ret == return)) {
11411 							static if(is(Ret == void)) {
11412 								t(s.tupleof);
11413 							} else {
11414 								auto ret = t(s.tupleof);
11415 								import std.conv;
11416 								messageBox(to!string(ret), "Returned Value");
11417 							}
11418 						}
11419 					} catch(Exception e)
11420 						autoExceptionHandler(e);
11421 				}, null, __traits(identifier, fn));
11422 			};
11423 		}
11424 	} else static assert(0, fn.stringof ~ " isn't a function but in a menu block");
11425 }
11426 
11427 private template hasAnyRelevantAnnotations(a...) {
11428 	bool helper() {
11429 		bool any;
11430 		foreach(attr; a) {
11431 			static if(is(typeof(attr) == .menu))
11432 				any = true;
11433 			else static if(is(typeof(attr) == .toolbar))
11434 				any = true;
11435 			else static if(is(attr == .separator))
11436 				any = true;
11437 			else static if(is(typeof(attr) == .accelerator))
11438 				any = true;
11439 			else static if(is(typeof(attr) == .hotkey))
11440 				any = true;
11441 			else static if(is(typeof(attr) == .icon))
11442 				any = true;
11443 			else static if(is(typeof(attr) == .label))
11444 				any = true;
11445 			else static if(is(typeof(attr) == .tip))
11446 				any = true;
11447 		}
11448 		return any;
11449 	}
11450 
11451 	enum bool hasAnyRelevantAnnotations = helper();
11452 }
11453 
11454 /++
11455 	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.
11456 +/
11457 class MainWindow : Window {
11458 	///
11459 	this(string title = null, int initialWidth = 500, int initialHeight = 500) {
11460 		super(initialWidth, initialHeight, title);
11461 
11462 		_clientArea = new ClientAreaWidget();
11463 		_clientArea.x = 0;
11464 		_clientArea.y = 0;
11465 		_clientArea.width = this.width;
11466 		_clientArea.height = this.height;
11467 		_clientArea.tabStop = false;
11468 
11469 		super.addChild(_clientArea);
11470 
11471 		statusBar = new StatusBar(this);
11472 	}
11473 
11474 	/++
11475 		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).
11476 
11477 		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')`.
11478 
11479 		You can also use `@separator` to put a separating line in the menu before the function.
11480 
11481 		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.
11482 
11483 		Let's look at a complete example:
11484 
11485 	---
11486 	import arsd.minigui;
11487 
11488 	void main() {
11489 		auto window = new MainWindow();
11490 
11491 		// we can add widgets before or after setting the menu, either way is fine.
11492 		// i'll do it before here so the local variables are available to the commands.
11493 
11494 		auto textEdit = new TextEdit(window);
11495 
11496 		// Remember, in D, you can define structs inside of functions
11497 		// and those structs can access the function's local variables.
11498 		//
11499 		// Of course, you might also want to do this separately, and if you
11500 		// do, make sure you keep a reference to the window as a struct data
11501 		// member so you can refer to it in cases like this Exit function.
11502 		struct Commands {
11503 			// the & in the string indicates that the next letter is the hotkey
11504 			// to access it from the keyboard (so here, alt+f will open the
11505 			// file menu)
11506 			@menu("&File") {
11507 				@accelerator("Ctrl+N")
11508 				@hotkey('n')
11509 				@icon(GenericIcons.New) // add an icon to the action
11510 				@toolbar("File") // adds it to a toolbar.
11511 				// The toolbar name is never visible to the user, but is used to group icons.
11512 				void New() {
11513 					previousFileReferenced = null;
11514 					textEdit.content = "";
11515 				}
11516 
11517 				@icon(GenericIcons.Open)
11518 				@toolbar("File")
11519 				@hotkey('s')
11520 				@accelerator("Ctrl+O")
11521 				void Open(FileName!() filename) {
11522 					import std.file;
11523 					textEdit.content = std.file.readText(filename);
11524 				}
11525 
11526 				@icon(GenericIcons.Save)
11527 				@toolbar("File")
11528 				@accelerator("Ctrl+S")
11529 				@hotkey('s')
11530 				void Save() {
11531 					// these are still functions, so of course you can
11532 					// still call them yourself too
11533 					Save_As(previousFileReferenced);
11534 				}
11535 
11536 				// underscores translate to spaces in the visible name
11537 				@hotkey('a')
11538 				void Save_As(FileName!() filename) {
11539 					import std.file;
11540 					std.file.write(previousFileReferenced, textEdit.content);
11541 				}
11542 
11543 				// you can put the annotations before or after the function name+args and it works the same way
11544 				@separator
11545 				void Exit() @accelerator("Alt+F4") @hotkey('x') {
11546 					window.close();
11547 				}
11548 			}
11549 
11550 			@menu("&Edit") {
11551 				// not putting accelerators here because the text edit widget
11552 				// does it locally, so no need to duplicate it globally.
11553 
11554 				@icon(GenericIcons.Undo)
11555 				void Undo() @toolbar("Undo") {
11556 					textEdit.undo();
11557 				}
11558 
11559 				@separator
11560 
11561 				@icon(GenericIcons.Cut)
11562 				void Cut() @toolbar("Edit") {
11563 					textEdit.cut();
11564 				}
11565 				@icon(GenericIcons.Copy)
11566 				void Copy() @toolbar("Edit") {
11567 					textEdit.copy();
11568 				}
11569 				@icon(GenericIcons.Paste)
11570 				void Paste() @toolbar("Edit") {
11571 					textEdit.paste();
11572 				}
11573 
11574 				@separator
11575 				void Select_All() {
11576 					textEdit.selectAll();
11577 				}
11578 			}
11579 
11580 			@menu("Help") {
11581 				void About() @accelerator("F1") {
11582 					window.messageBox("A minigui sample program.");
11583 				}
11584 
11585 				// @label changes the name in the menu from what is in the code
11586 				@label("In Menu Name")
11587 				void otherNameInCode() {}
11588 			}
11589 		}
11590 
11591 		// declare the object that holds the commands, and set
11592 		// and members you want from it
11593 		Commands commands;
11594 
11595 		// and now tell minigui to do its magic and create the ui for it!
11596 		window.setMenuAndToolbarFromAnnotatedCode(commands);
11597 
11598 		// then, loop the window normally;
11599 		window.loop();
11600 
11601 		// important to note that the `commands` variable must live through the window's whole life cycle,
11602 		// or you can have crashes. If you declare the variable and loop in different functions, make sure
11603 		// you do `new Commands` so the garbage collector can take over management of it for you.
11604 	}
11605 	---
11606 
11607 	Note that you can call this function multiple times and it will add the items in order to the given items.
11608 
11609 	+/
11610 	void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) {
11611 		setMenuAndToolbarFromAnnotatedCode_internal(t);
11612 	}
11613 	/// ditto
11614 	void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) {
11615 		setMenuAndToolbarFromAnnotatedCode_internal(t);
11616 	}
11617 	void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
11618 		auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
11619 		Menu[string] mcs;
11620 
11621 		alias ToolbarSection = ToolBar.ToolbarSection;
11622 		ToolbarSection[] toolbarSections;
11623 
11624 		foreach(menu; menuBar.subMenus) {
11625 			mcs[menu.label] = menu;
11626 		}
11627 
11628 		foreach(memberName; __traits(derivedMembers, T)) {
11629 			static if(memberName != "this")
11630 			static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
11631 				.menu menu;
11632 				.toolbar toolbar;
11633 				bool separator;
11634 				.accelerator accelerator;
11635 				.hotkey hotkey;
11636 				.icon icon;
11637 				string label;
11638 				string tip;
11639 				foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
11640 					static if(is(typeof(attr) == .menu))
11641 						menu = attr;
11642 					else static if(is(typeof(attr) == .toolbar))
11643 						toolbar = attr;
11644 					else static if(is(attr == .separator))
11645 						separator = true;
11646 					else static if(is(typeof(attr) == .accelerator))
11647 						accelerator = attr;
11648 					else static if(is(typeof(attr) == .hotkey))
11649 						hotkey = attr;
11650 					else static if(is(typeof(attr) == .icon))
11651 						icon = attr;
11652 					else static if(is(typeof(attr) == .label))
11653 						label = attr.label;
11654 					else static if(is(typeof(attr) == .tip))
11655 						tip = attr.tip;
11656 				}
11657 
11658 				if(menu !is .menu.init || toolbar !is .toolbar.init) {
11659 					ushort correctIcon = icon.id; // FIXME
11660 					if(label.length == 0)
11661 						label = memberName.toMenuLabel;
11662 
11663 					auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName));
11664 
11665 					auto action = new Action(label, correctIcon, handler);
11666 
11667 					if(accelerator.keyString.length) {
11668 						auto ke = KeyEvent.parse(accelerator.keyString);
11669 						action.accelerator = ke;
11670 						accelerators[ke.toStr] = handler;
11671 					}
11672 
11673 					if(toolbar !is .toolbar.init) {
11674 						bool found;
11675 						foreach(ref section; toolbarSections)
11676 							if(section.name == toolbar.groupName) {
11677 								section.actions ~= action;
11678 								found = true;
11679 								break;
11680 							}
11681 						if(!found) {
11682 							toolbarSections ~= ToolbarSection(toolbar.groupName, [action]);
11683 						}
11684 					}
11685 					if(menu !is .menu.init) {
11686 						Menu mc;
11687 						if(menu.name in mcs) {
11688 							mc = mcs[menu.name];
11689 						} else {
11690 							mc = new Menu(menu.name, this);
11691 							menuBar.addItem(mc);
11692 							mcs[menu.name] = mc;
11693 						}
11694 
11695 						if(separator)
11696 							mc.addSeparator();
11697 						auto mi = mc.addItem(new MenuItem(action));
11698 
11699 						if(hotkey !is .hotkey.init)
11700 							mi.hotkey = hotkey.ch;
11701 					}
11702 				}
11703 			}
11704 		}
11705 
11706 		this.menuBar = menuBar;
11707 
11708 		if(toolbarSections.length) {
11709 			auto tb = new ToolBar(toolbarSections, this);
11710 		}
11711 	}
11712 
11713 	void delegate()[string] accelerators;
11714 
11715 	override void defaultEventHandler_keydown(KeyDownEvent event) {
11716 		auto str = event.originalKeyEvent.toStr;
11717 		if(auto acl = str in accelerators)
11718 			(*acl)();
11719 
11720 		// Windows this this automatically so only on custom need we implement it
11721 		version(custom_widgets) {
11722 			if(event.altKey && this.menuBar) {
11723 				foreach(item; this.menuBar.items) {
11724 					if(item.hotkey == keyToLetterCharAssumingLotsOfThingsThatYouMightBetterNotAssume(event.key)) {
11725 						// FIXME this kinda sucks but meh just pretending to click on it to trigger other existing mediocre code
11726 						item.dynamicState = DynamicState.hover | DynamicState.depressed;
11727 						item.redraw();
11728 						auto e = new MouseDownEvent(item);
11729 						e.dispatch();
11730 						break;
11731 					}
11732 				}
11733 			}
11734 
11735 			if(event.key == Key.Menu) {
11736 				showContextMenu(-1, -1);
11737 			}
11738 		}
11739 
11740 		super.defaultEventHandler_keydown(event);
11741 	}
11742 
11743 	override void defaultEventHandler_mouseover(MouseOverEvent event) {
11744 		super.defaultEventHandler_mouseover(event);
11745 		if(this.statusBar !is null && event.target.statusTip.length)
11746 			this.statusBar.parts[0].content = event.target.statusTip;
11747 		else if(this.statusBar !is null && this.statusTip.length)
11748 			this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString();
11749 	}
11750 
11751 	override void addChild(Widget c, int position = int.max) {
11752 		if(auto tb = cast(ToolBar) c)
11753 			version(win32_widgets)
11754 				super.addChild(c, 0);
11755 			else version(custom_widgets)
11756 				super.addChild(c, menuBar ? 1 : 0);
11757 			else static assert(0);
11758 		else
11759 			clientArea.addChild(c, position);
11760 	}
11761 
11762 	ToolBar _toolBar;
11763 	///
11764 	ToolBar toolBar() { return _toolBar; }
11765 	///
11766 	ToolBar toolBar(ToolBar t) {
11767 		_toolBar = t;
11768 		foreach(child; this.children)
11769 			if(child is t)
11770 				return t;
11771 		version(win32_widgets)
11772 			super.addChild(t, 0);
11773 		else version(custom_widgets)
11774 			super.addChild(t, menuBar ? 1 : 0);
11775 		else static assert(0);
11776 		return t;
11777 	}
11778 
11779 	MenuBar _menu;
11780 	///
11781 	MenuBar menuBar() { return _menu; }
11782 	///
11783 	MenuBar menuBar(MenuBar m) {
11784 		if(m is _menu) {
11785 			version(custom_widgets)
11786 				queueRecomputeChildLayout();
11787 			return m;
11788 		}
11789 
11790 		if(_menu !is null) {
11791 			// make sure it is sanely removed
11792 			// FIXME
11793 		}
11794 
11795 		_menu = m;
11796 
11797 		version(win32_widgets) {
11798 			SetMenu(parentWindow.win.impl.hwnd, m.handle);
11799 		} else version(custom_widgets) {
11800 			super.addChild(m, 0);
11801 
11802 		//	clientArea.y = menu.height;
11803 		//	clientArea.height = this.height - menu.height;
11804 
11805 			queueRecomputeChildLayout();
11806 		} else static assert(false);
11807 
11808 		return _menu;
11809 	}
11810 	private Widget _clientArea;
11811 	///
11812 	@property Widget clientArea() { return _clientArea; }
11813 	protected @property void clientArea(Widget wid) {
11814 		_clientArea = wid;
11815 	}
11816 
11817 	private StatusBar _statusBar;
11818 	/++
11819 		Returns the window's [StatusBar]. Be warned it may be `null`.
11820 	+/
11821 	@property StatusBar statusBar() { return _statusBar; }
11822 	/// ditto
11823 	@property void statusBar(StatusBar bar) {
11824 		if(_statusBar !is null)
11825 			_statusBar.removeWidget();
11826 		_statusBar = bar;
11827 		if(bar !is null)
11828 			super.addChild(_statusBar);
11829 	}
11830 }
11831 
11832 /+
11833 	This is really an implementation detail of [MainWindow]
11834 +/
11835 private class ClientAreaWidget : Widget {
11836 	this() {
11837 		this.tabStop = false;
11838 		super(null);
11839 		//sa = new ScrollableWidget(this);
11840 	}
11841 	/*
11842 	ScrollableWidget sa;
11843 	override void addChild(Widget w, int position) {
11844 		if(sa is null)
11845 			super.addChild(w, position);
11846 		else {
11847 			sa.addChild(w, position);
11848 			sa.setContentSize(this.minWidth + 1, this.minHeight);
11849 			writeln(sa.contentWidth, "x", sa.contentHeight);
11850 		}
11851 	}
11852 	*/
11853 }
11854 
11855 /**
11856 	Toolbars are lists of buttons (typically icons) that appear under the menu.
11857 	Each button ought to correspond to a menu item, represented by [Action] objects.
11858 */
11859 class ToolBar : Widget {
11860 	version(win32_widgets) {
11861 		private int idealHeight;
11862 		override int minHeight() { return idealHeight; }
11863 		override int maxHeight() { return idealHeight; }
11864 	} else version(custom_widgets) {
11865 		override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; }
11866 		override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; }
11867 	} else static assert(false);
11868 	override int heightStretchiness() { return 0; }
11869 
11870 	static struct ToolbarSection {
11871 		string name;
11872 		Action[] actions;
11873 	}
11874 
11875 	version(win32_widgets) {
11876 		HIMAGELIST imageListSmall;
11877 		HIMAGELIST imageListLarge;
11878 	}
11879 
11880 	this(Widget parent) {
11881 		this(cast(ToolbarSection[]) null, parent);
11882 	}
11883 
11884 	version(win32_widgets)
11885 	void changeIconSize(bool useLarge) {
11886 		SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall));
11887 
11888 		/+
11889 		SIZE size;
11890 		import core.sys.windows.commctrl;
11891 		SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size);
11892 		idealHeight = size.cy + 4; // the plus 4 is a hack
11893 		+/
11894 
11895 		idealHeight = useLarge ? 34 : 26;
11896 
11897 		if(parent) {
11898 			parent.queueRecomputeChildLayout();
11899 			parent.redraw();
11900 		}
11901 
11902 		SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4));
11903 		SendMessageW(hwnd, TB_AUTOSIZE, 0, 0);
11904 	}
11905 
11906 	/++
11907 		History:
11908 			The `ToolbarSection` overload was added December 31, 2024
11909 	+/
11910 	this(Action[] actions, Widget parent) {
11911 		this([ToolbarSection(null, actions)], parent);
11912 	}
11913 
11914 	/// ditto
11915 	this(ToolbarSection[] sections, Widget parent) {
11916 		super(parent);
11917 
11918 		tabStop = false;
11919 
11920 		version(win32_widgets) {
11921 			// so i like how the flat thing looks on windows, but not on wine
11922 			// and eh, with windows visual styles enabled it looks cool anyway soooo gonna
11923 			// leave it commented
11924 			createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS);
11925 
11926 			SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/);
11927 
11928 			imageListSmall = ImageList_Create(
11929 				// width, height
11930 				16, 16,
11931 				ILC_COLOR16 | ILC_MASK,
11932 				16 /*numberOfButtons*/, 0);
11933 
11934 			imageListLarge = ImageList_Create(
11935 				// width, height
11936 				24, 24,
11937 				ILC_COLOR16 | ILC_MASK,
11938 				16 /*numberOfButtons*/, 0);
11939 
11940 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall);
11941 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL);
11942 
11943 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge);
11944 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL);
11945 
11946 			SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0);
11947 
11948 			TBBUTTON[] buttons;
11949 
11950 			// FIXME: I_IMAGENONE is if here is no icon
11951 			foreach(sidx, section; sections) {
11952 				if(sidx)
11953 					buttons ~= TBBUTTON(
11954 						scaleWithDpi(4),
11955 						0,
11956 						TBSTATE_ENABLED, // state
11957 						TBSTYLE_SEP | BTNS_SEP, // style
11958 						0, // reserved array, just zero it out
11959 						0, // dwData
11960 						-1
11961 					);
11962 
11963 				foreach(action; section.actions)
11964 					buttons ~= TBBUTTON(
11965 						MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0),
11966 						action.id,
11967 						TBSTATE_ENABLED, // state
11968 						0, // style
11969 						0, // reserved array, just zero it out
11970 						0, // dwData
11971 						cast(size_t) toWstringzInternal(action.label) // INT_PTR
11972 					);
11973 			}
11974 
11975 			SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0);
11976 			SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr);
11977 
11978 			/*
11979 			RECT rect;
11980 			GetWindowRect(hwnd, &rect);
11981 			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
11982 			*/
11983 
11984 			dpiChanged(); // to load the things calling changeIconSize the first time
11985 
11986 			assert(idealHeight);
11987 		} else version(custom_widgets) {
11988 			foreach(sidx, section; sections) {
11989 				if(sidx)
11990 					new HorizontalSpacer(4, this);
11991 				foreach(action; section.actions)
11992 					new ToolButton(action, this);
11993 			}
11994 		} else static assert(false);
11995 	}
11996 
11997 	override void recomputeChildLayout() {
11998 		.recomputeChildLayout!"width"(this);
11999 	}
12000 
12001 
12002 	version(win32_widgets)
12003 	override protected void dpiChanged() {
12004 		auto sz = scaleWithDpi(16);
12005 		if(sz >= 20)
12006 			changeIconSize(true);
12007 		else
12008 			changeIconSize(false);
12009 	}
12010 }
12011 
12012 /// 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.
12013 class ToolButton : Button {
12014 	///
12015 	this(Action action, Widget parent) {
12016 		super(action.label, parent);
12017 		tabStop = false;
12018 		this.action = action;
12019 	}
12020 
12021 	version(custom_widgets)
12022 	override void defaultEventHandler_click(ClickEvent event) {
12023 		foreach(handler; action.triggered)
12024 			handler();
12025 	}
12026 
12027 	Action action;
12028 
12029 	override int maxWidth() { return toolbarIconSize; }
12030 	override int minWidth() { return toolbarIconSize; }
12031 	override int maxHeight() { return toolbarIconSize; }
12032 	override int minHeight() { return toolbarIconSize; }
12033 
12034 	version(custom_widgets)
12035 	override void paint(WidgetPainter painter) {
12036 	painter.drawThemed(delegate Rectangle (const Rectangle bounds) {
12037 		painter.outlineColor = Color.black;
12038 
12039 		immutable multiplier = toolbarIconSize / 4;
12040 		immutable divisor = 16 / 4;
12041 
12042 		int ScaledNumber(int n) {
12043 			// return n * multiplier / divisor;
12044 			auto s = n * multiplier;
12045 			auto it = s / divisor;
12046 			auto rem = s % divisor;
12047 			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
12048 				it++;
12049 			return it;
12050 		}
12051 
12052 		arsd.color.Point Point(int x, int y) {
12053 			return arsd.color.Point(ScaledNumber(x), ScaledNumber(y));
12054 		}
12055 
12056 		switch(action.iconId) {
12057 			case GenericIcons.New:
12058 				painter.fillColor = Color.white;
12059 				painter.drawPolygon(
12060 					Point(3, 2), Point(3, 13), Point(12, 13), Point(12, 6),
12061 					Point(8, 2), Point(8, 6), Point(12, 6), Point(8, 2),
12062 					Point(3, 2), Point(3, 13)
12063 				);
12064 			break;
12065 			case GenericIcons.Save:
12066 				painter.fillColor = Color.white;
12067 				painter.outlineColor = Color.black;
12068 				painter.drawRectangle(Point(2, 2), Point(13, 13));
12069 
12070 				// the label
12071 				painter.drawRectangle(Point(4, 8), Point(11, 13));
12072 
12073 				// the slider
12074 				painter.fillColor = Color.black;
12075 				painter.outlineColor = Color.black;
12076 				painter.drawRectangle(Point(4, 3), Point(10, 6));
12077 
12078 				painter.fillColor = Color.white;
12079 				painter.outlineColor = Color.white;
12080 				// the disc window
12081 				painter.drawRectangle(Point(5, 3), Point(6, 5));
12082 			break;
12083 			case GenericIcons.Open:
12084 				painter.fillColor = Color.white;
12085 				painter.drawPolygon(
12086 					Point(4, 4), Point(4, 12), Point(13, 12), Point(13, 3),
12087 					Point(9, 3), Point(9, 4), Point(4, 4));
12088 				painter.drawPolygon(
12089 					Point(2, 6), Point(11, 6),
12090 					Point(12, 12), Point(4, 12),
12091 					Point(2, 6));
12092 				//painter.drawLine(Point(9, 6), Point(13, 7));
12093 			break;
12094 			case GenericIcons.Copy:
12095 				painter.fillColor = Color.white;
12096 				painter.drawRectangle(Point(3, 2), Point(9, 10));
12097 				painter.drawRectangle(Point(6, 5), Point(12, 13));
12098 			break;
12099 			case GenericIcons.Cut:
12100 				painter.fillColor = Color.transparent;
12101 				painter.outlineColor = getComputedStyle.foregroundColor();
12102 				painter.drawLine(Point(3, 2), Point(10, 9));
12103 				painter.drawLine(Point(4, 9), Point(11, 2));
12104 				painter.drawRectangle(Point(3, 9), Point(5, 13));
12105 				painter.drawRectangle(Point(9, 9), Point(11, 12));
12106 			break;
12107 			case GenericIcons.Paste:
12108 				painter.fillColor = Color.white;
12109 				painter.drawRectangle(Point(2, 3), Point(11, 11));
12110 				painter.drawRectangle(Point(6, 8), Point(13, 13));
12111 				painter.drawLine(Point(6, 2), Point(4, 5));
12112 				painter.drawLine(Point(6, 2), Point(9, 5));
12113 				painter.fillColor = Color.black;
12114 				painter.drawRectangle(Point(4, 5), Point(9, 6));
12115 			break;
12116 			case GenericIcons.Help:
12117 				painter.outlineColor = getComputedStyle.foregroundColor();
12118 				painter.drawText(arsd.color.Point(0, 0), "?", arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
12119 			break;
12120 			case GenericIcons.Undo:
12121 				painter.fillColor = Color.transparent;
12122 				painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64);
12123 				painter.outlineColor = Color.black;
12124 				painter.fillColor = Color.black;
12125 				painter.drawPolygon(
12126 					Point(4, 4),
12127 					Point(8, 2),
12128 					Point(8, 6),
12129 					Point(4, 4),
12130 				);
12131 			break;
12132 			case GenericIcons.Redo:
12133 				painter.fillColor = Color.transparent;
12134 				painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64);
12135 				painter.outlineColor = Color.black;
12136 				painter.fillColor = Color.black;
12137 				painter.drawPolygon(
12138 					Point(10, 4),
12139 					Point(6, 2),
12140 					Point(6, 6),
12141 					Point(10, 4),
12142 				);
12143 			break;
12144 			default:
12145 				painter.outlineColor = getComputedStyle.foregroundColor;
12146 				painter.drawText(arsd.color.Point(0, 0), action.label, arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
12147 		}
12148 		return bounds;
12149 		});
12150 	}
12151 
12152 }
12153 
12154 
12155 /++
12156 	You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode].
12157 +/
12158 class MenuBar : Widget {
12159 	MenuItem[] items;
12160 	Menu[] subMenus;
12161 
12162 	version(win32_widgets) {
12163 		HMENU handle;
12164 		///
12165 		this(Widget parent = null) {
12166 			super(parent);
12167 
12168 			handle = CreateMenu();
12169 			tabStop = false;
12170 		}
12171 	} else version(custom_widgets) {
12172 		///
12173 		this(Widget parent = null) {
12174 			tabStop = false; // these are selected some other way
12175 			super(parent);
12176 		}
12177 
12178 		mixin Padding!q{2};
12179 	} else static assert(false);
12180 
12181 	version(custom_widgets)
12182 	override void paint(WidgetPainter painter) {
12183 		draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color);
12184 	}
12185 
12186 	///
12187 	MenuItem addItem(MenuItem item) {
12188 		this.addChild(item);
12189 		items ~= item;
12190 		version(win32_widgets) {
12191 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
12192 		}
12193 		return item;
12194 	}
12195 
12196 
12197 	///
12198 	Menu addItem(Menu item) {
12199 
12200 		subMenus ~= item;
12201 
12202 		auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane
12203 
12204 		addChild(mbItem);
12205 		items ~= mbItem;
12206 
12207 		version(win32_widgets) {
12208 			AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label));
12209 		} else version(custom_widgets) {
12210 			mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) {
12211 				item.popup(mbItem);
12212 			};
12213 		} else static assert(false);
12214 
12215 		return item;
12216 	}
12217 
12218 	override void recomputeChildLayout() {
12219 		.recomputeChildLayout!"width"(this);
12220 	}
12221 
12222 	override int maxHeight() { return defaultLineHeight + 4; }
12223 	override int minHeight() { return defaultLineHeight + 4; }
12224 }
12225 
12226 
12227 /**
12228 	Status bars appear at the bottom of a MainWindow.
12229 	They are made out of Parts, with a width and content.
12230 
12231 	They can have multiple parts or be in simple mode. FIXME: implement simple mode.
12232 
12233 
12234 	sb.parts[0].content = "Status bar text!";
12235 */
12236 // https://learn.microsoft.com/en-us/windows/win32/controls/status-bars#owner-drawn-status-bars
12237 class StatusBar : Widget {
12238 	private Part[] partsArray;
12239 	///
12240 	struct Parts {
12241 		@disable this();
12242 		this(StatusBar owner) { this.owner = owner; }
12243 		//@disable this(this);
12244 		///
12245 		@property int length() { return cast(int) owner.partsArray.length; }
12246 		private StatusBar owner;
12247 		private this(StatusBar owner, Part[] parts) {
12248 			this.owner.partsArray = parts;
12249 			this.owner = owner;
12250 		}
12251 		///
12252 		Part opIndex(int p) {
12253 			if(owner.partsArray.length == 0)
12254 				this ~= new StatusBar.Part(0);
12255 			return owner.partsArray[p];
12256 		}
12257 
12258 		///
12259 		Part opOpAssign(string op : "~" )(Part p) {
12260 			assert(owner.partsArray.length < 255);
12261 			p.owner = this.owner;
12262 			p.idx = cast(int) owner.partsArray.length;
12263 			owner.partsArray ~= p;
12264 
12265 			owner.queueRecomputeChildLayout();
12266 
12267 			version(win32_widgets) {
12268 				int[256] pos;
12269 				int cpos;
12270 				foreach(idx, part; owner.partsArray) {
12271 					if(idx + 1 == owner.partsArray.length)
12272 						pos[idx] = -1;
12273 					else {
12274 						cpos += part.currentlyAssignedWidth;
12275 						pos[idx] = cpos;
12276 					}
12277 				}
12278 				SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr);
12279 			} else version(custom_widgets) {
12280 				owner.redraw();
12281 			} else static assert(false);
12282 
12283 			return p;
12284 		}
12285 
12286 		/++
12287 			Sets up proportional parts in one function call. You can use negative numbers to indicate device-independent pixels, and positive numbers to indicate proportions.
12288 
12289 			No given item should be 0.
12290 
12291 			History:
12292 				Added December 31, 2024
12293 		+/
12294 		void setSizes(int[] proportions...) {
12295 			assert(this.owner);
12296 			this.owner.partsArray = null;
12297 
12298 			foreach(n; proportions) {
12299 				assert(n, "do not give 0 to statusBar.parts.set, it would make an invisible part. Try 1 instead.");
12300 
12301 				this.opOpAssign!"~"(new StatusBar.Part(n > 0 ? n : -n, n > 0 ? StatusBar.Part.WidthUnits.Proportional : StatusBar.Part.WidthUnits.DeviceIndependentPixels));
12302 			}
12303 
12304 		}
12305 	}
12306 
12307 	private Parts _parts;
12308 	///
12309 	final @property Parts parts() {
12310 		return _parts;
12311 	}
12312 
12313 	/++
12314 
12315 	+/
12316 	static class Part {
12317 		/++
12318 			History:
12319 				Added September 1, 2023 (dub v11.1)
12320 		+/
12321 		enum WidthUnits {
12322 			/++
12323 				Unscaled pixels as they appear on screen.
12324 
12325 				If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui.
12326 			+/
12327 			DeviceDependentPixels,
12328 			/++
12329 				Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui.
12330 			+/
12331 			DeviceIndependentPixels,
12332 			/++
12333 				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`).
12334 			+/
12335 			ApproximateCharacters,
12336 			/++
12337 				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.
12338 
12339 				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.
12340 			+/
12341 			Proportional
12342 		}
12343 		private WidthUnits units;
12344 		private int width;
12345 		private StatusBar owner;
12346 
12347 		private int currentlyAssignedWidth;
12348 
12349 		/++
12350 			History:
12351 				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.
12352 
12353 				It now allows you to provide your own value for [WidthUnits].
12354 
12355 				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`.
12356 		+/
12357 		this(int w, WidthUnits units = WidthUnits.Proportional) {
12358 			this.units = units;
12359 			this.width = w;
12360 		}
12361 
12362 		/// ditto
12363 		this(int w = 0) {
12364 			if(w == 0)
12365 				this(w, WidthUnits.Proportional);
12366 			else
12367 				this(w, WidthUnits.DeviceDependentPixels);
12368 		}
12369 
12370 		private int idx;
12371 		private string _content;
12372 		///
12373 		@property string content() { return _content; }
12374 		///
12375 		@property void content(string s) {
12376 			version(win32_widgets) {
12377 				_content = s;
12378 				WCharzBuffer bfr = WCharzBuffer(s);
12379 				SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr);
12380 			} else version(custom_widgets) {
12381 				if(_content != s) {
12382 					_content = s;
12383 					owner.redraw();
12384 				}
12385 			} else static assert(false);
12386 		}
12387 	}
12388 	string simpleModeContent;
12389 	bool inSimpleMode;
12390 
12391 
12392 	///
12393 	this(Widget parent) {
12394 		super(null); // FIXME
12395 		_parts = Parts(this);
12396 		tabStop = false;
12397 		version(win32_widgets) {
12398 			parentWindow = parent.parentWindow;
12399 			createWin32Window(this, "msctls_statusbar32"w, "", 0);
12400 
12401 			RECT rect;
12402 			GetWindowRect(hwnd, &rect);
12403 			idealHeight = rect.bottom - rect.top;
12404 			assert(idealHeight);
12405 		} else version(custom_widgets) {
12406 		} else static assert(false);
12407 	}
12408 
12409 	override void recomputeChildLayout() {
12410 		int remainingLength = this.width;
12411 
12412 		int proportionalSum;
12413 		int proportionalCount;
12414 		foreach(idx, part; this.partsArray) {
12415 			with(Part.WidthUnits)
12416 			final switch(part.units) {
12417 				case DeviceDependentPixels:
12418 					part.currentlyAssignedWidth = part.width;
12419 					remainingLength -= part.currentlyAssignedWidth;
12420 				break;
12421 				case DeviceIndependentPixels:
12422 					part.currentlyAssignedWidth = scaleWithDpi(part.width);
12423 					remainingLength -= part.currentlyAssignedWidth;
12424 				break;
12425 				case ApproximateCharacters:
12426 					auto cs = getComputedStyle();
12427 					auto font = cs.font;
12428 
12429 					part.currentlyAssignedWidth = castFnumToCnum(font.averageWidth * this.width);
12430 					remainingLength -= part.currentlyAssignedWidth;
12431 				break;
12432 				case Proportional:
12433 					proportionalSum += part.width;
12434 					proportionalCount ++;
12435 				break;
12436 			}
12437 		}
12438 
12439 		foreach(part; this.partsArray) {
12440 			if(part.units == Part.WidthUnits.Proportional) {
12441 				auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width;
12442 				if(proportion == 0)
12443 					proportion = 1;
12444 
12445 				if(proportionalSum == 0)
12446 					proportionalSum = proportionalCount;
12447 
12448 				part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum;
12449 			}
12450 		}
12451 
12452 		super.recomputeChildLayout();
12453 	}
12454 
12455 	version(win32_widgets)
12456 	override protected void dpiChanged() {
12457 		RECT rect;
12458 		GetWindowRect(hwnd, &rect);
12459 		idealHeight = rect.bottom - rect.top;
12460 		assert(idealHeight);
12461 	}
12462 
12463 	version(custom_widgets)
12464 	override void paint(WidgetPainter painter) {
12465 		auto cs = getComputedStyle();
12466 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
12467 		int cpos = 0;
12468 		foreach(idx, part; this.partsArray) {
12469 			auto partWidth = part.currentlyAssignedWidth;
12470 			// part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100);
12471 			painter.setClipRectangle(Point(cpos, 0), partWidth, height);
12472 			draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color);
12473 			painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4);
12474 
12475 			painter.outlineColor = cs.foregroundColor();
12476 			painter.fillColor = cs.foregroundColor();
12477 
12478 			painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter);
12479 			cpos += partWidth;
12480 		}
12481 	}
12482 
12483 
12484 	version(win32_widgets) {
12485 		private int idealHeight;
12486 		override int maxHeight() { return idealHeight; }
12487 		override int minHeight() { return idealHeight; }
12488 	} else version(custom_widgets) {
12489 		override int maxHeight() { return defaultLineHeight + 4; }
12490 		override int minHeight() { return defaultLineHeight + 4; }
12491 	} else static assert(false);
12492 }
12493 
12494 /// Displays an in-progress indicator without known values
12495 version(none)
12496 class IndefiniteProgressBar : Widget {
12497 	version(win32_widgets)
12498 	this(Widget parent) {
12499 		super(parent);
12500 		createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */);
12501 		tabStop = false;
12502 	}
12503 	override int minHeight() { return 10; }
12504 }
12505 
12506 /// A progress bar with a known endpoint and completion amount
12507 class ProgressBar : Widget {
12508 	/++
12509 		History:
12510 			Added March 16, 2022 (dub v10.7)
12511 	+/
12512 	this(int min, int max, Widget parent) {
12513 		this(parent);
12514 		setRange(cast(ushort) min, cast(ushort) max); // FIXME
12515 	}
12516 	this(Widget parent) {
12517 		version(win32_widgets) {
12518 			super(parent);
12519 			createWin32Window(this, "msctls_progress32"w, "", 0);
12520 			tabStop = false;
12521 		} else version(custom_widgets) {
12522 			super(parent);
12523 			max = 100;
12524 			step = 10;
12525 			tabStop = false;
12526 		} else static assert(0);
12527 	}
12528 
12529 	version(custom_widgets)
12530 	override void paint(WidgetPainter painter) {
12531 		auto cs = getComputedStyle();
12532 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
12533 		painter.fillColor = cs.progressBarColor;
12534 		painter.drawRectangle(Point(0, 0), width * current / max, height);
12535 	}
12536 
12537 
12538 	version(custom_widgets) {
12539 		int current;
12540 		int max;
12541 		int step;
12542 	}
12543 
12544 	///
12545 	void advanceOneStep() {
12546 		version(win32_widgets)
12547 			SendMessageW(hwnd, PBM_STEPIT, 0, 0);
12548 		else version(custom_widgets)
12549 			addToPosition(step);
12550 		else static assert(false);
12551 	}
12552 
12553 	///
12554 	void setStepIncrement(int increment) {
12555 		version(win32_widgets)
12556 			SendMessageW(hwnd, PBM_SETSTEP, increment, 0);
12557 		else version(custom_widgets)
12558 			step = increment;
12559 		else static assert(false);
12560 	}
12561 
12562 	///
12563 	void addToPosition(int amount) {
12564 		version(win32_widgets)
12565 			SendMessageW(hwnd, PBM_DELTAPOS, amount, 0);
12566 		else version(custom_widgets)
12567 			setPosition(current + amount);
12568 		else static assert(false);
12569 	}
12570 
12571 	///
12572 	void setPosition(int pos) {
12573 		version(win32_widgets)
12574 			SendMessageW(hwnd, PBM_SETPOS, pos, 0);
12575 		else version(custom_widgets) {
12576 			current = pos;
12577 			if(current > max)
12578 				current = max;
12579 			redraw();
12580 		}
12581 		else static assert(false);
12582 	}
12583 
12584 	///
12585 	void setRange(ushort min, ushort max) {
12586 		version(win32_widgets)
12587 			SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max));
12588 		else version(custom_widgets) {
12589 			this.max = max;
12590 		}
12591 		else static assert(false);
12592 	}
12593 
12594 	override int minHeight() { return 10; }
12595 }
12596 
12597 version(custom_widgets)
12598 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) {
12599 	thisLabel.reserve(label.length);
12600 	bool justSawAmpersand;
12601 	foreach(ch; label) {
12602 		if(justSawAmpersand) {
12603 			justSawAmpersand = false;
12604 			if(ch == '&') {
12605 				goto plain;
12606 			}
12607 			thisAccelerator = ch;
12608 		} else {
12609 			if(ch == '&') {
12610 				justSawAmpersand = true;
12611 				continue;
12612 			}
12613 			plain:
12614 			thisLabel ~= ch;
12615 		}
12616 	}
12617 }
12618 
12619 /++
12620 	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.
12621 
12622 
12623 	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
12624 
12625 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
12626 
12627 	History:
12628 		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.
12629 +/
12630 class Fieldset : Widget {
12631 	// FIXME: on Windows,it doesn't draw the background on the label
12632 	// on X, it doesn't fix the clipping rectangle for it
12633 	version(win32_widgets)
12634 		override int paddingTop() { return defaultLineHeight; }
12635 	else version(custom_widgets)
12636 		override int paddingTop() { return defaultLineHeight + 2; }
12637 	else static assert(false);
12638 	override int paddingBottom() { return 6; }
12639 	override int paddingLeft() { return 6; }
12640 	override int paddingRight() { return 6; }
12641 
12642 	override int marginLeft() { return 6; }
12643 	override int marginRight() { return 6; }
12644 	override int marginTop() { return 2; }
12645 	override int marginBottom() { return 2; }
12646 
12647 	string legend;
12648 
12649 	version(custom_widgets) private dchar accelerator;
12650 
12651 	this(string legend, Widget parent) {
12652 		version(win32_widgets) {
12653 			super(parent);
12654 			this.legend = legend;
12655 			createWin32Window(this, "button"w, legend, BS_GROUPBOX);
12656 			tabStop = false;
12657 		} else version(custom_widgets) {
12658 			super(parent);
12659 			tabStop = false;
12660 
12661 			legend.extractWindowsStyleLabel(this.legend, this.accelerator);
12662 		} else static assert(0);
12663 	}
12664 
12665 	version(custom_widgets)
12666 	override void paint(WidgetPainter painter) {
12667 		auto dlh = defaultLineHeight;
12668 
12669 		painter.fillColor = Color.transparent;
12670 		auto cs = getComputedStyle();
12671 		painter.pen = Pen(cs.foregroundColor, 1);
12672 		painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2);
12673 
12674 		auto tx = painter.textSize(legend);
12675 		painter.outlineColor = Color.transparent;
12676 
12677 		version(Windows) {
12678 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
12679 			painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height);
12680 			SelectObject(painter.impl.hdc, b);
12681 		} else static if(UsingSimpledisplayX11) {
12682 			painter.fillColor = getComputedStyle().windowBackgroundColor;
12683 			painter.drawRectangle(Point(8, 0), tx.width, tx.height);
12684 		}
12685 		painter.outlineColor = cs.foregroundColor;
12686 		painter.drawText(Point(8, 0), legend);
12687 	}
12688 
12689 	override int maxHeight() {
12690 		auto m = paddingTop() + paddingBottom();
12691 		foreach(child; children) {
12692 			auto mh = child.maxHeight();
12693 			if(mh == int.max)
12694 				return int.max;
12695 			m += mh;
12696 			m += child.marginBottom();
12697 			m += child.marginTop();
12698 		}
12699 		m += 6;
12700 		if(m < minHeight)
12701 			return minHeight;
12702 		return m;
12703 	}
12704 
12705 	override int minHeight() {
12706 		auto m = paddingTop() + paddingBottom();
12707 		foreach(child; children) {
12708 			m += child.minHeight();
12709 			m += child.marginBottom();
12710 			m += child.marginTop();
12711 		}
12712 		return m + 6;
12713 	}
12714 
12715 	override int minWidth() {
12716 		return 6 + cast(int) this.legend.length * 7;
12717 	}
12718 }
12719 
12720 /++
12721 	$(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")
12722 	$(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.)
12723 +/
12724 version(minigui_screenshots)
12725 @Screenshot("Fieldset")
12726 unittest {
12727 	auto window = new Window(200, 100);
12728 	auto set = new Fieldset("Baby will", window);
12729 	auto option1 = new Radiobox("Eat", set);
12730 	auto option2 = new Radiobox("Cry", set);
12731 	auto option3 = new Radiobox("Sleep", set);
12732 	window.loop();
12733 }
12734 
12735 /// Draws a line
12736 class HorizontalRule : Widget {
12737 	mixin Margin!q{ 2 };
12738 	override int minHeight() { return 2; }
12739 	override int maxHeight() { return 2; }
12740 
12741 	///
12742 	this(Widget parent) {
12743 		super(parent);
12744 	}
12745 
12746 	override void paint(WidgetPainter painter) {
12747 		auto cs = getComputedStyle();
12748 		painter.outlineColor = cs.darkAccentColor;
12749 		painter.drawLine(Point(0, 0), Point(width, 0));
12750 		painter.outlineColor = cs.lightAccentColor;
12751 		painter.drawLine(Point(0, 1), Point(width, 1));
12752 	}
12753 }
12754 
12755 version(minigui_screenshots)
12756 @Screenshot("HorizontalRule")
12757 /++
12758 	$(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.)
12759 
12760 +/
12761 unittest {
12762 	auto window = new Window(200, 100);
12763 	auto above = new TextLabel("Above the line", TextAlignment.Left, window);
12764 	new HorizontalRule(window);
12765 	auto below = new TextLabel("Below the line", TextAlignment.Left, window);
12766 	window.loop();
12767 }
12768 
12769 /// ditto
12770 class VerticalRule : Widget {
12771 	mixin Margin!q{ 2 };
12772 	override int minWidth() { return 2; }
12773 	override int maxWidth() { return 2; }
12774 
12775 	///
12776 	this(Widget parent) {
12777 		super(parent);
12778 	}
12779 
12780 	override void paint(WidgetPainter painter) {
12781 		auto cs = getComputedStyle();
12782 		painter.outlineColor = cs.darkAccentColor;
12783 		painter.drawLine(Point(0, 0), Point(0, height));
12784 		painter.outlineColor = cs.lightAccentColor;
12785 		painter.drawLine(Point(1, 0), Point(1, height));
12786 	}
12787 }
12788 
12789 
12790 ///
12791 class Menu : Window {
12792 	void remove() {
12793 		foreach(i, child; parentWindow.children)
12794 			if(child is this) {
12795 				parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $];
12796 				break;
12797 			}
12798 		parentWindow.redraw();
12799 
12800 		parentWindow.releaseMouseCapture();
12801 	}
12802 
12803 	///
12804 	void addSeparator() {
12805 		version(win32_widgets)
12806 			AppendMenu(handle, MF_SEPARATOR, 0, null);
12807 		else version(custom_widgets)
12808 			auto hr = new HorizontalRule(this);
12809 		else static assert(0);
12810 	}
12811 
12812 	override int paddingTop() { return 4; }
12813 	override int paddingBottom() { return 4; }
12814 	override int paddingLeft() { return 2; }
12815 	override int paddingRight() { return 2; }
12816 
12817 	version(win32_widgets) {}
12818 	else version(custom_widgets) {
12819 
12820 		Widget previouslyFocusedWidget;
12821 		Widget* previouslyFocusedWidgetBelongsIn;
12822 
12823 		SimpleWindow dropDown;
12824 		Widget menuParent;
12825 		void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
12826 			this.menuParent = parent;
12827 
12828 			previouslyFocusedWidget = parent.parentWindow.focusedWidget;
12829 			previouslyFocusedWidgetBelongsIn = &parent.parentWindow.focusedWidget;
12830 			parent.parentWindow.focusedWidget = this;
12831 
12832 			int w = 150;
12833 			int h = paddingTop + paddingBottom;
12834 			if(this.children.length) {
12835 				// hacking it to get the ideal height out of recomputeChildLayout
12836 				this.width = w;
12837 				this.height = h;
12838 				this.recomputeChildLayoutEntry();
12839 				h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
12840 				h += paddingBottom;
12841 
12842 				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
12843 			}
12844 
12845 			if(offsetY == int.min)
12846 				offsetY = parent.defaultLineHeight;
12847 
12848 			auto coord = parent.globalCoordinates();
12849 			dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
12850 			this.x = 0;
12851 			this.y = 0;
12852 			this.width = dropDown.width;
12853 			this.height = dropDown.height;
12854 			this.drawableWindow = dropDown;
12855 			this.recomputeChildLayoutEntry();
12856 
12857 			static if(UsingSimpledisplayX11)
12858 				XSync(XDisplayConnection.get, 0);
12859 
12860 			dropDown.visibilityChanged = (bool visible) {
12861 				if(visible) {
12862 					this.redraw();
12863 					dropDown.grabInput();
12864 				} else {
12865 					dropDown.releaseInputGrab();
12866 				}
12867 			};
12868 
12869 			dropDown.show();
12870 
12871 			clickListener = this.addEventListener((scope ClickEvent ev) {
12872 				unpopup();
12873 				// need to unlock asap just in case other user handlers block...
12874 				static if(UsingSimpledisplayX11)
12875 					flushGui();
12876 			}, true /* again for asap action */);
12877 		}
12878 
12879 		EventListener clickListener;
12880 	}
12881 	else static assert(false);
12882 
12883 	version(custom_widgets)
12884 	void unpopup() {
12885 		mouseLastOver = mouseLastDownOn = null;
12886 		dropDown.hide();
12887 		if(!menuParent.parentWindow.win.closed) {
12888 			if(auto maw = cast(MouseActivatedWidget) menuParent) {
12889 				maw.setDynamicState(DynamicState.depressed, false);
12890 				maw.setDynamicState(DynamicState.hover, false);
12891 				maw.redraw();
12892 			}
12893 			// menuParent.parentWindow.win.focus();
12894 		}
12895 		clickListener.disconnect();
12896 
12897 		if(previouslyFocusedWidgetBelongsIn)
12898 			*previouslyFocusedWidgetBelongsIn = previouslyFocusedWidget;
12899 	}
12900 
12901 	MenuItem[] items;
12902 
12903 	///
12904 	MenuItem addItem(MenuItem item) {
12905 		addChild(item);
12906 		items ~= item;
12907 		version(win32_widgets) {
12908 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
12909 		}
12910 		return item;
12911 	}
12912 
12913 	string label;
12914 
12915 	version(win32_widgets) {
12916 		HMENU handle;
12917 		///
12918 		this(string label, Widget parent) {
12919 			// not actually passing the parent since it effs up the drawing
12920 			super(cast(Widget) null);// parent);
12921 			this.label = label;
12922 			handle = CreatePopupMenu();
12923 		}
12924 	} else version(custom_widgets) {
12925 		///
12926 		this(string label, Widget parent) {
12927 
12928 			if(dropDown) {
12929 				dropDown.close();
12930 			}
12931 			dropDown = new SimpleWindow(
12932 				150, 4,
12933 				// FIXME: what if it is a popupMenu ?
12934 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null);
12935 
12936 			this.label = label;
12937 
12938 			super(dropDown);
12939 		}
12940 	} else static assert(false);
12941 
12942 	override int maxHeight() { return defaultLineHeight; }
12943 	override int minHeight() { return defaultLineHeight; }
12944 
12945 	version(custom_widgets) {
12946 		Widget currentPlace;
12947 
12948 		void changeCurrentPlace(Widget n) {
12949 			if(currentPlace) {
12950 				currentPlace.dynamicState = 0;
12951 			}
12952 
12953 			if(n) {
12954 				n.dynamicState = DynamicState.hover;
12955 			}
12956 
12957 			currentPlace = n;
12958 		}
12959 
12960 		override void paint(WidgetPainter painter) {
12961 			this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color);
12962 		}
12963 
12964 		override void defaultEventHandler_keydown(KeyDownEvent ke) {
12965 			switch(ke.key) {
12966 				case Key.Down:
12967 					Widget next;
12968 					Widget first;
12969 					foreach(w; this.children) {
12970 						if((cast(MenuItem) w) is null)
12971 							continue;
12972 
12973 						if(first is null)
12974 							first = w;
12975 
12976 						if(next !is null) {
12977 							next = w;
12978 							break;
12979 						}
12980 
12981 						if(currentPlace is null) {
12982 							next = w;
12983 							break;
12984 						}
12985 
12986 						if(w is currentPlace) {
12987 							next = w;
12988 						}
12989 					}
12990 
12991 					if(next is currentPlace)
12992 						next = first;
12993 
12994 					changeCurrentPlace(next);
12995 					break;
12996 				case Key.Up:
12997 					Widget prev;
12998 					foreach(w; this.children) {
12999 						if((cast(MenuItem) w) is null)
13000 							continue;
13001 						if(w is currentPlace) {
13002 							if(prev is null) {
13003 								foreach_reverse(c; this.children) {
13004 									if((cast(MenuItem) c) !is null) {
13005 										prev = c;
13006 										break;
13007 									}
13008 								}
13009 							}
13010 							break;
13011 						}
13012 						prev = w;
13013 					}
13014 					changeCurrentPlace(prev);
13015 					break;
13016 				case Key.Left:
13017 				case Key.Right:
13018 					if(menuParent) {
13019 						Menu first;
13020 						Menu last;
13021 						Menu prev;
13022 						Menu next;
13023 						bool found;
13024 
13025 						size_t prev_idx;
13026 						size_t next_idx;
13027 
13028 						MenuBar mb = cast(MenuBar) menuParent.parent;
13029 
13030 						if(mb) {
13031 							foreach(idx, menu; mb.subMenus) {
13032 								if(first is null)
13033 									first = menu;
13034 								last = menu;
13035 								if(found && next is null) {
13036 									next = menu;
13037 									next_idx = idx;
13038 								}
13039 								if(menu is this)
13040 									found = true;
13041 								if(!found) {
13042 									prev = menu;
13043 									prev_idx = idx;
13044 								}
13045 							}
13046 
13047 							Menu nextMenu;
13048 							size_t nextMenuIdx;
13049 							if(ke.key == Key.Left) {
13050 								nextMenu = prev ? prev : last;
13051 								nextMenuIdx = prev ? prev_idx : mb.subMenus.length - 1;
13052 							} else {
13053 								nextMenu = next ? next : first;
13054 								nextMenuIdx = next ? next_idx : 0;
13055 							}
13056 
13057 							unpopup();
13058 
13059 							auto rent = mb.children[nextMenuIdx]; // FIXME thsi is not necessarily right
13060 							rent.dynamicState = DynamicState.depressed | DynamicState.hover;
13061 							nextMenu.popup(rent);
13062 						}
13063 					}
13064 					break;
13065 				case Key.Enter:
13066 				case Key.PadEnter:
13067 					// because the key up and char events will go back to the other window after we unpopup!
13068 					// we will wait for the char event to come (in the following method)
13069 					break;
13070 				case Key.Escape:
13071 					unpopup();
13072 					break;
13073 				default:
13074 			}
13075 		}
13076 		override void defaultEventHandler_char(CharEvent ke) {
13077 			// if one is selected, enter activates it
13078 			if(currentPlace) {
13079 				if(ke.character == '\n') {
13080 					// enter selects
13081 					auto event = new Event(EventType.triggered, currentPlace);
13082 					event.dispatch();
13083 					unpopup();
13084 					return;
13085 				}
13086 			}
13087 
13088 			// otherwise search for a hotkey
13089 			foreach(item; items) {
13090 				if(item.hotkey == ke.character) {
13091 					auto event = new Event(EventType.triggered, item);
13092 					event.dispatch();
13093 					unpopup();
13094 					return;
13095 				}
13096 			}
13097 		}
13098 		override void defaultEventHandler_mouseover(MouseOverEvent moe) {
13099 			if(moe.target && moe.target.parent is this)
13100 				changeCurrentPlace(moe.target);
13101 		}
13102 	}
13103 }
13104 
13105 /++
13106 	A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked.
13107 +/
13108 class MenuItem : MouseActivatedWidget {
13109 	Menu submenu;
13110 
13111 	Action action;
13112 	string label;
13113 	dchar hotkey;
13114 
13115 	override int paddingLeft() { return 4; }
13116 
13117 	override int maxHeight() { return defaultLineHeight + 4; }
13118 	override int minHeight() { return defaultLineHeight + 4; }
13119 	override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); }
13120 	override int maxWidth() {
13121 		if(cast(MenuBar) parent) {
13122 			return minWidth();
13123 		}
13124 		return int.max;
13125 	}
13126 	/// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu.
13127 	this(string lbl, Widget parent = null) {
13128 		super(parent);
13129 		//label = lbl; // FIXME
13130 		foreach(idx, char ch; lbl) // FIXME
13131 			if(ch != '&') { // FIXME
13132 				label ~= ch; // FIXME
13133 			} else {
13134 				if(idx + 1 < lbl.length) {
13135 					hotkey = lbl[idx + 1];
13136 					if(hotkey >= 'A' && hotkey <= 'Z')
13137 						hotkey += 32;
13138 				}
13139 			}
13140 		tabStop = false; // these are selected some other way
13141 	}
13142 
13143 	///
13144 	this(Action action, Widget parent = null) {
13145 		assert(action !is null);
13146 		this(action.label, parent);
13147 		this.action = action;
13148 		tabStop = false; // these are selected some other way
13149 	}
13150 
13151 	version(custom_widgets)
13152 	override void paint(WidgetPainter painter) {
13153 		auto cs = getComputedStyle();
13154 		if(dynamicState & DynamicState.depressed)
13155 			this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
13156 		else {
13157 			if(dynamicState & DynamicState.hover) {
13158 				painter.fillColor = cs.hoveringColor;
13159 				painter.outlineColor = Color.transparent;
13160 			} else {
13161 				painter.fillColor = cs.background.color;
13162 				painter.outlineColor = Color.transparent;
13163 			}
13164 
13165 			painter.drawRectangle(Point(0, 0), Size(this.width, this.height));
13166 		}
13167 
13168 		if(dynamicState & DynamicState.hover)
13169 			painter.outlineColor = cs.activeMenuItemColor;
13170 		else
13171 			painter.outlineColor = cs.foregroundColor;
13172 		painter.fillColor = Color.transparent;
13173 		painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
13174 		if(action && action.accelerator !is KeyEvent.init) {
13175 			painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter);
13176 
13177 		}
13178 	}
13179 
13180 	static class Style : Widget.Style {
13181 		override bool variesWithState(ulong dynamicStateFlags) {
13182 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
13183 		}
13184 	}
13185 	mixin OverrideStyle!Style;
13186 
13187 	override void defaultEventHandler_triggered(Event event) {
13188 		if(action)
13189 		foreach(handler; action.triggered)
13190 			handler();
13191 
13192 		if(auto pmenu = cast(Menu) this.parent)
13193 			pmenu.remove();
13194 
13195 		super.defaultEventHandler_triggered(event);
13196 	}
13197 }
13198 
13199 version(win32_widgets)
13200 /// A "mouse activiated widget" is really just an abstract variant of button.
13201 class MouseActivatedWidget : Widget {
13202 	@property bool isChecked() {
13203 		assert(hwnd);
13204 		return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED;
13205 
13206 	}
13207 	@property void isChecked(bool state) {
13208 		assert(hwnd);
13209 		SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0);
13210 
13211 	}
13212 
13213 	override void handleWmCommand(ushort cmd, ushort id) {
13214 		if(cmd == 0) {
13215 			auto event = new Event(EventType.triggered, this);
13216 			event.dispatch();
13217 		}
13218 	}
13219 
13220 	this(Widget parent) {
13221 		super(parent);
13222 	}
13223 }
13224 else version(custom_widgets)
13225 /// ditto
13226 class MouseActivatedWidget : Widget {
13227 	@property bool isChecked() { return isChecked_; }
13228 	@property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;}
13229 
13230 	private bool isChecked_;
13231 
13232 	this(Widget parent) {
13233 		super(parent);
13234 
13235 		addEventListener((MouseDownEvent ev) {
13236 			if(ev.button == MouseButton.left) {
13237 				setDynamicState(DynamicState.depressed, true);
13238 				setDynamicState(DynamicState.hover, true);
13239 				redraw();
13240 			}
13241 		});
13242 
13243 		addEventListener((MouseUpEvent ev) {
13244 			if(ev.button == MouseButton.left) {
13245 				setDynamicState(DynamicState.depressed, false);
13246 				setDynamicState(DynamicState.hover, false);
13247 				redraw();
13248 			}
13249 		});
13250 
13251 		addEventListener((MouseMoveEvent mme) {
13252 			if(!(mme.state & ModifierState.leftButtonDown)) {
13253 				if(dynamicState_ & DynamicState.depressed) {
13254 					setDynamicState(DynamicState.depressed, false);
13255 					redraw();
13256 				}
13257 			}
13258 		});
13259 	}
13260 
13261 	override void defaultEventHandler_focus(FocusEvent ev) {
13262 		super.defaultEventHandler_focus(ev);
13263 		this.redraw();
13264 	}
13265 	override void defaultEventHandler_blur(BlurEvent ev) {
13266 		super.defaultEventHandler_blur(ev);
13267 		setDynamicState(DynamicState.depressed, false);
13268 		this.redraw();
13269 	}
13270 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
13271 		super.defaultEventHandler_keydown(ev);
13272 		if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) {
13273 			setDynamicState(DynamicState.depressed, true);
13274 			setDynamicState(DynamicState.hover, true);
13275 			this.redraw();
13276 		}
13277 	}
13278 	override void defaultEventHandler_keyup(KeyUpEvent ev) {
13279 		super.defaultEventHandler_keyup(ev);
13280 		if(!(dynamicState & DynamicState.depressed))
13281 			return;
13282 		if(!enabled)
13283 			return;
13284 		setDynamicState(DynamicState.depressed, false);
13285 		setDynamicState(DynamicState.hover, false);
13286 		this.redraw();
13287 
13288 		auto event = new Event(EventType.triggered, this);
13289 		event.sendDirectly();
13290 	}
13291 	override void defaultEventHandler_click(ClickEvent ev) {
13292 		super.defaultEventHandler_click(ev);
13293 		if(ev.button == MouseButton.left && enabled) {
13294 			auto event = new Event(EventType.triggered, this);
13295 			event.sendDirectly();
13296 		}
13297 	}
13298 
13299 }
13300 else static assert(false);
13301 
13302 /*
13303 /++
13304 	Like the tablet thing, it would have a label, a description, and a switch slider thingy.
13305 
13306 	Basically the same as a checkbox.
13307 +/
13308 class OnOffSwitch : MouseActivatedWidget {
13309 
13310 }
13311 */
13312 
13313 /++
13314 	History:
13315 		Added June 15, 2021 (dub v10.1)
13316 +/
13317 struct ImageLabel {
13318 	/++
13319 		Defines a label+image combo used by some widgets.
13320 
13321 		If you provide just a text label, that is all the widget will try to
13322 		display. Or just an image will display just that. If you provide both,
13323 		it may display both text and image side by side or display the image
13324 		and offer text on an input event depending on the widget.
13325 
13326 		History:
13327 			The `alignment` parameter was added on September 27, 2021
13328 	+/
13329 	this(string label, TextAlignment alignment = TextAlignment.Center) {
13330 		this.label = label;
13331 		this.displayFlags = DisplayFlags.displayText;
13332 		this.alignment = alignment;
13333 	}
13334 
13335 	/// ditto
13336 	this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
13337 		this.label = label;
13338 		this.image = image;
13339 		this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
13340 		this.alignment = alignment;
13341 	}
13342 
13343 	/// ditto
13344 	this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
13345 		this.image = image;
13346 		this.displayFlags = DisplayFlags.displayImage;
13347 		this.alignment = alignment;
13348 	}
13349 
13350 	/// ditto
13351 	this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) {
13352 		this.label = label;
13353 		this.image = image;
13354 		this.alignment = alignment;
13355 		this.displayFlags = displayFlags;
13356 	}
13357 
13358 	string label;
13359 	MemoryImage image;
13360 
13361 	enum DisplayFlags {
13362 		displayText = 1 << 0,
13363 		displayImage = 1 << 1,
13364 	}
13365 
13366 	int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
13367 
13368 	TextAlignment alignment;
13369 }
13370 
13371 /++
13372 	A basic checked or not checked box with an attached label.
13373 
13374 
13375 	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
13376 
13377 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
13378 
13379 	History:
13380 		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.
13381 +/
13382 class Checkbox : MouseActivatedWidget {
13383 	version(win32_widgets) {
13384 		override int maxHeight() { return scaleWithDpi(16); }
13385 		override int minHeight() { return scaleWithDpi(16); }
13386 	} else version(custom_widgets) {
13387 		private enum buttonSize = 16;
13388 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
13389 		override int minHeight() { return maxHeight(); }
13390 	} else static assert(0);
13391 
13392 	override int marginLeft() { return 4; }
13393 
13394 	override int flexBasisWidth() { return 24 + cast(int) label.length * 7; }
13395 
13396 	/++
13397 		Just an alias because I keep typing checked out of web habit.
13398 
13399 		History:
13400 			Added May 31, 2021
13401 	+/
13402 	alias checked = isChecked;
13403 
13404 	private string label;
13405 	private dchar accelerator;
13406 
13407 	/++
13408 	+/
13409 	this(string label, Widget parent) {
13410 		this(ImageLabel(label), Appearance.checkbox, parent);
13411 	}
13412 
13413 	/// ditto
13414 	this(string label, Appearance appearance, Widget parent) {
13415 		this(ImageLabel(label), appearance, parent);
13416 	}
13417 
13418 	/++
13419 		Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific.
13420 
13421 		History:
13422 			Added June 29, 2021 (dub v10.2)
13423 	+/
13424 	enum Appearance {
13425 		checkbox, /// a normal checkbox
13426 		pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad.
13427 		//sliderswitch,
13428 	}
13429 	private Appearance appearance;
13430 
13431 	/// ditto
13432 	private this(ImageLabel label, Appearance appearance, Widget parent) {
13433 		super(parent);
13434 		version(win32_widgets) {
13435 			this.label = label.label;
13436 
13437 			uint extraStyle;
13438 			final switch(appearance) {
13439 				case Appearance.checkbox:
13440 				break;
13441 				case Appearance.pushbutton:
13442 					extraStyle |= BS_PUSHLIKE;
13443 				break;
13444 			}
13445 
13446 			createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle);
13447 		} else version(custom_widgets) {
13448 			label.label.extractWindowsStyleLabel(this.label, this.accelerator);
13449 		} else static assert(0);
13450 	}
13451 
13452 	version(custom_widgets)
13453 	override void paint(WidgetPainter painter) {
13454 		auto cs = getComputedStyle();
13455 		if(isFocused()) {
13456 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
13457 			painter.fillColor = cs.windowBackgroundColor;
13458 			painter.drawRectangle(Point(0, 0), width, height);
13459 			painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
13460 		} else {
13461 			painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid);
13462 			painter.fillColor = cs.windowBackgroundColor;
13463 			painter.drawRectangle(Point(0, 0), width, height);
13464 		}
13465 
13466 
13467 		painter.outlineColor = Color.black;
13468 		painter.fillColor = Color.white;
13469 		enum rectOffset = 2;
13470 		painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset));
13471 
13472 		if(isChecked) {
13473 			auto size = scaleWithDpi(2);
13474 			painter.pen = Pen(Color.black, size);
13475 			// I'm using height so the checkbox is square
13476 			enum padding = 3;
13477 			painter.drawLine(
13478 				scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)),
13479 				scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2)
13480 			);
13481 			painter.drawLine(
13482 				scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0),
13483 				scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 -  size % 2)
13484 			);
13485 
13486 			painter.pen = Pen(Color.black, 1);
13487 		}
13488 
13489 		if(label !is null) {
13490 			painter.outlineColor = cs.foregroundColor();
13491 			painter.fillColor = cs.foregroundColor();
13492 
13493 			// i want the centerline of the text to be aligned with the centerline of the checkbox
13494 			/+
13495 			auto font = cs.font();
13496 			auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2;
13497 			painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label);
13498 			+/
13499 			painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter);
13500 		}
13501 	}
13502 
13503 	override void defaultEventHandler_triggered(Event ev) {
13504 		isChecked = !isChecked;
13505 
13506 		this.emit!(ChangeEvent!bool)(&isChecked);
13507 
13508 		redraw();
13509 	}
13510 
13511 	/// Emits a change event with the checked state
13512 	mixin Emits!(ChangeEvent!bool);
13513 }
13514 
13515 /// Adds empty space to a layout.
13516 class VerticalSpacer : Widget {
13517 	private int mh;
13518 
13519 	/++
13520 		History:
13521 			The overload with `maxHeight` was added on December 31, 2024
13522 	+/
13523 	this(Widget parent) {
13524 		this(0, parent);
13525 	}
13526 
13527 	/// ditto
13528 	this(int maxHeight, Widget parent) {
13529 		this.mh = maxHeight;
13530 		super(parent);
13531 		this.tabStop = false;
13532 	}
13533 
13534 	override int maxHeight() {
13535 		return mh ? scaleWithDpi(mh) : super.maxHeight();
13536 	}
13537 }
13538 
13539 
13540 /// ditto
13541 class HorizontalSpacer : Widget {
13542 	private int mw;
13543 
13544 	/++
13545 		History:
13546 			The overload with `maxWidth` was added on December 31, 2024
13547 	+/
13548 	this(Widget parent) {
13549 		this(0, parent);
13550 	}
13551 
13552 	/// ditto
13553 	this(int maxWidth, Widget parent) {
13554 		this.mw = maxWidth;
13555 		super(parent);
13556 		this.tabStop = false;
13557 	}
13558 
13559 	override int maxWidth() {
13560 		return mw ? scaleWithDpi(mw) : super.maxWidth();
13561 	}
13562 }
13563 
13564 
13565 /++
13566 	Creates a radio button with an associated label. These are usually put inside a [Fieldset].
13567 
13568 
13569 	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
13570 
13571 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
13572 
13573 	History:
13574 		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.
13575 +/
13576 class Radiobox : MouseActivatedWidget {
13577 
13578 	version(win32_widgets) {
13579 		override int maxHeight() { return scaleWithDpi(16); }
13580 		override int minHeight() { return scaleWithDpi(16); }
13581 	} else version(custom_widgets) {
13582 		private enum buttonSize = 16;
13583 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
13584 		override int minHeight() { return maxHeight(); }
13585 	} else static assert(0);
13586 
13587 	override int marginLeft() { return 4; }
13588 
13589 	// FIXME: make a label getter
13590 	private string label;
13591 	private dchar accelerator;
13592 
13593 	/++
13594 
13595 	+/
13596 	this(string label, Widget parent) {
13597 		super(parent);
13598 		version(win32_widgets) {
13599 			this.label = label;
13600 			createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON);
13601 		} else version(custom_widgets) {
13602 			label.extractWindowsStyleLabel(this.label, this.accelerator);
13603 			height = 16;
13604 			width = height + 4 + cast(int) label.length * 16;
13605 		}
13606 	}
13607 
13608 	version(custom_widgets)
13609 	override void paint(WidgetPainter painter) {
13610 		auto cs = getComputedStyle();
13611 
13612 		if(isFocused) {
13613 			painter.fillColor = cs.windowBackgroundColor;
13614 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
13615 		} else {
13616 			painter.fillColor = cs.windowBackgroundColor;
13617 			painter.outlineColor = cs.windowBackgroundColor;
13618 		}
13619 		painter.drawRectangle(Point(0, 0), width, height);
13620 
13621 		painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
13622 
13623 		painter.outlineColor = Color.black;
13624 		painter.fillColor = Color.white;
13625 		painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2)));
13626 		if(isChecked) {
13627 			painter.outlineColor = Color.black;
13628 			painter.fillColor = Color.black;
13629 			// I'm using height so the checkbox is square
13630 			auto size = scaleWithDpi(2);
13631 			painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2));
13632 		}
13633 
13634 		painter.outlineColor = cs.foregroundColor();
13635 		painter.fillColor = cs.foregroundColor();
13636 
13637 		painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
13638 	}
13639 
13640 
13641 	override void defaultEventHandler_triggered(Event ev) {
13642 		isChecked = true;
13643 
13644 		if(this.parent) {
13645 			foreach(child; this.parent.children) {
13646 				if(child is this) continue;
13647 				if(auto rb = cast(Radiobox) child) {
13648 					rb.isChecked = false;
13649 					rb.emit!(ChangeEvent!bool)(&rb.isChecked);
13650 					rb.redraw();
13651 				}
13652 			}
13653 		}
13654 
13655 		this.emit!(ChangeEvent!bool)(&this.isChecked);
13656 
13657 		redraw();
13658 	}
13659 
13660 	/// 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.
13661 	mixin Emits!(ChangeEvent!bool);
13662 }
13663 
13664 
13665 /++
13666 	Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event.
13667 
13668 
13669 	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
13670 
13671 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
13672 
13673 	History:
13674 		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.
13675 +/
13676 class Button : MouseActivatedWidget {
13677 	override int heightStretchiness() { return 3; }
13678 	override int widthStretchiness() { return 3; }
13679 
13680 	/++
13681 		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.
13682 
13683 		History:
13684 			Added July 2, 2021
13685 	+/
13686 	public bool triggersOnMultiClick;
13687 
13688 	private string label_;
13689 	private TextAlignment alignment;
13690 	private dchar accelerator;
13691 
13692 	///
13693 	string label() { return label_; }
13694 	///
13695 	void label(string l) {
13696 		label_ = l;
13697 		version(win32_widgets) {
13698 			WCharzBuffer bfr = WCharzBuffer(l);
13699 			SetWindowTextW(hwnd, bfr.ptr);
13700 		} else version(custom_widgets) {
13701 			redraw();
13702 		}
13703 	}
13704 
13705 	override void defaultEventHandler_dblclick(DoubleClickEvent ev) {
13706 		super.defaultEventHandler_dblclick(ev);
13707 		if(triggersOnMultiClick) {
13708 			if(ev.button == MouseButton.left) {
13709 				auto event = new Event(EventType.triggered, this);
13710 				event.sendDirectly();
13711 			}
13712 		}
13713 	}
13714 
13715 	private Sprite sprite;
13716 	private int displayFlags;
13717 
13718 	protected bool needsOwnerDraw() {
13719 		return &this.paint !is &Button.paint || &this.useStyleProperties !is &Button.useStyleProperties || &this.paintContent !is &Button.paintContent;
13720 	}
13721 
13722 	version(win32_widgets)
13723 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis) {
13724 		auto itemId = dis.itemID;
13725 		auto hdc = dis.hDC;
13726 		auto rect = dis.rcItem;
13727 		switch(dis.itemAction) {
13728 			// skipping setDynamicState because i don't want to queue the redraw unnecessarily
13729 			case ODA_SELECT:
13730 				dynamicState_ &= ~DynamicState.depressed;
13731 				if(dis.itemState & ODS_SELECTED)
13732 					dynamicState_ |= DynamicState.depressed;
13733 			goto case;
13734 			case ODA_FOCUS:
13735 				dynamicState_ &= ~DynamicState.focus;
13736 				if(dis.itemState & ODS_FOCUS)
13737 					dynamicState_ |= DynamicState.focus;
13738 			goto case;
13739 			case ODA_DRAWENTIRE:
13740 				auto painter = WidgetPainter(this.simpleWindowWrappingHwnd.draw(true), this);
13741 				//painter.impl.hdc = hdc;
13742 				paint(painter);
13743 			break;
13744 			default:
13745 		}
13746 		return 1;
13747 
13748 	}
13749 
13750 	/++
13751 		Creates a push button with the given label, which may be an image or some text.
13752 
13753 		Bugs:
13754 			If the image is bigger than the button, it may not be displayed in the right position on Linux.
13755 
13756 		History:
13757 			The [ImageLabel] overload was added on June 21, 2021 (dub v10.1).
13758 
13759 			The button with label and image will respect requests to show both on Windows as
13760 			of March 28, 2022 iff you provide a manifest file to opt into common controls v6.
13761 	+/
13762 	this(string label, Widget parent) {
13763 		this(ImageLabel(label), parent);
13764 	}
13765 
13766 	/// ditto
13767 	this(ImageLabel label, Widget parent) {
13768 		bool needsImage;
13769 		version(win32_widgets) {
13770 			super(parent);
13771 
13772 			// BS_BITMAP is set when we want image only, so checking for exactly that combination
13773 			enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText;
13774 			auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0;
13775 
13776 			// could also do a virtual method needsOwnerDraw which default returns true and we control it here. typeid(this) == typeid(Button) for override check.
13777 
13778 			if(needsOwnerDraw) {
13779 				extraStyle |= BS_OWNERDRAW;
13780 				needsImage = true;
13781 			}
13782 
13783 			// the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter
13784 			createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 );
13785 
13786 			if(label.image) {
13787 				sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true);
13788 
13789 				SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle);
13790 			}
13791 
13792 			this.label = label.label;
13793 		} else version(custom_widgets) {
13794 			super(parent);
13795 
13796 			label.label.extractWindowsStyleLabel(this.label_, this.accelerator);
13797 			needsImage = true;
13798 		}
13799 
13800 
13801 		if(needsImage && label.image) {
13802 			this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image);
13803 			this.displayFlags = label.displayFlags;
13804 		}
13805 
13806 		this.alignment = label.alignment;
13807 	}
13808 
13809 	override int minHeight() { return defaultLineHeight + 4; }
13810 
13811 	static class Style : Widget.Style {
13812 		override WidgetBackground background() {
13813 			auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive
13814 
13815 			auto pressed = DynamicState.depressed | DynamicState.hover;
13816 			if((widget.dynamicState & pressed) == pressed && widget.enabled) {
13817 				return WidgetBackground(cs.depressedButtonColor());
13818 			} else if(widget.dynamicState & DynamicState.hover && widget.enabled) {
13819 				return WidgetBackground(cs.hoveringColor());
13820 			} else {
13821 				return WidgetBackground(cs.buttonColor());
13822 			}
13823 		}
13824 
13825 		override Color foregroundColor() {
13826 			auto clr = super.foregroundColor();
13827 			if(widget.enabled) return clr;
13828 
13829 			return Color(clr.r, clr.g, clr.b, clr.a / 2);
13830 		}
13831 
13832 		override FrameStyle borderStyle() {
13833 			auto pressed = DynamicState.depressed | DynamicState.hover;
13834 			if((widget.dynamicState & pressed) == pressed && widget.enabled) {
13835 				return FrameStyle.sunk;
13836 			} else {
13837 				return FrameStyle.risen;
13838 			}
13839 
13840 		}
13841 
13842 		override bool variesWithState(ulong dynamicStateFlags) {
13843 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
13844 		}
13845 	}
13846 	mixin OverrideStyle!Style;
13847 
13848 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
13849 		if(sprite) {
13850 			sprite.drawAt(
13851 				painter,
13852 				bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2),
13853 				Point(0, 0)
13854 			);
13855 		} else {
13856 			Point pos = bounds.upperLeft;
13857 			if(this.height == 16)
13858 				pos.y -= 2; // total hack omg
13859 
13860 			painter.drawText(pos, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter);
13861 		}
13862 		return bounds;
13863 	}
13864 
13865 	override int flexBasisWidth() {
13866 		version(win32_widgets) {
13867 			SIZE size;
13868 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
13869 			if(size.cx == 0)
13870 				goto fallback;
13871 			return size.cx + scaleWithDpi(16);
13872 		}
13873 		fallback:
13874 			return scaleWithDpi(cast(int) label.length * 8 + 16);
13875 	}
13876 
13877 	override int flexBasisHeight() {
13878 		version(win32_widgets) {
13879 			SIZE size;
13880 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
13881 			if(size.cy == 0)
13882 				goto fallback;
13883 			return size.cy + scaleWithDpi(6);
13884 		}
13885 		fallback:
13886 			return defaultLineHeight + 4;
13887 	}
13888 }
13889 
13890 /++
13891 	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.
13892 
13893 	History:
13894 		Added January 14, 2024
13895 +/
13896 class CustomButton : Button {
13897 	this(ImageLabel label, Widget parent) {
13898 		super(label, parent);
13899 	}
13900 
13901 	this(string label, Widget parent) {
13902 		super(label, parent);
13903 	}
13904 
13905 	version(win32_widgets)
13906 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
13907 		// paint is driven by handleWmDrawItem instead of minigui's redraw events
13908 		if(hwnd)
13909 			InvalidateRect(hwnd, null, false); // get Windows to trigger the actual redraw
13910 		return;
13911 	}
13912 
13913 	override void paint(WidgetPainter painter) {
13914 		// the parent does `if(hwnd) return;` because
13915 		// normally we don't want to draw on standard controls,
13916 		// but this is an exception if it is an owner drawn button
13917 		// (which is determined in the constructor by testing,
13918 		// at runtime, for the existence of an overridden paint
13919 		// member anyway, so this needed to trigger BS_OWNERDRAW)
13920 		// sdpyPrintDebugString("drawing");
13921 		painter.drawThemed(&paintContent);
13922 	}
13923 }
13924 
13925 /++
13926 	A button with a consistent size, suitable for user commands like OK and CANCEL.
13927 +/
13928 class CommandButton : Button {
13929 	this(string label, Widget parent) {
13930 		super(label, parent);
13931 	}
13932 
13933 	// FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there
13934 
13935 	override int maxHeight() {
13936 		return defaultLineHeight + 4;
13937 	}
13938 
13939 	override int maxWidth() {
13940 		return defaultLineHeight * 4;
13941 	}
13942 
13943 	override int marginLeft() { return 12; }
13944 	override int marginRight() { return 12; }
13945 	override int marginTop() { return 12; }
13946 	override int marginBottom() { return 12; }
13947 }
13948 
13949 ///
13950 enum ArrowDirection {
13951 	left, ///
13952 	right, ///
13953 	up, ///
13954 	down ///
13955 }
13956 
13957 ///
13958 version(custom_widgets)
13959 class ArrowButton : Button {
13960 	///
13961 	this(ArrowDirection direction, Widget parent) {
13962 		super("", parent);
13963 		this.direction = direction;
13964 		triggersOnMultiClick = true;
13965 	}
13966 
13967 	private ArrowDirection direction;
13968 
13969 	override int minHeight() { return scaleWithDpi(16); }
13970 	override int maxHeight() { return scaleWithDpi(16); }
13971 	override int minWidth() { return scaleWithDpi(16); }
13972 	override int maxWidth() { return scaleWithDpi(16); }
13973 
13974 	override void paint(WidgetPainter painter) {
13975 		super.paint(painter);
13976 
13977 		auto cs = getComputedStyle();
13978 
13979 		painter.outlineColor = cs.foregroundColor;
13980 		painter.fillColor = cs.foregroundColor;
13981 
13982 		auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2);
13983 
13984 		final switch(direction) {
13985 			case ArrowDirection.up:
13986 				painter.drawPolygon(
13987 					scaleWithDpi(Point(2, 10) + offset),
13988 					scaleWithDpi(Point(7, 5) + offset),
13989 					scaleWithDpi(Point(12, 10) + offset),
13990 					scaleWithDpi(Point(2, 10) + offset)
13991 				);
13992 			break;
13993 			case ArrowDirection.down:
13994 				painter.drawPolygon(
13995 					scaleWithDpi(Point(2, 6) + offset),
13996 					scaleWithDpi(Point(7, 11) + offset),
13997 					scaleWithDpi(Point(12, 6) + offset),
13998 					scaleWithDpi(Point(2, 6) + offset)
13999 				);
14000 			break;
14001 			case ArrowDirection.left:
14002 				painter.drawPolygon(
14003 					scaleWithDpi(Point(10, 2) + offset),
14004 					scaleWithDpi(Point(5, 7) + offset),
14005 					scaleWithDpi(Point(10, 12) + offset),
14006 					scaleWithDpi(Point(10, 2) + offset)
14007 				);
14008 			break;
14009 			case ArrowDirection.right:
14010 				painter.drawPolygon(
14011 					scaleWithDpi(Point(6, 2) + offset),
14012 					scaleWithDpi(Point(11, 7) + offset),
14013 					scaleWithDpi(Point(6, 12) + offset),
14014 					scaleWithDpi(Point(6, 2) + offset)
14015 				);
14016 			break;
14017 		}
14018 	}
14019 }
14020 
14021 private
14022 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow {
14023 	int x, y;
14024 	Widget par = c;
14025 	while(par) {
14026 		x += par.x;
14027 		y += par.y;
14028 		par = par.parent;
14029 	}
14030 	return [x, y];
14031 }
14032 
14033 version(win32_widgets)
14034 private
14035 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow {
14036 // MapWindowPoints?
14037 	int x, y;
14038 	Widget par = c;
14039 	while(par) {
14040 		x += par.x;
14041 		y += par.y;
14042 		par = par.parent;
14043 		if(par !is null && par.useNativeDrawing())
14044 			break;
14045 	}
14046 	return [x, y];
14047 }
14048 
14049 ///
14050 class ImageBox : Widget {
14051 	private MemoryImage image_;
14052 
14053 	override int widthStretchiness() { return 1; }
14054 	override int heightStretchiness() { return 1; }
14055 	override int widthShrinkiness() { return 1; }
14056 	override int heightShrinkiness() { return 1; }
14057 
14058 	override int flexBasisHeight() {
14059 		return image_.height;
14060 	}
14061 
14062 	override int flexBasisWidth() {
14063 		return image_.width;
14064 	}
14065 
14066 	///
14067 	public void setImage(MemoryImage image){
14068 		this.image_ = image;
14069 		if(this.parentWindow && this.parentWindow.win) {
14070 			if(sprite)
14071 				sprite.dispose();
14072 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
14073 		}
14074 		redraw();
14075 	}
14076 
14077 	/// How to fit the image in the box if they aren't an exact match in size?
14078 	enum HowToFit {
14079 		center, /// centers the image, cropping around all the edges as needed
14080 		crop, /// always draws the image in the upper left, cropping the lower right if needed
14081 		// stretch, /// not implemented
14082 	}
14083 
14084 	private Sprite sprite;
14085 	private HowToFit howToFit_;
14086 
14087 	private Color backgroundColor_;
14088 
14089 	///
14090 	this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) {
14091 		this.image_ = image;
14092 		this.tabStop = false;
14093 		this.howToFit_ = howToFit;
14094 		this.backgroundColor_ = backgroundColor;
14095 		super(parent);
14096 		updateSprite();
14097 	}
14098 
14099 	/// ditto
14100 	this(MemoryImage image, HowToFit howToFit, Widget parent) {
14101 		this(image, howToFit, Color.transparent, parent);
14102 	}
14103 
14104 	private void updateSprite() {
14105 		if(sprite is null && this.parentWindow && this.parentWindow.win) {
14106 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
14107 		}
14108 	}
14109 
14110 	override void paint(WidgetPainter painter) {
14111 		updateSprite();
14112 		if(backgroundColor_.a) {
14113 			painter.fillColor = backgroundColor_;
14114 			painter.drawRectangle(Point(0, 0), width, height);
14115 		}
14116 		if(howToFit_ == HowToFit.crop)
14117 			sprite.drawAt(painter, Point(0, 0));
14118 		else if(howToFit_ == HowToFit.center) {
14119 			sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2));
14120 		}
14121 	}
14122 }
14123 
14124 ///
14125 class TextLabel : Widget {
14126 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; }
14127 	override int maxHeight() { return minHeight; }
14128 	override int minWidth() { return 32; }
14129 
14130 	override int flexBasisHeight() { return minHeight(); }
14131 	override int flexBasisWidth() { return defaultTextWidth(label); }
14132 
14133 	string label_;
14134 
14135 	/++
14136 		Indicates which other control this label is here for. Similar to HTML `for` attribute.
14137 
14138 		In practice this means a click on the label will focus the `labelFor`. In future versions
14139 		it will also set screen reader hints but that is not yet implemented.
14140 
14141 		History:
14142 			Added October 3, 2021 (dub v10.4)
14143 	+/
14144 	Widget labelFor;
14145 
14146 	///
14147 	@scriptable
14148 	string label() { return label_; }
14149 
14150 	///
14151 	@scriptable
14152 	void label(string l) {
14153 		label_ = l;
14154 		version(win32_widgets) {
14155 			WCharzBuffer bfr = WCharzBuffer(l);
14156 			SetWindowTextW(hwnd, bfr.ptr);
14157 		} else version(custom_widgets)
14158 			redraw();
14159 	}
14160 
14161 	override void defaultEventHandler_click(scope ClickEvent ce) {
14162 		if(this.labelFor !is null)
14163 			this.labelFor.focus();
14164 	}
14165 
14166 	/++
14167 		WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version.
14168 		For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly.
14169 	+/
14170 	this(string label, TextAlignment alignment, Widget parent) {
14171 		this.label_ = label;
14172 		this.alignment = alignment;
14173 		this.tabStop = false;
14174 		super(parent);
14175 
14176 		version(win32_widgets)
14177 		createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT);
14178 	}
14179 
14180 	/// ditto
14181 	this(string label, Widget parent) {
14182 		this(label, TextAlignment.Right, parent);
14183 	}
14184 
14185 	TextAlignment alignment;
14186 
14187 	version(custom_widgets)
14188 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
14189 		painter.outlineColor = getComputedStyle().foregroundColor;
14190 		painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment);
14191 		return bounds;
14192 	}
14193 }
14194 
14195 class TextDisplayHelper : Widget {
14196 	protected TextLayouter l;
14197 	protected ScrollMessageWidget smw;
14198 
14199 	private const(TextLayouter.State)*[] undoStack;
14200 	private const(TextLayouter.State)*[] redoStack;
14201 
14202 	private string preservedPrimaryText;
14203 	protected void selectionChanged() {
14204 		// sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString());
14205 		static if(UsingSimpledisplayX11)
14206 		with(l.selection()) {
14207 			if(!isEmpty()) {
14208 				//sdpyPrintDebugString("!isEmpty");
14209 
14210 				getPrimarySelection(parentWindow.win, (in char[] txt) {
14211 					// sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")");
14212 					// import std.stdio; writeln("txt: ", txt, " sel: ", getContentString);
14213 					if(txt.length) {
14214 						preservedPrimaryText = txt.idup;
14215 						// writeln(preservedPrimaryText);
14216 					}
14217 
14218 					setPrimarySelection(parentWindow.win, getContentString());
14219 				});
14220 			}
14221 		}
14222 	}
14223 
14224 	final TextLayouter layouter() {
14225 		return l;
14226 	}
14227 
14228 	bool readonly;
14229 	bool caretNavigation; // scroll lock can flip this
14230 	bool singleLine;
14231 	bool acceptsTabInput;
14232 
14233 	private Menu ctx;
14234 	override Menu contextMenu(int x, int y) {
14235 		if(ctx is null) {
14236 			ctx = new Menu("Actions", this);
14237 			if(!readonly) {
14238 				ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo)));
14239 				ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo)));
14240 				ctx.addSeparator();
14241 			}
14242 			if(!readonly)
14243 				ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut)));
14244 			ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, &copy)));
14245 			if(!readonly)
14246 				ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste)));
14247 			if(!readonly)
14248 				ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection)));
14249 			ctx.addSeparator();
14250 			ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll)));
14251 		}
14252 		return ctx;
14253 	}
14254 
14255 	override void defaultEventHandler_blur(BlurEvent ev) {
14256 		super.defaultEventHandler_blur(ev);
14257 		if(l.wasMutated()) {
14258 			auto evt = new ChangeEvent!string(this, &this.content);
14259 			evt.dispatch();
14260 			l.clearWasMutatedFlag();
14261 		}
14262 	}
14263 
14264 	private string content() {
14265 		return l.getTextString();
14266 	}
14267 
14268 	void undo() {
14269 		if(readonly) return;
14270 		if(undoStack.length) {
14271 			auto state = undoStack[$-1];
14272 			undoStack = undoStack[0 .. $-1];
14273 			undoStack.assumeSafeAppend();
14274 			redoStack ~= l.saveState();
14275 			l.restoreState(state);
14276 			adjustScrollbarSizes();
14277 			scrollForCaret();
14278 			redraw();
14279 			stateCheckpoint = true;
14280 		}
14281 	}
14282 
14283 	void redo() {
14284 		if(readonly) return;
14285 		if(redoStack.length) {
14286 			doStateCheckpoint();
14287 			auto state = redoStack[$-1];
14288 			redoStack = redoStack[0 .. $-1];
14289 			redoStack.assumeSafeAppend();
14290 			l.restoreState(state);
14291 			adjustScrollbarSizes();
14292 			scrollForCaret();
14293 			redraw();
14294 			stateCheckpoint = true;
14295 		}
14296 	}
14297 
14298 	void cut() {
14299 		if(readonly) return;
14300 		with(l.selection()) {
14301 			if(!isEmpty()) {
14302 				setClipboardText(parentWindow.win, getContentString());
14303 				doStateCheckpoint();
14304 				replaceContent("");
14305 				adjustScrollbarSizes();
14306 				scrollForCaret();
14307 				this.redraw();
14308 			}
14309 		}
14310 
14311 	}
14312 
14313 	void copy() {
14314 		with(l.selection()) {
14315 			if(!isEmpty()) {
14316 				setClipboardText(parentWindow.win, getContentString());
14317 				this.redraw();
14318 			}
14319 		}
14320 	}
14321 
14322 	void paste() {
14323 		if(readonly) return;
14324 		getClipboardText(parentWindow.win, (txt) {
14325 			doStateCheckpoint();
14326 			if(singleLine)
14327 				l.selection.replaceContent(txt.stripInternal());
14328 			else
14329 				l.selection.replaceContent(txt);
14330 			adjustScrollbarSizes();
14331 			scrollForCaret();
14332 			this.redraw();
14333 		});
14334 	}
14335 
14336 	void deleteContentOfSelection() {
14337 		if(readonly) return;
14338 		doStateCheckpoint();
14339 		l.selection.replaceContent("");
14340 		l.selection.setUserXCoordinate();
14341 		adjustScrollbarSizes();
14342 		scrollForCaret();
14343 		redraw();
14344 	}
14345 
14346 	void selectAll() {
14347 		with(l.selection) {
14348 			moveToStartOfDocument();
14349 			setAnchor();
14350 			moveToEndOfDocument();
14351 			setFocus();
14352 
14353 			selectionChanged();
14354 		}
14355 		redraw();
14356 	}
14357 
14358 	protected bool stateCheckpoint = true;
14359 
14360 	protected void doStateCheckpoint() {
14361 		if(stateCheckpoint) {
14362 			undoStack ~= l.saveState();
14363 			stateCheckpoint = false;
14364 		}
14365 	}
14366 
14367 	protected void adjustScrollbarSizes() {
14368 		// FIXME: will want a content area helper function instead of doing all these subtractions myself
14369 		auto borderWidth = 2;
14370 		this.smw.setTotalArea(l.width, l.height);
14371 		this.smw.setViewableArea(
14372 			this.width - this.paddingLeft - this.paddingRight - borderWidth * 2,
14373 			this.height - this.paddingTop - this.paddingBottom - borderWidth * 2);
14374 	}
14375 
14376 	protected void scrollForCaret() {
14377 		// writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom);
14378 		smw.scrollIntoView(l.selection.focusBoundingBox());
14379 	}
14380 
14381 	// FIXME: this should be a theme changed event listener instead
14382 	private BaseVisualTheme currentTheme;
14383 	override void recomputeChildLayout() {
14384 		if(currentTheme is null)
14385 			currentTheme = WidgetPainter.visualTheme;
14386 		if(WidgetPainter.visualTheme !is currentTheme) {
14387 			currentTheme = WidgetPainter.visualTheme;
14388 			auto ds = this.l.defaultStyle;
14389 			if(auto ms = cast(MyTextStyle) ds) {
14390 				auto cs = getComputedStyle();
14391 				auto font = cs.font();
14392 				if(font !is null)
14393 					ms.font_ = font;
14394 				else {
14395 					auto osc = new OperatingSystemFont();
14396 					osc.loadDefault;
14397 					ms.font_ = osc;
14398 				}
14399 			}
14400 		}
14401 		super.recomputeChildLayout();
14402 	}
14403 
14404 	private Point adjustForSingleLine(Point p) {
14405 		if(singleLine)
14406 			return Point(p.x, this.height / 2);
14407 		else
14408 			return p;
14409 	}
14410 
14411 	private bool wordWrapEnabled_;
14412 
14413 	this(TextLayouter l, ScrollMessageWidget parent) {
14414 		this.smw = parent;
14415 
14416 		smw.addDefaultWheelListeners(16, 16, 8);
14417 		smw.movementPerButtonClick(16, 16);
14418 
14419 		this.defaultPadding = Rectangle(2, 2, 2, 2);
14420 
14421 		this.l = l;
14422 		super(parent);
14423 
14424 		smw.addEventListener((scope ScrollEvent se) {
14425 			this.redraw();
14426 		});
14427 
14428 		this.addEventListener((scope ResizeEvent re) {
14429 			// FIXME: I should add a method to give this client area width thing
14430 			if(wordWrapEnabled_)
14431 				this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight;
14432 
14433 			adjustScrollbarSizes();
14434 			scrollForCaret();
14435 
14436 			this.redraw();
14437 		});
14438 
14439 	}
14440 
14441 	private {
14442 		bool mouseDown;
14443 		bool mouseActuallyMoved;
14444 
14445 		Point downAt;
14446 
14447 		Timer autoscrollTimer;
14448 		int autoscrollDirection;
14449 		int autoscrollAmount;
14450 
14451 		void autoscroll() {
14452 			switch(autoscrollDirection) {
14453 				case 0: smw.scrollUp(autoscrollAmount); break;
14454 				case 1: smw.scrollDown(autoscrollAmount); break;
14455 				case 2: smw.scrollLeft(autoscrollAmount); break;
14456 				case 3: smw.scrollRight(autoscrollAmount); break;
14457 				default: assert(0);
14458 			}
14459 
14460 			this.redraw();
14461 		}
14462 
14463 		void setAutoscrollTimer(int direction, int amount) {
14464 			if(autoscrollTimer is null) {
14465 				autoscrollTimer = new Timer(1000 / 60, &autoscroll);
14466 			}
14467 
14468 			autoscrollDirection = direction;
14469 			autoscrollAmount = amount;
14470 		}
14471 
14472 		void stopAutoscrollTimer() {
14473 			if(autoscrollTimer !is null) {
14474 				autoscrollTimer.dispose();
14475 				autoscrollTimer = null;
14476 			}
14477 			autoscrollAmount = 0;
14478 			autoscrollDirection = 0;
14479 		}
14480 	}
14481 
14482 	override void defaultEventHandler_mousemove(scope MouseMoveEvent ce) {
14483 		if(mouseDown) {
14484 			auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
14485 
14486 			// FIXME: when scrolling i actually do want a timer.
14487 			// i also want a zone near the sides of the window where i can auto scroll
14488 
14489 			auto scrollMultiplier = scaleWithDpi(16);
14490 			auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster
14491 
14492 			if(!singleLine && movedTo.y < 4) {
14493 				setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor);
14494 			} else
14495 			if(!singleLine && (movedTo.y + 6) > this.height) {
14496 				setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor);
14497 			} else
14498 			if(movedTo.x < 4) {
14499 				setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor);
14500 			} else
14501 			if((movedTo.x + 6) > this.width) {
14502 				setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor);
14503 			} else
14504 				stopAutoscrollTimer();
14505 
14506 			l.selection.moveTo(adjustForSingleLine(smw.position + movedTo));
14507 			l.selection.setFocus();
14508 			mouseActuallyMoved = true;
14509 			this.redraw();
14510 		}
14511 
14512 		super.defaultEventHandler_mousemove(ce);
14513 	}
14514 
14515 	override void defaultEventHandler_mouseup(scope MouseUpEvent ce) {
14516 		// FIXME: assert primary selection
14517 		if(mouseDown && ce.button == MouseButton.left) {
14518 			stateCheckpoint = true;
14519 			//l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop)));
14520 			//l.selection.setFocus();
14521 			mouseDown = false;
14522 			parentWindow.releaseMouseCapture();
14523 			stopAutoscrollTimer();
14524 			this.redraw();
14525 
14526 			if(mouseActuallyMoved)
14527 				selectionChanged();
14528 		}
14529 		//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
14530 
14531 		super.defaultEventHandler_mouseup(ce);
14532 	}
14533 
14534 	static if(UsingSimpledisplayX11)
14535 	override void defaultEventHandler_click(scope ClickEvent ce) {
14536 		if(ce.button == MouseButton.middle) {
14537 			parentWindow.win.getPrimarySelection((txt) {
14538 				doStateCheckpoint();
14539 
14540 				// import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText);
14541 
14542 				if(txt == l.selection.getContentString && preservedPrimaryText.length)
14543 					l.selection.replaceContent(preservedPrimaryText);
14544 				else
14545 					l.selection.replaceContent(txt);
14546 				redraw();
14547 			});
14548 		}
14549 
14550 		super.defaultEventHandler_click(ce);
14551 	}
14552 
14553 	final const(char)[] wordSplitHelper(scope return const(char)[] ch) {
14554 		if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r")
14555 			return ch;
14556 		return null;
14557 	}
14558 
14559 	override void defaultEventHandler_dblclick(scope DoubleClickEvent dce) {
14560 		if(dce.button == MouseButton.left) {
14561 			with(l.selection()) {
14562 				// FIXME: for a url or file picker i might wanna use / as a separator intead
14563 				scope dg = &wordSplitHelper;
14564 				find(dg, 1, true).moveToEnd.setAnchor;
14565 				find(dg, 1, false).moveTo.setFocus;
14566 				selectionChanged();
14567 				redraw();
14568 			}
14569 		}
14570 
14571 		super.defaultEventHandler_dblclick(dce);
14572 	}
14573 
14574 	override void defaultEventHandler_mousedown(scope MouseDownEvent ce) {
14575 		if(ce.button == MouseButton.left) {
14576 			downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
14577 			l.selection.moveTo(adjustForSingleLine(smw.position + downAt));
14578 			if(ce.shiftKey)
14579 				l.selection.setFocus();
14580 			else
14581 				l.selection.setAnchor();
14582 			mouseDown = true;
14583 			mouseActuallyMoved = false;
14584 			parentWindow.captureMouse(this);
14585 			this.redraw();
14586 		}
14587 		//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
14588 
14589 		super.defaultEventHandler_mousedown(ce);
14590 	}
14591 
14592 	override void defaultEventHandler_char(scope CharEvent ce) {
14593 		super.defaultEventHandler_char(ce);
14594 
14595 		if(readonly)
14596 			return;
14597 		if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b')
14598 			return; // skip the ctrl+x characters we don't care about as plain text
14599 
14600 		if(singleLine && ce.character == '\n')
14601 			return;
14602 		if(!acceptsTabInput && ce.character == '\t')
14603 			return;
14604 
14605 		doStateCheckpoint();
14606 
14607 		char[4] buffer;
14608 		import arsd.core;
14609 		auto stride = encodeUtf8(buffer, ce.character);
14610 		l.selection.replaceContent(buffer[0 .. stride]);
14611 		l.selection.setUserXCoordinate();
14612 		adjustScrollbarSizes();
14613 		scrollForCaret();
14614 		redraw();
14615 
14616 	}
14617 
14618 	override void defaultEventHandler_keydown(scope KeyDownEvent kde) {
14619 		switch(kde.key) {
14620 			case Key.Up, Key.Down, Key.Left, Key.Right:
14621 			case Key.Home, Key.End:
14622 				stateCheckpoint = true;
14623 				bool setPosition = false;
14624 				switch(kde.key) {
14625 					case Key.Up: l.selection.moveUp(); break;
14626 					case Key.Down: l.selection.moveDown(); break;
14627 					case Key.Left:
14628 						l.selection.moveLeft();
14629 
14630 						if(kde.ctrlKey) {
14631 							l.selection.find(&wordSplitHelper, 1, true).moveToEnd;
14632 						}
14633 
14634 						setPosition = true;
14635 					break;
14636 					case Key.Right:
14637 						l.selection.moveRight();
14638 
14639 						if(kde.ctrlKey) {
14640 							l.selection.find(&wordSplitHelper, 1, false).moveTo;
14641 						}
14642 
14643 						setPosition = true;
14644 					break;
14645 					case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break;
14646 					case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break;
14647 					default: assert(0);
14648 				}
14649 
14650 				if(kde.shiftKey)
14651 					l.selection.setFocus();
14652 				else
14653 					l.selection.setAnchor();
14654 
14655 				selectionChanged();
14656 
14657 				if(setPosition)
14658 					l.selection.setUserXCoordinate();
14659 				scrollForCaret();
14660 				redraw();
14661 			break;
14662 			case Key.PageUp, Key.PageDown:
14663 				// want to act like the user clicked on the caret again
14664 				// after the scroll operation completed, so it would remain at
14665 				// about the same place on the viewport
14666 				auto oldY = smw.vsb.position;
14667 				smw.defaultKeyboardListener(kde);
14668 				auto newY = smw.vsb.position;
14669 				with(l.selection) {
14670 					auto uc = getUserCoordinate();
14671 					uc.y += newY - oldY;
14672 					moveTo(uc);
14673 
14674 					if(kde.shiftKey)
14675 						setFocus();
14676 					else
14677 						setAnchor();
14678 				}
14679 			break;
14680 			case Key.Delete:
14681 				if(l.selection.isEmpty()) {
14682 					l.selection.setAnchor();
14683 					l.selection.moveRight();
14684 					l.selection.setFocus();
14685 				}
14686 				deleteContentOfSelection();
14687 				adjustScrollbarSizes();
14688 				scrollForCaret();
14689 			break;
14690 			case Key.Insert:
14691 			break;
14692 			case Key.A:
14693 				if(kde.ctrlKey)
14694 					selectAll();
14695 			break;
14696 			case Key.F:
14697 				// find
14698 			break;
14699 			case Key.Z:
14700 				if(kde.ctrlKey)
14701 					undo();
14702 			break;
14703 			case Key.R:
14704 				if(kde.ctrlKey)
14705 					redo();
14706 			break;
14707 			case Key.X:
14708 				if(kde.ctrlKey)
14709 					cut();
14710 			break;
14711 			case Key.C:
14712 				if(kde.ctrlKey)
14713 					copy();
14714 			break;
14715 			case Key.V:
14716 				if(kde.ctrlKey)
14717 					paste();
14718 			break;
14719 			case Key.F1:
14720 				with(l.selection()) {
14721 					moveToStartOfLine();
14722 					setAnchor();
14723 					moveToEndOfLine();
14724 					moveToIncludeAdjacentEndOfLineMarker();
14725 					setFocus();
14726 					replaceContent("");
14727 				}
14728 
14729 				redraw();
14730 			break;
14731 			/*
14732 			case Key.F2:
14733 				l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle(
14734 					//(cast(MyTextStyle) old).font,
14735 					font2,
14736 					Color.red)));
14737 				redraw();
14738 			break;
14739 			*/
14740 			case Key.Tab:
14741 				// we process the char event, so don't want to change focus on it, unless the user overrides that with ctrl
14742 				if(acceptsTabInput && !kde.ctrlKey)
14743 					kde.preventDefault();
14744 			break;
14745 			default:
14746 		}
14747 
14748 		if(!kde.defaultPrevented)
14749 			super.defaultEventHandler_keydown(kde);
14750 	}
14751 
14752 	// we want to delegate all the Widget.Style stuff up to the other class that the user can see
14753 	override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) {
14754 		// this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidget Parent
14755 		if(parent && parent.parent && parent.parent.parent)
14756 			parent.parent.parent.useStyleProperties(dg);
14757 		else
14758 			super.useStyleProperties(dg);
14759 	}
14760 
14761 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; }
14762 	override int maxHeight() {
14763 		if(singleLine)
14764 			return minHeight;
14765 		else
14766 			return super.maxHeight();
14767 	}
14768 
14769 	void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
14770 		painter.setFont(myStyle.font);
14771 		painter.drawText(upperLeft, text);
14772 	}
14773 
14774 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
14775 		//painter.setFont(font);
14776 
14777 		auto cs = getComputedStyle();
14778 		auto defaultColor = cs.foregroundColor;
14779 
14780 		auto old = painter.setClipRectangleForWidget(bounds.upperLeft, bounds.width, bounds.height);
14781 		scope(exit) painter.setClipRectangleForWidget(old.upperLeft, old.width, old.height);
14782 
14783 		l.getDrawableText(delegate bool(txt, style, info, carets...) {
14784 			//writeln("Segment: ", txt);
14785 			assert(style !is null);
14786 
14787 			if(info.selections && info.boundingBox.width > 0) {
14788 				auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode
14789 				painter.fillColor = color;
14790 				painter.outlineColor = color;
14791 				painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size));
14792 				painter.outlineColor = cs.selectionForegroundColor;
14793 				//painter.fillColor = Color.white;
14794 			} else {
14795 				painter.outlineColor = defaultColor;
14796 			}
14797 
14798 			if(this.isFocused)
14799 			foreach(idx, caret; carets) {
14800 				if(idx == 0)
14801 					painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height);
14802 				painter.drawLine(
14803 					caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(),
14804 					bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position()
14805 				);
14806 			}
14807 
14808 			if(txt.stripInternal.length) {
14809 				// defaultColor = myStyle.color; // FIXME: so wrong
14810 				if(auto myStyle = cast(MyTextStyle) style)
14811 					drawTextSegment(myStyle, painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
14812 				else if(auto myStyle = cast(MyImageStyle) style)
14813 					myStyle.draw(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
14814 			}
14815 
14816 			if(info.boundingBox.upperLeft.y - smw.position().y > this.height) {
14817 				return false;
14818 			} else {
14819 				return true;
14820 			}
14821 		}, Rectangle(smw.position(), bounds.size));
14822 
14823 		/+
14824 		int place = 0;
14825 		int y = 75;
14826 		foreach(width; widths) {
14827 			painter.fillColor = Color.red;
14828 			painter.drawRectangle(Point(place, y), Size(width, 75));
14829 			//y += 15;
14830 			place += width;
14831 		}
14832 		+/
14833 
14834 		return bounds;
14835 	}
14836 
14837 	static class MyTextStyle : TextStyle {
14838 		OperatingSystemFont font_;
14839 		this(OperatingSystemFont font, bool passwordMode = false) {
14840 			this.font_ = font;
14841 		}
14842 
14843 		override OperatingSystemFont font() {
14844 			return font_;
14845 		}
14846 
14847 		bool foregroundColorOverridden;
14848 		bool backgroundColorOverridden;
14849 		Color foregroundColor;
14850 		Color backgroundColor; // should this be inline segment or the whole paragraph block?
14851 		bool italic;
14852 		bool bold;
14853 		bool underline;
14854 		bool strikeout;
14855 		bool subscript;
14856 		bool superscript;
14857 	}
14858 
14859 	static class MyImageStyle : TextStyle, MeasurableFont {
14860 		MemoryImage image_;
14861 		Image converted;
14862 		this(MemoryImage image) {
14863 			this.image_ =  image;
14864 			this.converted = Image.fromMemoryImage(image);
14865 		}
14866 
14867 		bool isMonospace() { return false; }
14868 		fnum averageWidth() { return image_.width; }
14869 		fnum height() { return image_.height; }
14870 		fnum ascent() { return image_.height; }
14871 		fnum descent() { return 0; }
14872 
14873 		fnum stringWidth(scope const(char)[] s, SimpleWindow window = null) {
14874 			return image_.width;
14875 		}
14876 
14877 		override MeasurableFont font() {
14878 			return this;
14879 		}
14880 
14881 		void draw(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
14882 			painter.drawImage(upperLeft, converted);
14883 		}
14884 	}
14885 }
14886 
14887 /+
14888 class TextWidget : Widget {
14889 	TextLayouter l;
14890 	ScrollMessageWidget smw;
14891 	TextDisplayHelper helper;
14892 	this(TextLayouter l, Widget parent) {
14893 		this.l = l;
14894 		super(parent);
14895 
14896 		smw = new ScrollMessageWidget(this);
14897 		//smw.horizontalScrollBar.hide;
14898 		//smw.verticalScrollBar.hide;
14899 		smw.addDefaultWheelListeners(16, 16, 8);
14900 		smw.movementPerButtonClick(16, 16);
14901 		helper = new TextDisplayHelper(l, smw);
14902 
14903 		// no need to do this here since there's gonna be a resize
14904 		// event immediately before any drawing
14905 		// smw.setTotalArea(l.width, l.height);
14906 		smw.setViewableArea(
14907 			this.width - this.paddingLeft - this.paddingRight,
14908 			this.height - this.paddingTop - this.paddingBottom);
14909 
14910 		/+
14911 		writeln(l.width, "x", l.height);
14912 		+/
14913 	}
14914 }
14915 +/
14916 
14917 
14918 
14919 
14920 /+
14921 	make sure it calls parentWindow.inputProxy.setIMEPopupLocation too
14922 +/
14923 
14924 /++
14925 	Contains the implementation of text editing and shared basic api. You should construct one of the child classes instead, like [TextEdit], [LineEdit], or [PasswordEdit].
14926 +/
14927 abstract class EditableTextWidget : Widget {
14928 	protected this(Widget parent) {
14929 		version(custom_widgets)
14930 			this(true, parent);
14931 		else
14932 			this(false, parent);
14933 	}
14934 
14935 	private bool useCustomWidget;
14936 
14937 	protected this(bool useCustomWidget, Widget parent) {
14938 		this.useCustomWidget = useCustomWidget;
14939 
14940 		super(parent);
14941 
14942 		if(useCustomWidget)
14943 			setupCustomTextEditing();
14944 	}
14945 
14946 	private bool wordWrapEnabled_;
14947 	/++
14948 		Enables or disables wrapping of long lines on word boundaries.
14949 	+/
14950 	void wordWrapEnabled(bool enabled) {
14951 		if(useCustomWidget) {
14952 			wordWrapEnabled_ = enabled;
14953 			if(tdh)
14954 				tdh.wordWrapEnabled_ = true;
14955 			textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME
14956 		} else version(win32_widgets) {
14957 			SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0);
14958 		}
14959 	}
14960 
14961 	override int minWidth() { return scaleWithDpi(16); }
14962 	override int widthStretchiness() { return 7; }
14963 	override int widthShrinkiness() { return 1; }
14964 
14965 	override int maxHeight() {
14966 		if(useCustomWidget)
14967 			return tdh.maxHeight;
14968 		else
14969 			return super.maxHeight();
14970 	}
14971 
14972 	override void focus() {
14973 		if(useCustomWidget && tdh)
14974 			tdh.focus();
14975 		else
14976 			super.focus();
14977 	}
14978 
14979 	override void defaultEventHandler_focusout(FocusOutEvent foe) {
14980 		if(tdh !is null && foe.target is tdh)
14981 			tdh.redraw();
14982 	}
14983 
14984 	override void defaultEventHandler_focusin(FocusInEvent foe) {
14985 		if(tdh !is null && foe.target is tdh)
14986 			tdh.redraw();
14987 	}
14988 
14989 
14990 	/++
14991 		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.
14992 	+/
14993 	void selectAll() {
14994 		if(useCustomWidget) {
14995 			tdh.selectAll();
14996 		} else version(win32_widgets) {
14997 			SendMessage(hwnd, EM_SETSEL, 0, -1);
14998 		}
14999 	}
15000 
15001 	/++
15002 		Basic clipboard operations.
15003 
15004 		History:
15005 			Added December 31, 2024
15006 	+/
15007 	void copy() {
15008 		if(useCustomWidget) {
15009 			tdh.copy();
15010 		} else version(win32_widgets) {
15011 			SendMessage(hwnd, WM_COPY, 0, 0);
15012 		}
15013 	}
15014 
15015 	/// ditto
15016 	void cut() {
15017 		if(useCustomWidget) {
15018 			tdh.cut();
15019 		} else version(win32_widgets) {
15020 			SendMessage(hwnd, WM_CUT, 0, 0);
15021 		}
15022 	}
15023 
15024 	/// ditto
15025 	void paste() {
15026 		if(useCustomWidget) {
15027 			tdh.paste();
15028 		} else version(win32_widgets) {
15029 			SendMessage(hwnd, WM_PASTE, 0, 0);
15030 		}
15031 	}
15032 
15033 	///
15034 	void undo() {
15035 		if(useCustomWidget) {
15036 			tdh.undo();
15037 		} else version(win32_widgets) {
15038 			SendMessage(hwnd, EM_UNDO, 0, 0);
15039 		}
15040 	}
15041 
15042 	// note that WM_CLEAR deletes the selection without copying it to the clipboard
15043 	// also windows supports margins, modified flag, and much more
15044 
15045 	// EM_UNDO and EM_CANUNDO. EM_REDO is only supported in rich text boxes here
15046 
15047 	// EM_GETSEL, EM_REPLACESEL, and EM_SETSEL might be usable for find etc.
15048 
15049 
15050 
15051 	/*protected*/ TextDisplayHelper tdh;
15052 	/*protected*/ TextLayouter textLayout;
15053 
15054 	/++
15055 		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.
15056 	+/
15057 	@property string content() {
15058 		if(useCustomWidget) {
15059 			return textLayout.getTextString();
15060 		} else version(win32_widgets) {
15061 			wchar[4096] bufferstack;
15062 			wchar[] buffer;
15063 			auto len = GetWindowTextLength(hwnd);
15064 			if(len < bufferstack.length)
15065 				buffer = bufferstack[0 .. len + 1];
15066 			else
15067 				buffer = new wchar[](len + 1);
15068 
15069 			auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length);
15070 			if(l >= 0)
15071 				return makeUtf8StringFromWindowsString(buffer[0 .. l]);
15072 			else
15073 				return null;
15074 		}
15075 
15076 		assert(0);
15077 	}
15078 	/// ditto
15079 	@property void content(string s) {
15080 		if(useCustomWidget) {
15081 			with(textLayout.selection) {
15082 				moveToStartOfDocument();
15083 				setAnchor();
15084 				moveToEndOfDocument();
15085 				setFocus();
15086 				replaceContent(s);
15087 			}
15088 
15089 			tdh.adjustScrollbarSizes();
15090 			// these don't seem to help
15091 			// tdh.smw.setPosition(0, 0);
15092 			// tdh.scrollForCaret();
15093 
15094 			redraw();
15095 		} else version(win32_widgets) {
15096 			WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines);
15097 			SetWindowTextW(hwnd, bfr.ptr);
15098 		}
15099 	}
15100 
15101 	/++
15102 		Appends some text to the widget at the end, without affecting the user selection or cursor position.
15103 	+/
15104 	void addText(string txt) {
15105 		if(useCustomWidget) {
15106 			textLayout.appendText(txt);
15107 			tdh.adjustScrollbarSizes();
15108 			redraw();
15109 		} else version(win32_widgets) {
15110 			// get the current selection
15111 			DWORD StartPos, EndPos;
15112 			SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) );
15113 
15114 			// move the caret to the end of the text
15115 			int outLength = GetWindowTextLengthW(hwnd);
15116 			SendMessageW( hwnd, EM_SETSEL, outLength, outLength );
15117 
15118 			// insert the text at the new caret position
15119 			WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines);
15120 			SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr );
15121 
15122 			// restore the previous selection
15123 			SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos );
15124 		}
15125 	}
15126 
15127 	// EM_SCROLLCARET scrolls the caret into view
15128 
15129 	void scrollToBottom() {
15130 		if(useCustomWidget) {
15131 			tdh.smw.scrollDown(int.max);
15132 		} else version(win32_widgets) {
15133 			SendMessageW( hwnd, EM_LINESCROLL, 0, int.max );
15134 		}
15135 	}
15136 
15137 	protected TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
15138 		return new TextDisplayHelper(textLayout, smw);
15139 	}
15140 
15141 	protected TextStyle defaultTextStyle() {
15142 		return new TextDisplayHelper.MyTextStyle(getUsedFont());
15143 	}
15144 
15145 	private OperatingSystemFont getUsedFont() {
15146 		auto cs = getComputedStyle();
15147 		auto font = cs.font;
15148 		if(font is null) {
15149 			font = new OperatingSystemFont;
15150 			font.loadDefault();
15151 		}
15152 		return font;
15153 	}
15154 
15155 	protected void setupCustomTextEditing() {
15156 		textLayout = new TextLayouter(defaultTextStyle());
15157 
15158 		auto smw = new ScrollMessageWidget(this);
15159 		if(!showingHorizontalScroll)
15160 			smw.horizontalScrollBar.hide();
15161 		if(!showingVerticalScroll)
15162 			smw.verticalScrollBar.hide();
15163 		this.tabStop = false;
15164 		smw.tabStop = false;
15165 		tdh = textDisplayHelperFactory(textLayout, smw);
15166 	}
15167 
15168 	override void newParentWindow(Window old, Window n) {
15169 		if(n is null) return;
15170 		this.parentWindow.addEventListener((scope DpiChangedEvent dce) {
15171 			if(textLayout) {
15172 				if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) {
15173 					// the dpi change can change the font, so this informs the layouter that it has changed too
15174 					style.font_ = getUsedFont();
15175 
15176 					// arsd.core.writeln(this.parentWindow.win.actualDpi);
15177 				}
15178 			}
15179 		});
15180 	}
15181 
15182 	static class Style : Widget.Style {
15183 		override WidgetBackground background() {
15184 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
15185 		}
15186 
15187 		override Color foregroundColor() {
15188 			return WidgetPainter.visualTheme.foregroundColor;
15189 		}
15190 
15191 		override FrameStyle borderStyle() {
15192 			return FrameStyle.sunk;
15193 		}
15194 
15195 		override MouseCursor cursor() {
15196 			return GenericCursor.Text;
15197 		}
15198 	}
15199 	mixin OverrideStyle!Style;
15200 
15201 	version(win32_widgets) {
15202 		private string lastContentBlur;
15203 
15204 		override void defaultEventHandler_blur(BlurEvent ev) {
15205 			super.defaultEventHandler_blur(ev);
15206 
15207 			if(!useCustomWidget)
15208 			if(this.content != lastContentBlur) {
15209 				auto evt = new ChangeEvent!string(this, &this.content);
15210 				evt.dispatch();
15211 				lastContentBlur = this.content;
15212 			}
15213 		}
15214 	}
15215 
15216 
15217 	bool showingVerticalScroll() { return true; }
15218 	bool showingHorizontalScroll() { return true; }
15219 }
15220 
15221 /++
15222 	A `LineEdit` is an editor of a single line of text, comparable to a HTML `<input type="text" />`.
15223 
15224 	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.
15225 
15226 	See_Also:
15227 		[PasswordEdit] for a `LineEdit` that obscures its input.
15228 
15229 		[TextEdit] for a multi-line plain text editor widget.
15230 
15231 		[TextLabel] for a single line piece of static text.
15232 
15233 		[TextDisplay] for a read-only display of a larger piece of plain text.
15234 +/
15235 class LineEdit : EditableTextWidget {
15236 	override bool showingVerticalScroll() { return false; }
15237 	override bool showingHorizontalScroll() { return false; }
15238 
15239 	override int flexBasisWidth() { return 250; }
15240 	override int widthShrinkiness() { return 10; }
15241 
15242 	///
15243 	this(Widget parent) {
15244 		super(parent);
15245 		version(win32_widgets) {
15246 			createWin32Window(this, "edit"w, "",
15247 				0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
15248 		} else version(custom_widgets) {
15249 		} else static assert(false);
15250 	}
15251 
15252 	private this(bool useCustomWidget, Widget parent) {
15253 		if(!useCustomWidget)
15254 			this(parent);
15255 		else
15256 			super(true, parent);
15257 	}
15258 
15259 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
15260 		auto tdh = new TextDisplayHelper(textLayout, smw);
15261 		tdh.singleLine = true;
15262 		return tdh;
15263 	}
15264 
15265 	version(win32_widgets) {
15266 		mixin Padding!q{0};
15267 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
15268 		override int maxHeight() { return minHeight; }
15269 	}
15270 
15271 	/+
15272 	@property void passwordMode(bool p) {
15273 		SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD);
15274 	}
15275 	+/
15276 }
15277 
15278 /// ditto
15279 class CustomLineEdit : LineEdit {
15280 	this(Widget parent) {
15281 		super(true, parent);
15282 	}
15283 }
15284 
15285 /++
15286 	A [LineEdit] that displays `*` in place of the actual characters.
15287 
15288 	Alas, Windows requires the window to be created differently to use this style,
15289 	so it had to be a new class instead of a toggle on and off on an existing object.
15290 
15291 	History:
15292 		Added January 24, 2021
15293 
15294 		Implemented on Linux on January 31, 2023.
15295 +/
15296 class PasswordEdit : EditableTextWidget {
15297 	override bool showingVerticalScroll() { return false; }
15298 	override bool showingHorizontalScroll() { return false; }
15299 
15300 	override int flexBasisWidth() { return 250; }
15301 
15302 	override TextStyle defaultTextStyle() {
15303 		auto cs = getComputedStyle();
15304 
15305 		auto osf = new class OperatingSystemFont {
15306 			this() {
15307 				super(cs.font);
15308 			}
15309 			override fnum stringWidth(scope const(char)[] text, SimpleWindow window = null) {
15310 				int count = 0;
15311 				foreach(dchar ch; text)
15312 					count++;
15313 				return count * super.stringWidth("*", window);
15314 			}
15315 		};
15316 
15317 		return new TextDisplayHelper.MyTextStyle(osf);
15318 	}
15319 
15320 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
15321 		static class TDH : TextDisplayHelper {
15322 			this(TextLayouter textLayout, ScrollMessageWidget smw) {
15323 				singleLine = true;
15324 				super(textLayout, smw);
15325 			}
15326 
15327 			override void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
15328 				char[256] buffer = void;
15329 				int bufferLength = 0;
15330 				foreach(dchar ch; text)
15331 					buffer[bufferLength++] = '*';
15332 				painter.setFont(myStyle.font);
15333 				painter.drawText(upperLeft, buffer[0..bufferLength]);
15334 			}
15335 		}
15336 
15337 		return new TDH(textLayout, smw);
15338 	}
15339 
15340 	///
15341 	this(Widget parent) {
15342 		super(parent);
15343 		version(win32_widgets) {
15344 			createWin32Window(this, "edit"w, "",
15345 				ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
15346 		} else version(custom_widgets) {
15347 		} else static assert(false);
15348 	}
15349 
15350 	private this(bool useCustomWidget, Widget parent) {
15351 		if(!useCustomWidget)
15352 			this(parent);
15353 		else
15354 			super(true, parent);
15355 	}
15356 
15357 	version(win32_widgets) {
15358 		mixin Padding!q{2};
15359 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
15360 		override int maxHeight() { return minHeight; }
15361 	}
15362 }
15363 
15364 /// ditto
15365 class CustomPasswordEdit : PasswordEdit {
15366 	this(Widget parent) {
15367 		super(true, parent);
15368 	}
15369 }
15370 
15371 
15372 /++
15373 	A `TextEdit` is a multi-line plain text editor, comparable to a HTML `<textarea>`.
15374 
15375 	See_Also:
15376 		[TextDisplay] for a read-only text display.
15377 
15378 		[LineEdit] for a single line text editor.
15379 
15380 		[PasswordEdit] for a single line text editor that obscures its input.
15381 +/
15382 class TextEdit : EditableTextWidget {
15383 	///
15384 	this(Widget parent) {
15385 		super(parent);
15386 		version(win32_widgets) {
15387 			createWin32Window(this, "edit"w, "",
15388 				0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE);
15389 		} else version(custom_widgets) {
15390 		} else static assert(false);
15391 	}
15392 
15393 	private this(bool useCustomWidget, Widget parent) {
15394 		if(!useCustomWidget)
15395 			this(parent);
15396 		else
15397 			super(true, parent);
15398 	}
15399 
15400 	override int maxHeight() { return int.max; }
15401 	override int heightStretchiness() { return 7; }
15402 
15403 	override int flexBasisWidth() { return 250; }
15404 	override int flexBasisHeight() { return 25; }
15405 }
15406 
15407 /// ditto
15408 class CustomTextEdit : TextEdit {
15409 	this(Widget parent) {
15410 		super(true, parent);
15411 	}
15412 }
15413 
15414 /+
15415 /++
15416 
15417 +/
15418 version(none)
15419 class RichTextDisplay : Widget {
15420 	@property void content(string c) {}
15421 	void appendContent(string c) {}
15422 }
15423 +/
15424 
15425 /++
15426 	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.
15427 
15428 	History:
15429 		Added October 31, 2023 (dub v11.3)
15430 +/
15431 class TextDisplay : EditableTextWidget {
15432 	this(string text, Widget parent) {
15433 		super(true, parent);
15434 		this.content = text;
15435 	}
15436 
15437 	override int maxHeight() { return int.max; }
15438 	override int minHeight() { return Window.defaultLineHeight; }
15439 	override int heightStretchiness() { return 7; }
15440 	override int heightShrinkiness() { return 2; }
15441 
15442 	override int flexBasisWidth() {
15443 		return scaleWithDpi(250);
15444 	}
15445 	override int flexBasisHeight() {
15446 		if(textLayout is null || this.tdh is null)
15447 			return Window.defaultLineHeight;
15448 
15449 		auto textHeight = borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textLayout.height))).height;
15450 		return this.tdh.borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textHeight))).height;
15451 	}
15452 
15453 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
15454 		return new MyTextDisplayHelper(textLayout, smw);
15455 	}
15456 
15457 	override void registerMovement() {
15458 		super.registerMovement();
15459 		this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally
15460 	}
15461 
15462 	static class MyTextDisplayHelper : TextDisplayHelper {
15463 		this(TextLayouter textLayout, ScrollMessageWidget smw) {
15464 			smw.verticalScrollBar.hide();
15465 			smw.horizontalScrollBar.hide();
15466 			super(textLayout, smw);
15467 			this.readonly = true;
15468 		}
15469 
15470 		override void registerMovement() {
15471 			super.registerMovement();
15472 
15473 			// FIXME: do the horizontal one too as needed and make sure that it does
15474 			// wordwrapping again
15475 			if(l.height + smw.horizontalScrollBar.height > this.height)
15476 				smw.verticalScrollBar.show();
15477 			else
15478 				smw.verticalScrollBar.hide();
15479 
15480 			l.wordWrapWidth = this.width;
15481 
15482 			smw.verticalScrollBar.setPosition = 0;
15483 		}
15484 	}
15485 
15486 	static class Style : Widget.Style {
15487 		// just want the generic look for these
15488 	}
15489 
15490 	mixin OverrideStyle!Style;
15491 }
15492 
15493 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active
15494 /++
15495 	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.
15496 
15497 
15498 	When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`.
15499 
15500 	Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand.
15501 
15502 	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.
15503 
15504 	History:
15505 		Added August 12, 2024 (dub v11.6)
15506 +/
15507 abstract class GenericListViewWidget : Widget {
15508 	/++
15509 
15510 	+/
15511 	this(Widget parent) {
15512 		super(parent);
15513 
15514 		smw = new ScrollMessageWidget(this);
15515 		smw.addDefaultKeyboardListeners(itemSize.height, itemSize.width);
15516 		smw.addDefaultWheelListeners(itemSize.height, itemSize.width);
15517 		smw.hsb.hide(); // FIXME: this might actually be useful but we can't really communicate that yet
15518 
15519 		inner = new GenericListViewWidgetInner(this, smw, new GenericListViewInnerContainer(smw));
15520 		inner.tabStop = this.tabStop;
15521 		this.tabStop = false;
15522 	}
15523 
15524 	private ScrollMessageWidget smw;
15525 	private GenericListViewWidgetInner inner;
15526 
15527 	/++
15528 
15529 	+/
15530 	abstract GenericListViewItem itemFactory(Widget parent);
15531 	// in device-dependent pixels
15532 	/++
15533 
15534 	+/
15535 	abstract Size itemSize(); // use 0 to indicate it can stretch?
15536 
15537 	enum LayoutMode {
15538 		rows,
15539 		columns,
15540 		gridRowsFirst,
15541 		gridColumnsFirst
15542 	}
15543 	LayoutMode layoutMode() {
15544 		return LayoutMode.rows;
15545 	}
15546 
15547 	private int itemCount_;
15548 
15549 	/++
15550 		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.
15551 	+/
15552 	void setItemCount(int count) {
15553 		smw.setTotalArea(inner.width, count * itemSize().height);
15554 		smw.setViewableArea(inner.width, inner.height);
15555 		this.itemCount_ = count;
15556 	}
15557 
15558 	/++
15559 		Returns the current count of items expected to available in the list.
15560 	+/
15561 	int itemCount() {
15562 		return this.itemCount_;
15563 	}
15564 
15565 	/++
15566 		Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data.
15567 
15568 		Note you must $(I also) call [setItemCount] if the total item count has changed.
15569 	+/
15570 	void notifyItemsChanged(int index, int count = 1) {
15571 	}
15572 	/// ditto
15573 	void notifyItemsInserted(int index, int count = 1) {
15574 	}
15575 	/// ditto
15576 	void notifyItemsRemoved(int index, int count = 1) {
15577 	}
15578 	/// ditto
15579 	void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) {
15580 	}
15581 
15582 	/++
15583 		History:
15584 			Added January 1, 2025
15585 	+/
15586 	void ensureItemVisibleInScroll(int index) {
15587 		auto itemPos = index * itemSize().height;
15588 		auto vsb = smw.verticalScrollBar;
15589 		auto viewable = vsb.viewableArea_;
15590 
15591 		if(viewable == 0) {
15592 			// viewable == 0 isn't actually supposed to happen, this means
15593 			// this method is being called before having our size assigned, it should
15594 			// probably just queue it up for later.
15595 			queuedScroll = index;
15596 			return;
15597 		}
15598 
15599 		queuedScroll = int.min;
15600 
15601 		if(itemPos < vsb.position) {
15602 			// scroll up to it
15603 			vsb.setPosition(itemPos);
15604 			smw.notify();
15605 		} else if(itemPos + itemSize().height > (vsb.position + viewable)) {
15606 			// scroll down to it, so it is at the bottom
15607 
15608 			auto lastViewableItemPosition = (viewable - itemSize.height) / itemSize.height * itemSize.height;
15609 			// need the itemPos to be at the lastViewableItemPosition after scrolling, so subtraction does it
15610 
15611 			vsb.setPosition(itemPos - lastViewableItemPosition);
15612 			smw.notify();
15613 		}
15614 	}
15615 
15616 	/++
15617 		History:
15618 			Added January 1, 2025;
15619 	+/
15620 	int numberOfCurrentlyFullyVisibleItems() {
15621 		return smw.verticalScrollBar.viewableArea_ / itemSize.height;
15622 	}
15623 
15624 	private int queuedScroll = int.min;
15625 
15626 	override void recomputeChildLayout() {
15627 		super.recomputeChildLayout();
15628 		if(queuedScroll != int.min)
15629 			ensureItemVisibleInScroll(queuedScroll);
15630 	}
15631 
15632 	private GenericListViewItem[] items;
15633 
15634 	override void paint(WidgetPainter painter) {}
15635 }
15636 
15637 /// ditto
15638 abstract class GenericListViewItem : Widget {
15639 	/++
15640 	+/
15641 	this(Widget parent) {
15642 		super(parent);
15643 	}
15644 
15645 	private int _currentIndex = -1;
15646 
15647 	private void showItemPrivate(int idx) {
15648 		showItem(idx);
15649 		_currentIndex = idx;
15650 	}
15651 
15652 	/++
15653 		Implement this to show an item from your data backing to the list.
15654 
15655 		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.
15656 	+/
15657 	abstract void showItem(int idx);
15658 
15659 	/++
15660 		Maintained by the library after calling [showItem] so the object knows which data index it currently has.
15661 
15662 		It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent).
15663 
15664 		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.
15665 	+/
15666 	final int currentIndexLoaded() {
15667 		return _currentIndex;
15668 	}
15669 }
15670 
15671 ///
15672 unittest {
15673 	import arsd.minigui;
15674 
15675 	import std.conv;
15676 
15677 	void main() {
15678 		auto mw = new MainWindow();
15679 
15680 		static class MyListViewItem : GenericListViewItem {
15681 			this(Widget parent) {
15682 				super(parent);
15683 
15684 				label = new TextLabel("unloaded", TextAlignment.Left, this);
15685 				button = new Button("Click", this);
15686 
15687 				button.addEventListener("triggered", (){
15688 					messageBox(text("clicked ", currentIndexLoaded()));
15689 				});
15690 			}
15691 			override void showItem(int idx) {
15692 				label.label = "Item " ~ to!string(idx);
15693 			}
15694 
15695 			TextLabel label;
15696 			Button button;
15697 		}
15698 
15699 		auto widget = new class GenericListViewWidget {
15700 			this() {
15701 				super(mw);
15702 			}
15703 			override GenericListViewItem itemFactory(Widget parent) {
15704 				return new MyListViewItem(parent);
15705 			}
15706 			override Size itemSize() {
15707 				return Size(0, scaleWithDpi(80));
15708 			}
15709 		};
15710 
15711 		widget.setItemCount(5000);
15712 
15713 		mw.loop();
15714 	}
15715 }
15716 
15717 // this exists just to wrap the actual GenericListViewWidgetInner so borders
15718 // and padding and stuff can work
15719 private class GenericListViewInnerContainer : Widget {
15720 	this(Widget parent) {
15721 		super(parent);
15722 		this.tabStop = false;
15723 	}
15724 
15725 	override void recomputeChildLayout() {
15726 		registerMovement();
15727 
15728 		auto cs = getComputedStyle();
15729 		auto bw = getBorderWidth(cs.borderStyle);
15730 
15731 		assert(children.length < 2);
15732 		foreach(child; children) {
15733 			child.x = bw + paddingLeft();
15734 			child.y = bw + paddingTop();
15735 			child.width = this.width.NonOverflowingUint - bw - bw - paddingLeft() - paddingRight();
15736 			child.height = this.height.NonOverflowingUint - bw - bw - paddingTop() - paddingBottom();
15737 
15738 			child.recomputeChildLayout();
15739 		}
15740 	}
15741 
15742 	override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) {
15743 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15744 			return parent.parent.parent.useStyleProperties(dg);
15745 		else
15746 			return super.useStyleProperties(dg);
15747 	}
15748 
15749 	override int paddingTop() {
15750 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15751 			return parent.parent.parent.paddingTop();
15752 		else
15753 			return super.paddingTop();
15754 	}
15755 
15756 	override int paddingBottom() {
15757 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15758 			return parent.parent.parent.paddingBottom();
15759 		else
15760 			return super.paddingBottom();
15761 	}
15762 
15763 	override int paddingLeft() {
15764 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15765 			return parent.parent.parent.paddingLeft();
15766 		else
15767 			return super.paddingLeft();
15768 	}
15769 
15770 	override int paddingRight() {
15771 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15772 			return parent.parent.parent.paddingRight();
15773 		else
15774 			return super.paddingRight();
15775 	}
15776 
15777 
15778 }
15779 
15780 private class GenericListViewWidgetInner : Widget {
15781 	this(GenericListViewWidget glvw, ScrollMessageWidget smw, GenericListViewInnerContainer parent) {
15782 		super(parent);
15783 		this.glvw = glvw;
15784 
15785 		reloadVisible();
15786 
15787 		smw.addEventListener("scroll", () {
15788 			reloadVisible();
15789 		});
15790 	}
15791 
15792 	override void registerMovement() {
15793 		super.registerMovement();
15794 		if(glvw && glvw.smw)
15795 			glvw.smw.setViewableArea(this.width, this.height);
15796 	}
15797 
15798 	void reloadVisible() {
15799 		auto y = glvw.smw.position.y / glvw.itemSize.height;
15800 
15801 		// idk why i had this here it doesn't seem to be ueful and actually made last items diasppear
15802 		//int offset = glvw.smw.position.y % glvw.itemSize.height;
15803 		//if(offset || y >= glvw.itemCount())
15804 			//y--;
15805 
15806 		if(y < 0)
15807 			y = 0;
15808 
15809 		recomputeChildLayout();
15810 
15811 		foreach(item; glvw.items) {
15812 			if(y < glvw.itemCount()) {
15813 				item.showItemPrivate(y);
15814 				item.show();
15815 			} else {
15816 				item.hide();
15817 			}
15818 			y++;
15819 		}
15820 
15821 		this.redraw();
15822 	}
15823 
15824 	private GenericListViewWidget glvw;
15825 
15826 	private bool inRcl;
15827 	override void recomputeChildLayout() {
15828 		if(inRcl)
15829 			return;
15830 		inRcl = true;
15831 		scope(exit)
15832 			inRcl = false;
15833 
15834 		registerMovement();
15835 
15836 		auto ih = glvw.itemSize().height;
15837 
15838 		auto itemCount = this.height / ih + 2; // extra for partial display before and after
15839 		bool hadNew;
15840 		while(glvw.items.length < itemCount) {
15841 			// FIXME: free the old items? maybe just set length
15842 			glvw.items ~= glvw.itemFactory(this);
15843 			hadNew = true;
15844 		}
15845 
15846 		if(hadNew)
15847 			reloadVisible();
15848 
15849 		int y = -(glvw.smw.position.y % ih) + this.paddingTop();
15850 		foreach(child; children) {
15851 			child.x = this.paddingLeft();
15852 			child.y = y;
15853 			y += glvw.itemSize().height;
15854 			child.width = this.width.NonOverflowingUint - this.paddingLeft() - this.paddingRight();
15855 			child.height = ih;
15856 
15857 			child.recomputeChildLayout();
15858 		}
15859 	}
15860 }
15861 
15862 
15863 
15864 /++
15865 	History:
15866 		It was a child of Window before, but as of September 29, 2024, it is now a child of `Dialog`.
15867 +/
15868 class MessageBox : Dialog {
15869 	private string message;
15870 	MessageBoxButton buttonPressed = MessageBoxButton.None;
15871 	/++
15872 
15873 		History:
15874 		The overload that takes `Window originator` was added on September 29, 2024.
15875 	+/
15876 	this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
15877 		this(null, message, buttons, buttonIds);
15878 	}
15879 	/// ditto
15880 	this(Window originator, string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
15881 		message = message.stripRightInternal;
15882 		int mainWidth;
15883 
15884 		// estimate longest line
15885 		int count;
15886 		foreach(ch; message) {
15887 			if(ch == '\n') {
15888 				if(count > mainWidth)
15889 					mainWidth = count;
15890 				count = 0;
15891 			} else {
15892 				count++;
15893 			}
15894 		}
15895 		mainWidth *= 8;
15896 		if(mainWidth < 300)
15897 			mainWidth = 300;
15898 		if(mainWidth > 600)
15899 			mainWidth = 600;
15900 
15901 		super(originator, mainWidth, 100);
15902 
15903 		assert(buttons.length);
15904 		assert(buttons.length ==  buttonIds.length);
15905 
15906 		this.message = message;
15907 
15908 		auto label = new TextDisplay(message, this);
15909 
15910 		auto hl = new HorizontalLayout(this);
15911 		auto spacer = new HorizontalSpacer(hl); // to right align
15912 
15913 		foreach(idx, buttonText; buttons) {
15914 			auto button = new CommandButton(buttonText, hl);
15915 
15916 			button.addEventListener(EventType.triggered, ((size_t idx) { return () {
15917 				this.buttonPressed = buttonIds[idx];
15918 				win.close();
15919 			}; })(idx));
15920 
15921 			if(idx == 0)
15922 				button.focus();
15923 		}
15924 
15925 		if(buttons.length == 1)
15926 			auto spacer2 = new HorizontalSpacer(hl); // to center it
15927 
15928 		auto size = label.flexBasisHeight() + hl.minHeight() + this.paddingTop + this.paddingBottom;
15929 		auto max = scaleWithDpi(600); // random max height
15930 		if(size > max)
15931 			size = max;
15932 
15933 		win.resize(scaleWithDpi(mainWidth), size);
15934 
15935 		win.show();
15936 		redraw();
15937 	}
15938 
15939 	override void OK() {
15940 		this.win.close();
15941 	}
15942 
15943 	mixin Padding!q{16};
15944 }
15945 
15946 ///
15947 enum MessageBoxStyle {
15948 	OK, ///
15949 	OKCancel, ///
15950 	RetryCancel, ///
15951 	YesNo, ///
15952 	YesNoCancel, ///
15953 	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.
15954 }
15955 
15956 ///
15957 enum MessageBoxIcon {
15958 	None, ///
15959 	Info, ///
15960 	Warning, ///
15961 	Error ///
15962 }
15963 
15964 /// Identifies the button the user pressed on a message box.
15965 enum MessageBoxButton {
15966 	None, /// The user closed the message box without clicking any of the buttons.
15967 	OK, ///
15968 	Cancel, ///
15969 	Retry, ///
15970 	Yes, ///
15971 	No, ///
15972 	Continue ///
15973 }
15974 
15975 
15976 /++
15977 	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.
15978 
15979 	Returns: the button pressed.
15980 +/
15981 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15982 	return messageBox(null, title, message, style, icon);
15983 }
15984 
15985 /// ditto
15986 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15987 	return messageBox(null, null, message, style, icon);
15988 }
15989 
15990 /++
15991 
15992 +/
15993 MessageBoxButton messageBox(Window originator, string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15994 	version(win32_widgets) {
15995 		WCharzBuffer t = WCharzBuffer(title);
15996 		WCharzBuffer m = WCharzBuffer(message);
15997 		UINT type;
15998 		with(MessageBoxStyle)
15999 		final switch(style) {
16000 			case OK: type |= MB_OK; break;
16001 			case OKCancel: type |= MB_OKCANCEL; break;
16002 			case RetryCancel: type |= MB_RETRYCANCEL; break;
16003 			case YesNo: type |= MB_YESNO; break;
16004 			case YesNoCancel: type |= MB_YESNOCANCEL; break;
16005 			case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break;
16006 		}
16007 		with(MessageBoxIcon)
16008 		final switch(icon) {
16009 			case None: break;
16010 			case Info: type |= MB_ICONINFORMATION; break;
16011 			case Warning: type |= MB_ICONWARNING; break;
16012 			case Error: type |= MB_ICONERROR; break;
16013 		}
16014 		switch(MessageBoxW(originator is null ? null : originator.win.hwnd, m.ptr, t.ptr, type)) {
16015 			case IDOK: return MessageBoxButton.OK;
16016 			case IDCANCEL: return MessageBoxButton.Cancel;
16017 			case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry;
16018 			case IDYES: return MessageBoxButton.Yes;
16019 			case IDNO: return MessageBoxButton.No;
16020 			case IDCONTINUE: return MessageBoxButton.Continue;
16021 			default: return MessageBoxButton.None;
16022 		}
16023 	} else {
16024 		string[] buttons;
16025 		MessageBoxButton[] buttonIds;
16026 		with(MessageBoxStyle)
16027 		final switch(style) {
16028 			case OK:
16029 				buttons = ["OK"];
16030 				buttonIds = [MessageBoxButton.OK];
16031 			break;
16032 			case OKCancel:
16033 				buttons = ["OK", "Cancel"];
16034 				buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel];
16035 			break;
16036 			case RetryCancel:
16037 				buttons = ["Retry", "Cancel"];
16038 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel];
16039 			break;
16040 			case YesNo:
16041 				buttons = ["Yes", "No"];
16042 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No];
16043 			break;
16044 			case YesNoCancel:
16045 				buttons = ["Yes", "No", "Cancel"];
16046 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel];
16047 			break;
16048 			case RetryCancelContinue:
16049 				buttons = ["Try Again", "Cancel", "Continue"];
16050 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue];
16051 			break;
16052 		}
16053 		auto mb = new MessageBox(originator, message, buttons, buttonIds);
16054 		EventLoop el = EventLoop.get;
16055 		el.run(() { return !mb.win.closed; });
16056 		return mb.buttonPressed;
16057 	}
16058 
16059 }
16060 
16061 /// ditto
16062 int messageBox(Window originator, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
16063 	return messageBox(originator, null, message, style, icon);
16064 }
16065 
16066 
16067 ///
16068 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler;
16069 
16070 /++
16071 	This is an opaque type you can use to disconnect an event handler when you're no longer interested.
16072 
16073 	History:
16074 		The data members were `public` (albeit undocumented and not intended for use) prior to May 13, 2021. They are now `private`, reflecting the single intended use of this object.
16075 +/
16076 struct EventListener {
16077 	private Widget widget;
16078 	private string event;
16079 	private EventHandler handler;
16080 	private bool useCapture;
16081 
16082 	///
16083 	void disconnect() {
16084 		if(widget !is null && handler !is null)
16085 			widget.removeEventListener(this);
16086 	}
16087 }
16088 
16089 /++
16090 	The purpose of this enum was to give a compile-time checked version of various standard event strings.
16091 
16092 	Now, I recommend you use a statically typed event object instead.
16093 
16094 	See_Also: [Event]
16095 +/
16096 enum EventType : string {
16097 	click = "click", ///
16098 
16099 	mouseenter = "mouseenter", ///
16100 	mouseleave = "mouseleave", ///
16101 	mousein = "mousein", ///
16102 	mouseout = "mouseout", ///
16103 	mouseup = "mouseup", ///
16104 	mousedown = "mousedown", ///
16105 	mousemove = "mousemove", ///
16106 
16107 	keydown = "keydown", ///
16108 	keyup = "keyup", ///
16109 	char_ = "char", ///
16110 
16111 	focus = "focus", ///
16112 	blur = "blur", ///
16113 
16114 	triggered = "triggered", ///
16115 
16116 	change = "change", ///
16117 }
16118 
16119 /++
16120 	Represents an event that is currently being processed.
16121 
16122 
16123 	Minigui's event model is based on the web browser. An event has a name, a target,
16124 	and an associated data object. It starts from the window and works its way down through
16125 	the target through all intermediate [Widget]s, triggering capture phase handlers as it goes,
16126 	then goes back up again all the way back to the window, triggering bubble phase handlers. At
16127 	the end, if [Event.preventDefault] has not been called, it calls the target widget's default
16128 	handlers for the event (please note that default handlers will be called even if [Event.stopPropagation]
16129 	was called; that just stops it from calling other handlers in the widget tree, but the default happens
16130 	whenever propagation is done, not only if it gets to the end of the chain).
16131 
16132 	This model has several nice points:
16133 
16134 	$(LIST
16135 		* It is easy to delegate dynamic handlers to a parent. You can have a parent container
16136 		  with event handlers set, then add/remove children as much as you want without needing
16137 		  to manage the event handlers on them - the parent alone can manage everything.
16138 
16139 		* It is easy to create new custom events in your application.
16140 
16141 		* It is familiar to many web developers.
16142 	)
16143 
16144 	There's a few downsides though:
16145 
16146 	$(LIST
16147 		* There's not a lot of type safety.
16148 
16149 		* You don't get a static list of what events a widget can emit.
16150 
16151 		* Tracing where an event got cancelled along the chain can get difficult; the downside of
16152 		  the central delegation benefit is it can be lead to debugging of action at a distance.
16153 	)
16154 
16155 	In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript
16156 	while keeping the benefits - and most compatibility with - the existing model. The main idea is
16157 	to simply use a D object type which provides a static interface as well as a built-in event name.
16158 	Then, a new static interface allows you to see what an event can emit and attach handlers to it
16159 	similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still
16160 	delegate to a parent and use custom events as well as using the runtime dynamic access, in addition
16161 	to having a little more help from the D compiler and documentation generator.
16162 
16163 	Your code would change like this:
16164 
16165 	---
16166 	// old
16167 	widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture );
16168 
16169 	// new
16170 	widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture );
16171 	---
16172 
16173 	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.
16174 
16175 	All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class.
16176 
16177 	Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you!
16178 
16179 	Thus the family of functions are:
16180 
16181 	[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.
16182 
16183 	[Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate.
16184 
16185 	[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.
16186 
16187 	Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property:
16188 
16189 	---
16190 	class MyCheckbox : Widget {
16191 		/// This gives a chance to document it and generates a convenience function to send it and attach handlers.
16192 		/// It is NOT actually required but should be used whenever possible.
16193 		mixin Emits!(ChangeEvent!bool);
16194 
16195 		this(Widget parent) {
16196 			super(parent);
16197 			setDefaultEventHandler((ClickEvent) { checked = !checked; });
16198 		}
16199 
16200 		private bool _checked;
16201 		@property bool checked() { return _checked; }
16202 		@property void checked(bool set) {
16203 			_checked = set;
16204 			emit!(ChangeEvent!bool)(&checked);
16205 		}
16206 	}
16207 	---
16208 
16209 	## Creating Your Own Events
16210 
16211 	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.
16212 
16213 	---
16214 	final class MyEvent : Event {
16215 		this(Widget target) { super(EventString, target); }
16216 		mixin Register; // adds EventString and other reflection information
16217 	}
16218 	---
16219 
16220 	Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it.
16221 
16222 	History:
16223 		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.
16224 
16225 		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.
16226 +/
16227 /+
16228 
16229 	## General Conventions
16230 
16231 	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.
16232 
16233 
16234 	## Qt-style signals and slots
16235 
16236 	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.
16237 
16238 	The intention is for events to be used when
16239 
16240 	---
16241 	class Demo : Widget {
16242 		this() {
16243 			myPropertyChanged = Signal!int(this);
16244 		}
16245 		@property myProperty(int v) {
16246 			myPropertyChanged.emit(v);
16247 		}
16248 
16249 		Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name...
16250 		// but it can just genuinely not care about `this` since that's not really passed.
16251 	}
16252 
16253 	class Foo : Widget {
16254 		// the slot uda is not necessary, but it helps the script and ui builder find it.
16255 		@slot void setValue(int v) { ... }
16256 	}
16257 
16258 	demo.myPropertyChanged.connect(&foo.setValue);
16259 	---
16260 
16261 	The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor.
16262 
16263 	Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically.
16264 
16265 	class StringChangeEvent : ChangeEvent, Signal!string {
16266 		mixin SignalImpl
16267 	}
16268 
16269 +/
16270 class Event : ReflectableProperties {
16271 	/// Creates an event without populating any members and without sending it. See [dispatch]
16272 	this(string eventName, Widget emittedBy) {
16273 		this.eventName = eventName;
16274 		this.srcElement = emittedBy;
16275 	}
16276 
16277 
16278 	/// Implementations for the [ReflectableProperties] interface/
16279 	void getPropertiesList(scope void delegate(string name) sink) const {}
16280 	/// ditto
16281 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { }
16282 	/// ditto
16283 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) {
16284 		return SetPropertyResult.notPermitted;
16285 	}
16286 
16287 
16288 	/+
16289 	/++
16290 		This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice.
16291 
16292 		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.
16293 	+/
16294 	protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) {
16295 		if(value.length == 0) {
16296 			finalSink(memberName, `""`);
16297 			return;
16298 		}
16299 
16300 		char[1024] bufferBacking;
16301 		char[] buffer = bufferBacking;
16302 		int bufferPosition;
16303 
16304 		void sink(char ch) {
16305 			if(bufferPosition >= buffer.length)
16306 				buffer.length = buffer.length + 1024;
16307 			buffer[bufferPosition++] = ch;
16308 		}
16309 
16310 		sink('"');
16311 
16312 		foreach(ch; value) {
16313 			switch(ch) {
16314 				case '\\':
16315 					sink('\\'); sink('\\');
16316 				break;
16317 				case '"':
16318 					sink('\\'); sink('"');
16319 				break;
16320 				case '\n':
16321 					sink('\\'); sink('n');
16322 				break;
16323 				case '\r':
16324 					sink('\\'); sink('r');
16325 				break;
16326 				case '\t':
16327 					sink('\\'); sink('t');
16328 				break;
16329 				default:
16330 					sink(ch);
16331 			}
16332 		}
16333 
16334 		sink('"');
16335 
16336 		finalSink(memberName, buffer[0 .. bufferPosition]);
16337 	}
16338 	+/
16339 
16340 	/+
16341 	enum EventInitiator {
16342 		system,
16343 		minigui,
16344 		user
16345 	}
16346 
16347 	immutable EventInitiator; initiatedBy;
16348 	+/
16349 
16350 	/++
16351 		Events should generally follow the propagation model, but there's some exceptions
16352 		to that rule. If so, they should override this to return false. In that case, only
16353 		bubbling event handlers on the target itself and capturing event handlers on the containing
16354 		window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal
16355 		capture -> target -> bubble process.)
16356 
16357 		History:
16358 			Added May 12, 2021
16359 	+/
16360 	bool propagates() const pure nothrow @nogc @safe {
16361 		return true;
16362 	}
16363 
16364 	/++
16365 		hints as to whether preventDefault will actually do anything. not entirely reliable.
16366 
16367 		History:
16368 			Added May 14, 2021
16369 	+/
16370 	bool cancelable() const pure nothrow @nogc @safe {
16371 		return true;
16372 	}
16373 
16374 	/++
16375 		You can mix this into child class to register some boilerplate. It includes the `EventString`
16376 		member, a constructor, and implementations of the dynamic get data interfaces.
16377 
16378 		If you fail to do this, your event will probably not have full compatibility but it might still work for you.
16379 
16380 
16381 		You can override the default EventString by simply providing your own in the form of
16382 		`enum string EventString = "some.name";` The default is the name of your class and its parent entity
16383 		which provides some namespace protection against conflicts in other libraries while still being fairly
16384 		easy to use.
16385 
16386 		If you provide your own constructor, it will override the default constructor provided here. A constructor
16387 		must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the
16388 		first argument to your constructor.
16389 
16390 		History:
16391 			Added May 13, 2021.
16392 	+/
16393 	protected static mixin template Register() {
16394 		public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~  __traits(identifier, typeof(this));
16395 		this(Widget target) { super(EventString, target); }
16396 
16397 		mixin ReflectableProperties.RegisterGetters;
16398 	}
16399 
16400 	/++
16401 		This is the widget that emitted the event.
16402 
16403 
16404 		The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms.
16405 
16406 		History:
16407 			The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms,
16408 			but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web
16409 			so I don't intend to remove these aliases.
16410 	+/
16411 	Widget source;
16412 	/// ditto
16413 	alias source target;
16414 	/// ditto
16415 	alias source srcElement;
16416 
16417 	Widget relatedTarget; /// Note: likely to be deprecated at some point.
16418 
16419 	/// Prevents the default event handler (if there is one) from being called
16420 	void preventDefault() {
16421 		lastDefaultPrevented = true;
16422 		defaultPrevented_ = true;
16423 	}
16424 
16425 	/// Stops the event propagation immediately.
16426 	void stopPropagation() {
16427 		propagationStopped = true;
16428 	}
16429 
16430 	private bool defaultPrevented_;
16431 	public bool defaultPrevented() {
16432 		return defaultPrevented_;
16433 	}
16434 	private bool propagationStopped;
16435 	private string eventName;
16436 
16437 	private bool isBubbling;
16438 
16439 	/// 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.
16440 	protected void adjustClientCoordinates(int deltaX, int deltaY) { }
16441 
16442 	/++
16443 		this sends it only to the target. If you want propagation, use dispatch() instead.
16444 
16445 		This should be made private!!!
16446 
16447 	+/
16448 	void sendDirectly() {
16449 		if(srcElement is null)
16450 			return;
16451 
16452 		// 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.
16453 
16454 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
16455 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement);
16456 
16457 		if(auto e = target.parentWindow) {
16458 			if(auto handlers = "*" in e.capturingEventHandlers)
16459 			foreach(handler; *handlers)
16460 				if(handler) handler(e, this);
16461 			if(auto handlers = eventName in e.capturingEventHandlers)
16462 			foreach(handler; *handlers)
16463 				if(handler) handler(e, this);
16464 		}
16465 
16466 		auto e = srcElement;
16467 
16468 		if(auto handlers = eventName in e.bubblingEventHandlers)
16469 		foreach(handler; *handlers)
16470 			if(handler) handler(e, this);
16471 
16472 		if(auto handlers = "*" in e.bubblingEventHandlers)
16473 		foreach(handler; *handlers)
16474 			if(handler) handler(e, this);
16475 
16476 		// there's never a default for a catch-all event
16477 		if(!defaultPrevented)
16478 			if(eventName in e.defaultEventHandlers)
16479 				e.defaultEventHandlers[eventName](e, this);
16480 	}
16481 
16482 	/// this dispatches the element using the capture -> target -> bubble process
16483 	void dispatch() {
16484 		if(srcElement is null)
16485 			return;
16486 
16487 		if(!propagates) {
16488 			sendDirectly;
16489 			return;
16490 		}
16491 
16492 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
16493 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement);
16494 
16495 		// first capture, then bubble
16496 
16497 		Widget[] chain;
16498 		Widget curr = srcElement;
16499 		while(curr) {
16500 			auto l = curr;
16501 			chain ~= l;
16502 			curr = curr.parent;
16503 		}
16504 
16505 		isBubbling = false;
16506 
16507 		foreach_reverse(e; chain) {
16508 			if(auto handlers = "*" in e.capturingEventHandlers)
16509 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
16510 
16511 			if(propagationStopped)
16512 				break;
16513 
16514 			if(auto handlers = eventName in e.capturingEventHandlers)
16515 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
16516 
16517 			// the default on capture should really be to always do nothing
16518 
16519 			//if(!defaultPrevented)
16520 			//	if(eventName in e.defaultEventHandlers)
16521 			//		e.defaultEventHandlers[eventName](e.element, this);
16522 
16523 			if(propagationStopped)
16524 				break;
16525 		}
16526 
16527 		int adjustX;
16528 		int adjustY;
16529 
16530 		isBubbling = true;
16531 		if(!propagationStopped)
16532 		foreach(e; chain) {
16533 			if(auto handlers = eventName in e.bubblingEventHandlers)
16534 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
16535 
16536 			if(propagationStopped)
16537 				break;
16538 
16539 			if(auto handlers = "*" in e.bubblingEventHandlers)
16540 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
16541 
16542 			if(propagationStopped)
16543 				break;
16544 
16545 			if(e.encapsulatedChildren()) {
16546 				adjustClientCoordinates(adjustX, adjustY);
16547 				target = e;
16548 			} else {
16549 				adjustX += e.x;
16550 				adjustY += e.y;
16551 			}
16552 		}
16553 
16554 		if(!defaultPrevented)
16555 		foreach(e; chain) {
16556 			if(eventName in e.defaultEventHandlers)
16557 				e.defaultEventHandlers[eventName](e, this);
16558 		}
16559 	}
16560 
16561 
16562 	/* old compatibility things */
16563 	deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!")
16564 	final @property {
16565 		Key key() { return (cast(KeyEventBase) this).key; }
16566 		KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; }
16567 
16568 		bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; }
16569 		bool altKey() { return (cast(KeyEventBase) this).altKey; }
16570 		bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; }
16571 	}
16572 
16573 	deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!")
16574 	final @property {
16575 		int clientX() { return (cast(MouseEventBase) this).clientX; }
16576 		int clientY() { return (cast(MouseEventBase) this).clientY; }
16577 
16578 		int viewportX() { return (cast(MouseEventBase) this).viewportX; }
16579 		int viewportY() { return (cast(MouseEventBase) this).viewportY; }
16580 
16581 		int button() { return (cast(MouseEventBase) this).button; }
16582 		int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; }
16583 	}
16584 
16585 	deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward")
16586 	final @property {
16587 		int state() {
16588 			if(auto meb = cast(MouseEventBase) this)
16589 				return meb.state;
16590 			if(auto keb = cast(KeyEventBase) this)
16591 				return keb.state;
16592 			assert(0);
16593 		}
16594 	}
16595 
16596 	deprecated("Use a CharEvent instead of Event in your handler going forward")
16597 	final @property {
16598 		dchar character() {
16599 			if(auto ce = cast(CharEvent) this)
16600 				return ce.character;
16601 			return dchar_invalid;
16602 		}
16603 	}
16604 
16605 	// for change events
16606 	@property {
16607 		///
16608 		int intValue() { return 0; }
16609 		///
16610 		string stringValue() { return null; }
16611 	}
16612 }
16613 
16614 /++
16615 	This lets you statically verify you send the events you claim you send and gives you a hook to document them.
16616 
16617 	Please note that a widget may send events not listed as Emits. You can always construct and dispatch
16618 	dynamic and custom events, but the static list helps ensure you get them right.
16619 
16620 	If this is declared, you can use [Widget.emit] to send the event.
16621 
16622 	All events work the same way though, following the capture->widget->bubble model described under [Event].
16623 
16624 	History:
16625 		Added May 4, 2021
16626 +/
16627 mixin template Emits(EventType) {
16628 	import arsd.minigui : EventString;
16629 	static if(is(EventType : Event) && !is(EventType == Event))
16630 		mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";");
16631 	else
16632 		static assert(0, "You can only emit subclasses of Event");
16633 }
16634 
16635 /// ditto
16636 mixin template Emits(string eventString) {
16637 	mixin("private Event[0] emits_" ~ eventString ~";");
16638 }
16639 
16640 /*
16641 class SignalEvent(string name) : Event {
16642 
16643 }
16644 */
16645 
16646 /++
16647 	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".
16648 
16649 
16650 	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.
16651 
16652 	History:
16653 		Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things.
16654 +/
16655 class CommandEvent : Event {
16656 	enum EventString = "command";
16657 	this(Widget source, string CommandString = EventString) {
16658 		super(CommandString, source);
16659 	}
16660 }
16661 
16662 /++
16663 	A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments.
16664 +/
16665 class CommandEventWithArgs(Args...) : CommandEvent {
16666 	this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; }
16667 	Args args;
16668 }
16669 
16670 /++
16671 	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.
16672 
16673 	See [CommandEvent] for more information.
16674 
16675 	Returns:
16676 		The [EventListener] you can use to remove the handler.
16677 +/
16678 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) {
16679 	return w.addEventListener(CommandString, (Event ev) {
16680 		if(ev.target is w)
16681 			return; // it does not consume its own commands!
16682 		if(auto cev = cast(CommandEventWithArgs!Args) ev) {
16683 			handler(cev.args);
16684 			ev.stopPropagation();
16685 		}
16686 	});
16687 }
16688 
16689 /++
16690 	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.
16691 +/
16692 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) {
16693 	auto event = new CommandEventWithArgs!Args(w, CommandString, args);
16694 	event.dispatch();
16695 }
16696 
16697 /++
16698 	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.
16699 
16700 	If you need to know the old size, you need to store it yourself.
16701 
16702 	History:
16703 		Made final on January 3, 2025 (dub v12.0)
16704 +/
16705 final class ResizeEvent : Event {
16706 	enum EventString = "resize";
16707 
16708 	this(Widget target) { super(EventString, target); }
16709 
16710 	override bool propagates() const { return false; }
16711 }
16712 
16713 /++
16714 	ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close.
16715 
16716 	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.
16717 
16718 	History:
16719 		Added June 21, 2021 (dub v10.1)
16720 
16721 		Made final on January 3, 2025 (dub v12.0)
16722 +/
16723 final class ClosingEvent : Event {
16724 	enum EventString = "closing";
16725 
16726 	this(Widget target) { super(EventString, target); }
16727 
16728 	override bool propagates() const { return false; }
16729 	override bool cancelable() const { return true; }
16730 }
16731 
16732 /// ditto
16733 final class ClosedEvent : Event {
16734 	enum EventString = "closed";
16735 
16736 	this(Widget target) { super(EventString, target); }
16737 
16738 	override bool propagates() const { return false; }
16739 	override bool cancelable() const { return false; }
16740 }
16741 
16742 ///
16743 final class BlurEvent : Event {
16744 	enum EventString = "blur";
16745 
16746 	// FIXME: related target?
16747 	this(Widget target) { super(EventString, target); }
16748 
16749 	override bool propagates() const { return false; }
16750 }
16751 
16752 ///
16753 final class FocusEvent : Event {
16754 	enum EventString = "focus";
16755 
16756 	// FIXME: related target?
16757 	this(Widget target) { super(EventString, target); }
16758 
16759 	override bool propagates() const { return false; }
16760 }
16761 
16762 /++
16763 	FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates.
16764 
16765 	History:
16766 		Added July 3, 2021
16767 +/
16768 final class FocusInEvent : Event {
16769 	enum EventString = "focusin";
16770 
16771 	// FIXME: related target?
16772 	this(Widget target) { super(EventString, target); }
16773 
16774 	override bool cancelable() const { return false; }
16775 }
16776 
16777 /// ditto
16778 final class FocusOutEvent : Event {
16779 	enum EventString = "focusout";
16780 
16781 	// FIXME: related target?
16782 	this(Widget target) { super(EventString, target); }
16783 
16784 	override bool cancelable() const { return false; }
16785 }
16786 
16787 ///
16788 final class ScrollEvent : Event {
16789 	enum EventString = "scroll";
16790 	this(Widget target) { super(EventString, target); }
16791 
16792 	override bool cancelable() const { return false; }
16793 }
16794 
16795 /++
16796 	Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget.
16797 
16798 	History:
16799 		Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class.
16800 +/
16801 final class CharEvent : Event {
16802 	enum EventString = "char";
16803 	this(Widget target, dchar ch) {
16804 		character = ch;
16805 		super(EventString, target);
16806 	}
16807 
16808 	immutable dchar character;
16809 }
16810 
16811 /++
16812 	You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information.
16813 +/
16814 abstract class ChangeEventBase : Event {
16815 	enum EventString = "change";
16816 	this(Widget target) {
16817 		super(EventString, target);
16818 	}
16819 
16820 	/+
16821 		// idk where or how exactly i want to do this.
16822 		// i might come back to it later.
16823 
16824 	// If a widget itself broadcasts one of theses itself, it stops propagation going down
16825 	// this way the source doesn't get too confused (think of a nested scroll widget)
16826 	//
16827 	// the idea is like the scroll bar emits a command event saying like "scroll left one line"
16828 	// then you consume that command and change you scroll x position to whatever. then you do
16829 	// some kind of change event that is broadcast back to the children and any horizontal scroll
16830 	// listeners are now able to update, without having an explicit connection between them.
16831 	void broadcastToChildren(string fieldName) {
16832 
16833 	}
16834 	+/
16835 }
16836 
16837 /++
16838 	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.
16839 
16840 
16841 	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).
16842 
16843 	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);`
16844 
16845 	Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything.
16846 
16847 	History:
16848 		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.
16849 +/
16850 final class ChangeEvent(T) : ChangeEventBase {
16851 	this(Widget target, T delegate() getNewValue) {
16852 		assert(getNewValue !is null);
16853 		this.getNewValue = getNewValue;
16854 		super(target);
16855 	}
16856 
16857 	private T delegate() getNewValue;
16858 
16859 	/++
16860 		Gets the new value that just changed.
16861 	+/
16862 	@property T value() {
16863 		return getNewValue();
16864 	}
16865 
16866 	/// compatibility method for old generic Events
16867 	static if(is(immutable T == immutable int))
16868 		override int intValue() { return value; }
16869 	/// ditto
16870 	static if(is(immutable T == immutable string))
16871 		override string stringValue() { return value; }
16872 }
16873 
16874 /++
16875 	Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s.
16876 
16877 
16878 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16879 
16880 	History:
16881 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
16882 +/
16883 abstract class KeyEventBase : Event {
16884 	this(string name, Widget target) {
16885 		super(name, target);
16886 	}
16887 
16888 	// for key events
16889 	Key key; ///
16890 
16891 	KeyEvent originalKeyEvent;
16892 
16893 	/++
16894 		Indicates the current state of the given keyboard modifier keys.
16895 
16896 		History:
16897 			Added to events on April 15, 2020.
16898 	+/
16899 	bool ctrlKey;
16900 
16901 	/// ditto
16902 	bool altKey;
16903 
16904 	/// ditto
16905 	bool shiftKey;
16906 
16907 	/++
16908 		The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey].
16909 
16910 		See [arsd.simpledisplay.ModifierState] for other possible flags.
16911 	+/
16912 	int state;
16913 
16914 	mixin Register;
16915 }
16916 
16917 /++
16918 	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].
16919 
16920 
16921 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16922 
16923 	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.
16924 
16925 	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.
16926 
16927 	See_Also: [KeyUpEvent], [CharEvent]
16928 
16929 	History:
16930 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners.
16931 +/
16932 final class KeyDownEvent : KeyEventBase {
16933 	enum EventString = "keydown";
16934 	this(Widget target) { super(EventString, target); }
16935 }
16936 
16937 /++
16938 	Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase].
16939 
16940 
16941 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16942 
16943 	See_Also: [KeyDownEvent], [CharEvent]
16944 
16945 	History:
16946 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners.
16947 +/
16948 final class KeyUpEvent : KeyEventBase {
16949 	enum EventString = "keyup";
16950 	this(Widget target) { super(EventString, target); }
16951 }
16952 
16953 /++
16954 	Contains shared properties for various mouse events;
16955 
16956 
16957 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16958 
16959 	History:
16960 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
16961 +/
16962 abstract class MouseEventBase : Event {
16963 	this(string name, Widget target) {
16964 		super(name, target);
16965 	}
16966 
16967 	// for mouse events
16968 	int clientX; /// The mouse event location relative to the target widget
16969 	int clientY; /// ditto
16970 
16971 	int viewportX; /// The mouse event location relative to the window origin
16972 	int viewportY; /// ditto
16973 
16974 	int button; /// See: [MouseEvent.button]
16975 	int buttonLinear; /// See: [MouseEvent.buttonLinear]
16976 
16977 	/++
16978 		Indicates the current state of the given keyboard modifier keys.
16979 
16980 		History:
16981 			Added to mouse events on September 28, 2010.
16982 	+/
16983 	bool ctrlKey;
16984 
16985 	/// ditto
16986 	bool altKey;
16987 
16988 	/// ditto
16989 	bool shiftKey;
16990 
16991 
16992 
16993 	int state; ///
16994 
16995 	/++
16996 		for consistent names with key event.
16997 
16998 		History:
16999 			Added September 28, 2021 (dub v10.3)
17000 	+/
17001 	alias modifierState = state;
17002 
17003 	/++
17004 		Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out.
17005 
17006 		History:
17007 			Added May 15, 2021
17008 	+/
17009 	bool isMouseWheel() {
17010 		return button == MouseButton.wheelUp || button == MouseButton.wheelDown || button == MouseButton.wheelLeft || button == MouseButton.wheelRight;
17011 	}
17012 
17013 	// private
17014 	override void adjustClientCoordinates(int deltaX, int deltaY) {
17015 		clientX += deltaX;
17016 		clientY += deltaY;
17017 	}
17018 
17019 	mixin Register;
17020 }
17021 
17022 /++
17023 	Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase].
17024 
17025 
17026 	$(WARNING
17027 		Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and
17028 		for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct
17029 		behavior.
17030 
17031 		Use [MouseEventBase.isMouseWheel] to filter wheel events while keeping others.
17032 	)
17033 
17034 	[MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement.
17035 
17036 	[MouseUpEvent] is sent when the user releases a mouse button.
17037 
17038 	[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.)
17039 
17040 	[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.
17041 
17042 	[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.
17043 
17044 	[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.
17045 
17046 	[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.
17047 
17048 	[MouseEnterEvent] is sent when the mouse enters the bounding box of a widget.
17049 
17050 	[MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget.
17051 
17052 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
17053 
17054 	Rationale:
17055 
17056 		If you only want to do drag, mousedown/up works just fine being consistently sent.
17057 
17058 		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).
17059 
17060 		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.
17061 
17062 	History:
17063 		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.
17064 +/
17065 final class MouseUpEvent : MouseEventBase {
17066 	enum EventString = "mouseup"; ///
17067 	this(Widget target) { super(EventString, target); }
17068 }
17069 /// ditto
17070 final class MouseDownEvent : MouseEventBase {
17071 	enum EventString = "mousedown"; ///
17072 	this(Widget target) { super(EventString, target); }
17073 }
17074 /// ditto
17075 final class MouseMoveEvent : MouseEventBase {
17076 	enum EventString = "mousemove"; ///
17077 	this(Widget target) { super(EventString, target); }
17078 }
17079 /// ditto
17080 final class ClickEvent : MouseEventBase {
17081 	enum EventString = "click"; ///
17082 	this(Widget target) { super(EventString, target); }
17083 }
17084 /// ditto
17085 final class DoubleClickEvent : MouseEventBase {
17086 	enum EventString = "dblclick"; ///
17087 	this(Widget target) { super(EventString, target); }
17088 }
17089 /// ditto
17090 final class MouseOverEvent : Event {
17091 	enum EventString = "mouseover"; ///
17092 	this(Widget target) { super(EventString, target); }
17093 }
17094 /// ditto
17095 final class MouseOutEvent : Event {
17096 	enum EventString = "mouseout"; ///
17097 	this(Widget target) { super(EventString, target); }
17098 }
17099 /// ditto
17100 final class MouseEnterEvent : Event {
17101 	enum EventString = "mouseenter"; ///
17102 	this(Widget target) { super(EventString, target); }
17103 
17104 	override bool propagates() const { return false; }
17105 }
17106 /// ditto
17107 final class MouseLeaveEvent : Event {
17108 	enum EventString = "mouseleave"; ///
17109 	this(Widget target) { super(EventString, target); }
17110 
17111 	override bool propagates() const { return false; }
17112 }
17113 
17114 private bool isAParentOf(Widget a, Widget b) {
17115 	if(a is null || b is null)
17116 		return false;
17117 
17118 	while(b !is null) {
17119 		if(a is b)
17120 			return true;
17121 		b = b.parent;
17122 	}
17123 
17124 	return false;
17125 }
17126 
17127 private struct WidgetAtPointResponse {
17128 	Widget widget;
17129 
17130 	// x, y relative to the widget in the response.
17131 	int x;
17132 	int y;
17133 }
17134 
17135 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) {
17136 	assert(starting !is null);
17137 
17138 	starting.addScrollPosition(x, y);
17139 
17140 	auto child = starting.getChildAtPosition(x, y);
17141 	while(child) {
17142 		if(child.hidden)
17143 			continue;
17144 		starting = child;
17145 		x -= child.x;
17146 		y -= child.y;
17147 		auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y);
17148 		child = r.widget;
17149 		if(child is starting)
17150 			break;
17151 	}
17152 	return WidgetAtPointResponse(starting, x, y);
17153 }
17154 
17155 version(win32_widgets) {
17156 private:
17157 	import core.sys.windows.commctrl;
17158 
17159 	pragma(lib, "comctl32");
17160 	shared static this() {
17161 		// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx
17162 		INITCOMMONCONTROLSEX ic;
17163 		ic.dwSize = cast(DWORD) ic.sizeof;
17164 		ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES;
17165 		if(!InitCommonControlsEx(&ic)) {
17166 			//writeln("ICC failed");
17167 		}
17168 	}
17169 
17170 
17171 	// everything from here is just win32 headers copy pasta
17172 private:
17173 extern(Windows):
17174 
17175 	alias HANDLE HMENU;
17176 	HMENU CreateMenu();
17177 	bool SetMenu(HWND, HMENU);
17178 	HMENU CreatePopupMenu();
17179 	enum MF_POPUP = 0x10;
17180 	enum MF_STRING = 0;
17181 
17182 
17183 	BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*);
17184 	struct INITCOMMONCONTROLSEX {
17185 		DWORD dwSize;
17186 		DWORD dwICC;
17187 	}
17188 	enum HINST_COMMCTRL = cast(HINSTANCE) (-1);
17189 enum {
17190         IDB_STD_SMALL_COLOR,
17191         IDB_STD_LARGE_COLOR,
17192         IDB_VIEW_SMALL_COLOR = 4,
17193         IDB_VIEW_LARGE_COLOR = 5
17194 }
17195 enum {
17196         STD_CUT,
17197         STD_COPY,
17198         STD_PASTE,
17199         STD_UNDO,
17200         STD_REDOW,
17201         STD_DELETE,
17202         STD_FILENEW,
17203         STD_FILEOPEN,
17204         STD_FILESAVE,
17205         STD_PRINTPRE,
17206         STD_PROPERTIES,
17207         STD_HELP,
17208         STD_FIND,
17209         STD_REPLACE,
17210         STD_PRINT // = 14
17211 }
17212 
17213 alias HANDLE HIMAGELIST;
17214 	HIMAGELIST ImageList_Create(int, int, UINT, int, int);
17215 	int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP);
17216         BOOL ImageList_Destroy(HIMAGELIST);
17217 
17218 uint MAKELONG(ushort a, ushort b) {
17219         return cast(uint) ((b << 16) | a);
17220 }
17221 
17222 
17223 struct TBBUTTON {
17224 	int   iBitmap;
17225 	int   idCommand;
17226 	BYTE  fsState;
17227 	BYTE  fsStyle;
17228 	version(Win64)
17229 	BYTE[6] bReserved;
17230 	else
17231 	BYTE[2]  bReserved;
17232 	DWORD dwData;
17233 	INT_PTR   iString;
17234 }
17235 
17236 	enum {
17237 		TB_ADDBUTTONSA   = WM_USER + 20,
17238 		TB_INSERTBUTTONA = WM_USER + 21,
17239 		TB_GETIDEALSIZE = WM_USER + 99,
17240 	}
17241 
17242 struct SIZE {
17243 	LONG cx;
17244 	LONG cy;
17245 }
17246 
17247 
17248 enum {
17249 	TBSTATE_CHECKED       = 1,
17250 	TBSTATE_PRESSED       = 2,
17251 	TBSTATE_ENABLED       = 4,
17252 	TBSTATE_HIDDEN        = 8,
17253 	TBSTATE_INDETERMINATE = 16,
17254 	TBSTATE_WRAP          = 32
17255 }
17256 
17257 
17258 
17259 enum {
17260 	ILC_COLOR    = 0,
17261 	ILC_COLOR4   = 4,
17262 	ILC_COLOR8   = 8,
17263 	ILC_COLOR16  = 16,
17264 	ILC_COLOR24  = 24,
17265 	ILC_COLOR32  = 32,
17266 	ILC_COLORDDB = 254,
17267 	ILC_MASK     = 1,
17268 	ILC_PALETTE  = 2048
17269 }
17270 
17271 
17272 alias TBBUTTON*       PTBBUTTON, LPTBBUTTON;
17273 
17274 
17275 enum {
17276 	TB_ENABLEBUTTON          = WM_USER + 1,
17277 	TB_CHECKBUTTON,
17278 	TB_PRESSBUTTON,
17279 	TB_HIDEBUTTON,
17280 	TB_INDETERMINATE, //     = WM_USER + 5,
17281 	TB_ISBUTTONENABLED       = WM_USER + 9,
17282 	TB_ISBUTTONCHECKED,
17283 	TB_ISBUTTONPRESSED,
17284 	TB_ISBUTTONHIDDEN,
17285 	TB_ISBUTTONINDETERMINATE, // = WM_USER + 13,
17286 	TB_SETSTATE              = WM_USER + 17,
17287 	TB_GETSTATE              = WM_USER + 18,
17288 	TB_ADDBITMAP             = WM_USER + 19,
17289 	TB_DELETEBUTTON          = WM_USER + 22,
17290 	TB_GETBUTTON,
17291 	TB_BUTTONCOUNT,
17292 	TB_COMMANDTOINDEX,
17293 	TB_SAVERESTOREA,
17294 	TB_CUSTOMIZE,
17295 	TB_ADDSTRINGA,
17296 	TB_GETITEMRECT,
17297 	TB_BUTTONSTRUCTSIZE,
17298 	TB_SETBUTTONSIZE,
17299 	TB_SETBITMAPSIZE,
17300 	TB_AUTOSIZE, //          = WM_USER + 33,
17301 	TB_GETTOOLTIPS           = WM_USER + 35,
17302 	TB_SETTOOLTIPS           = WM_USER + 36,
17303 	TB_SETPARENT             = WM_USER + 37,
17304 	TB_SETROWS               = WM_USER + 39,
17305 	TB_GETROWS,
17306 	TB_GETBITMAPFLAGS,
17307 	TB_SETCMDID,
17308 	TB_CHANGEBITMAP,
17309 	TB_GETBITMAP,
17310 	TB_GETBUTTONTEXTA,
17311 	TB_REPLACEBITMAP, //     = WM_USER + 46,
17312 	TB_GETBUTTONSIZE         = WM_USER + 58,
17313 	TB_SETBUTTONWIDTH        = WM_USER + 59,
17314 	TB_GETBUTTONTEXTW        = WM_USER + 75,
17315 	TB_SAVERESTOREW          = WM_USER + 76,
17316 	TB_ADDSTRINGW            = WM_USER + 77,
17317 }
17318 
17319 extern(Windows)
17320 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM);
17321 
17322 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC;
17323 
17324 
17325 	enum {
17326 		TB_SETINDENT = WM_USER + 47,
17327 		TB_SETIMAGELIST,
17328 		TB_GETIMAGELIST,
17329 		TB_LOADIMAGES,
17330 		TB_GETRECT,
17331 		TB_SETHOTIMAGELIST,
17332 		TB_GETHOTIMAGELIST,
17333 		TB_SETDISABLEDIMAGELIST,
17334 		TB_GETDISABLEDIMAGELIST,
17335 		TB_SETSTYLE,
17336 		TB_GETSTYLE,
17337 		//TB_GETBUTTONSIZE,
17338 		//TB_SETBUTTONWIDTH,
17339 		TB_SETMAXTEXTROWS,
17340 		TB_GETTEXTROWS // = WM_USER + 61
17341 	}
17342 
17343 enum {
17344 	CCM_FIRST            = 0x2000,
17345 	CCM_LAST             = CCM_FIRST + 0x200,
17346 	CCM_SETBKCOLOR       = 8193,
17347 	CCM_SETCOLORSCHEME   = 8194,
17348 	CCM_GETCOLORSCHEME   = 8195,
17349 	CCM_GETDROPTARGET    = 8196,
17350 	CCM_SETUNICODEFORMAT = 8197,
17351 	CCM_GETUNICODEFORMAT = 8198,
17352 	CCM_SETVERSION       = 0x2007,
17353 	CCM_GETVERSION       = 0x2008,
17354 	CCM_SETNOTIFYWINDOW  = 0x2009
17355 }
17356 
17357 
17358 enum {
17359 	PBM_SETRANGE     = WM_USER + 1,
17360 	PBM_SETPOS,
17361 	PBM_DELTAPOS,
17362 	PBM_SETSTEP,
17363 	PBM_STEPIT,   // = WM_USER + 5
17364 	PBM_SETRANGE32   = 1030,
17365 	PBM_GETRANGE,
17366 	PBM_GETPOS,
17367 	PBM_SETBARCOLOR, // = 1033
17368 	PBM_SETBKCOLOR   = CCM_SETBKCOLOR
17369 }
17370 
17371 enum {
17372 	PBS_SMOOTH   = 1,
17373 	PBS_VERTICAL = 4
17374 }
17375 
17376 enum {
17377         ICC_LISTVIEW_CLASSES = 1,
17378         ICC_TREEVIEW_CLASSES = 2,
17379         ICC_BAR_CLASSES      = 4,
17380         ICC_TAB_CLASSES      = 8,
17381         ICC_UPDOWN_CLASS     = 16,
17382         ICC_PROGRESS_CLASS   = 32,
17383         ICC_HOTKEY_CLASS     = 64,
17384         ICC_ANIMATE_CLASS    = 128,
17385         ICC_WIN95_CLASSES    = 255,
17386         ICC_DATE_CLASSES     = 256,
17387         ICC_USEREX_CLASSES   = 512,
17388         ICC_COOL_CLASSES     = 1024,
17389 	ICC_STANDARD_CLASSES = 0x00004000,
17390 }
17391 
17392 	enum WM_USER = 1024;
17393 }
17394 
17395 version(win32_widgets)
17396 	pragma(lib, "comdlg32");
17397 
17398 
17399 ///
17400 enum GenericIcons : ushort {
17401 	None, ///
17402 	// these happen to match the win32 std icons numerically if you just subtract one from the value
17403 	Cut, ///
17404 	Copy, ///
17405 	Paste, ///
17406 	Undo, ///
17407 	Redo, ///
17408 	Delete, ///
17409 	New, ///
17410 	Open, ///
17411 	Save, ///
17412 	PrintPreview, ///
17413 	Properties, ///
17414 	Help, ///
17415 	Find, ///
17416 	Replace, ///
17417 	Print, ///
17418 }
17419 
17420 enum FileDialogType {
17421 	Automatic,
17422 	Open,
17423 	Save
17424 }
17425 
17426 /++
17427 	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.
17428 +/
17429 string previousFileReferenced;
17430 
17431 /++
17432 	Used in automatic menu functions to indicate that the user should be able to browse for a file.
17433 
17434 	Params:
17435 		storage = an alias to a `static string` variable that stores the last file referenced. It will
17436 		use this to pre-fill the dialog with a suggestion.
17437 
17438 		Please note that it MUST be `static` or you will get compile errors.
17439 
17440 		filters = the filters param to [getFileName]
17441 
17442 		type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will
17443 		guess based on the function name. If it has the word "Save" or "Export" in it, it will show
17444 		a save dialog box. Otherwise, it will show an open dialog box.
17445 +/
17446 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) {
17447 	string name;
17448 	alias name this;
17449 
17450 	@implicit this(string name) {
17451 		this.name = name;
17452 	}
17453 }
17454 
17455 /++
17456 	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.
17457 
17458 	History:
17459 		onCancel was added November 6, 2021.
17460 
17461 		The dialog itself on Linux was modified on December 2, 2021 to include
17462 		a directory picker in addition to the command line completion view.
17463 
17464 		The `initialDirectory` argument was added November 9, 2022 (dub v10.10)
17465 
17466 		The `owner` argument was added September 29, 2024. The overloads without this argument are likely to be deprecated in the next major version.
17467 	Future_directions:
17468 		I want to add some kind of custom preview and maybe thumbnail thing in the future,
17469 		at least on Linux, maybe on Windows too.
17470 +/
17471 void getOpenFileName(
17472 	Window owner,
17473 	void delegate(string) onOK,
17474 	string prefilledName = null,
17475 	string[] filters = null,
17476 	void delegate() onCancel = null,
17477 	string initialDirectory = null,
17478 )
17479 {
17480 	return getFileName(owner, true, onOK, prefilledName, filters, onCancel, initialDirectory);
17481 }
17482 
17483 /// ditto
17484 void getSaveFileName(
17485 	Window owner,
17486 	void delegate(string) onOK,
17487 	string prefilledName = null,
17488 	string[] filters = null,
17489 	void delegate() onCancel = null,
17490 	string initialDirectory = null,
17491 )
17492 {
17493 	return getFileName(owner, false, onOK, prefilledName, filters, onCancel, initialDirectory);
17494 }
17495 
17496 /// ditto
17497 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.")
17498 void getOpenFileName(
17499 	void delegate(string) onOK,
17500 	string prefilledName = null,
17501 	string[] filters = null,
17502 	void delegate() onCancel = null,
17503 	string initialDirectory = null,
17504 )
17505 {
17506 	return getFileName(null, true, onOK, prefilledName, filters, onCancel, initialDirectory);
17507 }
17508 
17509 /// ditto
17510 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.")
17511 void getSaveFileName(
17512 	void delegate(string) onOK,
17513 	string prefilledName = null,
17514 	string[] filters = null,
17515 	void delegate() onCancel = null,
17516 	string initialDirectory = null,
17517 )
17518 {
17519 	return getFileName(null, false, onOK, prefilledName, filters, onCancel, initialDirectory);
17520 }
17521 
17522 /++
17523 	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.
17524 
17525 	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.
17526 
17527 	History:
17528 		Added January 1, 2025
17529 +/
17530 class FileDialogDelegate {
17531 
17532 	/++
17533 
17534 	+/
17535 	static abstract class PreviewWidget : Widget {
17536 		/// Call this from your subclass' constructor
17537 		this(Widget parent) {
17538 			super(parent);
17539 		}
17540 
17541 		/// Load the file given to you and show its preview inside the widget here
17542 		abstract void previewFile(string filename);
17543 	}
17544 
17545 	/++
17546 		Override this to add preview capabilities to the dialog for certain files.
17547 	+/
17548 	protected PreviewWidget makePreviewWidget(Widget parent) {
17549 		return null;
17550 	}
17551 
17552 	/++
17553 		Override this to change the dialog entirely.
17554 
17555 		This function IS allowed to block, but is NOT required to.
17556 	+/
17557 	protected void getFileName(
17558 		Window owner,
17559 		bool openOrSave, // true if open, false if save
17560 		void delegate(string) onOK,
17561 		string prefilledName,
17562 		string[] filters, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
17563 		void delegate() onCancel,
17564 		string initialDirectory,
17565 	)
17566 	{
17567 
17568 		version(win32_widgets) {
17569 			import core.sys.windows.commdlg;
17570 		/*
17571 		Ofn.lStructSize = sizeof(OPENFILENAME);
17572 		Ofn.hwndOwner = hWnd;
17573 		Ofn.lpstrFilter = szFilter;
17574 		Ofn.lpstrFile= szFile;
17575 		Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile);
17576 		Ofn.lpstrFileTitle = szFileTitle;
17577 		Ofn.nMaxFileTitle = sizeof(szFileTitle);
17578 		Ofn.lpstrInitialDir = (LPSTR)NULL;
17579 		Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT;
17580 		Ofn.lpstrTitle = szTitle;
17581 		 */
17582 
17583 
17584 			wchar[1024] file = 0;
17585 			wchar[1024] filterBuffer = 0;
17586 			makeWindowsString(prefilledName, file[]);
17587 			OPENFILENAME ofn;
17588 			ofn.lStructSize = ofn.sizeof;
17589 			ofn.hwndOwner = owner is null ? null : owner.win.hwnd;
17590 			if(filters.length) {
17591 				string filter;
17592 				foreach(i, f; filters) {
17593 					filter ~= f;
17594 					filter ~= "\0";
17595 				}
17596 				filter ~= "\0";
17597 				ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr;
17598 			}
17599 			ofn.lpstrFile = file.ptr;
17600 			ofn.nMaxFile = file.length;
17601 
17602 			wchar[1024] initialDir = 0;
17603 			if(initialDirectory !is null) {
17604 				makeWindowsString(initialDirectory, initialDir[]);
17605 				ofn.lpstrInitialDir = file.ptr;
17606 			}
17607 
17608 			if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn))
17609 			{
17610 				string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile);
17611 				if(okString.length && okString[$-1] == '\0')
17612 					okString = okString[0..$-1];
17613 				onOK(okString);
17614 			} else {
17615 				if(onCancel)
17616 					onCancel();
17617 			}
17618 		} else version(custom_widgets) {
17619 			filters ~= ["All Files\0*.*"];
17620 			auto picker = new FilePicker(false, openOrSave, prefilledName, filters, initialDirectory, owner);
17621 			picker.onOK = onOK;
17622 			picker.onCancel = onCancel;
17623 			picker.show();
17624 		}
17625 	}
17626 }
17627 
17628 /// ditto
17629 FileDialogDelegate fileDialogDelegate() {
17630 	if(fileDialogDelegate_ is null)
17631 		fileDialogDelegate_ = new FileDialogDelegate();
17632 	return fileDialogDelegate_;
17633 }
17634 
17635 /// ditto
17636 void fileDialogDelegate(FileDialogDelegate replacement) {
17637 	fileDialogDelegate_ = replacement;
17638 }
17639 
17640 private FileDialogDelegate fileDialogDelegate_;
17641 
17642 struct FileNameFilter {
17643 	string description;
17644 	string[] globPatterns;
17645 
17646 	string toString() {
17647 		string ret;
17648 		ret ~= description;
17649 		ret ~= " (";
17650 		foreach(idx, pattern; globPatterns) {
17651 			if(idx)
17652 				ret ~= "; ";
17653 			ret ~= pattern;
17654 		}
17655 		ret ~= ")";
17656 
17657 		return ret;
17658 	}
17659 
17660 	static FileNameFilter fromString(string s) {
17661 		size_t end = s.length;
17662 		size_t start = 0;
17663 		foreach_reverse(idx, ch; s) {
17664 			if(ch == ')' && end == s.length)
17665 				end = idx;
17666 			else if(ch == '(' && end != s.length) {
17667 				start = idx + 1;
17668 				break;
17669 			}
17670 		}
17671 
17672 		FileNameFilter fnf;
17673 		fnf.description = s[0 .. start ? start - 1 : 0];
17674 		size_t globStart = 0;
17675 		s = s[start .. end];
17676 		foreach(idx, ch; s)
17677 			if(ch == ';') {
17678 				auto ptn = stripInternal(s[globStart .. idx]);
17679 				if(ptn.length)
17680 					fnf.globPatterns ~= ptn;
17681 				globStart = idx + 1;
17682 
17683 			}
17684 		auto ptn = stripInternal(s[globStart .. $]);
17685 		if(ptn.length)
17686 			fnf.globPatterns ~= ptn;
17687 		return fnf;
17688 	}
17689 }
17690 
17691 struct FileNameFilterSet {
17692 	FileNameFilter[] filters;
17693 
17694 	static FileNameFilterSet fromWindowsFileNameFilterDescription(string[] filters) {
17695 		FileNameFilter[] ret;
17696 
17697 		foreach(filter; filters) {
17698 			FileNameFilter fnf;
17699 			size_t filterStartPoint;
17700 			foreach(idx, ch; filter) {
17701 				if(ch == 0) {
17702 					fnf.description = filter[0 .. idx];
17703 					filterStartPoint = idx + 1;
17704 				} else if(filterStartPoint && ch == ';') {
17705 					fnf.globPatterns ~= filter[filterStartPoint .. idx];
17706 					filterStartPoint = idx + 1;
17707 				}
17708 			}
17709 			fnf.globPatterns ~= filter[filterStartPoint .. $];
17710 
17711 			ret ~= fnf;
17712 		}
17713 
17714 		return FileNameFilterSet(ret);
17715 	}
17716 }
17717 
17718 void getFileName(
17719 	Window owner,
17720 	bool openOrSave,
17721 	void delegate(string) onOK,
17722 	string prefilledName = null,
17723 	string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
17724 	void delegate() onCancel = null,
17725 	string initialDirectory = null,
17726 )
17727 {
17728 	return fileDialogDelegate().getFileName(owner, openOrSave, onOK, prefilledName, filters, onCancel, initialDirectory);
17729 }
17730 
17731 version(custom_widgets)
17732 private
17733 class FilePicker : Dialog {
17734 	void delegate(string) onOK;
17735 	void delegate() onCancel;
17736 	LabeledLineEdit lineEdit;
17737 	bool isOpenDialogInsteadOfSave;
17738 	bool requireExistingFile;
17739 
17740 	static struct HistoryItem {
17741 		string cwd;
17742 		FileNameFilter filters;
17743 	}
17744 	HistoryItem[] historyStack;
17745 	size_t historyStackPosition;
17746 
17747 	void back() {
17748 		if(historyStackPosition) {
17749 			historyStackPosition--;
17750 			currentDirectory = historyStack[historyStackPosition].cwd;
17751 			currentFilter = historyStack[historyStackPosition].filters;
17752 			filesOfType.content = currentFilter.toString();
17753 			loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true);
17754 			lineEdit.focus();
17755 		}
17756 	}
17757 
17758 	void forward() {
17759 		if(historyStackPosition + 1 < historyStack.length) {
17760 			historyStackPosition++;
17761 			currentDirectory = historyStack[historyStackPosition].cwd;
17762 			currentFilter = historyStack[historyStackPosition].filters;
17763 			filesOfType.content = currentFilter.toString();
17764 			loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true);
17765 			lineEdit.focus();
17766 		}
17767 	}
17768 
17769 	void up() {
17770 		currentDirectory = currentDirectory ~ "..";
17771 		loadFiles(currentDirectory, currentFilter);
17772 		lineEdit.focus();
17773 	}
17774 
17775 	void refresh() {
17776 		loadFiles(currentDirectory, currentFilter);
17777 		lineEdit.focus();
17778 	}
17779 
17780 	// returns common prefix
17781 	static struct CommonPrefixInfo {
17782 		string commonPrefix;
17783 		int fileCount;
17784 		string exactMatch;
17785 	}
17786 	CommonPrefixInfo loadFiles(string cwd, FileNameFilter filters, bool comingFromHistory = false) {
17787 
17788 		if(!comingFromHistory) {
17789 			if(historyStack.length) {
17790 				historyStack = historyStack[0 .. historyStackPosition + 1];
17791 				historyStack.assumeSafeAppend();
17792 			}
17793 			historyStack ~= HistoryItem(cwd, filters);
17794 			historyStackPosition = historyStack.length - 1;
17795 		}
17796 
17797 		string[] files;
17798 		string[] dirs;
17799 
17800 		dirs ~= "$HOME";
17801 		dirs ~= "$PWD";
17802 
17803 		string commonPrefix;
17804 		int commonPrefixCount;
17805 		string exactMatch;
17806 
17807 		bool matchesFilter(string name) {
17808 			foreach(filter; filters.globPatterns) {
17809 			if(
17810 				filter.length <= 1 ||
17811 				filter == "*.*" || // we always treat *.* the same as *, but it is a bit different than .*
17812 				(filter[0] == '*' && name.endsWith(filter[1 .. $])) ||
17813 				(filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1]))
17814 			)
17815 			{
17816 				if(name.length > 1 && name[0] == '.')
17817 					if(filter.length == 0 || filter[0] != '.')
17818 						return false;
17819 
17820 				return true;
17821 			}
17822 			}
17823 
17824 			return false;
17825 		}
17826 
17827 		void considerCommonPrefix(string name, bool prefiltered) {
17828 			if(!prefiltered && !matchesFilter(name))
17829 				return;
17830 
17831 			if(commonPrefix is null) {
17832 				commonPrefix = name;
17833 				commonPrefixCount = 1;
17834 				exactMatch = commonPrefix;
17835 			} else {
17836 				foreach(idx, char i; name) {
17837 					if(idx >= commonPrefix.length || i != commonPrefix[idx]) {
17838 						commonPrefix = commonPrefix[0 .. idx];
17839 						commonPrefixCount ++;
17840 						exactMatch = null;
17841 						break;
17842 					}
17843 				}
17844 			}
17845 		}
17846 
17847 		bool applyFilterToDirectories = true;
17848 		bool showDotFiles = false;
17849 		foreach(filter; filters.globPatterns) {
17850 			if(filter == ".*")
17851 				showDotFiles = true;
17852 			else foreach(ch; filter)
17853 				if(ch == '.') {
17854 					// a filter like *.exe should not apply to the directory
17855 					applyFilterToDirectories = false;
17856 					break;
17857 				}
17858 		}
17859 
17860 		try
17861 		getFiles(cwd, (string name, bool isDirectory) {
17862 			if(name == ".")
17863 				return; // skip this as unnecessary
17864 			if(isDirectory) {
17865 				if(applyFilterToDirectories) {
17866 					if(matchesFilter(name)) {
17867 						dirs ~= name;
17868 						considerCommonPrefix(name, false);
17869 					}
17870 				} else if(name != ".." && name.length > 1 && name[0] == '.') {
17871 					if(showDotFiles) {
17872 						dirs ~= name;
17873 						considerCommonPrefix(name, false);
17874 					}
17875 				} else {
17876 					dirs ~= name;
17877 					considerCommonPrefix(name, false);
17878 				}
17879 			} else {
17880 				if(matchesFilter(name)) {
17881 					files ~= name;
17882 
17883 					//if(filter.length > 0 && filter[$-1] == '*') {
17884 						considerCommonPrefix(name, true);
17885 					//}
17886 				}
17887 			}
17888 		});
17889 		catch(ArsdExceptionBase e) {
17890 			messageBox("Unable to read requested directory");
17891 			// FIXME: give them a chance to create it? or at least go back?
17892 			/+
17893 			comingFromHistory = true;
17894 			back();
17895 			return null;
17896 			+/
17897 		}
17898 
17899 		extern(C) static int comparator(scope const void* a, scope const void* b) {
17900 			auto sa = *cast(string*) a;
17901 			auto sb = *cast(string*) b;
17902 
17903 			/+
17904 				Goal here:
17905 
17906 				Dot first. This puts `foo.d` before `foo2.d`
17907 				Then numbers , natural sort order (so 9 comes before 10) for positive numbers
17908 				Then letters, in order Aa, Bb, Cc
17909 				Then other symbols in ascii order
17910 			+/
17911 			static int nextPiece(ref string whole) {
17912 				if(whole.length == 0)
17913 					return -1;
17914 
17915 				enum specialZoneSize = 1;
17916 				string originalString = whole;
17917 				bool fallback;
17918 
17919 				start_over:
17920 
17921 				char current = whole[0];
17922 				if(!fallback && current >= '0' && current <= '9') {
17923 					// if this overflows, it can mess up the sort, so it will fallback to string sort in that event
17924 					int accumulator;
17925 					do {
17926 						auto before = accumulator;
17927 						whole = whole[1 .. $];
17928 						accumulator *= 10;
17929 						accumulator += current - '0';
17930 						current = whole.length ? whole[0] : 0;
17931 						if(accumulator < before) {
17932 							fallback = true;
17933 							whole = originalString;
17934 							goto start_over;
17935 						}
17936 					} while (current >= '0' && current <= '9');
17937 
17938 					return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols
17939 				} else {
17940 					whole = whole[1 .. $];
17941 
17942 					if(current == '.')
17943 						return 0; // the special case to put it before numbers
17944 
17945 					// anything above should be < specialZoneSize
17946 
17947 					int letterZoneSize = 26 * 2;
17948 					int base = int.max - letterZoneSize - char.max; // leaves space at end for symbols too if we want them after chars
17949 
17950 					if(current >= 'A' && current <= 'Z')
17951 						return base + (current - 'A') * 2;
17952 					if(current >= 'a' && current <= 'z')
17953 						return base + (current - 'a') * 2 + 1;
17954 					// return base + letterZoneSize + current; // would put symbols after numbers and letters
17955 					return specialZoneSize + current; // puts symbols before numbers and letters, but after the special zone
17956 				}
17957 			}
17958 
17959 			while(sa.length || sb.length) {
17960 				auto pa = nextPiece(sa);
17961 				auto pb = nextPiece(sb);
17962 
17963 				auto diff = pa - pb;
17964 				if(diff)
17965 					return diff;
17966 			}
17967 
17968 			return 0;
17969 		}
17970 
17971 		nonPhobosSort(files, &comparator);
17972 		nonPhobosSort(dirs, &comparator);
17973 
17974 		listWidget.clear();
17975 		dirWidget.clear();
17976 		foreach(name; dirs)
17977 			dirWidget.addOption(name);
17978 		foreach(name; files)
17979 			listWidget.addOption(name);
17980 
17981 		return CommonPrefixInfo(commonPrefix, commonPrefixCount, exactMatch);
17982 	}
17983 
17984 	ListWidget listWidget;
17985 	ListWidget dirWidget;
17986 
17987 	FreeEntrySelection filesOfType;
17988 	LineEdit directoryHolder;
17989 
17990 	string currentDirectory_;
17991 	FileNameFilter currentNonTabFilter;
17992 	FileNameFilter currentFilter;
17993 	FileNameFilterSet filterOptions;
17994 
17995 	void currentDirectory(string s) {
17996 		currentDirectory_ = FilePath(s).makeAbsolute(getCurrentWorkingDirectory()).toString();
17997 		directoryHolder.content = currentDirectory_;
17998 	}
17999 	string currentDirectory() {
18000 		return currentDirectory_;
18001 	}
18002 
18003 	private string getUserHomeDir() {
18004 		import core.stdc.stdlib;
18005 		version(Windows)
18006 			return (stringz(getenv("HOMEDRIVE")).borrow ~ stringz(getenv("HOMEPATH")).borrow).idup;
18007 		else
18008 			return (stringz(getenv("HOME")).borrow).idup;
18009 	}
18010 
18011 	private string expandTilde(string s) {
18012 		// FIXME: cannot look up other user dirs
18013 		if(s.length == 1 && s == "~")
18014 			return getUserHomeDir();
18015 		if(s.length > 1 && s[0] == '~' && s[1] == '/')
18016 			return getUserHomeDir() ~ s[1 .. $];
18017 		return s;
18018 	}
18019 
18020 	// FIXME: allow many files to be picked too sometimes
18021 
18022 	//string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
18023 	this(bool requireExistingFile, bool isOpenDialogInsteadOfSave, string prefilledName, string[] filtersInWindowsFormat, string initialDirectory, Window owner = null) {
18024 		this.filterOptions = FileNameFilterSet.fromWindowsFileNameFilterDescription(filtersInWindowsFormat);
18025 		this.isOpenDialogInsteadOfSave = isOpenDialogInsteadOfSave;
18026 		this.requireExistingFile = requireExistingFile;
18027 		super(owner, 500, 400, "Choose File..."); // owner);
18028 
18029 		{
18030 			auto navbar = new HorizontalLayout(24, this);
18031 			auto backButton = new ToolButton(new Action("<", 0, &this.back), navbar);
18032 			auto forwardButton = new ToolButton(new Action(">", 0, &this.forward), navbar);
18033 			auto upButton = new ToolButton(new Action("^", 0, &this.up), navbar); // hmm with .. in the dir list we don't really need an up button
18034 
18035 			directoryHolder = new LineEdit(navbar);
18036 
18037 			directoryHolder.addEventListener(delegate(scope KeyDownEvent kde) {
18038 				if(kde.key == Key.Enter || kde.key == Key.PadEnter) {
18039 					kde.stopPropagation();
18040 
18041 					currentDirectory = directoryHolder.content;
18042 					loadFiles(currentDirectory, currentFilter);
18043 
18044 					lineEdit.focus();
18045 				}
18046 			});
18047 
18048 			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.
18049 
18050 			/+
18051 			auto newDirectoryButton = new ToolButton(new Action("N"), navbar);
18052 
18053 			// FIXME: make sure putting `.` in the dir filter goes back to the CWD
18054 			// and that ~ goes back to the home dir
18055 			// and blanking it goes back to the suggested dir
18056 
18057 			auto homeButton = new ToolButton(new Action("H"), navbar);
18058 			auto cwdButton = new ToolButton(new Action("."), navbar);
18059 			auto suggestedDirectoryButton = new ToolButton(new Action("*"), navbar);
18060 			+/
18061 
18062 			filesOfType = new class FreeEntrySelection {
18063 				this() {
18064 					string[] opt;
18065 					foreach(option; filterOptions.filters)
18066 						opt ~=  option.toString;
18067 					super(opt, navbar);
18068 				}
18069 				override int flexBasisWidth() {
18070 					return scaleWithDpi(150);
18071 				}
18072 				override int widthStretchiness() {
18073 					return 1;//super.widthStretchiness() / 2;
18074 				}
18075 			};
18076 			filesOfType.setSelection(0);
18077 			currentFilter = filterOptions.filters[0];
18078 			currentNonTabFilter = currentFilter;
18079 		}
18080 
18081 		{
18082 			auto mainGrid = new GridLayout(4, 1, this);
18083 
18084 			dirWidget = new ListWidget(mainGrid);
18085 			listWidget = new ListWidget(mainGrid);
18086 			listWidget.tabStop = false;
18087 			dirWidget.tabStop = false;
18088 
18089 			FileDialogDelegate.PreviewWidget previewWidget = fileDialogDelegate.makePreviewWidget(mainGrid);
18090 
18091 			mainGrid.setChildPosition(dirWidget, 0, 0, 1, 1);
18092 			mainGrid.setChildPosition(listWidget, 1, 0, previewWidget !is null ? 2 : 3, 1);
18093 			if(previewWidget)
18094 				mainGrid.setChildPosition(previewWidget, 2, 0, 1, 1);
18095 
18096 			// double click events normally trigger something else but
18097 			// here user might be clicking kinda fast and we'd rather just
18098 			// keep it
18099 			dirWidget.addEventListener((scope DoubleClickEvent dev) {
18100 				auto ce = new ChangeEvent!void(dirWidget, () {});
18101 				ce.dispatch();
18102 				lineEdit.focus();
18103 			});
18104 
18105 			dirWidget.addEventListener((scope ChangeEvent!void sce) {
18106 				string v;
18107 				foreach(o; dirWidget.options)
18108 					if(o.selected) {
18109 						v = o.label;
18110 						break;
18111 					}
18112 				if(v.length) {
18113 					if(v == "$HOME")
18114 						currentDirectory = getUserHomeDir();
18115 					else if(v == "$PWD")
18116 						currentDirectory = ".";
18117 					else
18118 						currentDirectory = currentDirectory ~ "/" ~ v;
18119 					loadFiles(currentDirectory, currentFilter);
18120 				}
18121 
18122 				dirWidget.focusOn = -1;
18123 				lineEdit.focus();
18124 			});
18125 
18126 			// double click here, on the other hand, selects the file
18127 			// and moves on
18128 			listWidget.addEventListener((scope DoubleClickEvent dev) {
18129 				OK();
18130 			});
18131 		}
18132 
18133 		lineEdit = new LabeledLineEdit("File name:", TextAlignment.Right, this);
18134 		lineEdit.focus();
18135 		lineEdit.addEventListener(delegate(CharEvent event) {
18136 			if(event.character == '\t' || event.character == '\n')
18137 				event.preventDefault();
18138 		});
18139 
18140 		listWidget.addEventListener(EventType.change, () {
18141 			foreach(o; listWidget.options)
18142 				if(o.selected)
18143 					lineEdit.content = o.label;
18144 		});
18145 
18146 		currentDirectory = initialDirectory is null ? "." : initialDirectory;
18147 
18148 		auto prefilledPath = FilePath(expandTilde(prefilledName)).makeAbsolute(FilePath(currentDirectory));
18149 		currentDirectory = prefilledPath.directoryName;
18150 		prefilledName = prefilledPath.filename;
18151 		loadFiles(currentDirectory, currentFilter);
18152 
18153 		filesOfType.addEventListener(delegate (FreeEntrySelection.SelectionChangedEvent ce) {
18154 			currentFilter = FileNameFilter.fromString(ce.stringValue);
18155 			currentNonTabFilter = currentFilter;
18156 			loadFiles(currentDirectory, currentFilter);
18157 			// lineEdit.focus(); // this causes a recursive crash.....
18158 		});
18159 
18160 		filesOfType.addEventListener(delegate(KeyDownEvent event) {
18161 			if(event.key == Key.Enter) {
18162 				currentFilter = FileNameFilter.fromString(filesOfType.content);
18163 				currentNonTabFilter = currentFilter;
18164 				loadFiles(currentDirectory, currentFilter);
18165 				event.stopPropagation();
18166 				// FIXME: refocus on the line edit
18167 			}
18168 		});
18169 
18170 		lineEdit.addEventListener((KeyDownEvent event) {
18171 			if(event.key == Key.Tab && !event.ctrlKey && !event.shiftKey) {
18172 
18173 				auto path = FilePath(expandTilde(lineEdit.content)).makeAbsolute(FilePath(currentDirectory));
18174 				currentDirectory = path.directoryName;
18175 				auto current = path.filename;
18176 
18177 				auto newFilter = current;
18178 				if(current.length && current[0] != '*' && current[$-1] != '*')
18179 					newFilter ~= "*";
18180 				else if(newFilter.length == 0)
18181 					newFilter = "*";
18182 
18183 				auto newFilterObj = FileNameFilter("Custom filter", [newFilter]);
18184 
18185 				CommonPrefixInfo commonPrefix = loadFiles(currentDirectory, newFilterObj);
18186 				if(commonPrefix.fileCount == 1) {
18187 					// exactly one file, let's see what it is
18188 					auto specificFile = FilePath(commonPrefix.exactMatch).makeAbsolute(FilePath(currentDirectory));
18189 					if(getFileType(specificFile.toString) == FileType.dir) {
18190 						// a directory means we should change to it and keep the old filter
18191 						currentDirectory = specificFile.toString();
18192 						lineEdit.content = specificFile.toString() ~ "/";
18193 						loadFiles(currentDirectory, currentFilter);
18194 					} else {
18195 						// any other file should be selected in the list
18196 						currentDirectory = specificFile.directoryName;
18197 						current = specificFile.filename;
18198 						lineEdit.content = current;
18199 						loadFiles(currentDirectory, currentFilter);
18200 					}
18201 				} else if(commonPrefix.fileCount > 1) {
18202 					currentFilter = newFilterObj;
18203 					filesOfType.content = currentFilter.toString();
18204 					lineEdit.content = commonPrefix.commonPrefix;
18205 				} else {
18206 					// if there were no files, we don't really want to change the filter..
18207 					//sdpyPrintDebugString("no files");
18208 				}
18209 
18210 				// FIXME: if that is a directory, add the slash? or even go inside?
18211 
18212 				event.preventDefault();
18213 			}
18214 			else if(event.key == Key.Left && event.altKey) {
18215 				this.back();
18216 				event.preventDefault();
18217 			}
18218 			else if(event.key == Key.Right && event.altKey) {
18219 				this.forward();
18220 				event.preventDefault();
18221 			}
18222 		});
18223 
18224 
18225 		lineEdit.content = prefilledName;
18226 
18227 		auto hl = new HorizontalLayout(60, this);
18228 		auto cancelButton = new Button("Cancel", hl);
18229 		auto okButton = new Button(isOpenDialogInsteadOfSave ? "Open" : "Save"/*"OK"*/, hl);
18230 
18231 		cancelButton.addEventListener(EventType.triggered, &Cancel);
18232 		okButton.addEventListener(EventType.triggered, &OK);
18233 
18234 		this.addEventListener((KeyDownEvent event) {
18235 			if(event.key == Key.Enter || event.key == Key.PadEnter) {
18236 				event.preventDefault();
18237 				OK();
18238 			}
18239 			else if(event.key == Key.Escape)
18240 				Cancel();
18241 			else if(event.key == Key.F5)
18242 				refresh();
18243 			else if(event.key == Key.Up && event.altKey)
18244 				up(); // ditto
18245 			else if(event.key == Key.Left && event.altKey)
18246 				back(); // FIXME: it sends the key to the line edit too
18247 			else if(event.key == Key.Right && event.altKey)
18248 				forward(); // ditto
18249 			else if(event.key == Key.Up)
18250 				listWidget.setSelection(listWidget.getSelection() - 1);
18251 			else if(event.key == Key.Down)
18252 				listWidget.setSelection(listWidget.getSelection() + 1);
18253 		});
18254 
18255 		// FIXME: set the list view's focusOn to -1 on most interactions so it doesn't keep a thing highlighted
18256 		// FIXME: button to create new directory
18257 		// FIXME: show dirs in the files list too? idk.
18258 
18259 		// FIXME: support ~ as alias for home in the input
18260 		// FIXME: tab complete ought to be able to change+complete dir too
18261 	}
18262 
18263 	override void OK() {
18264 		if(lineEdit.content.length) {
18265 			auto c = expandTilde(lineEdit.content);
18266 
18267 			FilePath accepted = FilePath(c).makeAbsolute(FilePath(currentDirectory));
18268 
18269 			auto ft = getFileType(accepted.toString);
18270 
18271 			if(ft == FileType.error && requireExistingFile) {
18272 				// FIXME: tell the user why
18273 				messageBox("Cannot open file: " ~ accepted.toString ~ "\nTry another or cancel.");
18274 				lineEdit.focus();
18275 				return;
18276 
18277 			}
18278 
18279 			// FIXME: symlinks to dirs should prolly also get this behavior
18280 			if(ft == FileType.dir) {
18281 				currentDirectory = accepted.toString;
18282 
18283 				currentFilter = currentNonTabFilter;
18284 				filesOfType.content = currentFilter.toString();
18285 
18286 				loadFiles(currentDirectory, currentFilter);
18287 				lineEdit.content = "";
18288 
18289 				lineEdit.focus();
18290 
18291 				return;
18292 			}
18293 
18294 			if(onOK)
18295 				onOK(accepted.toString);
18296 		}
18297 		close();
18298 	}
18299 
18300 	override void Cancel() {
18301 		if(onCancel)
18302 			onCancel();
18303 		close();
18304 	}
18305 }
18306 
18307 private enum FileType {
18308 	error,
18309 	dir,
18310 	other
18311 }
18312 
18313 private FileType getFileType(string name) {
18314 	version(Windows) {
18315 		auto ws = WCharzBuffer(name);
18316 		auto ret = GetFileAttributesW(ws.ptr);
18317 		if(ret == INVALID_FILE_ATTRIBUTES)
18318 			return FileType.error;
18319 		return ((ret & FILE_ATTRIBUTE_DIRECTORY) != 0) ? FileType.dir : FileType.other;
18320 	} else version(Posix) {
18321 		import core.sys.posix.sys.stat;
18322 		stat_t buf;
18323 		auto ret = stat((name ~ '\0').ptr, &buf);
18324 		if(ret == -1)
18325 			return FileType.error;
18326 		// FIXME: what about a symlink to a dir? S_IFLNK then readlink then i believe stat it again.
18327 		return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other;
18328 	} else assert(0, "Not implemented");
18329 }
18330 
18331 /*
18332 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes
18333 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx
18334 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx
18335 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx
18336 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx
18337 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box
18338 http://www.sbin.org/doc/Xlib/chapt_03.html
18339 
18340 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx
18341 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx
18342 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx
18343 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx
18344 */
18345 
18346 
18347 // These are all for setMenuAndToolbarFromAnnotatedCode
18348 /// This item in the menu will be preceded by a separator line
18349 /// Group: generating_from_code
18350 struct separator {}
18351 deprecated("It was misspelled, use separator instead") alias seperator = separator;
18352 /// Program-wide keyboard shortcut to trigger the action
18353 /// Group: generating_from_code
18354 struct accelerator { string keyString; } // FIXME: allow multiple aliases here
18355 /// tells which menu the action will be on
18356 /// Group: generating_from_code
18357 struct menu { string name; }
18358 /// Describes which toolbar section the action appears on
18359 /// Group: generating_from_code
18360 struct toolbar { string groupName; }
18361 ///
18362 /// Group: generating_from_code
18363 struct icon { ushort id; }
18364 ///
18365 /// Group: generating_from_code
18366 struct label { string label; }
18367 ///
18368 /// Group: generating_from_code
18369 struct hotkey { dchar ch; }
18370 ///
18371 /// Group: generating_from_code
18372 struct tip { string tip; }
18373 ///
18374 /// Group: generating_from_code
18375 enum context_menu = menu.init;
18376 /++
18377 	// FIXME: the options should have both a label and a value
18378 
18379 	if label is null, it will try to just stringify value.
18380 
18381 	if type is int or size_t and it returns a string array, we can use the index but this will implicitly not allow custom, even if allowCustom is set.
18382 +/
18383 /// Group: generating_from_code
18384 Choices!T choices(T)(T[] options, bool allowCustom = false, bool allowReordering = true, bool allowDuplicates = true) {
18385 	return Choices!T(() => options, allowCustom, allowReordering, allowDuplicates);
18386 }
18387 /// ditto
18388 Choices!T choices(T)(T[] function() options, bool allowCustom = false, bool allowReordering = true, bool allowDuplicates = true) {
18389 	return Choices!T(options, allowCustom, allowReordering, allowDuplicates);
18390 }
18391 /// ditto
18392 struct Choices(T) {
18393 	///
18394 	T[] function() options; // IMPORTANT: this MUST be function, not delegate, see https://github.com/dlang/dmd/issues/21915
18395 	bool allowCustom = false;
18396 	/// only relevant if attached to an array
18397 	bool allowReordering = true;
18398 	/// ditto
18399 	bool allowDuplicates = true;
18400 	/// makes no sense on a set
18401 	bool requireAll = false;
18402 }
18403 
18404 
18405 /++
18406 	Observes and allows inspection of an object via automatic gui
18407 +/
18408 /// Group: generating_from_code
18409 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) {
18410 	return new ObjectInspectionWindowImpl!(T)(t);
18411 }
18412 
18413 class ObjectInspectionWindow : Window {
18414 	this(int a, int b, string c) {
18415 		super(a, b, c);
18416 	}
18417 
18418 	abstract void readUpdatesFromObject();
18419 }
18420 
18421 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow {
18422 	T t;
18423 	this(T t) {
18424 		this.t = t;
18425 
18426 		super(300, 400, "ObjectInspectionWindow - " ~ T.stringof);
18427 
18428 		foreach(memberName; __traits(derivedMembers, T)) {{
18429 			alias member = I!(__traits(getMember, t, memberName))[0];
18430 			alias type = typeof(member);
18431 			static if(is(type == int)) {
18432 				auto le = new LabeledLineEdit(memberName ~ ": ", this);
18433 				//le.addEventListener("char", (Event ev) {
18434 					//if((ev.character < '0' || ev.character > '9') && ev.character != '-')
18435 						//ev.preventDefault();
18436 				//});
18437 				le.addEventListener(EventType.change, (Event ev) {
18438 					__traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue);
18439 				});
18440 
18441 				updateMemberDelegates[memberName] = () {
18442 					le.content = toInternal!string(__traits(getMember, t, memberName));
18443 				};
18444 			}
18445 		}}
18446 	}
18447 
18448 	void delegate()[string] updateMemberDelegates;
18449 
18450 	override void readUpdatesFromObject() {
18451 		foreach(k, v; updateMemberDelegates)
18452 			v();
18453 	}
18454 }
18455 
18456 /++
18457 	Creates a dialog based on a data structure.
18458 
18459 	---
18460 	dialog(window, (YourStructure value) {
18461 		// the user filled in the struct and clicked OK,
18462 		// you can check the members now
18463 	});
18464 	---
18465 
18466 	Params:
18467 		initialData = the initial value to show in the dialog. It will not modify this unless
18468 		it is a class then it might, no promises.
18469 
18470 	History:
18471 		The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5)
18472 
18473 		The overloads with `parent` were added September 29, 2024. The ones without it are likely to
18474 		be deprecated soon.
18475 +/
18476 /// Group: generating_from_code
18477 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.")
18478 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18479 	dialog(null, T.init, onOK, onCancel, title);
18480 }
18481 /// ditto
18482 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.")
18483 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18484 	dialog(null, T.init, onOK, onCancel, title);
18485 }
18486 /// ditto
18487 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18488 	dialog(parent, T.init, onOK, onCancel, title);
18489 }
18490 /// ditto
18491 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.")
18492 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18493 	dialog(parent, initialData, onOK, onCancel, title);
18494 }
18495 /// ditto
18496 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18497 	auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title);
18498 	dg.show();
18499 }
18500 
18501 private static template I(T...) { alias I = T; }
18502 
18503 
18504 private string beautify(string name, char space = ' ', bool allLowerCase = false) {
18505 	if(name == "id")
18506 		return allLowerCase ? name : "ID";
18507 
18508 	char[160] buffer;
18509 	int bufferIndex = 0;
18510 	bool shouldCap = true;
18511 	bool shouldSpace;
18512 	bool lastWasCap;
18513 	foreach(idx, char ch; name) {
18514 		if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
18515 
18516 		if((ch >= 'A' && ch <= 'Z') || ch == '_') {
18517 			if(lastWasCap) {
18518 				// two caps in a row, don't change. Prolly acronym.
18519 			} else {
18520 				if(idx)
18521 					shouldSpace = true; // new word, add space
18522 			}
18523 
18524 			lastWasCap = true;
18525 		} else {
18526 			lastWasCap = false;
18527 		}
18528 
18529 		if(shouldSpace) {
18530 			buffer[bufferIndex++] = space;
18531 			if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
18532 			shouldSpace = false;
18533 		}
18534 		if(shouldCap) {
18535 			if(ch >= 'a' && ch <= 'z')
18536 				ch -= 32;
18537 			shouldCap = false;
18538 		}
18539 		if(allLowerCase && ch >= 'A' && ch <= 'Z')
18540 			ch += 32;
18541 		buffer[bufferIndex++] = ch;
18542 	}
18543 	return buffer[0 .. bufferIndex].idup;
18544 }
18545 
18546 /++
18547 	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.
18548 +/
18549 class AutomaticDialog(T) : Dialog {
18550 	T t;
18551 
18552 	void delegate(T) onOK;
18553 	void delegate() onCancel;
18554 
18555 	override int paddingTop() { return defaultLineHeight; }
18556 	override int paddingBottom() { return defaultLineHeight; }
18557 	override int paddingRight() { return defaultLineHeight; }
18558 	override int paddingLeft() { return defaultLineHeight; }
18559 
18560 	this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) {
18561 		assert(onOK !is null);
18562 
18563 		t = initialData;
18564 
18565 		static if(is(T == class)) {
18566 			if(t is null)
18567 				t = new T();
18568 		}
18569 		this.onOK = onOK;
18570 		this.onCancel = onCancel;
18571 		super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title);
18572 
18573 		static if(is(T == class))
18574 			this.addDataControllerWidget(t);
18575 		else
18576 			this.addDataControllerWidget(&t);
18577 
18578 		auto hl = new HorizontalLayout(this);
18579 		auto stretch = new HorizontalSpacer(hl); // to right align
18580 		auto ok = new CommandButton("OK", hl);
18581 		auto cancel = new CommandButton("Cancel", hl);
18582 		ok.addEventListener(EventType.triggered, &OK);
18583 		cancel.addEventListener(EventType.triggered, &Cancel);
18584 
18585 		this.addEventListener((KeyDownEvent ev) {
18586 			if(ev.key == Key.Enter || ev.key == Key.PadEnter) {
18587 				ok.focus();
18588 				OK();
18589 				ev.preventDefault();
18590 			}
18591 			if(ev.key == Key.Escape) {
18592 				Cancel();
18593 				ev.preventDefault();
18594 			}
18595 		});
18596 
18597 		this.addEventListener((scope ClosedEvent ce) {
18598 			if(onCancel)
18599 				onCancel();
18600 		});
18601 
18602 		//this.children[0].focus();
18603 	}
18604 
18605 	override void OK() {
18606 		onOK(t);
18607 		close();
18608 	}
18609 
18610 	override void Cancel() {
18611 		if(onCancel)
18612 			onCancel();
18613 		close();
18614 	}
18615 }
18616 
18617 private template baseClassCount(Class) {
18618 	private int helper() {
18619 		int count = 0;
18620 		static if(is(Class bases == super)) {
18621 			foreach(base; bases)
18622 				static if(is(base == class))
18623 					count += 1 + baseClassCount!base;
18624 		}
18625 		return count;
18626 	}
18627 
18628 	enum int baseClassCount = helper();
18629 }
18630 
18631 private long stringToLong(string s) {
18632 	long ret;
18633 	if(s.length == 0)
18634 		return ret;
18635 	bool negative = s[0] == '-';
18636 	if(negative)
18637 		s = s[1 .. $];
18638 	foreach(ch; s) {
18639 		if(ch >= '0' && ch <= '9') {
18640 			ret *= 10;
18641 			ret += ch - '0';
18642 		}
18643 	}
18644 	if(negative)
18645 		ret = -ret;
18646 	return ret;
18647 }
18648 
18649 
18650 interface ReflectableProperties {
18651 	/++
18652 		Iterates the event's properties as strings. Note that keys may be repeated and a get property request may
18653 		call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by
18654 		json in the current implementation.
18655 
18656 		This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have
18657 		properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself
18658 		as of the June 2, 2021 release.
18659 
18660 		History:
18661 			Added June 2, 2021.
18662 
18663 		See_Also: [getPropertyAsString], [setPropertyFromString]
18664 	+/
18665 	void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow;
18666 	/++
18667 		Requests a property to be delivered to you as a string, through your `sink` delegate.
18668 
18669 		If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should
18670 		be interpreted as json, otherwise, it is just a plain string.
18671 
18672 		The sink should always be called exactly once for each call (it is basically a return value, but it might
18673 		use a local buffer it maintains instead of allocating a return value).
18674 
18675 		History:
18676 			Added June 2, 2021.
18677 
18678 		See_Also: [getPropertiesList], [setPropertyFromString]
18679 	+/
18680 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink);
18681 	/++
18682 		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.
18683 
18684 		History:
18685 			Added June 2, 2021.
18686 
18687 		See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult]
18688 	+/
18689 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson);
18690 
18691 	/// [setPropertyFromString] possible return values
18692 	enum SetPropertyResult {
18693 		success = 0, /// the property has been successfully set to the request value
18694 		notPermitted = -1, /// the property exists but it cannot be changed at this time
18695 		notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist)
18696 		noSuchProperty = -3, /// there is no property by that name
18697 		wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value
18698 		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)
18699 	}
18700 
18701 	/++
18702 		You can mix this in to get an implementation in child classes. This does [setPropertyFromString].
18703 
18704 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
18705 
18706 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
18707 		rarely need to use these building blocks directly.
18708 	+/
18709 	mixin template RegisterSetters() {
18710 		override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
18711 			switch(name) {
18712 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
18713 					case memberName:
18714 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
18715 							if(value != "true" && value != "false")
18716 								return SetPropertyResult.wrongFormat;
18717 							__traits(getMember, this, memberName) = value == "true" ? true : false;
18718 							return SetPropertyResult.success;
18719 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
18720 							import core.stdc.stdlib;
18721 							char[128] zero = 0;
18722 							if(buffer.length + 1 >= zero.length)
18723 								return SetPropertyResult.wrongFormat;
18724 							zero[0 .. buffer.length] = buffer[];
18725 							__traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10);
18726 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
18727 							import core.stdc.stdlib;
18728 							char[128] zero = 0;
18729 							if(buffer.length + 1 >= zero.length)
18730 								return SetPropertyResult.wrongFormat;
18731 							zero[0 .. buffer.length] = buffer[];
18732 							__traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10);
18733 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
18734 							__traits(getMember, this, memberName) = value.idup;
18735 						} else {
18736 							return SetPropertyResult.notImplemented;
18737 						}
18738 
18739 				}
18740 				default:
18741 					return super.setPropertyFromString(name, value, valueIsJson);
18742 			}
18743 		}
18744 	}
18745 
18746 	/++
18747 		You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList].
18748 
18749 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
18750 
18751 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
18752 		rarely need to use these building blocks directly.
18753 	+/
18754 	mixin template RegisterGetters() {
18755 		override void getPropertiesList(scope void delegate(string name) sink) const {
18756 			super.getPropertiesList(sink);
18757 
18758 			foreach(memberName; __traits(derivedMembers, typeof(this))) {
18759 				sink(memberName);
18760 			}
18761 		}
18762 		override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
18763 			switch(name) {
18764 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
18765 					case memberName:
18766 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
18767 							sink(name, __traits(getMember, this, memberName) ? "true" : "false", true);
18768 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
18769 							import core.stdc.stdio;
18770 							char[32] buffer;
18771 							auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName));
18772 							sink(name, buffer[0 .. len], true);
18773 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
18774 							import core.stdc.stdio;
18775 							char[32] buffer;
18776 							auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName));
18777 							sink(name, buffer[0 .. len], true);
18778 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
18779 							sink(name, __traits(getMember, this, memberName), false);
18780 							//sinkJsonString(memberName, __traits(getMember, this, memberName), sink);
18781 						} else {
18782 							sink(name, null, true);
18783 						}
18784 
18785 					return;
18786 				}
18787 				default:
18788 					return super.getPropertyAsString(name, sink);
18789 			}
18790 		}
18791 	}
18792 }
18793 
18794 private struct Stack(T) {
18795 	this(int maxSize) {
18796 		internalLength = 0;
18797 		arr = initialBuffer[];
18798 	}
18799 
18800 	///.
18801 	void push(T t) {
18802 		if(internalLength >= arr.length) {
18803 			auto oldarr = arr;
18804 			if(arr.length < 4096)
18805 				arr = new T[arr.length * 2];
18806 			else
18807 				arr = new T[arr.length + 4096];
18808 			arr[0 .. oldarr.length] = oldarr[];
18809 		}
18810 
18811 		arr[internalLength] = t;
18812 		internalLength++;
18813 	}
18814 
18815 	///.
18816 	T pop() {
18817 		assert(internalLength);
18818 		internalLength--;
18819 		return arr[internalLength];
18820 	}
18821 
18822 	///.
18823 	T peek() {
18824 		assert(internalLength);
18825 		return arr[internalLength - 1];
18826 	}
18827 
18828 	///.
18829 	@property bool empty() {
18830 		return internalLength ? false : true;
18831 	}
18832 
18833 	///.
18834 	private T[] arr;
18835 	private size_t internalLength;
18836 	private T[64] initialBuffer;
18837 	// 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),
18838 	// using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push()
18839 	// function thanks to this, and push() was actually one of the slowest individual functions in the code!
18840 }
18841 
18842 /// 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.
18843 private struct WidgetStream {
18844 
18845 	///.
18846 	@property Widget front() {
18847 		return current.widget;
18848 	}
18849 
18850 	/// Use Widget.tree instead.
18851 	this(Widget start) {
18852 		current.widget = start;
18853 		current.childPosition = -1;
18854 		isEmpty = false;
18855 		stack = typeof(stack)(0);
18856 	}
18857 
18858 	/*
18859 		Handle it
18860 		handle its children
18861 
18862 	*/
18863 
18864 	///.
18865 	void popFront() {
18866 	    more:
18867 	    	if(isEmpty) return;
18868 
18869 		// FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times)
18870 
18871 		current.childPosition++;
18872 		if(current.childPosition >= current.widget.children.length) {
18873 			if(stack.empty())
18874 				isEmpty = true;
18875 			else {
18876 				current = stack.pop();
18877 				goto more;
18878 			}
18879 		} else {
18880 			stack.push(current);
18881 			current.widget = current.widget.children[current.childPosition];
18882 			current.childPosition = -1;
18883 		}
18884 	}
18885 
18886 	///.
18887 	@property bool empty() {
18888 		return isEmpty;
18889 	}
18890 
18891 	private:
18892 
18893 	struct Current {
18894 		Widget widget;
18895 		int childPosition;
18896 	}
18897 
18898 	Current current;
18899 
18900 	Stack!(Current) stack;
18901 
18902 	bool isEmpty;
18903 }
18904 
18905 
18906 /+
18907 
18908 	I could fix up the hierarchy kinda like this
18909 
18910 	class Widget {
18911 		Widget[] children() { return null; }
18912 	}
18913 	interface WidgetContainer {
18914 		Widget asWidget();
18915 		void addChild(Widget w);
18916 
18917 		// alias asWidget this; // but meh
18918 	}
18919 
18920 	Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead.
18921 
18922 	class Layout : Widget, WidgetContainer {}
18923 
18924 	class Window : WidgetContainer {}
18925 
18926 
18927 	All constructors that previously took Widgets should now take WidgetContainers instead
18928 
18929 
18930 
18931 	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".
18932 +/
18933 
18934 /+
18935 	LAYOUTS 2.0
18936 
18937 	can just be assigned as a function. assigning a new one will cause it to be immediately called.
18938 
18939 	they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one.
18940 
18941 	recomputeChildLayout only really needs a property accessor proxy... just the layout info too.
18942 
18943 	and even Paint can just use computedStyle...
18944 
18945 		background color
18946 		font
18947 		border color and style
18948 
18949 	And actually the style proxy can offer some helper routines to draw these like the draw 3d box
18950 		please note that many widgets and in some modes will completely ignore properties as they will.
18951 		they are just hints you set, not promises.
18952 
18953 
18954 
18955 
18956 
18957 	So generally the existing virtual functions are just the default for the class. But individual objects
18958 	or stylesheets can override this. The virtual ones count as tag-level specificity in css.
18959 +/
18960 
18961 /++
18962 	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.
18963 
18964 	History:
18965 		Added May 24, 2021.
18966 +/
18967 struct WidgetBackground {
18968 	/++
18969 		A background with the given solid color.
18970 	+/
18971 	this(Color color) {
18972 		this.color = color;
18973 	}
18974 
18975 	this(WidgetBackground bg) {
18976 		this = bg;
18977 	}
18978 
18979 	/++
18980 		Creates a widget from the string.
18981 
18982 		Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css.
18983 	+/
18984 	static WidgetBackground fromString(string s) {
18985 		return WidgetBackground(Color.fromString(s));
18986 	}
18987 
18988 	/++
18989 		The background is not necessarily a solid color, but you can always specify a color as a fallback.
18990 
18991 		History:
18992 			Made `public` on December 18, 2022 (dub v10.10).
18993 	+/
18994 	Color color;
18995 }
18996 
18997 /++
18998 	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!)
18999 
19000 	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.
19001 
19002 	You should not inherit from this directly, but instead use [VisualTheme].
19003 
19004 	History:
19005 		Added May 8, 2021
19006 +/
19007 abstract class BaseVisualTheme {
19008 	/// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override.
19009 	abstract void doPaint(Widget widget, WidgetPainter painter);
19010 
19011 	/+
19012 	/// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override.
19013 	abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg);
19014 	+/
19015 
19016 	/++
19017 		Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css,
19018 		where the interpretation of the string varies for each property and may include things like measurement units.
19019 	+/
19020 	abstract string getPropertyString(Widget widget, string propertyName);
19021 
19022 	/++
19023 		Default background color of the window. Widgets also use this to simulate transparency.
19024 
19025 		Probably some shade of grey.
19026 	+/
19027 	abstract Color windowBackgroundColor();
19028 	abstract Color widgetBackgroundColor();
19029 	abstract Color foregroundColor();
19030 	abstract Color lightAccentColor();
19031 	abstract Color darkAccentColor();
19032 
19033 	/++
19034 		Colors used to indicate active selections in lists and text boxes, etc.
19035 	+/
19036 	abstract Color selectionForegroundColor();
19037 	/// ditto
19038 	abstract Color selectionBackgroundColor();
19039 
19040 	deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); }
19041 
19042 	/++
19043 		If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally.
19044 	+/
19045 	abstract OperatingSystemFont defaultFont(int dpi);
19046 
19047 	private OperatingSystemFont[int] defaultFontCache_;
19048 	private OperatingSystemFont defaultFontCached(int dpi) {
19049 		if(dpi !in defaultFontCache_) {
19050 			// FIXME: set this to false if X disconnect or if visual theme changes
19051 			defaultFontCache_[dpi] = defaultFont(dpi);
19052 		}
19053 		return defaultFontCache_[dpi];
19054 	}
19055 }
19056 
19057 /+
19058 	A widget should have:
19059 		classList
19060 		dataset
19061 		attributes
19062 		computedStyles
19063 		state (persistent)
19064 		dynamic state (focused, hover, etc)
19065 +/
19066 
19067 // visualTheme.computedStyle(this).paddingLeft
19068 
19069 
19070 /++
19071 	This is your entry point to create your own visual theme for custom widgets.
19072 
19073 	You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods.
19074 
19075 	Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating.
19076 +/
19077 abstract class VisualTheme(CRTP) : BaseVisualTheme {
19078 	override string getPropertyString(Widget widget, string propertyName) {
19079 		return null;
19080 	}
19081 
19082 	/+
19083 		mixin StyleOverride!Widget
19084 	final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) {
19085 		w.useStyleProperties(dg);
19086 	}
19087 	+/
19088 
19089 	final override void doPaint(Widget widget, WidgetPainter painter) {
19090 		auto derived = cast(CRTP) cast(void*) this;
19091 
19092 		scope void delegate(Widget, WidgetPainter) bestMatch;
19093 		int bestMatchScore;
19094 
19095 		static if(__traits(hasMember, CRTP, "paint"))
19096 		foreach(overload; __traits(getOverloads, CRTP, "paint")) {
19097 			static if(is(typeof(overload) Params == __parameters)) {
19098 				static assert(Params.length == 2);
19099 				static assert(is(Params[0] : Widget));
19100 				static assert(is(Params[1] == WidgetPainter));
19101 				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);
19102 
19103 				alias type = Params[0];
19104 				if(cast(type) widget) {
19105 					auto score = baseClassCount!type;
19106 
19107 					if(score > bestMatchScore) {
19108 						bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload);
19109 						bestMatchScore = score;
19110 					}
19111 				}
19112 			} else static assert(0, "paint should be a method.");
19113 		}
19114 
19115 		if(bestMatch)
19116 			bestMatch(widget, painter);
19117 		else
19118 			widget.paint(painter);
19119 	}
19120 
19121 	deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; }
19122 
19123 	// 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
19124 	// mixin Beautiful95Theme;
19125 	mixin DefaultLightTheme;
19126 
19127 	private static struct Cached {
19128 		// i prolly want to do this
19129 	}
19130 }
19131 
19132 /// ditto
19133 mixin template Beautiful95Theme() {
19134 	override Color windowBackgroundColor() { return Color(212, 212, 212); }
19135 	override Color widgetBackgroundColor() { return Color.white; }
19136 	override Color foregroundColor() { return Color.black; }
19137 	override Color darkAccentColor() { return Color(172, 172, 172); }
19138 	override Color lightAccentColor() { return Color(223, 223, 223); }
19139 	override Color selectionForegroundColor() { return Color.white; }
19140 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
19141 	override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr
19142 }
19143 
19144 /// ditto
19145 mixin template DefaultLightTheme() {
19146 	override Color windowBackgroundColor() { return Color(232, 232, 232); }
19147 	override Color widgetBackgroundColor() { return Color.white; }
19148 	override Color foregroundColor() { return Color.black; }
19149 	override Color darkAccentColor() { return Color(172, 172, 172); }
19150 	override Color lightAccentColor() { return Color(223, 223, 223); }
19151 	override Color selectionForegroundColor() { return Color.white; }
19152 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
19153 	override OperatingSystemFont defaultFont(int dpi) {
19154 		version(Windows)
19155 			return new OperatingSystemFont("Segoe UI");
19156 		else static if(UsingSimpledisplayCocoa) {
19157 			return (new OperatingSystemFont()).loadDefault;
19158 		} else {
19159 			// FIXME: undo xft's scaling so we don't end up double scaled
19160 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
19161 		}
19162 	}
19163 }
19164 
19165 /// ditto
19166 mixin template DefaultDarkTheme() {
19167 	override Color windowBackgroundColor() { return Color(64, 64, 64); }
19168 	override Color widgetBackgroundColor() { return Color.black; }
19169 	override Color foregroundColor() { return Color.white; }
19170 	override Color darkAccentColor() { return Color(20, 20, 20); }
19171 	override Color lightAccentColor() { return Color(80, 80, 80); }
19172 	override Color selectionForegroundColor() { return Color.white; }
19173 	override Color selectionBackgroundColor() { return Color(128, 0, 128); }
19174 	override OperatingSystemFont defaultFont(int dpi) {
19175 		version(Windows)
19176 			return new OperatingSystemFont("Segoe UI", 12);
19177 		else static if(UsingSimpledisplayCocoa) {
19178 			return (new OperatingSystemFont()).loadDefault;
19179 		} else {
19180 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
19181 		}
19182 	}
19183 }
19184 
19185 /// ditto
19186 alias DefaultTheme = DefaultLightTheme;
19187 
19188 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme {
19189 	/+
19190 	OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); }
19191 	Color windowBackgroundColor() { return Color(242, 242, 242); }
19192 	Color darkAccentColor() { return windowBackgroundColor; }
19193 	Color lightAccentColor() { return windowBackgroundColor; }
19194 	+/
19195 }
19196 
19197 /++
19198 	Event fired when an [Observable] variable changes. You will want to add an event listener referencing
19199 	the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });`
19200 
19201 	History:
19202 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
19203 
19204 		Made `final` on January 3, 2025
19205 +/
19206 final class StateChanged(alias field) : Event {
19207 	enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change";
19208 	override bool cancelable() const { return false; }
19209 	this(Widget target, typeof(field) newValue) {
19210 		this.newValue = newValue;
19211 		super(EventString, target);
19212 	}
19213 
19214 	typeof(field) newValue;
19215 }
19216 
19217 /++
19218 	Convenience function to add a `triggered` event listener.
19219 
19220 	Its implementation is simply `w.addEventListener("triggered", dg);`
19221 
19222 	History:
19223 		Added November 27, 2021 (dub v10.4)
19224 +/
19225 void addWhenTriggered(Widget w, void delegate() dg) {
19226 	w.addEventListener("triggered", dg);
19227 }
19228 
19229 /++
19230 	Observable variables can be added to widgets and when they are changed, it fires
19231 	off a [StateChanged] event so you can react to it.
19232 
19233 	It is implemented as a getter and setter property, along with another helper you
19234 	can use to subscribe with is `name_changed`. You can also subscribe to the [StateChanged]
19235 	event through the usual means. Just give the name of the variable. See [StateChanged] for an
19236 	example.
19237 
19238 	To get an `ObservableReference` to the observable, use `&yourname_changed`.
19239 
19240 	History:
19241 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
19242 
19243 		As of March 5, 2025, the changed function now returns an [EventListener] handle, which
19244 		you can use to disconnect the observer.
19245 +/
19246 mixin template Observable(T, string name) {
19247 	private T backing;
19248 
19249 	mixin(q{
19250 		EventListener } ~ name ~ q{_changed (void delegate(T) dg) {
19251 			return this.addEventListener((StateChanged!this_thing ev) {
19252 				dg(ev.newValue);
19253 			});
19254 		}
19255 
19256 		@property T } ~ name ~ q{ () {
19257 			return backing;
19258 		}
19259 
19260 		@property void } ~ name ~ q{ (T t) {
19261 			backing = t;
19262 			auto event = new StateChanged!this_thing(this, t);
19263 			event.dispatch();
19264 		}
19265 	});
19266 
19267 	mixin("private alias this_thing = " ~ name ~ ";");
19268 }
19269 
19270 /// ditto
19271 alias ObservableReference(T) = EventListener delegate(void delegate(T));
19272 
19273 private bool startsWith(string test, string thing) {
19274 	if(test.length < thing.length)
19275 		return false;
19276 	return test[0 .. thing.length] == thing;
19277 }
19278 
19279 private bool endsWith(string test, string thing) {
19280 	if(test.length < thing.length)
19281 		return false;
19282 	return test[$ - thing.length .. $] == thing;
19283 }
19284 
19285 /++
19286 	Context menus can have `@hotkey`, `@label`, `@tip`, `@separator`, and `@icon`
19287 
19288 	Note they can NOT have accelerators or toolbars; those annotations will be ignored.
19289 
19290 	Mark the functions callable from it with `@context_menu { ... }` Presence of other `@menu(...)` annotations will exclude it from the context menu at this time.
19291 
19292 	See_Also:
19293 		[Widget.setMenuAndToolbarFromAnnotatedCode]
19294 +/
19295 Menu createContextMenuFromAnnotatedCode(TWidget)(TWidget w) if(is(TWidget : Widget)) {
19296 	return createContextMenuFromAnnotatedCode(w, w);
19297 }
19298 
19299 /// ditto
19300 Menu createContextMenuFromAnnotatedCode(T)(Widget w, ref T t) if(!is(T == class) && !is(T == interface)) {
19301 	return createContextMenuFromAnnotatedCode_internal(w, t);
19302 }
19303 /// ditto
19304 Menu createContextMenuFromAnnotatedCode(T)(Widget w, T t) if(is(T == class) || is(T == interface)) {
19305 	return createContextMenuFromAnnotatedCode_internal(w, t);
19306 }
19307 Menu createContextMenuFromAnnotatedCode_internal(T)(Widget w, ref T t) {
19308 	Menu ret = new Menu("", w);
19309 
19310 	foreach(memberName; __traits(derivedMembers, T)) {
19311 		static if(memberName != "this")
19312 		static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
19313 			.menu menu;
19314 			bool separator;
19315 			.hotkey hotkey;
19316 			.icon icon;
19317 			string label;
19318 			string tip;
19319 			foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
19320 				static if(is(typeof(attr) == .menu))
19321 					menu = attr;
19322 				else static if(is(attr == .separator))
19323 					separator = true;
19324 				else static if(is(typeof(attr) == .hotkey))
19325 					hotkey = attr;
19326 				else static if(is(typeof(attr) == .icon))
19327 					icon = attr;
19328 				else static if(is(typeof(attr) == .label))
19329 					label = attr.label;
19330 				else static if(is(typeof(attr) == .tip))
19331 					tip = attr.tip;
19332 			}
19333 
19334 			if(menu is .menu.init) {
19335 				ushort correctIcon = icon.id; // FIXME
19336 				if(label.length == 0)
19337 					label = memberName.toMenuLabel;
19338 
19339 				auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(w.parentWindow, &__traits(getMember, t, memberName));
19340 
19341 				auto action = new Action(label, correctIcon, handler);
19342 
19343 				if(separator)
19344 					ret.addSeparator();
19345 					ret.addItem(new MenuItem(action));
19346 			}
19347 		}
19348 	}
19349 
19350 	return ret;
19351 }
19352 
19353 // still do layout delegation
19354 // and... split off Window from Widget.
19355 
19356 version(minigui_screenshots)
19357 struct Screenshot {
19358 	string name;
19359 }
19360 
19361 version(minigui_screenshots)
19362 static if(__VERSION__ > 2092)
19363 mixin(q{
19364 shared static this() {
19365 	import core.runtime;
19366 
19367 	static UnitTestResult screenshotMagic() {
19368 		string name;
19369 
19370 		import arsd.png;
19371 
19372 		auto results = new Window();
19373 		auto button = new Button("do it", results);
19374 
19375 		Window.newWindowCreated = delegate(Window w) {
19376 			Timer timer;
19377 			timer = new Timer(250, {
19378 				auto img = w.win.takeScreenshot();
19379 				timer.destroy();
19380 
19381 				version(Windows)
19382 					writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img);
19383 				else
19384 					writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img);
19385 
19386 				w.close();
19387 			});
19388 		};
19389 
19390 		button.addWhenTriggered( {
19391 
19392 		foreach(test; __traits(getUnitTests, mixin("arsd.minigui"))) {
19393 			name = null;
19394 			static foreach(attr; __traits(getAttributes, test)) {
19395 				static if(is(typeof(attr) == Screenshot))
19396 					name = attr.name;
19397 			}
19398 			if(name.length) {
19399 				test();
19400 			}
19401 		}
19402 
19403 		});
19404 
19405 		results.loop();
19406 
19407 		return UnitTestResult(0, 0, false, false);
19408 	}
19409 
19410 
19411 	Runtime.extendedModuleUnitTester = &screenshotMagic;
19412 }
19413 });
19414 version(minigui_screenshots) {
19415 	version(unittest)
19416 		void main() {}
19417 	else static assert(0, "dont forget the -unittest flag to dmd");
19418 }
19419 
19420 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut.
19421 // FIXME: make multiple accelerators disambiguate based ona rgs
19422 // FIXME: MainWindow ctor should have same arg order as Window
19423 // FIXME: mainwindow ctor w/ client area size instead of total size.
19424 // 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.
19425 // FIXME: tri-state checkbox
19426 // FIXME: subordinate controls grouping...