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 			static if(__VERSION__ >= 2_083)
682 			version(CRuntime_Microsoft) // FIXME: mingw?
683 				version=UseManifestMinigui;
684 		}
685 
686 	}
687 
688 
689 	version(UseManifestMinigui) {
690 		// assume we want commctrl6 whenever possible since there's really no reason not to
691 		// and this avoids some of the manifest hassle
692 		pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"");
693 	}
694 }
695 
696 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default.
697 private bool lastDefaultPrevented;
698 
699 /// Methods marked with this are available from scripts if added to the [arsd.script] engine.
700 alias scriptable = arsd_jsvar_compatible;
701 
702 version(Windows) {
703 	// use native widgets when available unless specifically asked otherwise
704 	version(custom_widgets) {
705 		enum bool UsingCustomWidgets = true;
706 		enum bool UsingWin32Widgets = false;
707 	} else {
708 		version = win32_widgets;
709 		enum bool UsingCustomWidgets = false;
710 		enum bool UsingWin32Widgets = true;
711 	}
712 	// and native theming when needed
713 	//version = win32_theming;
714 } else {
715 	enum bool UsingCustomWidgets = true;
716 	enum bool UsingWin32Widgets = false;
717 	version=custom_widgets;
718 }
719 
720 
721 
722 /*
723 
724 	The main goals of minigui.d are to:
725 		1) Provide basic widgets that just work in a lightweight lib.
726 		   I basically want things comparable to a plain HTML form,
727 		   plus the easy and obvious things you expect from Windows
728 		   apps like a menu.
729 		2) Use native things when possible for best functionality with
730 		   least library weight.
731 		3) Give building blocks to provide easy extension for your
732 		   custom widgets, or hooking into additional native widgets
733 		   I didn't wrap.
734 		4) Provide interfaces for easy interaction between third
735 		   party minigui extensions. (event model, perhaps
736 		   signals/slots, drop-in ease of use bits.)
737 		5) Zero non-system dependencies, including Phobos as much as
738 		   I reasonably can. It must only import arsd.color and
739 		   my simpledisplay.d. If you need more, it will have to be
740 		   an extension module.
741 		6) An easy layout system that generally works.
742 
743 	A stretch goal is to make it easy to make gui forms with code,
744 	some kind of resource file (xml?) and even a wysiwyg designer.
745 
746 	Another stretch goal is to make it easy to hook data into the gui,
747 	including from reflection. So like auto-generate a form from a
748 	function signature or struct definition, or show a list from an
749 	array that automatically updates as the array is changed. Then,
750 	your program focuses on the data more than the gui interaction.
751 
752 
753 
754 	STILL NEEDED:
755 		* combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect)
756 		* slider
757 		* listbox
758 		* spinner
759 		* label?
760 		* rich text
761 */
762 
763 
764 /+
765 	enum LayoutMethods {
766 		 verticalFlex,
767 		 horizontalFlex,
768 		 inlineBlock, // left to right, no stretch, goes to next line as needed
769 		 static, // just set to x, y
770 		 verticalNoStretch, // browser style default
771 
772 		 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line
773 
774 		 grid, // magic
775 	}
776 +/
777 
778 /++
779 	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.
780 
781 
782 	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.
783 
784 	---
785 	class MinimalWidget : Widget {
786 		this(Widget parent) {
787 			super(parent);
788 		}
789 	}
790 	---
791 
792 	$(SIDEBAR
793 		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.
794 	)
795 
796 	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.
797 
798 	Among the things you'll most likely want to change in your custom widget:
799 
800 	$(LIST
801 		* 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.)
802 
803 		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.
804 
805 		Do this $(I after) calling the `super` constructor.
806 
807 		* 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.
808 
809 		Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision.
810 
811 		* 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.
812 
813 		* 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.
814 	)
815 
816 	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.
817 
818 	It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example.
819 
820 	Your own custom-drawn and native system controls can exist side-by-side.
821 
822 	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.
823 +/
824 class Widget : ReflectableProperties {
825 
826 	private int toolbarIconSize() {
827 		return scaleWithDpi(24);
828 	}
829 
830 
831 	/++
832 		Returns the current size of the widget.
833 
834 		History:
835 			Added January 3, 2025
836 	+/
837 	final Size size() const {
838 		return Size(width, height);
839 	}
840 
841 	private bool willDraw() {
842 		return true;
843 	}
844 
845 	/+
846 	/++
847 		Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need.
848 
849 		History:
850 			Added September 15, 2021
851 			implemented.... ???
852 	+/
853 	void prepareReflection(this This)() {
854 
855 	}
856 	+/
857 
858 	private bool _enabled = true;
859 
860 	/++
861 		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.
862 
863 		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.
864 
865 		History:
866 			Added November 23, 2021 (dub v10.4)
867 
868 			Warning: the specific behavior of disabling with parents may change in the future.
869 		Bugs:
870 			Currently only implemented for widgets backed by native Windows controls.
871 
872 		See_Also: [disabledReason], [disabledBy]
873 	+/
874 	@property bool enabled() {
875 		return disabledBy() is null;
876 	}
877 
878 	/// ditto
879 	@property void enabled(bool yes) {
880 		_enabled = yes;
881 		version(win32_widgets) {
882 			if(hwnd)
883 				EnableWindow(hwnd, yes);
884 		}
885 		setDynamicState(DynamicState.disabled, yes);
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 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 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 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 			featureNotImplemented();
1879 		}
1880 
1881 		return Point(x, y);
1882 	}
1883 
1884 	version(win32_widgets)
1885 	int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; }
1886 
1887 	version(win32_widgets)
1888 	/// Called when a WM_COMMAND is sent to the associated hwnd.
1889 	void handleWmCommand(ushort cmd, ushort id) {}
1890 
1891 	version(win32_widgets)
1892 	/++
1893 		Called when a WM_NOTIFY is sent to the associated hwnd.
1894 
1895 		History:
1896 	+/
1897 	int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; }
1898 
1899 	version(win32_widgets)
1900 	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); }
1901 
1902 	/++
1903 		This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget.
1904 
1905 		Updates to this variable will only be made visible on the next mouse enter event.
1906 	+/
1907 	@scriptable string statusTip;
1908 	// string toolTip;
1909 	// string helpText;
1910 
1911 	/++
1912 		If true, this widget can be focused via keyboard control with the tab key.
1913 
1914 		If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to).
1915 	+/
1916 	bool tabStop = true;
1917 	/++
1918 		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.)
1919 	+/
1920 	int tabOrder;
1921 
1922 	version(win32_widgets) {
1923 		static Widget[HWND] nativeMapping;
1924 		/// The native handle, if there is one.
1925 		HWND hwnd;
1926 		WNDPROC originalWindowProcedure;
1927 
1928 		SimpleWindow simpleWindowWrappingHwnd;
1929 
1930 		// please note it IGNORES your return value and does NOT forward it to Windows!
1931 		int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) {
1932 			return 0;
1933 		}
1934 	}
1935 	private bool implicitlyCreated;
1936 
1937 	/// 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.
1938 	int x;
1939 	/// ditto
1940 	int y;
1941 	private int _width;
1942 	private int _height;
1943 	private Widget[] _children;
1944 	private Widget _parent;
1945 	private Window _parentWindow;
1946 
1947 	/++
1948 		Returns the window to which this widget is attached.
1949 
1950 		History:
1951 			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.
1952 	+/
1953 	final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; }
1954 	private @property void parentWindow(Window parent) {
1955 		auto old = _parentWindow;
1956 		_parentWindow = parent;
1957 		newParentWindow(old, _parentWindow);
1958 		foreach(child; children)
1959 			child.parentWindow = parent; // please note that this is recursive
1960 	}
1961 
1962 	/++
1963 		Called when the widget has been added to or remove from a parent window.
1964 
1965 		Note that either oldParent and/or newParent may be null any time this is called.
1966 
1967 		History:
1968 			Added September 13, 2024
1969 	+/
1970 	protected void newParentWindow(Window oldParent, Window newParent) {}
1971 
1972 	/++
1973 		Returns the list of the widget's children.
1974 
1975 		History:
1976 			Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private.
1977 
1978 			Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list.
1979 	+/
1980 	final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; }
1981 
1982 	/++
1983 		Returns the widget's parent.
1984 
1985 		History:
1986 			Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted.
1987 
1988 			The parent should only be managed by the [addChild] and [removeWidget] method.
1989 	+/
1990 	final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; }
1991 
1992 	/// The widget's current size.
1993 	final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; }
1994 	/// ditto
1995 	final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; }
1996 
1997 	/// Only the layout manager should be calling these.
1998 	final protected @property int width(int a) @safe { return _width = a; }
1999 	/// ditto
2000 	final protected @property int height(int a) @safe { return _height = a; }
2001 
2002 	/++
2003 		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.
2004 
2005 		It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size.
2006 	+/
2007 	protected void registerMovement() {
2008 		version(win32_widgets) {
2009 			if(hwnd) {
2010 				auto pos = getChildPositionRelativeToParentHwnd(this);
2011 				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
2012 				this.redraw();
2013 			}
2014 		}
2015 		sendResizeEvent();
2016 	}
2017 
2018 	/// Creates the widget and adds it to the parent.
2019 	this(Widget parent) {
2020 		if(parent !is null)
2021 			parent.addChild(this);
2022 		setupDefaultEventHandlers();
2023 	}
2024 
2025 	/// 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.
2026 	@scriptable
2027 	bool isFocused() {
2028 		return parentWindow && parentWindow.focusedWidget is this;
2029 	}
2030 
2031 	private bool showing_ = true;
2032 	///
2033 	bool showing() const { return showing_; }
2034 	///
2035 	bool hidden() const { return !showing_; }
2036 	/++
2037 		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.
2038 
2039 		Note that a widget only ever shows if all its parents are showing too.
2040 	+/
2041 	void showing(bool s, bool recalculate = true) {
2042 		if(s != showing_) {
2043 			showing_ = s;
2044 			// writeln(typeid(this).toString, " ", this.parent ? typeid(this.parent).toString : "null", " ", s);
2045 
2046 			showNativeWindowChildren(s);
2047 
2048 			if(parent && recalculate) {
2049 				parent.queueRecomputeChildLayout();
2050 				parent.redraw();
2051 			}
2052 
2053 			if(s) {
2054 				queueRecomputeChildLayout();
2055 				redraw();
2056 			}
2057 		}
2058 	}
2059 	/// Convenience method for `showing = true`
2060 	@scriptable
2061 	void show() {
2062 		showing = true;
2063 	}
2064 	/// Convenience method for `showing = false`
2065 	@scriptable
2066 	void hide() {
2067 		showing = false;
2068 	}
2069 
2070 	/++
2071 		If you are a native window, show/hide it based on shouldShow and return `true`.
2072 
2073 		Otherwise, do nothing and return false.
2074 	+/
2075 	protected bool showOrHideIfNativeWindow(bool shouldShow) {
2076 		version(win32_widgets) {
2077 			if(hwnd) {
2078 				ShowWindow(hwnd, shouldShow ? SW_SHOW : SW_HIDE);
2079 				return true;
2080 			} else {
2081 				return false;
2082 			}
2083 		} else {
2084 			return false;
2085 		}
2086 	}
2087 
2088 	private void showNativeWindowChildren(bool s) {
2089 		if(!showOrHideIfNativeWindow(s && showing))
2090 			foreach(child; children)
2091 				child.showNativeWindowChildren(s);
2092 	}
2093 
2094 	///
2095 	@scriptable
2096 	void focus() {
2097 		assert(parentWindow !is null);
2098 		if(isFocused())
2099 			return;
2100 
2101 		if(parentWindow.focusedWidget) {
2102 			// FIXME: more details here? like from and to
2103 			auto from = parentWindow.focusedWidget;
2104 			parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false);
2105 			parentWindow.focusedWidget = null;
2106 			from.emit!BlurEvent();
2107 			from.emit!FocusOutEvent();
2108 		}
2109 
2110 
2111 		version(win32_widgets) {
2112 			if(this.hwnd !is null)
2113 				SetFocus(this.hwnd);
2114 		}
2115 		//else static if(UsingSimpledisplayX11)
2116 			//this.parentWindow.win.focus();
2117 
2118 		parentWindow.focusedWidget = this;
2119 		parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true);
2120 		this.emit!FocusEvent();
2121 		this.emit!FocusInEvent();
2122 	}
2123 
2124 	/+
2125 	/++
2126 		Unfocuses the widget. This may reset
2127 	+/
2128 	@scriptable
2129 	void blur() {
2130 
2131 	}
2132 	+/
2133 
2134 
2135 	/++
2136 		This is called when the widget is added to a window. It gives you a chance to set up event hooks.
2137 
2138 		Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods.
2139 	+/
2140 	void attachedToWindow(Window w) {}
2141 	/++
2142 		Callback when the widget is added to another widget.
2143 
2144 		Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful.
2145 	+/
2146 	void addedTo(Widget w) {}
2147 
2148 	/++
2149 		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.
2150 
2151 		This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget.
2152 	+/
2153 	protected void addChild(Widget w, int position = int.max) {
2154 		assert(w._parent !is this, "Child cannot be added twice to the same parent");
2155 		assert(w !is this, "Child cannot be its own parent!");
2156 		w._parent = this;
2157 		if(position == int.max || position == children.length) {
2158 			_children ~= w;
2159 		} else {
2160 			assert(position < _children.length);
2161 			_children.length = _children.length + 1;
2162 			for(int i = cast(int) _children.length - 1; i > position; i--)
2163 				_children[i] = _children[i - 1];
2164 			_children[position] = w;
2165 		}
2166 
2167 		this.parentWindow = this._parentWindow;
2168 
2169 		w.addedTo(this);
2170 
2171 		bool parentIsNative;
2172 		version(win32_widgets) {
2173 			parentIsNative = hwnd !is null;
2174 		}
2175 		if(!parentIsNative && !showing)
2176 			w.showOrHideIfNativeWindow(false);
2177 
2178 		if(parentWindow !is null) {
2179 			w.attachedToWindow(parentWindow);
2180 			parentWindow.queueRecomputeChildLayout();
2181 			parentWindow.redraw();
2182 		}
2183 	}
2184 
2185 	/++
2186 		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.
2187 	+/
2188 	Widget getChildAtPosition(int x, int y) {
2189 		// it goes backward so the last one to show gets picked first
2190 		// might use z-index later
2191 		foreach_reverse(child; children) {
2192 			if(child.hidden)
2193 				continue;
2194 			if(child.x <= x && child.y <= y
2195 				&& ((x - child.x) < child.width)
2196 				&& ((y - child.y) < child.height))
2197 			{
2198 				return child;
2199 			}
2200 		}
2201 
2202 		return null;
2203 	}
2204 
2205 	/++
2206 		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.
2207 
2208 		History:
2209 			Added July 2, 2021 (v10.2)
2210 	+/
2211 	protected void addScrollPosition(ref int x, ref int y) {}
2212 
2213 	/++
2214 		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.
2215 
2216 		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.
2217 
2218 		[paint] is not called for system widgets as the OS library draws them instead.
2219 
2220 
2221 		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.
2222 
2223 		You should also look at [WidgetPainter.visualTheme] to be theme aware.
2224 
2225 		History:
2226 			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.
2227 	+/
2228 	void paint(WidgetPainter painter) {
2229 		version(win32_widgets)
2230 			if(hwnd) {
2231 				return;
2232 			}
2233 		painter.drawThemed(&paintContent); // note this refers to the following overload
2234 	}
2235 
2236 	/++
2237 		Responsible for drawing the content as the theme engine is responsible for other elements.
2238 
2239 		$(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.)
2240 
2241 		Params:
2242 			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.
2243 
2244 			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.
2245 
2246 			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.
2247 
2248 		Returns:
2249 			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.
2250 
2251 		History:
2252 			Added May 15, 2021
2253 	+/
2254 	Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
2255 		return bounds;
2256 	}
2257 
2258 	deprecated("Change ScreenPainter to WidgetPainter")
2259 	final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
2260 
2261 	/// I don't actually like the name of this
2262 	/// this draws a background on it
2263 	void erase(WidgetPainter painter) {
2264 		version(win32_widgets)
2265 			if(hwnd) return; // Windows will do it. I think.
2266 
2267 		auto c = getComputedStyle().background.color;
2268 		painter.fillColor = c;
2269 		painter.outlineColor = c;
2270 
2271 		version(win32_widgets) {
2272 			HANDLE b, p;
2273 			if(c.a == 0 && parent is parentWindow) {
2274 				// I don't remember why I had this really...
2275 				b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
2276 				p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
2277 			}
2278 		}
2279 		painter.drawRectangle(Point(0, 0), width, height);
2280 		version(win32_widgets) {
2281 			if(c.a == 0 && parent is parentWindow) {
2282 				SelectObject(painter.impl.hdc, p);
2283 				SelectObject(painter.impl.hdc, b);
2284 			}
2285 		}
2286 	}
2287 
2288 	///
2289 	WidgetPainter draw() {
2290 		int x = this.x, y = this.y;
2291 		auto parent = this.parent;
2292 		while(parent) {
2293 			x += parent.x;
2294 			y += parent.y;
2295 			parent = parent.parent;
2296 		}
2297 
2298 		auto painter = parentWindow.win.draw(true);
2299 		painter.originX = x;
2300 		painter.originY = y;
2301 		painter.setClipRectangle(Point(0, 0), width, height);
2302 		return WidgetPainter(painter, this);
2303 	}
2304 
2305 	/// 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.
2306 	protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
2307 		if(hidden)
2308 			return;
2309 
2310 		int paintX = x;
2311 		int paintY = y;
2312 		if(this.useNativeDrawing()) {
2313 			paintX = 0;
2314 			paintY = 0;
2315 			lox = 0;
2316 			loy = 0;
2317 			containment = Rectangle(0, 0, int.max, int.max);
2318 		}
2319 
2320 		painter.originX = lox + paintX;
2321 		painter.originY = loy + paintY;
2322 
2323 		bool actuallyPainted = false;
2324 
2325 		const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height)));
2326 		if(clip == Rectangle.init) {
2327 			// writeln(this, " clipped out");
2328 			return;
2329 		}
2330 
2331 		bool invalidateChildren = invalidate;
2332 
2333 		if(redrawRequested || force) {
2334 			painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
2335 
2336 			painter.drawingUpon = this;
2337 
2338 			erase(painter);
2339 			if(painter.visualTheme)
2340 				painter.visualTheme.doPaint(this, painter);
2341 			else
2342 				paint(painter);
2343 
2344 			if(invalidate) {
2345 				// sdpyPrintDebugString("invalidate " ~ typeid(this).name);
2346 				auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height));
2347 				painter.invalidateRect(region);
2348 				// children are contained inside this, so no need to do extra work
2349 				invalidateChildren = false;
2350 			}
2351 
2352 			redrawRequested = false;
2353 			actuallyPainted = true;
2354 		}
2355 
2356 		foreach(child; children) {
2357 			version(win32_widgets)
2358 				if(child.useNativeDrawing()) continue;
2359 			child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren);
2360 		}
2361 
2362 		version(win32_widgets)
2363 		foreach(child; children) {
2364 			if(child.useNativeDrawing) {
2365 				painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child);
2366 				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
2367 			}
2368 		}
2369 	}
2370 
2371 	protected bool useNativeDrawing() nothrow {
2372 		version(win32_widgets)
2373 			return hwnd !is null;
2374 		else
2375 			return false;
2376 	}
2377 
2378 	private static class RedrawEvent {}
2379 	private __gshared re = new RedrawEvent();
2380 
2381 	private bool redrawRequested;
2382 	///
2383 	final void redraw(string file = __FILE__, size_t line = __LINE__) {
2384 		redrawRequested = true;
2385 
2386 		if(this.parentWindow) {
2387 			auto sw = this.parentWindow.win;
2388 			assert(sw !is null);
2389 			if(!sw.eventQueued!RedrawEvent) {
2390 				sw.postEvent(re);
2391 				//  writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
2392 			}
2393 		}
2394 	}
2395 
2396 	private SimpleWindow drawableWindow;
2397 
2398 	/++
2399 		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.
2400 
2401 		Returns:
2402 			`true` if you should do your default behavior.
2403 
2404 		History:
2405 			Added May 5, 2021
2406 
2407 		Bugs:
2408 			It does not do the static checks on gdc right now.
2409 	+/
2410 	final protected bool emit(EventType, this This, Args...)(Args args) {
2411 		version(GNU) {} else
2412 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2413 		auto e = new EventType(this, args);
2414 		e.dispatch();
2415 		return !e.defaultPrevented;
2416 	}
2417 	/// ditto
2418 	final protected bool emit(string eventString, this This)() {
2419 		auto e = new Event(eventString, this);
2420 		e.dispatch();
2421 		return !e.defaultPrevented;
2422 	}
2423 
2424 	/++
2425 		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.
2426 
2427 		History:
2428 			Added May 5, 2021
2429 	+/
2430 	final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) {
2431 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2432 		return addEventListener(handler);
2433 	}
2434 
2435 	/++
2436 		Gets the computed style properties from the visual theme.
2437 
2438 		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].)
2439 
2440 		History:
2441 			Added May 8, 2021
2442 	+/
2443 	final StyleInformation getComputedStyle() {
2444 		return StyleInformation(this);
2445 	}
2446 
2447 	int focusableWidgets(scope int delegate(Widget) dg) {
2448 		foreach(widget; WidgetStream(this)) {
2449 			if(widget.tabStop && !widget.hidden) {
2450 				int result = dg(widget);
2451 				if (result)
2452 					return result;
2453 			}
2454 		}
2455 		return 0;
2456 	}
2457 
2458 	/++
2459 		Calculates the border box (that is, the full width/height of the widget, from border edge to border edge)
2460 		for the given content box (the area between the padding)
2461 
2462 		History:
2463 			Added January 4, 2023 (dub v11.0)
2464 	+/
2465 	Rectangle borderBoxForContentBox(Rectangle contentBox) {
2466 		auto cs = getComputedStyle();
2467 
2468 		auto borderWidth = getBorderWidth(cs.borderStyle);
2469 
2470 		auto rect = contentBox;
2471 
2472 		rect.left -= borderWidth;
2473 		rect.right += borderWidth;
2474 		rect.top -= borderWidth;
2475 		rect.bottom += borderWidth;
2476 
2477 		auto insideBorderRect = rect;
2478 
2479 		rect.left -= cs.paddingLeft;
2480 		rect.right += cs.paddingRight;
2481 		rect.top -= cs.paddingTop;
2482 		rect.bottom += cs.paddingBottom;
2483 
2484 		return rect;
2485 	}
2486 
2487 
2488 	// FIXME: I kinda want to hide events from implementation widgets
2489 	// so it just catches them all and stops propagation...
2490 	// i guess i can do it with a event listener on star.
2491 
2492 	mixin Emits!KeyDownEvent; ///
2493 	mixin Emits!KeyUpEvent; ///
2494 	mixin Emits!CharEvent; ///
2495 
2496 	mixin Emits!MouseDownEvent; ///
2497 	mixin Emits!MouseUpEvent; ///
2498 	mixin Emits!ClickEvent; ///
2499 	mixin Emits!DoubleClickEvent; ///
2500 	mixin Emits!MouseMoveEvent; ///
2501 	mixin Emits!MouseOverEvent; ///
2502 	mixin Emits!MouseOutEvent; ///
2503 	mixin Emits!MouseEnterEvent; ///
2504 	mixin Emits!MouseLeaveEvent; ///
2505 
2506 	mixin Emits!ResizeEvent; ///
2507 
2508 	mixin Emits!BlurEvent; ///
2509 	mixin Emits!FocusEvent; ///
2510 
2511 	mixin Emits!FocusInEvent; ///
2512 	mixin Emits!FocusOutEvent; ///
2513 }
2514 
2515 /+
2516 /++
2517 	Interface to indicate that the widget has a simple value property.
2518 
2519 	History:
2520 		Added August 26, 2021
2521 +/
2522 interface HasValue!T {
2523 	/// Getter
2524 	@property T value();
2525 	/// Setter
2526 	@property void value(T);
2527 }
2528 
2529 /++
2530 	Interface to indicate that the widget has a range of possible values for its simple value property.
2531 	This would be present on something like a slider or possibly a number picker.
2532 
2533 	History:
2534 		Added September 11, 2021
2535 +/
2536 interface HasRangeOfValues!T : HasValue!T {
2537 	/// The minimum and maximum values in the range, inclusive.
2538 	@property T minValue();
2539 	@property void minValue(T); /// ditto
2540 	@property T maxValue(); /// ditto
2541 	@property void maxValue(T); /// ditto
2542 
2543 	/// The smallest step the user interface allows. User may still type in values without this limitation.
2544 	@property void step(T);
2545 	@property T step(); /// ditto
2546 }
2547 
2548 /++
2549 	Interface to indicate that the widget has a list of possible values the user can choose from.
2550 	This would be present on something like a drop-down selector.
2551 
2552 	The value is NOT necessarily one of the items on the list. Consider the case of a free-entry
2553 	combobox.
2554 
2555 	History:
2556 		Added September 11, 2021
2557 +/
2558 interface HasListOfValues!T : HasValue!T {
2559 	@property T[] values;
2560 	@property void values(T[]);
2561 
2562 	@property int selectedIndex(); // note it may return -1!
2563 	@property void selectedIndex(int);
2564 }
2565 +/
2566 
2567 /++
2568 	History:
2569 		Added September 2021 (dub v10.4)
2570 +/
2571 class GridLayout : Layout {
2572 
2573 	// 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.
2574 
2575 	/++
2576 		If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to.
2577 	+/
2578 	enum Gravity {
2579 		Center    = 0,
2580 		NorthWest = North | West,
2581 		North     = 0b10_00,
2582 		NorthEast = North | East,
2583 		West      = 0b00_10,
2584 		East      = 0b00_01,
2585 		SouthWest = South | West,
2586 		South     = 0b01_00,
2587 		SouthEast = South | East,
2588 	}
2589 
2590 	/++
2591 		The width and height are in some proportional units and can often just be 12.
2592 	+/
2593 	this(int width, int height, Widget parent) {
2594 		this.gridWidth = width;
2595 		this.gridHeight = height;
2596 		super(parent);
2597 	}
2598 
2599 	/++
2600 		Sets the position of the given child.
2601 
2602 		The units of these arguments are in the proportional grid units you set in the constructor.
2603 	+/
2604 	Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) {
2605 		// ensure it is in bounds
2606 		// then ensure no overlaps
2607 
2608 		ChildPosition p = ChildPosition(child, x, y, width, height, gravity);
2609 
2610 		foreach(ref position; positions) {
2611 			if(position.widget is child) {
2612 				position = p;
2613 				goto set;
2614 			}
2615 		}
2616 
2617 		positions ~= p;
2618 
2619 		set:
2620 
2621 		// FIXME: should this batch?
2622 		queueRecomputeChildLayout();
2623 
2624 		return child;
2625 	}
2626 
2627 	override void addChild(Widget w, int position = int.max) {
2628 		super.addChild(w, position);
2629 		//positions ~= ChildPosition(w);
2630 		if(position != int.max) {
2631 			// FIXME: align it so they actually match.
2632 		}
2633 	}
2634 
2635 	override void widgetRemoved(size_t idx, Widget w) {
2636 		// FIXME: keep the positions array aligned
2637 		// positions[idx].widget = null;
2638 	}
2639 
2640 	override void recomputeChildLayout() {
2641 		registerMovement();
2642 		int onGrid = cast(int) positions.length;
2643 		c: foreach(child; children) {
2644 			// just snap it to the grid
2645 			if(onGrid)
2646 			foreach(position; positions)
2647 				if(position.widget is child) {
2648 					child.x = this.width * position.x / this.gridWidth;
2649 					child.y = this.height * position.y / this.gridHeight;
2650 					child.width = this.width * position.width / this.gridWidth;
2651 					child.height = this.height * position.height / this.gridHeight;
2652 
2653 					auto diff = child.width - child.maxWidth();
2654 					// FIXME: gravity?
2655 					if(diff > 0) {
2656 						child.width = child.width - diff;
2657 
2658 						if(position.gravity & Gravity.West) {
2659 							// nothing needed, already aligned
2660 						} else if(position.gravity & Gravity.East) {
2661 							child.x += diff;
2662 						} else {
2663 							child.x += diff / 2;
2664 						}
2665 					}
2666 
2667 					diff = child.height - child.maxHeight();
2668 					// FIXME: gravity?
2669 					if(diff > 0) {
2670 						child.height = child.height - diff;
2671 
2672 						if(position.gravity & Gravity.North) {
2673 							// nothing needed, already aligned
2674 						} else if(position.gravity & Gravity.South) {
2675 							child.y += diff;
2676 						} else {
2677 							child.y += diff / 2;
2678 						}
2679 					}
2680 					child.recomputeChildLayout();
2681 					onGrid--;
2682 					continue c;
2683 				}
2684 			// the position isn't given on the grid array, we'll just fill in from where the explicit ones left off.
2685 		}
2686 	}
2687 
2688 	private struct ChildPosition {
2689 		Widget widget;
2690 		int x;
2691 		int y;
2692 		int width;
2693 		int height;
2694 		Gravity gravity;
2695 	}
2696 	private ChildPosition[] positions;
2697 
2698 	int gridWidth = 12;
2699 	int gridHeight = 12;
2700 }
2701 
2702 ///
2703 abstract class ComboboxBase : Widget {
2704 	// if the user can enter arbitrary data, we want to use  2 == CBS_DROPDOWN
2705 	// or to always show the list, we want CBS_SIMPLE == 1
2706 	version(win32_widgets)
2707 		this(uint style, Widget parent) {
2708 			super(parent);
2709 			createWin32Window(this, "ComboBox"w, null, style);
2710 		}
2711 	else version(custom_widgets)
2712 		this(Widget parent) {
2713 			super(parent);
2714 
2715 			addEventListener((KeyDownEvent event) {
2716 				if(event.key == Key.Up) {
2717 					setSelection(selection_-1);
2718 					event.preventDefault();
2719 				}
2720 				if(event.key == Key.Down) {
2721 					setSelection(selection_+1);
2722 					event.preventDefault();
2723 				}
2724 
2725 			});
2726 
2727 		}
2728 	else static assert(false);
2729 
2730 	protected void scrollSelectionIntoView() {}
2731 
2732 	/++
2733 		Returns the current list of options in the selection.
2734 
2735 		History:
2736 			Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private.
2737 	+/
2738 	final @property string[] options() const {
2739 		return cast(string[]) options_;
2740 	}
2741 
2742 	/++
2743 		Replaces the list of options in the box. Note that calling this will also reset the selection.
2744 
2745 		History:
2746 			Added December, 29 2024
2747 	+/
2748 	final @property void options(string[] options) {
2749 		version(win32_widgets)
2750 			SendMessageW(hwnd, 331 /*CB_RESETCONTENT*/, 0, 0);
2751 		selection_ = -1;
2752 		options_ = null;
2753 		foreach(opt; options)
2754 			addOption(opt);
2755 
2756 		version(custom_widgets)
2757 			redraw();
2758 	}
2759 
2760 	private string[] options_;
2761 	private int selection_ = -1;
2762 
2763 	/++
2764 		Adds an option to the end of options array.
2765 	+/
2766 	void addOption(string s) {
2767 		options_ ~= s;
2768 		version(win32_widgets)
2769 		SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s));
2770 	}
2771 
2772 	/++
2773 		Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected.
2774 	+/
2775 	int getSelection() {
2776 		return selection_;
2777 	}
2778 
2779 	/++
2780 		Returns the current selection as a string.
2781 
2782 		History:
2783 			Added November 17, 2021
2784 	+/
2785 	string getSelectionString() {
2786 		return selection_ == -1 ? null : options[selection_];
2787 	}
2788 
2789 	/++
2790 		Sets the current selection to an index in the options array, or to the given option if present.
2791 		Please note that the string version may do a linear lookup.
2792 
2793 		Returns:
2794 			the index you passed in
2795 
2796 		History:
2797 			The `string` based overload was added on March 1, 2022 (dub v10.7).
2798 
2799 			The return value was `void` prior to March 1, 2022.
2800 	+/
2801 	int setSelection(int idx) {
2802 		if(idx < -1)
2803 			idx = -1;
2804 		if(idx + 1 > options.length)
2805 			idx = cast(int) options.length - 1;
2806 
2807 		selection_ = idx;
2808 
2809 		version(win32_widgets)
2810 		SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0);
2811 
2812 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2813 		t.dispatch();
2814 
2815 		scrollSelectionIntoView();
2816 
2817 		return idx;
2818 	}
2819 
2820 	/// ditto
2821 	int setSelection(string s) {
2822 		if(s !is null)
2823 		foreach(idx, item; options)
2824 			if(item == s) {
2825 				return setSelection(cast(int) idx);
2826 			}
2827 		return setSelection(-1);
2828 	}
2829 
2830 	/++
2831 		This event is fired when the selection changes. Both [Event.stringValue] and
2832 		[Event.intValue] are filled in - `stringValue` is the text in the selection
2833 		and `intValue` is the index of the selection. If the combo box allows multiple
2834 		selection, these values will include only one of the selected items - for those,
2835 		you should loop through the values and check their selected flag instead.
2836 
2837 		(I know that sucks, but it is how it is right now.)
2838 
2839 		History:
2840 			It originally inherited from `ChangeEvent!String`, but now does from [ChangeEventBase] as of January 3, 2025.
2841 			This shouldn't break anything if you used it through either its own name `SelectionChangedEvent` or through the
2842 			base `Event`, only if you specifically used `ChangeEvent!string` - those handlers may now get `null` or fail to
2843 			be called. If you did do this, just change it to generic `Event`, as `stringValue` and `intValue` are already there.
2844 	+/
2845 	static final class SelectionChangedEvent : ChangeEventBase {
2846 		this(Widget target, int iv, string sv) {
2847 			super(target);
2848 			this.iv = iv;
2849 			this.sv = sv;
2850 		}
2851 		immutable int iv;
2852 		immutable string sv;
2853 
2854 		deprecated("Use stringValue or intValue instead") @property string value() {
2855 			return sv;
2856 		}
2857 
2858 		override @property string stringValue() { return sv; }
2859 		override @property int intValue() { return iv; }
2860 	}
2861 
2862 	version(win32_widgets)
2863 	override void handleWmCommand(ushort cmd, ushort id) {
2864 		if(cmd == CBN_SELCHANGE) {
2865 			selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0);
2866 			fireChangeEvent();
2867 		}
2868 	}
2869 
2870 	private void fireChangeEvent() {
2871 		if(selection_ >= options.length)
2872 			selection_ = -1;
2873 
2874 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2875 		t.dispatch();
2876 	}
2877 
2878 	override int minWidth() { return scaleWithDpi(32); }
2879 
2880 	version(win32_widgets) {
2881 		override int minHeight() { return defaultLineHeight + 6; }
2882 		override int maxHeight() { return defaultLineHeight + 6; }
2883 	} else {
2884 		override int minHeight() { return defaultLineHeight + 4; }
2885 		override int maxHeight() { return defaultLineHeight + 4; }
2886 	}
2887 
2888 	version(custom_widgets)
2889 	void popup() {
2890 		CustomComboBoxPopup popup = new CustomComboBoxPopup(this);
2891 	}
2892 
2893 }
2894 
2895 private class CustomComboBoxPopup : Window {
2896 	private ComboboxBase associatedWidget;
2897 	private ListWidget lw;
2898 	private bool cancelled;
2899 
2900 	this(ComboboxBase associatedWidget) {
2901 		this.associatedWidget = associatedWidget;
2902 
2903 		// FIXME: this should scroll if there's too many elements to reasonably fit on screen
2904 
2905 		auto w = associatedWidget.width;
2906 		// FIXME: suggestedDropdownHeight see below
2907 		auto h = cast(int) associatedWidget.options.length * associatedWidget.defaultLineHeight + associatedWidget.scaleWithDpi(8);
2908 
2909 		// FIXME: this sux
2910 		if(h > associatedWidget.parentWindow.height)
2911 			h = associatedWidget.parentWindow.height;
2912 
2913 		auto mh = associatedWidget.scaleWithDpi(16 + 16 + 32); // to make the scrollbar look ok
2914 		if(h < mh)
2915 			h = mh;
2916 
2917 		auto coord = associatedWidget.globalCoordinates();
2918 		auto dropDown = new SimpleWindow(
2919 			w, h,
2920 			null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, associatedWidget.parentWindow ? associatedWidget.parentWindow.win : null);
2921 
2922 		super(dropDown);
2923 
2924 		dropDown.move(coord.x, coord.y + associatedWidget.height);
2925 
2926 		this.lw = new ListWidget(this);
2927 		version(custom_widgets)
2928 			lw.multiSelect = false;
2929 		foreach(option; associatedWidget.options)
2930 			lw.addOption(option);
2931 
2932 		auto originalSelection = associatedWidget.getSelection;
2933 		lw.setSelection(originalSelection);
2934 		lw.scrollSelectionIntoView();
2935 
2936 		/+
2937 		{
2938 			auto cs = getComputedStyle();
2939 			auto painter = dropDown.draw();
2940 			draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color);
2941 			auto p = Point(4, 4);
2942 			painter.outlineColor = cs.foregroundColor;
2943 			foreach(option; associatedWidget.options) {
2944 				painter.drawText(p, option);
2945 				p.y += defaultLineHeight;
2946 			}
2947 		}
2948 
2949 		dropDown.setEventHandlers(
2950 			(MouseEvent event) {
2951 				if(event.type == MouseEventType.buttonReleased) {
2952 					dropDown.close();
2953 					auto element = (event.y - 4) / defaultLineHeight;
2954 					if(element >= 0 && element <= associatedWidget.options.length) {
2955 						associatedWidget.selection_ = element;
2956 
2957 						associatedWidget.fireChangeEvent();
2958 					}
2959 				}
2960 			}
2961 		);
2962 		+/
2963 
2964 		Widget previouslyFocusedWidget;
2965 
2966 		dropDown.visibilityChanged = (bool visible) {
2967 			if(visible) {
2968 				this.redraw();
2969 				captureMouse(this);
2970 				//dropDown.grabInput();
2971 
2972 				if(previouslyFocusedWidget is null)
2973 					previouslyFocusedWidget = associatedWidget.parentWindow.focusedWidget;
2974 				associatedWidget.parentWindow.focusedWidget = lw;
2975 			} else {
2976 				//dropDown.releaseInputGrab();
2977 				releaseMouseCapture();
2978 
2979 				if(!cancelled)
2980 					associatedWidget.setSelection(lw.getSelection);
2981 
2982 				associatedWidget.parentWindow.focusedWidget = previouslyFocusedWidget;
2983 			}
2984 		};
2985 
2986 		dropDown.show();
2987 	}
2988 
2989 	private bool shouldCloseIfClicked(Widget w) {
2990 		if(w is this)
2991 			return true;
2992 		version(custom_widgets)
2993 		if(cast(TextListViewWidget.TextListViewItem) w)
2994 			return true;
2995 		return false;
2996 	}
2997 
2998 	override void defaultEventHandler_click(ClickEvent ce) {
2999 		if(ce.button == MouseButton.left && shouldCloseIfClicked(ce.target)) {
3000 			this.win.close();
3001 		}
3002 	}
3003 
3004 	override void defaultEventHandler_char(CharEvent ce) {
3005 		if(ce.character == '\n')
3006 			this.win.close();
3007 	}
3008 
3009 	override void defaultEventHandler_keydown(KeyDownEvent kde) {
3010 		if(kde.key == Key.Escape) {
3011 			cancelled = true;
3012 			this.win.close();
3013 		}/+ else if(kde.key == Key.Up || kde.key == Key.Down)
3014 			{} // intentionally blank, the list view handles these
3015 			// separately from the scroll message widget default handler
3016 		else if(lw && lw.glvw && lw.glvw.smw)
3017 			lw.glvw.smw.defaultKeyboardListener(kde);+/
3018 	}
3019 }
3020 
3021 /++
3022 	A drop-down list where the user must select one of the
3023 	given options. Like `<select>` in HTML.
3024 
3025 	The current selection is given as a string or an index.
3026 	It emits a SelectionChangedEvent when it changes.
3027 +/
3028 class DropDownSelection : ComboboxBase {
3029 	/++
3030 		Creates a drop down selection, optionally passing its initial list of options.
3031 
3032 		History:
3033 			The overload with the `options` parameter was added December 29, 2024.
3034 	+/
3035 	this(Widget parent) {
3036 		version(win32_widgets)
3037 			super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent);
3038 		else version(custom_widgets) {
3039 			super(parent);
3040 
3041 			addEventListener("focus", () { this.redraw; });
3042 			addEventListener("blur", () { this.redraw; });
3043 			addEventListener(EventType.change, () { this.redraw; });
3044 			addEventListener("mousedown", () { this.focus(); this.popup(); });
3045 			addEventListener((KeyDownEvent event) {
3046 				if(event.key == Key.Space)
3047 					popup();
3048 			});
3049 		} else static assert(false);
3050 	}
3051 
3052 	/// ditto
3053 	this(string[] options, Widget parent) {
3054 		this(parent);
3055 		this.options = options;
3056 	}
3057 
3058 	mixin Padding!q{2};
3059 	static class Style : Widget.Style {
3060 		override FrameStyle borderStyle() { return FrameStyle.risen; }
3061 	}
3062 	mixin OverrideStyle!Style;
3063 
3064 	version(custom_widgets)
3065 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
3066 		auto cs = getComputedStyle();
3067 
3068 		painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]);
3069 
3070 		painter.outlineColor = cs.foregroundColor;
3071 		painter.fillColor = cs.foregroundColor;
3072 
3073 		/+
3074 		Point[4] triangle;
3075 		enum padding = 6;
3076 		enum paddingV = 7;
3077 		enum triangleWidth = 10;
3078 		triangle[0] = Point(width - padding - triangleWidth, paddingV);
3079 		triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV);
3080 		triangle[2] = Point(width - padding - 0, paddingV);
3081 		triangle[3] = triangle[0];
3082 		painter.drawPolygon(triangle[]);
3083 		+/
3084 
3085 		auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2);
3086 
3087 		painter.drawPolygon(
3088 			scaleWithDpi(Point(2, 6) + offset),
3089 			scaleWithDpi(Point(7, 11) + offset),
3090 			scaleWithDpi(Point(12, 6) + offset),
3091 			scaleWithDpi(Point(2, 6) + offset)
3092 		);
3093 
3094 
3095 		return bounds;
3096 	}
3097 
3098 	version(win32_widgets)
3099 	override void registerMovement() {
3100 		version(win32_widgets) {
3101 			if(hwnd) {
3102 				auto pos = getChildPositionRelativeToParentHwnd(this);
3103 				// the height given to this from Windows' perspective is supposed
3104 				// to include the drop down's height. so I add to it to give some
3105 				// room for that.
3106 				// FIXME: maybe make the subclass provide a suggestedDropdownHeight thing
3107 				MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true);
3108 			}
3109 		}
3110 		sendResizeEvent();
3111 	}
3112 }
3113 
3114 /++
3115 	A text box with a drop down arrow listing selections.
3116 	The user can choose from the list, or type their own.
3117 +/
3118 class FreeEntrySelection : ComboboxBase {
3119 	this(Widget parent) {
3120 		this(null, parent);
3121 	}
3122 
3123 	this(string[] options, Widget parent) {
3124 		version(win32_widgets)
3125 			super(2 /* CBS_DROPDOWN */, parent);
3126 		else version(custom_widgets) {
3127 			super(parent);
3128 			auto hl = new HorizontalLayout(this);
3129 			lineEdit = new LineEdit(hl);
3130 
3131 			tabStop = false;
3132 
3133 			// lineEdit.addEventListener((FocusEvent fe) {  lineEdit.selectAll(); } );
3134 
3135 			auto btn = new class ArrowButton {
3136 				this() {
3137 					super(ArrowDirection.down, hl);
3138 				}
3139 				override int heightStretchiness() {
3140 					return 1;
3141 				}
3142 				override int heightShrinkiness() {
3143 					return 1;
3144 				}
3145 				override int maxHeight() {
3146 					return lineEdit.maxHeight;
3147 				}
3148 			};
3149 			//btn.addDirectEventListener("focus", &lineEdit.focus);
3150 			btn.addEventListener("triggered", &this.popup);
3151 			addEventListener(EventType.change, (Event event) {
3152 				lineEdit.content = event.stringValue;
3153 				lineEdit.focus();
3154 				redraw();
3155 			});
3156 		}
3157 		else static assert(false);
3158 
3159 		this.options = options;
3160 	}
3161 
3162 	string content() {
3163 		version(win32_widgets)
3164 			assert(0, "not implemented");
3165 		else version(custom_widgets)
3166 			return lineEdit.content;
3167 		else static assert(0);
3168 	}
3169 
3170 	void content(string s) {
3171 		version(win32_widgets)
3172 			assert(0, "not implemented");
3173 		else version(custom_widgets)
3174 			lineEdit.content = s;
3175 		else static assert(0);
3176 	}
3177 
3178 	version(custom_widgets) {
3179 		LineEdit lineEdit;
3180 
3181 		override int widthStretchiness() {
3182 			return lineEdit ? lineEdit.widthStretchiness : super.widthStretchiness;
3183 		}
3184 		override int flexBasisWidth() {
3185 			return lineEdit ? lineEdit.flexBasisWidth : super.flexBasisWidth;
3186 		}
3187 	}
3188 }
3189 
3190 /++
3191 	A combination of free entry with a list below it.
3192 +/
3193 class ComboBox : ComboboxBase {
3194 	this(Widget parent) {
3195 		version(win32_widgets)
3196 			super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent);
3197 		else version(custom_widgets) {
3198 			super(parent);
3199 			lineEdit = new LineEdit(this);
3200 			listWidget = new ListWidget(this);
3201 			listWidget.multiSelect = false;
3202 			listWidget.addEventListener(EventType.change, delegate(Widget, Event) {
3203 				string c = null;
3204 				foreach(option; listWidget.options)
3205 					if(option.selected) {
3206 						c = option.label;
3207 						break;
3208 					}
3209 				lineEdit.content = c;
3210 			});
3211 
3212 			listWidget.tabStop = false;
3213 			this.tabStop = false;
3214 			listWidget.addEventListener("focusin", &lineEdit.focus);
3215 			this.addEventListener("focusin", &lineEdit.focus);
3216 
3217 			addDirectEventListener(EventType.change, {
3218 				listWidget.setSelection(selection_);
3219 				if(selection_ != -1)
3220 					lineEdit.content = options[selection_];
3221 				lineEdit.focus();
3222 				redraw();
3223 			});
3224 
3225 			lineEdit.addEventListener("focusin", &lineEdit.selectAll);
3226 
3227 			listWidget.addDirectEventListener(EventType.change, {
3228 				int set = -1;
3229 				foreach(idx, opt; listWidget.options)
3230 					if(opt.selected) {
3231 						set = cast(int) idx;
3232 						break;
3233 					}
3234 				if(set != selection_)
3235 					this.setSelection(set);
3236 			});
3237 		} else static assert(false);
3238 	}
3239 
3240 	override int minHeight() { return defaultLineHeight * 3; }
3241 	override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; }
3242 	override int heightStretchiness() { return 5; }
3243 
3244 	version(custom_widgets) {
3245 		LineEdit lineEdit;
3246 		ListWidget listWidget;
3247 
3248 		override void addOption(string s) {
3249 			listWidget.addOption(s);
3250 			ComboboxBase.addOption(s);
3251 		}
3252 
3253 		override void scrollSelectionIntoView() {
3254 			listWidget.scrollSelectionIntoView();
3255 		}
3256 	}
3257 }
3258 
3259 /+
3260 class Spinner : Widget {
3261 	version(win32_widgets)
3262 	this(Widget parent) {
3263 		super(parent);
3264 		parentWindow = parent.parentWindow;
3265 		auto hlayout = new HorizontalLayout(this);
3266 		lineEdit = new LineEdit(hlayout);
3267 		upDownControl = new UpDownControl(hlayout);
3268 	}
3269 
3270 	LineEdit lineEdit;
3271 	UpDownControl upDownControl;
3272 }
3273 
3274 class UpDownControl : Widget {
3275 	version(win32_widgets)
3276 	this(Widget parent) {
3277 		super(parent);
3278 		parentWindow = parent.parentWindow;
3279 		createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */);
3280 	}
3281 
3282 	override int minHeight() { return defaultLineHeight; }
3283 	override int maxHeight() { return defaultLineHeight * 3/2; }
3284 
3285 	override int minWidth() { return defaultLineHeight * 3/2; }
3286 	override int maxWidth() { return defaultLineHeight * 3/2; }
3287 }
3288 +/
3289 
3290 /+
3291 class DataView : Widget {
3292 	// this is the omnibus data viewer
3293 	// the internal data layout is something like:
3294 	// string[string][] but also each node can have parents
3295 }
3296 +/
3297 
3298 
3299 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS
3300 
3301 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d
3302 
3303 // FIXME: menus should prolly capture the mouse. ugh i kno.
3304 /*
3305 	TextEdit needs:
3306 
3307 	* caret manipulation
3308 	* selection control
3309 	* convenience functions for appendText, insertText, insertTextAtCaret, etc.
3310 
3311 	For example:
3312 
3313 	connect(paste, &textEdit.insertTextAtCaret);
3314 
3315 	would be nice.
3316 
3317 
3318 
3319 	I kinda want an omnibus dataview that combines list, tree,
3320 	and table - it can be switched dynamically between them.
3321 
3322 	Flattening policy: only show top level, show recursive, show grouped
3323 	List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer)
3324 
3325 	Single select, multi select, organization, drag+drop
3326 */
3327 
3328 //static if(UsingSimpledisplayX11)
3329 version(win32_widgets) {}
3330 else version(custom_widgets) {
3331 	enum scrollClickRepeatInterval = 50;
3332 
3333 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") {
3334 	enum windowBackgroundColor = Color(212, 212, 212); // used to be 192
3335 	enum activeTabColor = lightAccentColor;
3336 	enum hoveringColor = Color(228, 228, 228);
3337 	enum buttonColor = windowBackgroundColor;
3338 	enum depressedButtonColor = darkAccentColor;
3339 	enum activeListXorColor = Color(255, 255, 127);
3340 	enum progressBarColor = Color(0, 0, 128);
3341 	enum activeMenuItemColor = Color(0, 0, 128);
3342 
3343 }}
3344 else static assert(false);
3345 deprecated("Get these properties off the `visualTheme` instead.") {
3346 	// these are used by horizontal rule so not just custom_widgets. for now at least.
3347 	enum darkAccentColor = Color(172, 172, 172);
3348 	enum lightAccentColor = Color(223, 223, 223); // used to be 223
3349 }
3350 
3351 private const(wchar)* toWstringzInternal(in char[] s) {
3352 	wchar[] str;
3353 	str.reserve(s.length + 1);
3354 	foreach(dchar ch; s)
3355 		str ~= ch;
3356 	str ~= '\0';
3357 	return str.ptr;
3358 }
3359 
3360 static if(SimpledisplayTimerAvailable)
3361 void setClickRepeat(Widget w, int interval, int delay = 250) {
3362 	Timer timer;
3363 	int delayRemaining = delay / interval;
3364 	if(delayRemaining <= 1)
3365 		delayRemaining = 2;
3366 
3367 	immutable originalDelayRemaining = delayRemaining;
3368 
3369 	w.addDirectEventListener((scope MouseDownEvent ev) {
3370 		if(ev.srcElement !is w)
3371 			return;
3372 		if(timer !is null) {
3373 			timer.destroy();
3374 			timer = null;
3375 		}
3376 		delayRemaining = originalDelayRemaining;
3377 		timer = new Timer(interval, () {
3378 			if(delayRemaining > 0)
3379 				delayRemaining--;
3380 			else {
3381 				auto ev = new Event("triggered", w);
3382 				ev.sendDirectly();
3383 			}
3384 		});
3385 	});
3386 
3387 	w.addDirectEventListener((scope MouseUpEvent ev) {
3388 		if(ev.srcElement !is w)
3389 			return;
3390 		if(timer !is null) {
3391 			timer.destroy();
3392 			timer = null;
3393 		}
3394 	});
3395 
3396 	w.addDirectEventListener((scope MouseLeaveEvent ev) {
3397 		if(ev.srcElement !is w)
3398 			return;
3399 		if(timer !is null) {
3400 			timer.destroy();
3401 			timer = null;
3402 		}
3403 	});
3404 
3405 }
3406 else
3407 void setClickRepeat(Widget w, int interval, int delay = 250) {}
3408 
3409 enum FrameStyle {
3410 	none, ///
3411 	risen, /// a 3d pop-out effect (think Windows 95 button)
3412 	sunk, /// a 3d sunken effect (think Windows 95 button as you click on it)
3413 	solid, ///
3414 	dotted, ///
3415 	fantasy, /// a style based on a popular fantasy video game
3416 	rounded, /// a rounded rectangle
3417 }
3418 
3419 version(custom_widgets)
3420 deprecated
3421 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) {
3422 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
3423 }
3424 
3425 version(custom_widgets)
3426 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) {
3427 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, background);
3428 }
3429 
3430 version(custom_widgets)
3431 deprecated
3432 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) {
3433 	draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
3434 }
3435 
3436 int getBorderWidth(FrameStyle style) {
3437 	final switch(style) {
3438 		case FrameStyle.sunk, FrameStyle.risen:
3439 			return 2;
3440 		case FrameStyle.none:
3441 			return 0;
3442 		case FrameStyle.solid:
3443 			return 1;
3444 		case FrameStyle.dotted:
3445 			return 1;
3446 		case FrameStyle.fantasy:
3447 			return 3;
3448 		case FrameStyle.rounded:
3449 			return 2;
3450 	}
3451 }
3452 
3453 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) {
3454 	int borderWidth = getBorderWidth(style);
3455 	final switch(style) {
3456 		case FrameStyle.sunk, FrameStyle.risen:
3457 			// outer layer
3458 			painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black;
3459 		break;
3460 		case FrameStyle.none:
3461 			painter.outlineColor = background;
3462 		break;
3463 		case FrameStyle.solid:
3464 		case FrameStyle.rounded:
3465 			painter.pen = Pen(border, 1);
3466 		break;
3467 		case FrameStyle.dotted:
3468 			painter.pen = Pen(border, 1, Pen.Style.Dotted);
3469 		break;
3470 		case FrameStyle.fantasy:
3471 			painter.pen = Pen(border, 3);
3472 		break;
3473 	}
3474 
3475 	painter.fillColor = background;
3476 
3477 	if(style == FrameStyle.rounded) {
3478 		painter.drawRectangleRounded(Point(x, y), Size(width, height), 6);
3479 	} else {
3480 		painter.drawRectangle(Point(x + 0, y + 0), width, height);
3481 
3482 		if(style == FrameStyle.sunk || style == FrameStyle.risen) {
3483 			// 3d effect
3484 			auto vt = WidgetPainter.visualTheme;
3485 
3486 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor;
3487 			painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0));
3488 			painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1));
3489 
3490 			// inner layer
3491 			//right, bottom
3492 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor;
3493 			painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2));
3494 			painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2));
3495 			// left, top
3496 			painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white;
3497 			painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1));
3498 			painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2));
3499 		} else if(style == FrameStyle.fantasy) {
3500 			painter.pen = Pen(Color.white, 1, Pen.Style.Solid);
3501 			painter.fillColor = Color.transparent;
3502 			painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1));
3503 		}
3504 	}
3505 
3506 	return borderWidth;
3507 }
3508 
3509 /++
3510 	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.
3511 
3512 	See_Also:
3513 		[MenuItem]
3514 		[ToolButton]
3515 		[Menu.addItem]
3516 +/
3517 class Action {
3518 	version(win32_widgets) {
3519 		private int id;
3520 		private static int lastId = 9000;
3521 		private static Action[int] mapping;
3522 	}
3523 
3524 	KeyEvent accelerator;
3525 
3526 	// FIXME: disable message
3527 	// and toggle thing?
3528 	// ??? and trigger arguments too ???
3529 
3530 	/++
3531 		Params:
3532 			label = the textual label
3533 			icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons.
3534 			triggered = initial handler, more can be added via the [triggered] member.
3535 	+/
3536 	this(string label, ushort icon = 0, void delegate() triggered = null) {
3537 		this.label = label;
3538 		this.iconId = icon;
3539 		if(triggered !is null)
3540 			this.triggered ~= triggered;
3541 		version(win32_widgets) {
3542 			id = ++lastId;
3543 			mapping[id] = this;
3544 		}
3545 	}
3546 
3547 	private string label;
3548 	private ushort iconId;
3549 	// icon
3550 
3551 	// when it is triggered, the triggered event is fired on the window
3552 	/// The list of handlers when it is triggered.
3553 	void delegate()[] triggered;
3554 }
3555 
3556 /*
3557 	plan:
3558 		keyboard accelerators
3559 
3560 		* menus (and popups and tooltips)
3561 		* status bar
3562 		* toolbars and buttons
3563 
3564 		sortable table view
3565 
3566 		maybe notification area icons
3567 		basic clipboard
3568 
3569 		* radio box
3570 		splitter
3571 		toggle buttons (optionally mutually exclusive, like in Paint)
3572 		label, rich text display, multi line plain text (selectable)
3573 		* fieldset
3574 		* nestable grid layout
3575 		single line text input
3576 		* multi line text input
3577 		slider
3578 		spinner
3579 		list box
3580 		drop down
3581 		combo box
3582 		auto complete box
3583 		* progress bar
3584 
3585 		terminal window/widget (on unix it might even be a pty but really idk)
3586 
3587 		ok button
3588 		cancel button
3589 
3590 		keyboard hotkeys
3591 
3592 		scroll widget
3593 
3594 		event redirections and network transparency
3595 		script integration
3596 */
3597 
3598 
3599 /*
3600 	MENUS
3601 
3602 	auto bar = new MenuBar(window);
3603 	window.menuBar = bar;
3604 
3605 	auto fileMenu = bar.addItem(new Menu("&File"));
3606 	fileMenu.addItem(new MenuItem("&Exit"));
3607 
3608 
3609 	EVENTS
3610 
3611 	For controls, you should usually use "triggered" rather than "click", etc., because
3612 	triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation.
3613 	This is the case on menus and pushbuttons.
3614 
3615 	"click", on the other hand, currently only fires when it is literally clicked by the mouse.
3616 */
3617 
3618 
3619 /*
3620 enum LinePreference {
3621 	AlwaysOnOwnLine, // always on its own line
3622 	PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way
3623 	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
3624 }
3625 */
3626 
3627 /++
3628 	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.
3629 
3630 	---
3631 	class MyWidget : Widget {
3632 		this(Widget parent) { super(parent); }
3633 
3634 		// set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go:
3635 		mixin Padding!q{4};
3636 
3637 		// set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go:
3638 		mixin Margin!q{8};
3639 
3640 		// but if I specify one outside, it overrides the override, so now marginLeft is 2,
3641 		// while Top/Bottom/Right remain 8 from the mixin above.
3642 		override int marginLeft() { return 2; }
3643 	}
3644 	---
3645 
3646 
3647 	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]).
3648 
3649 	Padding is the area inside a widget where its background is drawn, but the content avoids.
3650 
3651 	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!).
3652 
3653 	* 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.
3654 +/
3655 mixin template Padding(string code) {
3656 	override int paddingLeft() { return mixin(code);}
3657 	override int paddingRight() { return mixin(code);}
3658 	override int paddingTop() { return mixin(code);}
3659 	override int paddingBottom() { return mixin(code);}
3660 }
3661 
3662 /// ditto
3663 mixin template Margin(string code) {
3664 	override int marginLeft() { return mixin(code);}
3665 	override int marginRight() { return mixin(code);}
3666 	override int marginTop() { return mixin(code);}
3667 	override int marginBottom() { return mixin(code);}
3668 }
3669 
3670 private
3671 void recomputeChildLayout(string relevantMeasure)(Widget parent) {
3672 	enum calcingV = relevantMeasure == "height";
3673 
3674 	parent.registerMovement();
3675 
3676 	if(parent.children.length == 0)
3677 		return;
3678 
3679 	auto parentStyle = parent.getComputedStyle();
3680 
3681 	enum firstThingy = relevantMeasure == "height" ? "Top" : "Left";
3682 	enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right";
3683 
3684 	enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top";
3685 	enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom";
3686 
3687 	// my own width and height should already be set by the caller of this function...
3688 	int spaceRemaining = mixin("parent." ~ relevantMeasure) -
3689 		mixin("parentStyle.padding"~firstThingy~"()") -
3690 		mixin("parentStyle.padding"~secondThingy~"()");
3691 
3692 	int stretchinessSum;
3693 	int stretchyChildSum;
3694 	int lastMargin = 0;
3695 
3696 	int shrinkinessSum;
3697 	int shrinkyChildSum;
3698 
3699 	// set initial size
3700 	foreach(child; parent.children) {
3701 
3702 		auto childStyle = child.getComputedStyle();
3703 
3704 		if(cast(StaticPosition) child)
3705 			continue;
3706 		if(child.hidden)
3707 			continue;
3708 
3709 		const iw = child.flexBasisWidth();
3710 		const ih = child.flexBasisHeight();
3711 
3712 		static if(calcingV) {
3713 			child.width = parent.width -
3714 				mixin("childStyle.margin"~otherFirstThingy~"()") -
3715 				mixin("childStyle.margin"~otherSecondThingy~"()") -
3716 				mixin("parentStyle.padding"~otherFirstThingy~"()") -
3717 				mixin("parentStyle.padding"~otherSecondThingy~"()");
3718 
3719 			if(child.width < 0)
3720 				child.width = 0;
3721 			if(child.width > childStyle.maxWidth())
3722 				child.width = childStyle.maxWidth();
3723 
3724 			if(iw > 0) {
3725 				auto totalPossible = child.width;
3726 				if(child.width > iw && child.widthStretchiness() == 0)
3727 					child.width = iw;
3728 			}
3729 
3730 			child.height = mymax(childStyle.minHeight(), ih);
3731 		} else {
3732 			// set to take all the space
3733 			child.height = parent.height -
3734 				mixin("childStyle.margin"~firstThingy~"()") -
3735 				mixin("childStyle.margin"~secondThingy~"()") -
3736 				mixin("parentStyle.padding"~firstThingy~"()") -
3737 				mixin("parentStyle.padding"~secondThingy~"()");
3738 
3739 			// then clamp it
3740 			if(child.height < 0)
3741 				child.height = 0;
3742 			if(child.height > childStyle.maxHeight())
3743 				child.height = childStyle.maxHeight();
3744 
3745 			// and if possible, respect the ideal target
3746 			if(ih > 0) {
3747 				auto totalPossible = child.height;
3748 				if(child.height > ih && child.heightStretchiness() == 0)
3749 					child.height = ih;
3750 			}
3751 
3752 			// if we have an ideal, try to respect it, otehrwise, just use the minimum
3753 			child.width = mymax(childStyle.minWidth(), iw);
3754 		}
3755 
3756 		spaceRemaining -= mixin("child." ~ relevantMeasure);
3757 
3758 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3759 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3760 		lastMargin = margin;
3761 		spaceRemaining -= thisMargin + margin;
3762 
3763 		auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3764 		stretchinessSum += s;
3765 		if(s > 0)
3766 			stretchyChildSum++;
3767 
3768 		auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()");
3769 		shrinkinessSum += s2;
3770 		if(s2 > 0)
3771 			shrinkyChildSum++;
3772 	}
3773 
3774 	if(spaceRemaining < 0 && shrinkyChildSum) {
3775 		// shrink to get into the space if it is possible
3776 		auto toRemove = -spaceRemaining;
3777 		auto removalPerItem = toRemove / shrinkinessSum;
3778 		auto remainder = toRemove % shrinkinessSum;
3779 
3780 		// FIXME: wtf why am i shrinking things with no shrinkiness?
3781 
3782 		foreach(child; parent.children) {
3783 			auto childStyle = child.getComputedStyle();
3784 			if(cast(StaticPosition) child)
3785 				continue;
3786 			if(child.hidden)
3787 				continue;
3788 			static if(calcingV) {
3789 				auto minimum = childStyle.minHeight();
3790 				auto stretch = childStyle.heightShrinkiness();
3791 			} else {
3792 				auto minimum = childStyle.minWidth();
3793 				auto stretch = childStyle.widthShrinkiness();
3794 			}
3795 
3796 			if(mixin("child._" ~ relevantMeasure) <= minimum)
3797 				continue;
3798 			// import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch);
3799 
3800 			mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh.
3801 
3802 			spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum;
3803 		}
3804 	}
3805 
3806 	// stretch to fill space
3807 	while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) {
3808 		auto spacePerChild = spaceRemaining / stretchinessSum;
3809 		bool spreadEvenly;
3810 		bool giveToBiggest;
3811 		if(spacePerChild <= 0) {
3812 			spacePerChild = spaceRemaining / stretchyChildSum;
3813 			spreadEvenly = true;
3814 		}
3815 		if(spacePerChild <= 0) {
3816 			giveToBiggest = true;
3817 		}
3818 		int previousSpaceRemaining = spaceRemaining;
3819 		stretchinessSum = 0;
3820 		Widget mostStretchy;
3821 		int mostStretchyS;
3822 		foreach(child; parent.children) {
3823 			auto childStyle = child.getComputedStyle();
3824 			if(cast(StaticPosition) child)
3825 				continue;
3826 			if(child.hidden)
3827 				continue;
3828 			static if(calcingV) {
3829 				auto maximum = childStyle.maxHeight();
3830 			} else {
3831 				auto maximum = childStyle.maxWidth();
3832 			}
3833 
3834 			if(mixin("child." ~ relevantMeasure) >= maximum) {
3835 				auto adj = mixin("child." ~ relevantMeasure) - maximum;
3836 				mixin("child._" ~ relevantMeasure) -= adj;
3837 				spaceRemaining += adj;
3838 				continue;
3839 			}
3840 			auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3841 			if(s <= 0)
3842 				continue;
3843 			auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s);
3844 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3845 			spaceRemaining -= spaceAdjustment;
3846 			if(mixin("child." ~ relevantMeasure) > maximum) {
3847 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3848 				mixin("child._" ~ relevantMeasure) -= diff;
3849 				spaceRemaining += diff;
3850 			} else if(mixin("child._" ~ relevantMeasure) < maximum) {
3851 				stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3852 				if(mostStretchy is null || s >= mostStretchyS) {
3853 					mostStretchy = child;
3854 					mostStretchyS = s;
3855 				}
3856 			}
3857 		}
3858 
3859 		if(giveToBiggest && mostStretchy !is null) {
3860 			auto child = mostStretchy;
3861 			auto childStyle = child.getComputedStyle();
3862 			int spaceAdjustment = spaceRemaining;
3863 
3864 			static if(calcingV)
3865 				auto maximum = childStyle.maxHeight();
3866 			else
3867 				auto maximum = childStyle.maxWidth();
3868 
3869 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3870 			spaceRemaining -= spaceAdjustment;
3871 			if(mixin("child._" ~ relevantMeasure) > maximum) {
3872 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3873 				mixin("child._" ~ relevantMeasure) -= diff;
3874 				spaceRemaining += diff;
3875 			}
3876 		}
3877 
3878 		if(spaceRemaining == previousSpaceRemaining) {
3879 			if(mostStretchy !is null) {
3880 				static if(calcingV)
3881 					auto maximum = mostStretchy.maxHeight();
3882 				else
3883 					auto maximum = mostStretchy.maxWidth();
3884 
3885 				mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining;
3886 				if(mixin("mostStretchy._" ~ relevantMeasure) > maximum)
3887 					mixin("mostStretchy._" ~ relevantMeasure) = maximum;
3888 			}
3889 			break; // apparently nothing more we can do
3890 		}
3891 	}
3892 
3893 	foreach(child; parent.children) {
3894 		auto childStyle = child.getComputedStyle();
3895 		if(cast(StaticPosition) child)
3896 			continue;
3897 		if(child.hidden)
3898 			continue;
3899 
3900 		static if(calcingV)
3901 			auto maximum = childStyle.maxHeight();
3902 		else
3903 			auto maximum = childStyle.maxWidth();
3904 		if(mixin("child._" ~ relevantMeasure) > maximum)
3905 			mixin("child._" ~ relevantMeasure) = maximum;
3906 	}
3907 
3908 	// position
3909 	lastMargin = 0;
3910 	int currentPos = mixin("parent.padding"~firstThingy~"()");
3911 	foreach(child; parent.children) {
3912 		auto childStyle = child.getComputedStyle();
3913 		if(cast(StaticPosition) child) {
3914 			child.recomputeChildLayout();
3915 			continue;
3916 		}
3917 		if(child.hidden)
3918 			continue;
3919 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3920 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3921 		currentPos += thisMargin;
3922 		static if(calcingV) {
3923 			child.x = parentStyle.paddingLeft() + childStyle.marginLeft();
3924 			child.y = currentPos;
3925 		} else {
3926 			child.x = currentPos;
3927 			child.y = parentStyle.paddingTop() + childStyle.marginTop();
3928 
3929 		}
3930 		currentPos += mixin("child." ~ relevantMeasure);
3931 		currentPos += margin;
3932 		lastMargin = margin;
3933 
3934 		child.recomputeChildLayout();
3935 	}
3936 }
3937 
3938 int mymax(int a, int b) { return a > b ? a : b; }
3939 int mymax(int a, int b, int c) {
3940 	auto d = mymax(a, b);
3941 	return c > d ? c : d;
3942 }
3943 
3944 // OK so we need to make getting at the native window stuff possible in simpledisplay.d
3945 // and here, it must be integrable with the layout, the event system, and not be painted over.
3946 version(win32_widgets) {
3947 
3948 	// this function just does stuff that a parent window needs for redirection
3949 	int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
3950 		this_.hookedWndProc(msg, wParam, lParam);
3951 
3952 		switch(msg) {
3953 
3954 			case WM_VSCROLL, WM_HSCROLL:
3955 				auto pos = HIWORD(wParam);
3956 				auto m = LOWORD(wParam);
3957 
3958 				auto scrollbarHwnd = cast(HWND) lParam;
3959 
3960 				if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) {
3961 
3962 					//auto smw = cast(ScrollMessageWidget) widgetp.parent;
3963 
3964 					switch(m) {
3965 						/+
3966 						// I don't think those messages are ever actually sent normally by the widget itself,
3967 						// they are more used for the keyboard interface. methinks.
3968 						case SB_BOTTOM:
3969 							// writeln("end");
3970 							auto event = new Event("scrolltoend", *widgetp);
3971 							event.dispatch();
3972 							//if(!event.defaultPrevented)
3973 						break;
3974 						case SB_TOP:
3975 							// writeln("top");
3976 							auto event = new Event("scrolltobeginning", *widgetp);
3977 							event.dispatch();
3978 						break;
3979 						case SB_ENDSCROLL:
3980 							// idk
3981 						break;
3982 						+/
3983 						case SB_LINEDOWN:
3984 							(*widgetp).emitCommand!"scrolltonextline"();
3985 						return 0;
3986 						case SB_LINEUP:
3987 							(*widgetp).emitCommand!"scrolltopreviousline"();
3988 						return 0;
3989 						case SB_PAGEDOWN:
3990 							(*widgetp).emitCommand!"scrolltonextpage"();
3991 						return 0;
3992 						case SB_PAGEUP:
3993 							(*widgetp).emitCommand!"scrolltopreviouspage"();
3994 						return 0;
3995 						case SB_THUMBPOSITION:
3996 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3997 							ev.dispatch();
3998 						return 0;
3999 						case SB_THUMBTRACK:
4000 							// eh kinda lying but i like the real time update display
4001 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
4002 							ev.dispatch();
4003 
4004 							// the event loop doesn't seem to carry on with a requested redraw..
4005 							// so we request it to get our dirty bit set...
4006 							// then we need to immediately actually redraw it too for instant feedback to user
4007 							SimpleWindow.processAllCustomEvents();
4008 							SimpleWindow.processAllCustomEvents();
4009 							//if(this_.parentWindow)
4010 								//this_.parentWindow.actualRedraw();
4011 
4012 							// and this ensures the WM_PAINT message is sent fairly quickly
4013 							// still seems to lag a little in large windows but meh it basically works.
4014 							if(this_.parentWindow) {
4015 								// FIXME: if painting is slow, this does still lag
4016 								// we probably will want to expose some user hook to ScrollWindowEx
4017 								// or something.
4018 								UpdateWindow(this_.parentWindow.hwnd);
4019 							}
4020 						return 0;
4021 						default:
4022 					}
4023 				}
4024 			break;
4025 
4026 			case WM_CONTEXTMENU:
4027 				auto hwndFrom = cast(HWND) wParam;
4028 
4029 				auto xPos = cast(short) LOWORD(lParam);
4030 				auto yPos = cast(short) HIWORD(lParam);
4031 
4032 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
4033 					POINT p;
4034 					p.x = xPos;
4035 					p.y = yPos;
4036 					ScreenToClient(hwnd, &p);
4037 					auto clientX = cast(ushort) p.x;
4038 					auto clientY = cast(ushort) p.y;
4039 
4040 					auto wap = widgetAtPoint(*widgetp, clientX, clientY);
4041 
4042 					if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) {
4043 						return 0;
4044 					}
4045 				}
4046 			break;
4047 
4048 			case WM_DRAWITEM:
4049 				auto dis = cast(DRAWITEMSTRUCT*) lParam;
4050 				if(auto widgetp = dis.hwndItem in Widget.nativeMapping) {
4051 					return (*widgetp).handleWmDrawItem(dis);
4052 				}
4053 			break;
4054 
4055 			case WM_NOTIFY:
4056 				auto hdr = cast(NMHDR*) lParam;
4057 				auto hwndFrom = hdr.hwndFrom;
4058 				auto code = hdr.code;
4059 
4060 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
4061 					return (*widgetp).handleWmNotify(hdr, code, mustReturn);
4062 				}
4063 			break;
4064 			case WM_COMMAND:
4065 				auto handle = cast(HWND) lParam;
4066 				auto cmd = HIWORD(wParam);
4067 				return processWmCommand(hwnd, handle, cmd, LOWORD(wParam));
4068 
4069 			default:
4070 				// pass it on
4071 		}
4072 		return 0;
4073 	}
4074 
4075 
4076 
4077 	extern(Windows)
4078 	private
4079 	// this is called by native child windows, whereas the other hook is done by simpledisplay windows
4080 	// but can i merge them?!
4081 	LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
4082 		// try { writeln(iMessage); } catch(Exception e) {};
4083 
4084 		if(auto te = hWnd in Widget.nativeMapping) {
4085 			try {
4086 
4087 				te.hookedWndProc(iMessage, wParam, lParam);
4088 
4089 				int mustReturn;
4090 				auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn);
4091 				if(mustReturn)
4092 					return ret;
4093 
4094 				if(iMessage == WM_SETFOCUS) {
4095 					auto lol = *te;
4096 					while(lol !is null && lol.implicitlyCreated)
4097 						lol = lol.parent;
4098 					lol.focus();
4099 					//(*te).parentWindow.focusedWidget = lol;
4100 				}
4101 
4102 
4103 				if(iMessage == WM_CTLCOLOREDIT) {
4104 
4105 				}
4106 				if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) {
4107 					SetBkMode(cast(HDC) wParam, TRANSPARENT);
4108 					return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color...
4109 						//GetStockObject(NULL_BRUSH);
4110 				}
4111 
4112 				auto pos = getChildPositionRelativeToParentOrigin(*te);
4113 				lastDefaultPrevented = false;
4114 				// try { writeln(typeid(*te)); } catch(Exception e) {}
4115 				if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented)
4116 					return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam);
4117 				else {
4118 					// it was something we recognized, should only call the window procedure if the default was not prevented
4119 				}
4120 			} catch(Exception e) {
4121 				assert(0, e.toString());
4122 			}
4123 			return 0;
4124 		}
4125 		assert(0, "shouldn't be receiving messages for this window....");
4126 		//assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen
4127 	}
4128 
4129 	extern(Windows)
4130 	private
4131 	// see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/
4132 	LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
4133 		if(iMessage == WM_ERASEBKGND) {
4134 			auto dc = GetDC(hWnd);
4135 			auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE));
4136 			auto p = SelectObject(dc, GetStockObject(NULL_PEN));
4137 			RECT r;
4138 			GetWindowRect(hWnd, &r);
4139 			// since the pen is null, to fill the whole space, we need the +1 on both.
4140 			gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1);
4141 			SelectObject(dc, p);
4142 			SelectObject(dc, b);
4143 			ReleaseDC(hWnd, dc);
4144 			InvalidateRect(hWnd, null, false); // redraw the border
4145 			return 1;
4146 		}
4147 		return HookedWndProc(hWnd, iMessage, wParam, lParam);
4148 	}
4149 
4150 	/++
4151 		Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create
4152 		needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest
4153 		of minigui's expectations.
4154 
4155 		This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window
4156 		member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor.
4157 
4158 		It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`.
4159 
4160 		To check if you can use this, use `static if(UsingWin32Widgets)`.
4161 	+/
4162 	void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) {
4163 		assert(p.parentWindow !is null);
4164 		assert(p.parentWindow.win.impl.hwnd !is null);
4165 
4166 		auto bsgroupbox = style == BS_GROUPBOX;
4167 
4168 		HWND phwnd;
4169 
4170 		auto wtf = p.parent;
4171 		while(wtf) {
4172 			if(wtf.hwnd !is null) {
4173 				phwnd = wtf.hwnd;
4174 				break;
4175 			}
4176 			wtf = wtf.parent;
4177 		}
4178 
4179 		if(phwnd is null)
4180 			phwnd = p.parentWindow.win.impl.hwnd;
4181 
4182 		assert(phwnd !is null);
4183 
4184 		WCharzBuffer wt = WCharzBuffer(windowText);
4185 
4186 		style |= WS_VISIBLE | WS_CHILD;
4187 		//if(className != WC_TABCONTROL)
4188 			style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
4189 		p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style,
4190 				CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
4191 				phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null);
4192 
4193 		assert(p.hwnd !is null);
4194 
4195 
4196 		static HFONT font;
4197 		if(font is null) {
4198 			NONCLIENTMETRICS params;
4199 			params.cbSize = params.sizeof;
4200 			if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, &params, 0)) {
4201 				font = CreateFontIndirect(&params.lfMessageFont);
4202 			}
4203 		}
4204 
4205 		if(font)
4206 			SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true);
4207 
4208 		p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd);
4209 		p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
4210 		Widget.nativeMapping[p.hwnd] = p;
4211 
4212 		if(bsgroupbox)
4213 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK);
4214 		else
4215 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4216 
4217 		EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
4218 
4219 		p.registerMovement();
4220 	}
4221 }
4222 
4223 version(win32_widgets)
4224 private
4225 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) {
4226 	if(hwnd is null || hwnd in Widget.nativeMapping)
4227 		return true;
4228 	auto parent = cast(Widget) cast(void*) lparam;
4229 	Widget p = new Widget(null);
4230 	p._parent = parent;
4231 	p.parentWindow = parent.parentWindow;
4232 	p.hwnd = hwnd;
4233 	p.implicitlyCreated = true;
4234 	Widget.nativeMapping[p.hwnd] = p;
4235 	p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4236 	return true;
4237 }
4238 
4239 /++
4240 	Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness.
4241 +/
4242 struct WidgetPainter {
4243 	this(ScreenPainter screenPainter, Widget drawingUpon) {
4244 		this.drawingUpon = drawingUpon;
4245 		this.screenPainter = screenPainter;
4246 
4247 		this.widgetClipRectangle = screenPainter.currentClipRectangle;
4248 
4249 		// this.screenPainter.impl.enableXftDraw();
4250 		if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi))
4251 			this.screenPainter.setFont(font);
4252 	}
4253 
4254 	/++
4255 		EXPERIMENTAL. subject to change.
4256 
4257 		When you draw a cursor, you can draw this to notify your window of where it is,
4258 		for IME systems to use.
4259 	+/
4260 	void notifyCursorPosition(int x, int y, int width, int height) {
4261 		if(auto a = drawingUpon.parentWindow)
4262 		if(auto w = a.inputProxy) {
4263 			w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height);
4264 		}
4265 	}
4266 
4267 	private Rectangle widgetClipRectangle;
4268 
4269 	private Rectangle setClipRectangleForWidget(Point upperLeft, int width, int height) {
4270 		widgetClipRectangle = Rectangle(upperLeft, Size(width, height));
4271 
4272 		return screenPainter.setClipRectangle(widgetClipRectangle);
4273 	}
4274 
4275 	/++
4276 		Sets the clip rectangle to the given settings. It will automatically calculate the intersection
4277 		of your widget's content boundaries and your requested clip rectangle.
4278 
4279 		History:
4280 			Before February 26, 2025, you could sometimes exceed widget boundaries, as this forwarded
4281 			directly to the underlying `ScreenPainter`. It now wraps it to calculate the intersection.
4282 	+/
4283 	Rectangle setClipRectangle(Rectangle rectangle) {
4284 		return screenPainter.setClipRectangle(rectangle.intersectionOf(widgetClipRectangle));
4285 	}
4286 	/// ditto
4287 	Rectangle setClipRectangle(Point upperLeft, int width, int height) {
4288 		return setClipRectangle(Rectangle(upperLeft, Size(width, height)));
4289 	}
4290 	/// ditto
4291 	Rectangle setClipRectangle(Point upperLeft, Size size) {
4292 		return setClipRectangle(Rectangle(upperLeft, size));
4293 	}
4294 
4295 	///
4296 	ScreenPainter screenPainter;
4297 	/// Forward to the screen painter for all other methods, see [arsd.simpledisplay.ScreenPainter] for more information
4298 	alias screenPainter this;
4299 
4300 	private Widget drawingUpon;
4301 
4302 	/++
4303 		This is the list of rectangles that actually need to be redrawn.
4304 
4305 		Not actually implemented yet.
4306 	+/
4307 	Rectangle[] invalidatedRectangles;
4308 
4309 	private static BaseVisualTheme _visualTheme;
4310 
4311 	/++
4312 		Functions to access the visual theme and helpers to easily use it.
4313 
4314 		These are aware of the current widget's computed style out of the theme.
4315 	+/
4316 	static @property BaseVisualTheme visualTheme() {
4317 		if(_visualTheme is null)
4318 			_visualTheme = new DefaultVisualTheme();
4319 		return _visualTheme;
4320 	}
4321 
4322 	/// ditto
4323 	static @property void visualTheme(BaseVisualTheme theme) {
4324 		_visualTheme = theme;
4325 
4326 		// FIXME: notify all windows about the new theme, they should recompute layout and redraw.
4327 	}
4328 
4329 	/// ditto
4330 	Color themeForeground() {
4331 		return drawingUpon.getComputedStyle().foregroundColor();
4332 	}
4333 
4334 	/// ditto
4335 	Color themeBackground() {
4336 		return drawingUpon.getComputedStyle().background.color;
4337 	}
4338 
4339 	int isDarkTheme() {
4340 		return 0; // unspecified, yes, no as enum. FIXME
4341 	}
4342 
4343 	/++
4344 		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.
4345 
4346 		It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding.
4347 
4348 		If you change teh clip rectangle, you should change it back before you return.
4349 
4350 
4351 		The sequence it uses is:
4352 			background
4353 			content (delegated to you)
4354 			border
4355 			focused outline
4356 			selected overlay
4357 
4358 		Example code:
4359 
4360 		---
4361 		void paint(WidgetPainter painter) {
4362 			painter.drawThemed((bounds) {
4363 				return bounds; // if the selection overlay should be contained, you can return it here.
4364 			});
4365 		}
4366 		---
4367 	+/
4368 	void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) {
4369 		drawThemed((WidgetPainter painter, const Rectangle bounds) {
4370 			return drawBody(bounds);
4371 		});
4372 	}
4373 	// this overload is actually mroe for setting the delegate to a virtual function
4374 	void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) {
4375 		Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height);
4376 
4377 		auto cs = drawingUpon.getComputedStyle();
4378 
4379 		auto bg = cs.background.color;
4380 
4381 		auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor);
4382 
4383 		rect.left += borderWidth;
4384 		rect.right -= borderWidth;
4385 		rect.top += borderWidth;
4386 		rect.bottom -= borderWidth;
4387 
4388 		auto insideBorderRect = rect;
4389 
4390 		rect.left += cs.paddingLeft;
4391 		rect.right -= cs.paddingRight;
4392 		rect.top += cs.paddingTop;
4393 		rect.bottom -= cs.paddingBottom;
4394 
4395 		this.outlineColor = this.themeForeground;
4396 		this.fillColor = bg;
4397 
4398 		auto widgetFont = cs.fontCached;
4399 		if(widgetFont !is null)
4400 			this.setFont(widgetFont);
4401 
4402 		rect = drawBody(this, rect);
4403 
4404 		if(widgetFont !is null) {
4405 			if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi))
4406 				this.setFont(vtFont);
4407 			else
4408 				this.setFont(null);
4409 		}
4410 
4411 		if(auto os = cs.outlineStyle()) {
4412 			this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid);
4413 			this.fillColor = Color.transparent;
4414 			this.drawRectangle(insideBorderRect);
4415 		}
4416 	}
4417 
4418 	/++
4419 		First, draw the background.
4420 		Then draw your content.
4421 		Next, draw the border.
4422 		And the focused indicator.
4423 		And the is-selected box.
4424 
4425 		If it is focused i can draw the outline too...
4426 
4427 		If selected i can even do the xor action but that's at the end.
4428 	+/
4429 	void drawThemeBackground() {
4430 
4431 	}
4432 
4433 	void drawThemeBorder() {
4434 
4435 	}
4436 
4437 	// all this stuff is a dangerous experiment....
4438 	static class ScriptableVersion {
4439 		ScreenPainterImplementation* p;
4440 		int originX, originY;
4441 
4442 		@scriptable:
4443 		void drawRectangle(int x, int y, int width, int height) {
4444 			p.drawRectangle(x + originX, y + originY, width, height);
4445 		}
4446 		void drawLine(int x1, int y1, int x2, int y2) {
4447 			p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY);
4448 		}
4449 		void drawText(int x, int y, string text) {
4450 			p.drawText(x + originX, y + originY, 100000, 100000, text, 0);
4451 		}
4452 		void setOutlineColor(int r, int g, int b) {
4453 			p.pen = Pen(Color(r,g,b), 1);
4454 		}
4455 		void setFillColor(int r, int g, int b) {
4456 			p.fillColor = Color(r,g,b);
4457 		}
4458 	}
4459 
4460 	ScriptableVersion toArsdJsvar() {
4461 		auto sv = new ScriptableVersion;
4462 		sv.p = this.screenPainter.impl;
4463 		sv.originX = this.screenPainter.originX;
4464 		sv.originY = this.screenPainter.originY;
4465 		return sv;
4466 	}
4467 
4468 	static WidgetPainter fromJsVar(T)(T t) {
4469 		return WidgetPainter.init;
4470 	}
4471 	// done..........
4472 }
4473 
4474 
4475 struct Style {
4476 	static struct helper(string m, T) {
4477 		enum method = m;
4478 		T v;
4479 
4480 		mixin template MethodOverride(typeof(this) v) {
4481 			mixin("override typeof(v.v) "~v.method~"() { return v.v; }");
4482 		}
4483 	}
4484 
4485 	static auto opDispatch(string method, T)(T value) {
4486 		return helper!(method, T)(value);
4487 	}
4488 }
4489 
4490 /++
4491 	Implementation detail of the [ControlledBy] UDA.
4492 
4493 	History:
4494 		Added Oct 28, 2020
4495 +/
4496 struct ControlledBy_(T, Args...) {
4497 	Args args;
4498 
4499 	static if(Args.length)
4500 	this(Args args) {
4501 		this.args = args;
4502 	}
4503 
4504 	private T construct(Widget parent) {
4505 		return new T(args, parent);
4506 	}
4507 }
4508 
4509 /++
4510 	User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them.
4511 
4512 	History:
4513 		Added Oct 28, 2020
4514 +/
4515 auto ControlledBy(T, Args...)(Args args) {
4516 	return ControlledBy_!(T, Args)(args);
4517 }
4518 
4519 struct ContainerMeta {
4520 	string name;
4521 	ContainerMeta[] children;
4522 	Widget function(Widget parent) factory;
4523 
4524 	Widget instantiate(Widget parent) {
4525 		auto n = factory(parent);
4526 		n.name = name;
4527 		foreach(child; children)
4528 			child.instantiate(n);
4529 		return n;
4530 	}
4531 }
4532 
4533 /++
4534 	This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See
4535 	http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information.
4536 
4537 	Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level
4538 	structures. It works fine on structs declared inside functions though.
4539 
4540 	See: https://issues.dlang.org/show_bug.cgi?id=21984
4541 +/
4542 template Container(CArgs...) {
4543 	static if(CArgs.length && is(CArgs[0] : Widget)) {
4544 		private alias Super = CArgs[0];
4545 		private alias CArgs2 = CArgs[1 .. $];
4546 	} else {
4547 		private alias Super = Layout;
4548 		private alias CArgs2 = CArgs;
4549 	}
4550 
4551 	class Container : Super {
4552 		this(Widget parent) { super(parent); }
4553 
4554 		// just to partially support old gdc versions
4555 		version(GNU) {
4556 			static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); }
4557 			static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); }
4558 			static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); }
4559 			static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time");
4560 		} else mixin(q{
4561 			static foreach(Arg; CArgs2) {
4562 				mixin Arg.MethodOverride!(Arg);
4563 			}
4564 		});
4565 
4566 		static ContainerMeta opCall(string name, ContainerMeta[] children...) {
4567 			return ContainerMeta(
4568 				name,
4569 				children.dup,
4570 				function (Widget parent) { return new typeof(this)(parent); }
4571 			);
4572 		}
4573 
4574 		static ContainerMeta opCall(ContainerMeta[] children...) {
4575 			return opCall(null, children);
4576 		}
4577 	}
4578 }
4579 
4580 /++
4581 	The data controller widget is created by reflecting over the given
4582 	data type. You can use [ControlledBy] as a UDA on a struct or
4583 	just let it create things automatically.
4584 
4585 	Unlike [dialog], this uses real-time updating of the data and
4586 	you add it to another window yourself.
4587 
4588 	---
4589 		struct Test {
4590 			int x;
4591 			int y;
4592 		}
4593 
4594 		auto window = new Window();
4595 		auto dcw = new DataControllerWidget!Test(new Test, window);
4596 	---
4597 
4598 	The way it works is any public members are given a widget based
4599 	on their data type, and public methods trigger an action button
4600 	if no relevant parameters or a dialog action if it does have
4601 	parameters, similar to the [menu] facility.
4602 
4603 	If you change data programmatically, without going through the
4604 	DataControllerWidget methods, you will have to tell it something
4605 	has changed and it needs to redraw. This is done with the `invalidate`
4606 	method.
4607 
4608 	History:
4609 		Added Oct 28, 2020
4610 +/
4611 /// Group: generating_from_code
4612 class DataControllerWidget(T) : WidgetContainer {
4613 	static if(is(T == class) || is(T == interface) || is(T : const E[], E))
4614 		private alias Tref = T;
4615 	else
4616 		private alias Tref = T*;
4617 
4618 	Tref datum;
4619 
4620 	/++
4621 		See_also: [addDataControllerWidget]
4622 	+/
4623 	this(Tref datum, Widget parent) {
4624 		this.datum = datum;
4625 
4626 		Widget cp = this;
4627 
4628 		super(parent);
4629 
4630 		foreach(attr; __traits(getAttributes, T))
4631 			static if(is(typeof(attr) == ContainerMeta)) {
4632 				cp = attr.instantiate(this);
4633 			}
4634 
4635 		auto def = this.getByName("default");
4636 		if(def !is null)
4637 			cp = def;
4638 
4639 		Widget helper(string name) {
4640 			auto maybe = this.getByName(name);
4641 			if(maybe is null)
4642 				return cp;
4643 			return maybe;
4644 
4645 		}
4646 
4647 		foreach(member; __traits(allMembers, T))
4648 		static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011
4649 		static if(is(typeof(__traits(getMember, this.datum, member))))
4650 		static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") {
4651 			void delegate() update;
4652 
4653 			auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update);
4654 
4655 			if(update)
4656 				updaters ~= update;
4657 
4658 			static if(is(typeof(__traits(getMember, this.datum, member)) == function)) {
4659 				w.addEventListener("triggered", delegate() {
4660 					makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))();
4661 					notifyDataUpdated();
4662 				});
4663 			} else static if(is(typeof(w.isChecked) == bool)) {
4664 				w.addEventListener(EventType.change, (Event ev) {
4665 					__traits(getMember, this.datum, member) = w.isChecked;
4666 				});
4667 			} else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) {
4668 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } );
4669 			} else static if(is(typeof(w.value) == int)) {
4670 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4671 			} else static if(is(typeof(w) == DropDownSelection)) {
4672 				// special case for this to kinda support enums and such. coudl be better though
4673 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4674 			} else {
4675 				//static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof);
4676 			}
4677 		}
4678 	}
4679 
4680 	/++
4681 		If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages.
4682 
4683 		History:
4684 			Added May 28, 2021
4685 	+/
4686 	void notifyDataUpdated() {
4687 		foreach(updater; updaters)
4688 			updater();
4689 
4690 		this.emit!(ChangeEvent!void)(delegate{});
4691 	}
4692 
4693 	private Widget[string] memberWidgets;
4694 	private void delegate()[] updaters;
4695 
4696 	mixin Emits!(ChangeEvent!void);
4697 }
4698 
4699 private int saturatedSum(int[] values...) {
4700 	int sum;
4701 	foreach(value; values) {
4702 		if(value == int.max)
4703 			return int.max;
4704 		sum += value;
4705 	}
4706 	return sum;
4707 }
4708 
4709 void genericSetValue(T, W)(T* where, W what) {
4710 	import std.conv;
4711 	*where = to!T(what);
4712 	//*where = cast(T) stringToLong(what);
4713 }
4714 
4715 /++
4716 	Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent.
4717 
4718 	The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget.
4719 
4720 	Note that this creates the widget but does not attach any event handlers to it.
4721 +/
4722 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) {
4723 
4724 	string displayName = __traits(identifier, tt).beautify;
4725 
4726 	static if(controlledByCount!tt == 1) {
4727 		foreach(i, attr; __traits(getAttributes, tt)) {
4728 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) {
4729 				auto w = attr.construct(parent);
4730 				static if(__traits(compiles, w.setPosition(*valptr)))
4731 					update = () { w.setPosition(*valptr); };
4732 				else static if(__traits(compiles, w.setValue(*valptr)))
4733 					update = () { w.setValue(*valptr); };
4734 
4735 				if(update)
4736 					update();
4737 				return w;
4738 			}
4739 		}
4740 	} else static if(controlledByCount!tt == 0) {
4741 		static if(is(typeof(tt) == enum)) {
4742 			// FIXME: update
4743 			auto dds = new DropDownSelection(parent);
4744 			foreach(idx, option; __traits(allMembers, typeof(tt))) {
4745 				dds.addOption(option);
4746 				if(__traits(getMember, typeof(tt), option) == *valptr)
4747 					dds.setSelection(cast(int) idx);
4748 			}
4749 			return dds;
4750 		} else static if(is(typeof(tt) == bool)) {
4751 			auto box = new Checkbox(displayName, parent);
4752 			update = () { box.isChecked = *valptr; };
4753 			update();
4754 			return box;
4755 		} else static if(is(typeof(tt) : const long)) {
4756 			auto le = new LabeledLineEdit(displayName, parent);
4757 			update = () { le.content = toInternal!string(*valptr); };
4758 			update();
4759 			return le;
4760 		} else static if(is(typeof(tt) : const double)) {
4761 			auto le = new LabeledLineEdit(displayName, parent);
4762 			import std.conv;
4763 			update = () { le.content = to!string(*valptr); };
4764 			update();
4765 			return le;
4766 		} else static if(is(typeof(tt) : const string)) {
4767 			auto le = new LabeledLineEdit(displayName, parent);
4768 			update = () { le.content = *valptr; };
4769 			update();
4770 			return le;
4771 		} else static if(is(typeof(tt) == E[], E)) {
4772 			auto w = new ArrayEditingWidget!E(parent);
4773 			// FIXME update
4774 			return w;
4775 		} else static if(is(typeof(tt) == function)) {
4776 			auto w = new Button(displayName, parent);
4777 			return w;
4778 		} else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) {
4779 			return parent.addDataControllerWidget(tt);
4780 		} else static assert(0, typeof(tt).stringof);
4781 	} else static assert(0, "multiple controllers not yet supported");
4782 }
4783 
4784 class ArrayEditingWidget(T) : ArrayEditingWidgetBase {
4785 	this(Widget parent) {
4786 		super(parent);
4787 	}
4788 }
4789 
4790 class ArrayEditingWidgetBase : Widget {
4791 	this(Widget parent) {
4792 		super(parent);
4793 
4794 		// FIXME: a trash can to move items into to delete them?
4795 		static class MyListViewItem : GenericListViewItem {
4796 			this(Widget parent) {
4797 				super(parent);
4798 
4799 				/+
4800 					drag handle
4801 						left click lets you move the whole selection. if the current element is not selected, it changes the selection to it.
4802 						right click here gives you the movement controls too
4803 					index/key view zone
4804 						left click here selects/unselects
4805 					element view/edit zone
4806 					delete button
4807 				+/
4808 
4809 				// FIXME: make sure the index is viewable
4810 
4811 				auto hl = new HorizontalLayout(this);
4812 
4813 				button = new CommandButton("d", hl);
4814 
4815 				label = new TextLabel("unloaded", TextAlignment.Left, hl);
4816 				// if member editable, have edit view... get from the subclass.
4817 
4818 				// or a "..." menu?
4819 				button = new CommandButton("Up", hl); // shift+click is move to top
4820 				button = new CommandButton("Down", hl); // shift+click is move to bottom
4821 				button = new CommandButton("Move to", hl); // move before, after, or swap
4822 				button = new CommandButton("Delete", hl);
4823 
4824 				button.addEventListener("triggered", delegate(){
4825 					//messageBox(text("clicked ", currentIndexLoaded()));
4826 				});
4827 			}
4828 			override void showItem(int idx) {
4829 				label.label = "Item ";// ~ to!string(idx);
4830 			}
4831 
4832 			TextLabel label;
4833 			Button button;
4834 		}
4835 
4836 		auto outer_this = this;
4837 
4838 		// FIXME: make sure item count is easy to see
4839 
4840 		glvw = new class GenericListViewWidget {
4841 			this() {
4842 				super(outer_this);
4843 			}
4844 			override GenericListViewItem itemFactory(Widget parent) {
4845 				return new MyListViewItem(parent);
4846 			}
4847 			override Size itemSize() {
4848 				return Size(0, scaleWithDpi(80));
4849 			}
4850 
4851 			override Menu contextMenu(int x, int y) {
4852 				return createContextMenuFromAnnotatedCode(this);
4853 			}
4854 
4855 			@context_menu {
4856 				void Select_All() {
4857 
4858 				}
4859 
4860 				void Undo() {
4861 
4862 				}
4863 
4864 				void Redo() {
4865 
4866 				}
4867 
4868 				void Cut() {
4869 
4870 				}
4871 
4872 				void Copy() {
4873 
4874 				}
4875 
4876 				void Paste() {
4877 
4878 				}
4879 
4880 				void Delete() {
4881 
4882 				}
4883 
4884 				void Find() {
4885 
4886 				}
4887 			}
4888 		};
4889 
4890 		glvw.setItemCount(400);
4891 
4892 		auto hl = new HorizontalLayout(this);
4893 		add = new FreeEntrySelection(hl);
4894 		addButton = new Button("Add", hl);
4895 	}
4896 
4897 	GenericListViewWidget glvw;
4898 	ComboboxBase add;
4899 	Button addButton;
4900 	/+
4901 		Controls:
4902 			clear (select all / delete)
4903 			reset (confirmation blocked button, maybe only on the whole form? or hit undo so many times to get back there)
4904 			add item
4905 				palette of options to add to the array (add prolly a combo box)
4906 			rearrange - move up/down, drag and drop a selection? right click can always do, left click only drags when on a selection handle.
4907 			edit/input/view items (GLVW? or it could be a table view in a way.)
4908 			undo/redo
4909 			select whole elements (even if a struct)
4910 			cut/copy/paste elements
4911 
4912 			could have an element picker, a details pane, and an add bare?
4913 
4914 
4915 			put a handle on the elements for left click dragging. allow right click drag anywhere but pretty big wiggle until it enables.
4916 			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.
4917 			the handle should let dragging w/o changing the selection, or if part of the selection, drag the whole selection i think.
4918 			make it textured and use the grabby hand mouse cursor.
4919 	+/
4920 }
4921 
4922 /++
4923 	A button that pops up a menu on click for working on a particular item or selection.
4924 
4925 	History:
4926 		Added March 23, 2025
4927 +/
4928 class MenuPopupButton : Button {
4929 	/++
4930 		You might consider using [createContextMenuFromAnnotatedCode] to populate the `menu` argument.
4931 
4932 		You also may want to set the [prepare] delegate after construction.
4933 	+/
4934 	this(Menu menu, Widget parent) {
4935 		assert(menu !is null);
4936 
4937 		this.menu = menu;
4938 		super("...", parent);
4939 	}
4940 
4941 	private Menu menu;
4942 	/++
4943 		If set, this delegate is called before popping up the window. This gives you a chance
4944 		to prepare your dynamic data structures for the element(s) selected.
4945 
4946 		For example, if your `MenuPopupButton` is attached to a [GenericListViewItem], you can call
4947 		[GenericListViewItem.currentIndexLoaded] in here and set it to a variable in the object you
4948 		called [createContextMenuFromAnnotatedCode] to apply the operation to the right object.
4949 
4950 		(The api could probably be simpler...)
4951 	+/
4952 	void delegate() prepare;
4953 
4954 	override void defaultEventHandler_triggered(scope Event e) {
4955 		if(prepare)
4956 			prepare();
4957 		showContextMenu(this.x, this.y + this.height, -2, -2, menu);
4958 	}
4959 
4960 	override int maxHeight() {
4961 		return defaultLineHeight;
4962 	}
4963 
4964 	override int maxWidth() {
4965 		return defaultLineHeight;
4966 	}
4967 }
4968 
4969 /++
4970 	A button that pops up an information box, similar to a tooltip, but explicitly triggered.
4971 
4972 	FIXME: i want to be able to easily embed these in other things too.
4973 +/
4974 class TipPopupButton : Button {
4975 	/++
4976 	+/
4977 	this(Widget delegate(Widget p) factory, Widget parent) {
4978 		this.factory = factory;
4979 		super("?", parent);
4980 	}
4981 
4982 	private Widget delegate(Widget p) factory;
4983 
4984 	override void defaultEventHandler_triggered(scope Event e) {
4985 		auto window = new TooltipWindow(factory, this);
4986 		window.popup(this);
4987 	}
4988 }
4989 
4990 /++
4991 	History:
4992 		Added March 23, 2025
4993 +/
4994 class TooltipWindow : Window {
4995 	void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
4996 		/+
4997 		this.menuParent = parent;
4998 
4999 		previouslyFocusedWidget = parent.parentWindow.focusedWidget;
5000 		previouslyFocusedWidgetBelongsIn = &parent.parentWindow.focusedWidget;
5001 		parent.parentWindow.focusedWidget = this;
5002 
5003 		int w = 150;
5004 		int h = paddingTop + paddingBottom;
5005 		if(this.children.length) {
5006 			// hacking it to get the ideal height out of recomputeChildLayout
5007 			this.width = w;
5008 			this.height = h;
5009 			this.recomputeChildLayoutEntry();
5010 			h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
5011 			h += paddingBottom;
5012 
5013 			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
5014 		}
5015 		+/
5016 
5017 		if(offsetY == int.min)
5018 			offsetY = parent.defaultLineHeight;
5019 
5020 		int w = 150;
5021 		int h = 50;
5022 
5023 		auto coord = parent.globalCoordinates();
5024 		dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
5025 
5026 		static if(UsingSimpledisplayX11)
5027 			XSync(XDisplayConnection.get, 0);
5028 
5029 		dropDown.visibilityChanged = (bool visible) {
5030 			if(visible) {
5031 				this.redraw();
5032 				dropDown.grabInput();
5033 			} else {
5034 				dropDown.releaseInputGrab();
5035 			}
5036 		};
5037 
5038 		dropDown.show();
5039 
5040 		clickListener = this.addEventListener((scope ClickEvent ev) {
5041 			unpopup();
5042 			// need to unlock asap just in case other user handlers block...
5043 			static if(UsingSimpledisplayX11)
5044 				flushGui();
5045 		}, true /* again for asap action */);
5046 	}
5047 
5048 	private EventListener clickListener;
5049 
5050 	void unpopup() {
5051 		mouseLastOver = mouseLastDownOn = null;
5052 		dropDown.hide();
5053 		clickListener.disconnect();
5054 	}
5055 
5056 	private SimpleWindow dropDown;
5057 	private Widget child;
5058 
5059 	///
5060 	this(Widget delegate(Widget p) factory, Widget parent) {
5061 		assert(parent);
5062 		assert(parent.parentWindow);
5063 		assert(parent.parentWindow.win);
5064 		dropDown = new SimpleWindow(
5065 			250, 40,
5066 			null, OpenGlOptions.no, Resizability.fixedSize,
5067 			WindowTypes.tooltip,
5068 			WindowFlags.dontAutoShow,
5069 			parent ? parent.parentWindow.win : null
5070 		);
5071 
5072 		super(dropDown);
5073 
5074 		child = factory(this);
5075 	}
5076 }
5077 
5078 private template controlledByCount(alias tt) {
5079 	static int helper() {
5080 		int count;
5081 		foreach(i, attr; __traits(getAttributes, tt))
5082 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...))
5083 				count++;
5084 		return count;
5085 	}
5086 
5087 	enum controlledByCount = helper;
5088 }
5089 
5090 /++
5091 	Intended for UFCS action like `window.addDataControllerWidget(new MyObject());`
5092 
5093 	If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method.
5094 
5095 	History:
5096 		The `redrawOnChange` parameter was added on May 28, 2021.
5097 +/
5098 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) {
5099 	auto dcw = new DataControllerWidget!T(t, parent);
5100 	initializeDataControllerWidget(dcw, redrawOnChange);
5101 	return dcw;
5102 }
5103 
5104 /// ditto
5105 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) {
5106 	auto dcw = new DataControllerWidget!T(t, parent);
5107 	initializeDataControllerWidget(dcw, redrawOnChange);
5108 	return dcw;
5109 }
5110 
5111 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) {
5112 	if(redrawOnChange !is null)
5113 		w.addEventListener("change", delegate() { redrawOnChange.redraw(); });
5114 }
5115 
5116 /++
5117 	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.
5118 
5119 	History:
5120 		Finalized on June 3, 2021 for the dub v10.0 release
5121 +/
5122 struct StyleInformation {
5123 	private Widget w;
5124 	private BaseVisualTheme visualTheme;
5125 
5126 	private this(Widget w) {
5127 		this.w = w;
5128 		this.visualTheme = WidgetPainter.visualTheme;
5129 	}
5130 
5131 	/++
5132 		Forwards to [Widget.Style]
5133 
5134 		Bugs:
5135 			It is supposed to fall back to the [VisualTheme] if
5136 			the style doesn't override the default, but that is
5137 			not generally implemented. Many of them may end up
5138 			being explicit overloads instead of the generic
5139 			opDispatch fallback, like [font] is now.
5140 	+/
5141 	public @property opDispatch(string name)() {
5142 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
5143 		w.useStyleProperties((scope Widget.Style props) {
5144 		//visualTheme.useStyleProperties(w, (props) {
5145 			prop = __traits(getMember, props, name);
5146 		});
5147 		return prop;
5148 	}
5149 
5150 	/++
5151 		Returns the cached font object associated with the widget,
5152 		if overridden by the [Widget.Style|Style], or the [VisualTheme] if not.
5153 
5154 		History:
5155 			Prior to March 21, 2022 (dub v10.7), `font` went through
5156 			[opDispatch], which did not use the cache. You can now call it
5157 			repeatedly without guilt.
5158 	+/
5159 	public @property OperatingSystemFont font() {
5160 		OperatingSystemFont prop;
5161 		w.useStyleProperties((scope Widget.Style props) {
5162 			prop = props.fontCached;
5163 		});
5164 		if(prop is null) {
5165 			prop = visualTheme.defaultFontCached(w.currentDpi);
5166 		}
5167 		return prop;
5168 	}
5169 
5170 	@property {
5171 		// Layout helpers. Currently just forwarding since I haven't made up my mind on a better way.
5172 		/** */ int paddingLeft() { return w.paddingLeft(); }
5173 		/** */ int paddingRight() { return w.paddingRight(); }
5174 		/** */ int paddingTop() { return w.paddingTop(); }
5175 		/** */ int paddingBottom() { return w.paddingBottom(); }
5176 
5177 		/** */ int marginLeft() { return w.marginLeft(); }
5178 		/** */ int marginRight() { return w.marginRight(); }
5179 		/** */ int marginTop() { return w.marginTop(); }
5180 		/** */ int marginBottom() { return w.marginBottom(); }
5181 
5182 		/** */ int maxHeight() { return w.maxHeight(); }
5183 		/** */ int minHeight() { return w.minHeight(); }
5184 
5185 		/** */ int maxWidth() { return w.maxWidth(); }
5186 		/** */ int minWidth() { return w.minWidth(); }
5187 
5188 		/** */ int flexBasisWidth() { return w.flexBasisWidth(); }
5189 		/** */ int flexBasisHeight() { return w.flexBasisHeight(); }
5190 
5191 		/** */ int heightStretchiness() { return w.heightStretchiness(); }
5192 		/** */ int widthStretchiness() { return w.widthStretchiness(); }
5193 
5194 		/** */ int heightShrinkiness() { return w.heightShrinkiness(); }
5195 		/** */ int widthShrinkiness() { return w.widthShrinkiness(); }
5196 
5197 		// Global helpers some of these are unstable.
5198 		static:
5199 		/** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
5200 		/** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
5201 		/** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
5202 		/** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
5203 		/** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); }
5204 		/** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
5205 
5206 		/** */ Color activeTabColor() { return lightAccentColor; }
5207 		/** */ Color buttonColor() { return windowBackgroundColor; }
5208 		/** */ Color depressedButtonColor() { return darkAccentColor; }
5209 		/** the background color of the widget when mouse hovering over it, if it responds to mouse hovers */ Color hoveringColor() { return lightAccentColor; }
5210 		deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() {
5211 			auto c = WidgetPainter.visualTheme.selectionColor();
5212 			return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
5213 		}
5214 		/** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
5215 		/** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
5216 	}
5217 
5218 
5219 
5220 	/+
5221 
5222 	private static auto extractStyleProperty(string name)(Widget w) {
5223 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
5224 		w.useStyleProperties((props) {
5225 			prop = __traits(getMember, props, name);
5226 		});
5227 		return prop;
5228 	}
5229 
5230 	// FIXME: clear this upon a X server disconnect
5231 	private static OperatingSystemFont[string] fontCache;
5232 
5233 	T getProperty(T)(string name, lazy T default_) {
5234 		if(visualTheme !is null) {
5235 			auto str = visualTheme.getPropertyString(w, name);
5236 			if(str is null)
5237 				return default_;
5238 			static if(is(T == Color))
5239 				return Color.fromString(str);
5240 			else static if(is(T == Measurement))
5241 				return Measurement(cast(int) toInternal!int(str));
5242 			else static if(is(T == WidgetBackground))
5243 				return WidgetBackground.fromString(str);
5244 			else static if(is(T == OperatingSystemFont)) {
5245 				if(auto f = str in fontCache)
5246 					return *f;
5247 				else
5248 					return fontCache[str] = new OperatingSystemFont(str);
5249 			} else static if(is(T == FrameStyle)) {
5250 				switch(str) {
5251 					default:
5252 						return FrameStyle.none;
5253 					foreach(style; __traits(allMembers, FrameStyle))
5254 					case style:
5255 						return __traits(getMember, FrameStyle, style);
5256 				}
5257 			} else static assert(0);
5258 		} else
5259 			return default_;
5260 	}
5261 
5262 	static struct Measurement {
5263 		int value;
5264 		alias value this;
5265 	}
5266 
5267 	@property:
5268 
5269 	int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); }
5270 	int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); }
5271 	int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); }
5272 	int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); }
5273 
5274 	int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); }
5275 	int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); }
5276 	int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); }
5277 	int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); }
5278 
5279 	int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); }
5280 	int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); }
5281 
5282 	int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); }
5283 	int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); }
5284 
5285 
5286 	WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); }
5287 	Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); }
5288 
5289 	OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); }
5290 
5291 	FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); }
5292 	Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); }
5293 
5294 	FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); }
5295 	Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); }
5296 
5297 
5298 	Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
5299 	Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
5300 	Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
5301 	Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
5302 
5303 	Color activeTabColor() { return lightAccentColor; }
5304 	Color buttonColor() { return windowBackgroundColor; }
5305 	Color depressedButtonColor() { return darkAccentColor; }
5306 	Color hoveringColor() { return Color(228, 228, 228); }
5307 	Color activeListXorColor() {
5308 		auto c = WidgetPainter.visualTheme.selectionColor();
5309 		return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
5310 	}
5311 	Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); }
5312 	Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); }
5313 	+/
5314 }
5315 
5316 
5317 
5318 // pragma(msg, __traits(classInstanceSize, Widget));
5319 
5320 /*private*/ template EventString(E) {
5321 	static if(is(typeof(E.EventString)))
5322 		enum EventString = E.EventString;
5323 	else
5324 		enum EventString = E.mangleof; // FIXME fqn? or something more user friendly
5325 }
5326 
5327 /*private*/ template EventStringIdentifier(E) {
5328 	string helper() {
5329 		auto es = EventString!E;
5330 		char[] id = new char[](es.length * 2);
5331 		size_t idx;
5332 		foreach(char ch; es) {
5333 			id[idx++] = cast(char)('a' + (ch >> 4));
5334 			id[idx++] = cast(char)('a' + (ch & 0x0f));
5335 		}
5336 		return cast(string) id;
5337 	}
5338 
5339 	enum EventStringIdentifier = helper();
5340 }
5341 
5342 
5343 template classStaticallyEmits(This, EventType) {
5344 	static if(is(This Base == super))
5345 		static if(is(Base : Widget))
5346 			enum baseEmits = classStaticallyEmits!(Base, EventType);
5347 		else
5348 			enum baseEmits = false;
5349 	else
5350 		enum baseEmits = false;
5351 
5352 	enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]);
5353 
5354 	enum classStaticallyEmits = thisEmits || baseEmits;
5355 }
5356 
5357 /++
5358 	A helper to make widgets out of other native windows.
5359 
5360 	History:
5361 		Factored out of OpenGlWidget on November 5, 2021
5362 +/
5363 class NestedChildWindowWidget : Widget {
5364 	SimpleWindow win;
5365 
5366 	/++
5367 		Used on X to send focus to the appropriate child window when requested by the window manager.
5368 
5369 		Normally returns its own nested window. Can also return another child or null to revert to the parent
5370 		if you override it in a child class.
5371 
5372 		History:
5373 			Added April 2, 2022 (dub v10.8)
5374 	+/
5375 	SimpleWindow focusableWindow() {
5376 		return win;
5377 	}
5378 
5379 	///
5380 	// win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
5381 	this(SimpleWindow win, Widget parent) {
5382 		this.parentWindow = parent.parentWindow;
5383 		this.win = win;
5384 
5385 		super(parent);
5386 		windowsetup(win);
5387 	}
5388 
5389 	static protected SimpleWindow getParentWindow(Widget parent) {
5390 		assert(parent !is null);
5391 		SimpleWindow pwin = parent.parentWindow.win;
5392 
5393 		version(win32_widgets) {
5394 			HWND phwnd;
5395 			auto wtf = parent;
5396 			while(wtf) {
5397 				if(wtf.hwnd) {
5398 					phwnd = wtf.hwnd;
5399 					break;
5400 				}
5401 				wtf = wtf.parent;
5402 			}
5403 			// kinda a hack here just because the ctor below just needs a SimpleWindow wrapper....
5404 			if(phwnd)
5405 				pwin = new SimpleWindow(phwnd);
5406 		}
5407 
5408 		return pwin;
5409 	}
5410 
5411 	/++
5412 		Called upon the nested window being destroyed.
5413 		Remember the window has already been destroyed at
5414 		this point, so don't use the native handle for anything.
5415 
5416 		History:
5417 			Added April 3, 2022 (dub v10.8)
5418 	+/
5419 	protected void dispose() {
5420 
5421 	}
5422 
5423 	protected void windowsetup(SimpleWindow w) {
5424 		/*
5425 		win.onFocusChange = (bool getting) {
5426 			if(getting)
5427 				this.focus();
5428 		};
5429 		*/
5430 
5431 		/+
5432 		win.onFocusChange = (bool getting) {
5433 			if(getting) {
5434 				this.parentWindow.focusedWidget = this;
5435 				this.emit!FocusEvent();
5436 				this.emit!FocusInEvent();
5437 			} else {
5438 				this.emit!BlurEvent();
5439 				this.emit!FocusOutEvent();
5440 			}
5441 		};
5442 		+/
5443 
5444 		win.onDestroyed = () {
5445 			this.dispose();
5446 		};
5447 
5448 		version(win32_widgets) {
5449 			Widget.nativeMapping[win.hwnd] = this;
5450 			this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
5451 		} else {
5452 			win.setEventHandlers(
5453 				(MouseEvent e) {
5454 					Widget p = this;
5455 					while(p ! is parentWindow) {
5456 						e.x += p.x;
5457 						e.y += p.y;
5458 						p = p.parent;
5459 					}
5460 					parentWindow.dispatchMouseEvent(e);
5461 				},
5462 				(KeyEvent e) {
5463 					//writefln("%s %x   %s", cast(void*) win, cast(uint) e.key, e.key);
5464 					parentWindow.dispatchKeyEvent(e);
5465 				},
5466 				(dchar e) {
5467 					parentWindow.dispatchCharEvent(e);
5468 				},
5469 			);
5470 		}
5471 
5472 	}
5473 
5474 	override bool showOrHideIfNativeWindow(bool shouldShow) {
5475 		auto cur = hidden;
5476 		win.hidden = !shouldShow;
5477 		if(cur != shouldShow && shouldShow)
5478 			redraw();
5479 		return true;
5480 	}
5481 
5482 	/// OpenGL widgets cannot have child widgets. Do not call this.
5483 	/* @disable */ final override void addChild(Widget, int) {
5484 		throw new Error("cannot add children to OpenGL widgets");
5485 	}
5486 
5487 	/// When an opengl widget is laid out, it will adjust the glViewport for you automatically.
5488 	/// Keep in mind that events like mouse coordinates are still relative to your size.
5489 	override void registerMovement() {
5490 		// writefln("%d %d %d %d", x,y,width,height);
5491 		version(win32_widgets)
5492 			auto pos = getChildPositionRelativeToParentHwnd(this);
5493 		else
5494 			auto pos = getChildPositionRelativeToParentOrigin(this);
5495 		win.moveResize(pos[0], pos[1], width, height);
5496 
5497 		registerMovementAdditionalWork();
5498 		sendResizeEvent();
5499 	}
5500 
5501 	abstract void registerMovementAdditionalWork();
5502 }
5503 
5504 /++
5505 	Nests an opengl capable window inside this window as a widget.
5506 
5507 	You may also just want to create an additional [SimpleWindow] with
5508 	[OpenGlOptions.yes] yourself.
5509 
5510 	An OpenGL widget cannot have child widgets. It will throw if you try.
5511 +/
5512 static if(OpenGlEnabled)
5513 class OpenGlWidget : NestedChildWindowWidget {
5514 
5515 	override void registerMovementAdditionalWork() {
5516 		win.setAsCurrentOpenGlContext();
5517 	}
5518 
5519 	///
5520 	this(Widget parent) {
5521 		auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
5522 		super(win, parent);
5523 	}
5524 
5525 	override void paint(WidgetPainter painter) {
5526 		win.setAsCurrentOpenGlContext();
5527 		glViewport(0, 0, this.width, this.height);
5528 		win.redrawOpenGlSceneNow();
5529 	}
5530 
5531 	void redrawOpenGlScene(void delegate() dg) {
5532 		win.redrawOpenGlScene = dg;
5533 	}
5534 }
5535 
5536 /++
5537 	This demo shows how to draw text in an opengl scene.
5538 +/
5539 unittest {
5540 	import arsd.minigui;
5541 	import arsd.ttf;
5542 
5543 	void main() {
5544 		auto window = new Window();
5545 
5546 		auto widget = new OpenGlWidget(window);
5547 
5548 		// old means non-shader code so compatible with glBegin etc.
5549 		// tbh I haven't implemented new one in font yet...
5550 		// anyway, declaring here, will construct soon.
5551 		OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont;
5552 
5553 		// this is a little bit awkward, calling some methods through
5554 		// the underlying SimpleWindow `win` method, and you can't do this
5555 		// on a nanovega widget due to conflicts so I should probably fix
5556 		// the api to be a bit easier. But here it will work.
5557 		//
5558 		// Alternatively, you could load the font on the first draw, inside
5559 		// the redrawOpenGlScene, and keep a flag so you don't do it every
5560 		// time. That'd be a bit easier since the lib sets up the context
5561 		// by then guaranteed.
5562 		//
5563 		// But still, I wanna show this.
5564 		widget.win.visibleForTheFirstTime = delegate {
5565 			// must set the opengl context
5566 			widget.win.setAsCurrentOpenGlContext();
5567 
5568 			// if you were doing a OpenGL 3+ shader, this
5569 			// gets especially important to do in order. With
5570 			// old-style opengl, I think you can even do it
5571 			// in main(), but meh, let's show it more correctly.
5572 
5573 			// Anyway, now it is time to load the font from the
5574 			// OS (you can alternatively load one from a .ttf file
5575 			// you bundle with the application), then load the
5576 			// font into texture for drawing.
5577 
5578 			auto osfont = new OperatingSystemFont("DejaVu Sans", 18);
5579 
5580 			assert(!osfont.isNull()); // make sure it actually loaded
5581 
5582 			// using typeof to avoid repeating the long name lol
5583 			glfont = new typeof(glfont)(
5584 				// get the raw data from the font for loading in here
5585 				// since it doesn't use the OS function to draw the
5586 				// text, we gotta treat it more as a file than as
5587 				// a drawing api.
5588 				osfont.getTtfBytes(),
5589 				18, // need to respecify size since opengl world is different coordinate system
5590 
5591 				// these last two numbers are why it is called
5592 				// "Limited" font. It only loads the characters
5593 				// in the given range, since the texture atlas
5594 				// it references is all a big image generated ahead
5595 				// of time. You could maybe do the whole thing but
5596 				// idk how much memory that is.
5597 				//
5598 				// But here, 0-128 represents the ASCII range, so
5599 				// good enough for most English things, numeric labels,
5600 				// etc.
5601 				0,
5602 				128
5603 			);
5604 		};
5605 
5606 		widget.redrawOpenGlScene = () {
5607 			// now we can use the glfont's drawString function
5608 
5609 			// first some opengl setup. You can do this in one place
5610 			// on window first visible too in many cases, just showing
5611 			// here cuz it is easier for me.
5612 
5613 			// gonna need some alpha blending or it just looks awful
5614 			glEnable(GL_BLEND);
5615 			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
5616 			glClearColor(0,0,0,0);
5617 			glDepthFunc(GL_LEQUAL);
5618 
5619 			// Also need to enable 2d textures, since it draws the
5620 			// font characters as images baked in
5621 			glMatrixMode(GL_MODELVIEW);
5622 			glLoadIdentity();
5623 			glDisable(GL_DEPTH_TEST);
5624 			glEnable(GL_TEXTURE_2D);
5625 
5626 			// the orthographic matrix is best for 2d things like text
5627 			// so let's set that up. This matrix makes the coordinates
5628 			// in the opengl scene be one-to-one with the actual pixels
5629 			// on screen. (Not necessarily best, you may wish to scale
5630 			// things, but it does help keep fonts looking normal.)
5631 			glMatrixMode(GL_PROJECTION);
5632 			glLoadIdentity();
5633 			glOrtho(0, widget.width, widget.height, 0, 0, 1);
5634 
5635 			// you can do other glScale, glRotate, glTranslate, etc
5636 			// to the matrix here of course if you want.
5637 
5638 			// note the x,y coordinates here are for the text baseline
5639 			// NOT the upper-left corner. The baseline is like the line
5640 			// in the notebook you write on. Most the letters are actually
5641 			// above it, but some, like p and q, dip a bit below it.
5642 			//
5643 			// So if you're used to the upper left coordinate like the
5644 			// rest of simpledisplay/minigui usually do, do the
5645 			// y + glfont.ascent to bring it down a little. So this
5646 			// example puts the string in the upper left of the window.
5647 			glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green);
5648 
5649 			// re color btw: the function sets a solid color internally,
5650 			// but you actually COULD do your own thing for rainbow effects
5651 			// and the sort if you wanted too, by pulling its guts out.
5652 			// Just view its source for an idea of how it actually draws:
5653 			// http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332
5654 
5655 			// it gets a bit complicated with the character positioning,
5656 			// but the opengl parts are fairly simple: bind a texture,
5657 			// set the color, draw a quad for each letter.
5658 
5659 
5660 			// the last optional argument there btw is a bounding box
5661 			// it will/ use to word wrap and return an object you can
5662 			// use to implement scrolling or pagination; it tells how
5663 			// much of the string didn't fit in the box. But for simple
5664 			// labels we can just ignore that.
5665 
5666 
5667 			// I'd suggest drawing text as the last step, after you
5668 			// do your other drawing. You might use the push/pop matrix
5669 			// stuff to keep your place. You, in theory, should be able
5670 			// to do text in a 3d space but I've never actually tried
5671 			// that....
5672 		};
5673 
5674 		window.loop();
5675 	}
5676 }
5677 
5678 version(custom_widgets)
5679 private class TextListViewWidget : GenericListViewWidget {
5680 	static class TextListViewItem : GenericListViewItem {
5681 		ListWidget controller;
5682 		this(ListWidget controller, Widget parent) {
5683 			this.controller = controller;
5684 			this.tabStop = false;
5685 			super(parent);
5686 		}
5687 
5688 		ListWidget.Option* showing;
5689 
5690 		override void showItem(int idx) {
5691 			showing = idx < controller.options.length ? &controller.options[idx] : null;
5692 			redraw(); // is this necessary? the generic thing might call it...
5693 		}
5694 
5695 		override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
5696 			if(showing is null)
5697 				return bounds;
5698 			painter.drawText(bounds.upperLeft, showing.label);
5699 			return bounds;
5700 		}
5701 
5702 		static class Style : Widget.Style {
5703 			override WidgetBackground background() {
5704 				// FIXME: change it if it is focused or not
5705 				// 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
5706 				auto tlvi = cast(TextListViewItem) widget;
5707 				if(tlvi && tlvi.showing && tlvi && tlvi.showing.selected)
5708 					return WidgetBackground(true /*widget.parent.isFocused*/ ? WidgetPainter.visualTheme.selectionBackgroundColor : Color(128, 128, 128)); // FIXME: don't hardcode
5709 				return super.background();
5710 			}
5711 
5712 			override Color foregroundColor() {
5713 				auto tlvi = cast(TextListViewItem) widget;
5714 				return tlvi && tlvi.showing && tlvi && tlvi.showing.selected ? WidgetPainter.visualTheme.selectionForegroundColor : super.foregroundColor();
5715 			}
5716 
5717 			override FrameStyle outlineStyle() {
5718 				// FIXME: change it if it is focused or not
5719 				auto tlvi = cast(TextListViewItem) widget;
5720 				return (tlvi && tlvi.currentIndexLoaded() == tlvi.controller.focusOn) ? FrameStyle.dotted : super.outlineStyle();
5721 			}
5722 		}
5723 		mixin OverrideStyle!Style;
5724 
5725 		mixin Padding!q{2};
5726 
5727 		override void defaultEventHandler_click(ClickEvent event) {
5728 			if(event.button == MouseButton.left) {
5729 				controller.setSelection(currentIndexLoaded());
5730 				controller.focusOn = currentIndexLoaded();
5731 			}
5732 		}
5733 
5734 	}
5735 
5736 	ListWidget controller;
5737 
5738 	this(ListWidget parent) {
5739 		this.controller = parent;
5740 		this.tabStop = false; // this is only used as a child of the ListWidget
5741 		super(parent);
5742 
5743 		smw.movementPerButtonClick(1, itemSize().height);
5744 	}
5745 
5746 	override Size itemSize() {
5747 		return Size(0, defaultLineHeight + scaleWithDpi(4 /* the top and bottom padding */));
5748 	}
5749 
5750 	override GenericListViewItem itemFactory(Widget parent) {
5751 		return new TextListViewItem(controller, parent);
5752 	}
5753 
5754 	static class Style : Widget.Style {
5755 		override FrameStyle borderStyle() {
5756 			return FrameStyle.sunk;
5757 		}
5758 
5759 		override WidgetBackground background() {
5760 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
5761 		}
5762 	}
5763 	mixin OverrideStyle!Style;
5764 }
5765 
5766 /++
5767 	A list widget contains a list of strings that the user can examine and select.
5768 
5769 
5770 	In the future, items in the list may be possible to be more than just strings.
5771 
5772 	See_Also:
5773 		[TableView]
5774 +/
5775 class ListWidget : Widget {
5776 	/// 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.
5777 	mixin Emits!(ChangeEvent!void);
5778 
5779 	version(custom_widgets)
5780 		TextListViewWidget glvw;
5781 
5782 	static struct Option {
5783 		string label;
5784 		bool selected;
5785 		void* tag;
5786 	}
5787 	private Option[] options;
5788 
5789 	/++
5790 		Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete.
5791 	+/
5792 	void setSelection(int y) {
5793 		if(!multiSelect)
5794 			foreach(ref opt; options)
5795 				opt.selected = false;
5796 		if(y >= 0 && y < options.length)
5797 			options[y].selected = !options[y].selected;
5798 
5799 		version(custom_widgets)
5800 			focusOn = y;
5801 
5802 		this.emit!(ChangeEvent!void)(delegate {});
5803 
5804 		version(custom_widgets)
5805 			redraw();
5806 	}
5807 
5808 	/++
5809 		Gets the index of the selected item. In case of multi select, the index of the first selected item is returned.
5810 		Returns -1 if nothing is selected.
5811 	+/
5812 	int getSelection()
5813 	{
5814 		foreach(i, opt; options) {
5815 			if (opt.selected)
5816 				return cast(int) i;
5817 		}
5818 		return -1;
5819 	}
5820 
5821 	version(custom_widgets)
5822 	private int focusOn;
5823 
5824 	this(Widget parent) {
5825 		super(parent);
5826 
5827 		version(custom_widgets)
5828 			glvw = new TextListViewWidget(this);
5829 
5830 		version(win32_widgets)
5831 			createWin32Window(this, WC_LISTBOX, "",
5832 				0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0);
5833 	}
5834 
5835 	version(win32_widgets)
5836 	override void handleWmCommand(ushort code, ushort id) {
5837 		switch(code) {
5838 			case LBN_SELCHANGE:
5839 				auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0);
5840 				setSelection(cast(int) sel);
5841 			break;
5842 			default:
5843 		}
5844 	}
5845 
5846 
5847 	void addOption(string text, void* tag = null) {
5848 		options ~= Option(text, false, tag);
5849 		version(win32_widgets) {
5850 			WCharzBuffer buffer = WCharzBuffer(text);
5851 			SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr);
5852 		}
5853 		version(custom_widgets) {
5854 			glvw.setItemCount(cast(int) options.length);
5855 			//setContentSize(width, cast(int) (options.length * defaultLineHeight));
5856 			redraw();
5857 		}
5858 	}
5859 
5860 	void clear() {
5861 		options = null;
5862 		version(win32_widgets) {
5863 			while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0)
5864 				{}
5865 
5866 		} else version(custom_widgets) {
5867 			focusOn = -1;
5868 			glvw.setItemCount(0);
5869 			redraw();
5870 		}
5871 	}
5872 
5873 	version(custom_widgets)
5874 	override void defaultEventHandler_keydown(KeyDownEvent kde) {
5875 		void changedFocusOn() {
5876 			scrollFocusIntoView();
5877 			if(multiSelect)
5878 				redraw();
5879 			else
5880 				setSelection(focusOn);
5881 		}
5882 		switch(kde.key) {
5883 			case Key.Up:
5884 				if(focusOn) {
5885 					focusOn--;
5886 					changedFocusOn();
5887 				}
5888 			break;
5889 			case Key.Down:
5890 				if(focusOn + 1 < options.length) {
5891 					focusOn++;
5892 					changedFocusOn();
5893 				}
5894 			break;
5895 			case Key.Home:
5896 				if(focusOn) {
5897 					focusOn = 0;
5898 					changedFocusOn();
5899 				}
5900 			break;
5901 			case Key.End:
5902 				if(options.length && focusOn + 1 != options.length) {
5903 					focusOn = cast(int) options.length - 1;
5904 					changedFocusOn();
5905 				}
5906 			break;
5907 			case Key.PageUp:
5908 				auto n = glvw.numberOfCurrentlyFullyVisibleItems;
5909 				focusOn -= n;
5910 				if(focusOn < 0)
5911 					focusOn = 0;
5912 				changedFocusOn();
5913 			break;
5914 			case Key.PageDown:
5915 				if(options.length == 0)
5916 					break;
5917 				auto n = glvw.numberOfCurrentlyFullyVisibleItems;
5918 				focusOn += n;
5919 				if(focusOn >= options.length)
5920 					focusOn = cast(int) options.length - 1;
5921 				changedFocusOn();
5922 			break;
5923 
5924 			default:
5925 		}
5926 	}
5927 
5928 	version(custom_widgets)
5929 	override void defaultEventHandler_char(CharEvent ce) {
5930 		if(ce.character == '\n' || ce.character == ' ') {
5931 			setSelection(focusOn);
5932 		} else {
5933 			// search for the item that best matches and jump to it
5934 			// FIXME this sucks in tons of ways. the normal thing toolkits
5935 			// do here is to search for a substring on a timer, but i'd kinda
5936 			// rather make an actual little dialog with some options. still meh for now.
5937 			dchar search = ce.character;
5938 			if(search >= 'A' && search <= 'Z')
5939 				search += 32;
5940 			foreach(idx, option; options) {
5941 				auto ch = option.label.length ? option.label[0] : 0;
5942 				if(ch >= 'A' && ch <= 'Z')
5943 					ch += 32;
5944 				if(ch == search) {
5945 					setSelection(cast(int) idx);
5946 					scrollSelectionIntoView();
5947 					break;
5948 				}
5949 			}
5950 
5951 		}
5952 	}
5953 
5954 	version(win32_widgets)
5955 		enum multiSelect = false; /// not implemented yet
5956 	else
5957 		bool multiSelect;
5958 
5959 	override int heightStretchiness() { return 6; }
5960 
5961 	version(custom_widgets)
5962 	void scrollFocusIntoView() {
5963 		glvw.ensureItemVisibleInScroll(focusOn);
5964 	}
5965 
5966 	void scrollSelectionIntoView() {
5967 		// FIXME: implement on Windows
5968 
5969 		version(custom_widgets)
5970 			glvw.ensureItemVisibleInScroll(getSelection());
5971 	}
5972 
5973 	/*
5974 	version(custom_widgets)
5975 	override void defaultEventHandler_focusout(Event foe) {
5976 		glvw.redraw();
5977 	}
5978 
5979 	version(custom_widgets)
5980 	override void defaultEventHandler_focusin(Event foe) {
5981 		glvw.redraw();
5982 	}
5983 	*/
5984 
5985 }
5986 
5987 
5988 
5989 /// For [ScrollableWidget], determines when to show the scroll bar to the user.
5990 /// NEVER USED
5991 enum ScrollBarShowPolicy {
5992 	automatic, /// automatically show the scroll bar if it is necessary
5993 	never, /// never show the scroll bar (scrolling must be done programmatically)
5994 	always /// always show the scroll bar, even if it is disabled
5995 }
5996 
5997 /++
5998 	A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner.
5999 
6000 	It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code.
6001 +/
6002 // FIXME ScrollBarShowPolicy
6003 // FIXME: use the ScrollMessageWidget in here now that it exists
6004 deprecated("Use ScrollMessageWidget or ScrollableContainerWidget instead") // ugh compiler won't let me do it
6005 class ScrollableWidget : Widget {
6006 	// FIXME: make line size configurable
6007 	// FIXME: add keyboard controls
6008 	version(win32_widgets) {
6009 		override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) {
6010 			if(msg == WM_VSCROLL || msg == WM_HSCROLL) {
6011 				auto pos = HIWORD(wParam);
6012 				auto m = LOWORD(wParam);
6013 
6014 				// FIXME: I can reintroduce the
6015 				// scroll bars now by using this
6016 				// in the top-level window handler
6017 				// to forward comamnds
6018 				auto scrollbarHwnd = lParam;
6019 				switch(m) {
6020 					case SB_BOTTOM:
6021 						if(msg == WM_HSCROLL)
6022 							horizontalScrollTo(contentWidth_);
6023 						else
6024 							verticalScrollTo(contentHeight_);
6025 					break;
6026 					case SB_TOP:
6027 						if(msg == WM_HSCROLL)
6028 							horizontalScrollTo(0);
6029 						else
6030 							verticalScrollTo(0);
6031 					break;
6032 					case SB_ENDSCROLL:
6033 						// idk
6034 					break;
6035 					case SB_LINEDOWN:
6036 						if(msg == WM_HSCROLL)
6037 							horizontalScroll(scaleWithDpi(16));
6038 						else
6039 							verticalScroll(scaleWithDpi(16));
6040 					break;
6041 					case SB_LINEUP:
6042 						if(msg == WM_HSCROLL)
6043 							horizontalScroll(scaleWithDpi(-16));
6044 						else
6045 							verticalScroll(scaleWithDpi(-16));
6046 					break;
6047 					case SB_PAGEDOWN:
6048 						if(msg == WM_HSCROLL)
6049 							horizontalScroll(scaleWithDpi(100));
6050 						else
6051 							verticalScroll(scaleWithDpi(100));
6052 					break;
6053 					case SB_PAGEUP:
6054 						if(msg == WM_HSCROLL)
6055 							horizontalScroll(scaleWithDpi(-100));
6056 						else
6057 							verticalScroll(scaleWithDpi(-100));
6058 					break;
6059 					case SB_THUMBPOSITION:
6060 					case SB_THUMBTRACK:
6061 						if(msg == WM_HSCROLL)
6062 							horizontalScrollTo(pos);
6063 						else
6064 							verticalScrollTo(pos);
6065 
6066 						if(m == SB_THUMBTRACK) {
6067 							// the event loop doesn't seem to carry on with a requested redraw..
6068 							// so we request it to get our dirty bit set...
6069 							redraw();
6070 
6071 							// then we need to immediately actually redraw it too for instant feedback to user
6072 
6073 							SimpleWindow.processAllCustomEvents();
6074 							//if(parentWindow)
6075 								//parentWindow.actualRedraw();
6076 						}
6077 					break;
6078 					default:
6079 				}
6080 			}
6081 			return super.hookedWndProc(msg, wParam, lParam);
6082 		}
6083 	}
6084 	///
6085 	this(Widget parent) {
6086 		this.parentWindow = parent.parentWindow;
6087 
6088 		version(win32_widgets) {
6089 			createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "",
6090 				0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0);
6091 			super(parent);
6092 		} else version(custom_widgets) {
6093 			outerContainer = new InternalScrollableContainerWidget(this, parent);
6094 			super(outerContainer);
6095 		} else static assert(0);
6096 	}
6097 
6098 	version(custom_widgets)
6099 		InternalScrollableContainerWidget outerContainer;
6100 
6101 	override void defaultEventHandler_click(ClickEvent event) {
6102 		if(event.button == MouseButton.wheelUp)
6103 			verticalScroll(scaleWithDpi(-16));
6104 		if(event.button == MouseButton.wheelDown)
6105 			verticalScroll(scaleWithDpi(16));
6106 		super.defaultEventHandler_click(event);
6107 	}
6108 
6109 	override void defaultEventHandler_keydown(KeyDownEvent event) {
6110 		switch(event.key) {
6111 			case Key.Left:
6112 				horizontalScroll(scaleWithDpi(-16));
6113 			break;
6114 			case Key.Right:
6115 				horizontalScroll(scaleWithDpi(16));
6116 			break;
6117 			case Key.Up:
6118 				verticalScroll(scaleWithDpi(-16));
6119 			break;
6120 			case Key.Down:
6121 				verticalScroll(scaleWithDpi(16));
6122 			break;
6123 			case Key.Home:
6124 				verticalScrollTo(0);
6125 			break;
6126 			case Key.End:
6127 				verticalScrollTo(contentHeight);
6128 			break;
6129 			case Key.PageUp:
6130 				verticalScroll(scaleWithDpi(-160));
6131 			break;
6132 			case Key.PageDown:
6133 				verticalScroll(scaleWithDpi(160));
6134 			break;
6135 			default:
6136 		}
6137 		super.defaultEventHandler_keydown(event);
6138 	}
6139 
6140 
6141 	version(win32_widgets)
6142 	override void recomputeChildLayout() {
6143 		super.recomputeChildLayout();
6144 		SCROLLINFO info;
6145 		info.cbSize = info.sizeof;
6146 		info.nPage = viewportHeight;
6147 		info.fMask = SIF_PAGE | SIF_RANGE;
6148 		info.nMin = 0;
6149 		info.nMax = contentHeight_;
6150 		SetScrollInfo(hwnd, SB_VERT, &info, true);
6151 
6152 		info.cbSize = info.sizeof;
6153 		info.nPage = viewportWidth;
6154 		info.fMask = SIF_PAGE | SIF_RANGE;
6155 		info.nMin = 0;
6156 		info.nMax = contentWidth_;
6157 		SetScrollInfo(hwnd, SB_HORZ, &info, true);
6158 	}
6159 
6160 	/*
6161 		Scrolling
6162 		------------
6163 
6164 		You are assigned a width and a height by the layout engine, which
6165 		is your viewport box. However, you may draw more than that by setting
6166 		a contentWidth and contentHeight.
6167 
6168 		If these can be contained by the viewport, no scrollbar is displayed.
6169 		If they cannot fit though, it will automatically show scroll as necessary.
6170 
6171 		If contentWidth == 0, no horizontal scrolling is performed. If contentHeight
6172 		is zero, no vertical scrolling is performed.
6173 
6174 		If scrolling is necessary, the lib will automatically work with the bars.
6175 		When you redraw, the origin and clipping info in the painter is set so if
6176 		you just draw everything, it will work, but you can be more efficient by checking
6177 		the viewportWidth, viewportHeight, and scrollOrigin members.
6178 	*/
6179 
6180 	///
6181 	final @property int viewportWidth() {
6182 		return width - (showingVerticalScroll ? scaleWithDpi(16) : 0);
6183 	}
6184 	///
6185 	final @property int viewportHeight() {
6186 		return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0);
6187 	}
6188 
6189 	// FIXME property
6190 	Point scrollOrigin_;
6191 
6192 	///
6193 	final const(Point) scrollOrigin() {
6194 		return scrollOrigin_;
6195 	}
6196 
6197 	// the user sets these two
6198 	private int contentWidth_ = 0;
6199 	private int contentHeight_ = 0;
6200 
6201 	///
6202 	int contentWidth() { return contentWidth_; }
6203 	///
6204 	int contentHeight() { return contentHeight_; }
6205 
6206 	///
6207 	void setContentSize(int width, int height) {
6208 		contentWidth_ = width;
6209 		contentHeight_ = height;
6210 
6211 		version(custom_widgets) {
6212 			if(showingVerticalScroll || showingHorizontalScroll) {
6213 				outerContainer.queueRecomputeChildLayout();
6214 			}
6215 
6216 			if(showingVerticalScroll())
6217 				outerContainer.verticalScrollBar.redraw();
6218 			if(showingHorizontalScroll())
6219 				outerContainer.horizontalScrollBar.redraw();
6220 		} else version(win32_widgets) {
6221 			queueRecomputeChildLayout();
6222 		} else static assert(0);
6223 	}
6224 
6225 	///
6226 	void verticalScroll(int delta) {
6227 		verticalScrollTo(scrollOrigin.y + delta);
6228 	}
6229 	///
6230 	void verticalScrollTo(int pos) {
6231 		scrollOrigin_.y = pos;
6232 		if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight))
6233 			scrollOrigin_.y = contentHeight - viewportHeight;
6234 
6235 		if(scrollOrigin_.y < 0)
6236 			scrollOrigin_.y = 0;
6237 
6238 		version(win32_widgets) {
6239 			SCROLLINFO info;
6240 			info.cbSize = info.sizeof;
6241 			info.fMask = SIF_POS;
6242 			info.nPos = scrollOrigin_.y;
6243 			SetScrollInfo(hwnd, SB_VERT, &info, true);
6244 		} else version(custom_widgets) {
6245 			outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y);
6246 		} else static assert(0);
6247 
6248 		redraw();
6249 	}
6250 
6251 	///
6252 	void horizontalScroll(int delta) {
6253 		horizontalScrollTo(scrollOrigin.x + delta);
6254 	}
6255 	///
6256 	void horizontalScrollTo(int pos) {
6257 		scrollOrigin_.x = pos;
6258 		if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth))
6259 			scrollOrigin_.x = contentWidth - viewportWidth;
6260 
6261 		if(scrollOrigin_.x < 0)
6262 			scrollOrigin_.x = 0;
6263 
6264 		version(win32_widgets) {
6265 			SCROLLINFO info;
6266 			info.cbSize = info.sizeof;
6267 			info.fMask = SIF_POS;
6268 			info.nPos = scrollOrigin_.x;
6269 			SetScrollInfo(hwnd, SB_HORZ, &info, true);
6270 		} else version(custom_widgets) {
6271 			outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x);
6272 		} else static assert(0);
6273 
6274 		redraw();
6275 	}
6276 	///
6277 	void scrollTo(Point p) {
6278 		verticalScrollTo(p.y);
6279 		horizontalScrollTo(p.x);
6280 	}
6281 
6282 	///
6283 	void ensureVisibleInScroll(Point p) {
6284 		auto rect = viewportRectangle();
6285 		if(rect.contains(p))
6286 			return;
6287 		if(p.x < rect.left)
6288 			horizontalScroll(p.x - rect.left);
6289 		else if(p.x > rect.right)
6290 			horizontalScroll(p.x - rect.right);
6291 
6292 		if(p.y < rect.top)
6293 			verticalScroll(p.y - rect.top);
6294 		else if(p.y > rect.bottom)
6295 			verticalScroll(p.y - rect.bottom);
6296 	}
6297 
6298 	///
6299 	void ensureVisibleInScroll(Rectangle rect) {
6300 		ensureVisibleInScroll(rect.upperLeft);
6301 		ensureVisibleInScroll(rect.lowerRight);
6302 	}
6303 
6304 	///
6305 	Rectangle viewportRectangle() {
6306 		return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight));
6307 	}
6308 
6309 	///
6310 	bool showingHorizontalScroll() {
6311 		return contentWidth > width;
6312 	}
6313 	///
6314 	bool showingVerticalScroll() {
6315 		return contentHeight > height;
6316 	}
6317 
6318 	/// This is called before the ordinary paint delegate,
6319 	/// giving you a chance to draw the window frame, etc,
6320 	/// before the scroll clip takes effect
6321 	void paintFrameAndBackground(WidgetPainter painter) {
6322 		version(win32_widgets) {
6323 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
6324 			auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
6325 			// since the pen is null, to fill the whole space, we need the +1 on both.
6326 			gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
6327 			SelectObject(painter.impl.hdc, p);
6328 			SelectObject(painter.impl.hdc, b);
6329 		}
6330 
6331 	}
6332 
6333 	// make space for the scroll bar, and that's it.
6334 	final override int paddingRight() { return scaleWithDpi(16); }
6335 	final override int paddingBottom() { return scaleWithDpi(16); }
6336 
6337 	/*
6338 		END SCROLLING
6339 	*/
6340 
6341 	override WidgetPainter draw() {
6342 		int x = this.x, y = this.y;
6343 		auto parent = this.parent;
6344 		while(parent) {
6345 			x += parent.x;
6346 			y += parent.y;
6347 			parent = parent.parent;
6348 		}
6349 
6350 		//version(win32_widgets) {
6351 			//auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
6352 		//} else {
6353 			auto painter = parentWindow.win.draw(true);
6354 		//}
6355 		painter.originX = x;
6356 		painter.originY = y;
6357 
6358 		painter.originX = painter.originX - scrollOrigin.x;
6359 		painter.originY = painter.originY - scrollOrigin.y;
6360 		painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight());
6361 
6362 		return WidgetPainter(painter, this);
6363 	}
6364 
6365 	override void addScrollPosition(ref int x, ref int y) {
6366 		x += scrollOrigin.x;
6367 		y += scrollOrigin.y;
6368 	}
6369 
6370 	mixin ScrollableChildren;
6371 }
6372 
6373 // you need to have a Point scrollOrigin in the class somewhere
6374 // and a paintFrameAndBackground
6375 private mixin template ScrollableChildren() {
6376 	static assert(!__traits(isSame, this.addScrollPosition, Widget.addScrollPosition), "Your widget should provide `Point scrollOrigin()` and `override void addScrollPosition`");
6377 
6378 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
6379 		if(hidden)
6380 			return;
6381 
6382 		//version(win32_widgets)
6383 			//painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
6384 
6385 		painter.originX = lox + x;
6386 		painter.originY = loy + y;
6387 
6388 		bool actuallyPainted = false;
6389 
6390 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height)));
6391 		if(clip == Rectangle.init)
6392 			return;
6393 
6394 		if(force || redrawRequested) {
6395 			//painter.setClipRectangle(scrollOrigin, width, height);
6396 			painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
6397 			paintFrameAndBackground(painter);
6398 		}
6399 
6400 		/+
6401 		version(win32_widgets) {
6402 			if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW);
6403 		}
6404 		+/
6405 
6406 		painter.originX = painter.originX - scrollOrigin.x;
6407 		painter.originY = painter.originY - scrollOrigin.y;
6408 		if(force || redrawRequested) {
6409 			painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4);
6410 			//painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4);
6411 
6412 			//erase(painter); // we paintFrameAndBackground above so no need
6413 			if(painter.visualTheme)
6414 				painter.visualTheme.doPaint(this, painter);
6415 			else
6416 				paint(painter);
6417 
6418 			if(invalidate) {
6419 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
6420 				// children are contained inside this, so no need to do extra work
6421 				invalidate = false;
6422 			}
6423 
6424 
6425 			actuallyPainted = true;
6426 			redrawRequested = false;
6427 		}
6428 
6429 		foreach(child; children) {
6430 			if(cast(FixedPosition) child)
6431 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
6432 			else
6433 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
6434 		}
6435 	}
6436 }
6437 
6438 private class InternalScrollableContainerInsideWidget : ContainerWidget {
6439 	ScrollableContainerWidget scw;
6440 
6441 	this(ScrollableContainerWidget parent) {
6442 		scw = parent;
6443 		super(parent);
6444 	}
6445 
6446 	version(custom_widgets)
6447 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
6448 		if(hidden)
6449 			return;
6450 
6451 		bool actuallyPainted = false;
6452 
6453 		auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_);
6454 
6455 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_)));
6456 		if(clip == Rectangle.init)
6457 			return;
6458 
6459 		painter.originX = lox + x - scrollOrigin.x;
6460 		painter.originY = loy + y - scrollOrigin.y;
6461 		if(force || redrawRequested) {
6462 			painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
6463 
6464 			erase(painter);
6465 			if(painter.visualTheme)
6466 				painter.visualTheme.doPaint(this, painter);
6467 			else
6468 				paint(painter);
6469 
6470 			if(invalidate) {
6471 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
6472 				// children are contained inside this, so no need to do extra work
6473 				invalidate = false;
6474 			}
6475 
6476 			actuallyPainted = true;
6477 			redrawRequested = false;
6478 		}
6479 		foreach(child; children) {
6480 			if(cast(FixedPosition) child)
6481 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
6482 			else
6483 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
6484 		}
6485 	}
6486 
6487 	version(custom_widgets)
6488 	override protected void addScrollPosition(ref int x, ref int y) {
6489 		x += scw.scrollX_;
6490 		y += scw.scrollY_;
6491 	}
6492 }
6493 
6494 /++
6495 	A widget meant to contain other widgets that may need to scroll.
6496 
6497 	Currently buggy.
6498 
6499 	History:
6500 		Added July 1, 2021 (dub v10.2)
6501 
6502 		On January 3, 2022, I tried to use it in a few other cases
6503 		and found it only worked well in the original test case. Since
6504 		it still sucks, I think I'm going to rewrite it again.
6505 +/
6506 class ScrollableContainerWidget : ContainerWidget {
6507 	///
6508 	this(Widget parent) {
6509 		super(parent);
6510 
6511 		container = new InternalScrollableContainerInsideWidget(this);
6512 		hsb = new HorizontalScrollbar(this);
6513 		vsb = new VerticalScrollbar(this);
6514 
6515 		tabStop = false;
6516 		container.tabStop = false;
6517 		magic = true;
6518 
6519 
6520 		vsb.addEventListener("scrolltonextline", () {
6521 			scrollBy(0, scaleWithDpi(16));
6522 		});
6523 		vsb.addEventListener("scrolltopreviousline", () {
6524 			scrollBy(0,scaleWithDpi( -16));
6525 		});
6526 		vsb.addEventListener("scrolltonextpage", () {
6527 			scrollBy(0, container.height);
6528 		});
6529 		vsb.addEventListener("scrolltopreviouspage", () {
6530 			scrollBy(0, -container.height);
6531 		});
6532 		vsb.addEventListener((scope ScrollToPositionEvent spe) {
6533 			scrollTo(scrollX_, spe.value);
6534 		});
6535 
6536 		this.addEventListener(delegate (scope ClickEvent e) {
6537 			if(e.button == MouseButton.wheelUp) {
6538 				if(!e.defaultPrevented)
6539 					scrollBy(0, scaleWithDpi(-16));
6540 				e.stopPropagation();
6541 			} else if(e.button == MouseButton.wheelDown) {
6542 				if(!e.defaultPrevented)
6543 					scrollBy(0, scaleWithDpi(16));
6544 				e.stopPropagation();
6545 			}
6546 		});
6547 	}
6548 
6549 	/+
6550 	override void defaultEventHandler_click(ClickEvent e) {
6551 	}
6552 	+/
6553 
6554 	override void removeAllChildren() {
6555 		container.removeAllChildren();
6556 	}
6557 
6558 	void scrollTo(int x, int y) {
6559 		scrollBy(x - scrollX_, y - scrollY_);
6560 	}
6561 
6562 	void scrollBy(int x, int y) {
6563 		auto ox = scrollX_;
6564 		auto oy = scrollY_;
6565 
6566 		auto nx = ox + x;
6567 		auto ny = oy + y;
6568 
6569 		if(nx < 0)
6570 			nx = 0;
6571 		if(ny < 0)
6572 			ny = 0;
6573 
6574 		auto maxX = hsb.max - container.width;
6575 		if(maxX < 0) maxX = 0;
6576 		auto maxY = vsb.max - container.height;
6577 		if(maxY < 0) maxY = 0;
6578 
6579 		if(nx > maxX)
6580 			nx = maxX;
6581 		if(ny > maxY)
6582 			ny = maxY;
6583 
6584 		auto dx = nx - ox;
6585 		auto dy = ny - oy;
6586 
6587 		if(dx || dy) {
6588 			version(win32_widgets)
6589 				ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
6590 			else {
6591 				redraw();
6592 			}
6593 
6594 			hsb.setPosition = nx;
6595 			vsb.setPosition = ny;
6596 
6597 			scrollX_ = nx;
6598 			scrollY_ = ny;
6599 		}
6600 	}
6601 
6602 	private int scrollX_;
6603 	private int scrollY_;
6604 
6605 	void setTotalArea(int width, int height) {
6606 		hsb.setMax(width);
6607 		vsb.setMax(height);
6608 	}
6609 
6610 	///
6611 	void setViewableArea(int width, int height) {
6612 		hsb.setViewableArea(width);
6613 		vsb.setViewableArea(height);
6614 	}
6615 
6616 	private bool magic;
6617 	override void addChild(Widget w, int position = int.max) {
6618 		if(magic)
6619 			container.addChild(w, position);
6620 		else
6621 			super.addChild(w, position);
6622 	}
6623 
6624 	override void recomputeChildLayout() {
6625 		if(hsb is null || vsb is null || container is null) return;
6626 
6627 		/+
6628 		writeln(x, " ", y , " ", width, " ", height);
6629 		writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight());
6630 		+/
6631 
6632 		registerMovement();
6633 
6634 		hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane?
6635 		hsb.x = 0;
6636 		hsb.y = this.height - hsb.height;
6637 		hsb.width = this.width - scaleWithDpi(16);
6638 		hsb.recomputeChildLayout();
6639 
6640 		vsb.width = scaleWithDpi(16); // FIXME?
6641 		vsb.x = this.width - vsb.width;
6642 		vsb.y = 0;
6643 		vsb.height = this.height - scaleWithDpi(16);
6644 		vsb.recomputeChildLayout();
6645 
6646 		container.x = 0;
6647 		container.y = 0;
6648 		container.width = this.width - vsb.width;
6649 		container.height = this.height - hsb.height;
6650 		container.recomputeChildLayout();
6651 
6652 		scrollX_ = 0;
6653 		scrollY_ = 0;
6654 
6655 		hsb.setPosition(0);
6656 		vsb.setPosition(0);
6657 
6658 		int mw, mh;
6659 		Widget c = container;
6660 		// FIXME: hack here to handle a layout inside...
6661 		if(c.children.length == 1 && cast(Layout) c.children[0])
6662 			c = c.children[0];
6663 		foreach(child; c.children) {
6664 			auto w = child.x + child.width;
6665 			auto h = child.y + child.height;
6666 
6667 			if(w > mw) mw = w;
6668 			if(h > mh) mh = h;
6669 		}
6670 
6671 		setTotalArea(mw, mh);
6672 		setViewableArea(width, height);
6673 	}
6674 
6675 	override int minHeight() { return scaleWithDpi(64); }
6676 
6677 	HorizontalScrollbar hsb;
6678 	VerticalScrollbar vsb;
6679 	ContainerWidget container;
6680 }
6681 
6682 
6683 version(custom_widgets)
6684 deprecated
6685 private class InternalScrollableContainerWidget : Widget {
6686 
6687 	ScrollableWidget sw;
6688 
6689 	VerticalScrollbar verticalScrollBar;
6690 	HorizontalScrollbar horizontalScrollBar;
6691 
6692 	this(ScrollableWidget sw, Widget parent) {
6693 		this.sw = sw;
6694 
6695 		this.tabStop = false;
6696 
6697 		super(parent);
6698 
6699 		horizontalScrollBar = new HorizontalScrollbar(this);
6700 		verticalScrollBar = new VerticalScrollbar(this);
6701 
6702 		horizontalScrollBar.showing_ = false;
6703 		verticalScrollBar.showing_ = false;
6704 
6705 		horizontalScrollBar.addEventListener("scrolltonextline", {
6706 			horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
6707 			sw.horizontalScrollTo(horizontalScrollBar.position);
6708 		});
6709 		horizontalScrollBar.addEventListener("scrolltopreviousline", {
6710 			horizontalScrollBar.setPosition(horizontalScrollBar.position - 1);
6711 			sw.horizontalScrollTo(horizontalScrollBar.position);
6712 		});
6713 		verticalScrollBar.addEventListener("scrolltonextline", {
6714 			verticalScrollBar.setPosition(verticalScrollBar.position + 1);
6715 			sw.verticalScrollTo(verticalScrollBar.position);
6716 		});
6717 		verticalScrollBar.addEventListener("scrolltopreviousline", {
6718 			verticalScrollBar.setPosition(verticalScrollBar.position - 1);
6719 			sw.verticalScrollTo(verticalScrollBar.position);
6720 		});
6721 		horizontalScrollBar.addEventListener("scrolltonextpage", {
6722 			horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_);
6723 			sw.horizontalScrollTo(horizontalScrollBar.position);
6724 		});
6725 		horizontalScrollBar.addEventListener("scrolltopreviouspage", {
6726 			horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_);
6727 			sw.horizontalScrollTo(horizontalScrollBar.position);
6728 		});
6729 		verticalScrollBar.addEventListener("scrolltonextpage", {
6730 			verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_);
6731 			sw.verticalScrollTo(verticalScrollBar.position);
6732 		});
6733 		verticalScrollBar.addEventListener("scrolltopreviouspage", {
6734 			verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_);
6735 			sw.verticalScrollTo(verticalScrollBar.position);
6736 		});
6737 		horizontalScrollBar.addEventListener("scrolltoposition", (Event event) {
6738 			horizontalScrollBar.setPosition(event.intValue);
6739 			sw.horizontalScrollTo(horizontalScrollBar.position);
6740 		});
6741 		verticalScrollBar.addEventListener("scrolltoposition", (Event event) {
6742 			verticalScrollBar.setPosition(event.intValue);
6743 			sw.verticalScrollTo(verticalScrollBar.position);
6744 		});
6745 		horizontalScrollBar.addEventListener("scrolltrack", (Event event) {
6746 			horizontalScrollBar.setPosition(event.intValue);
6747 			sw.horizontalScrollTo(horizontalScrollBar.position);
6748 		});
6749 		verticalScrollBar.addEventListener("scrolltrack", (Event event) {
6750 			verticalScrollBar.setPosition(event.intValue);
6751 		});
6752 	}
6753 
6754 	// this is supposed to be basically invisible...
6755 	override int minWidth() { return sw.minWidth; }
6756 	override int minHeight() { return sw.minHeight; }
6757 	override int maxWidth() { return sw.maxWidth; }
6758 	override int maxHeight() { return sw.maxHeight; }
6759 	override int widthStretchiness() { return sw.widthStretchiness; }
6760 	override int heightStretchiness() { return sw.heightStretchiness; }
6761 	override int marginLeft() { return sw.marginLeft; }
6762 	override int marginRight() { return sw.marginRight; }
6763 	override int marginTop() { return sw.marginTop; }
6764 	override int marginBottom() { return sw.marginBottom; }
6765 	override int paddingLeft() { return sw.paddingLeft; }
6766 	override int paddingRight() { return sw.paddingRight; }
6767 	override int paddingTop() { return sw.paddingTop; }
6768 	override int paddingBottom() { return sw.paddingBottom; }
6769 	override void focus() { sw.focus(); }
6770 
6771 
6772 	override void recomputeChildLayout() {
6773 		// The stupid thing needs to calculate if a scroll bar is needed...
6774 		recomputeChildLayoutHelper();
6775 		// then running it again will position things correctly if the bar is NOT needed
6776 		recomputeChildLayoutHelper();
6777 
6778 		// this sucks but meh it barely works
6779 	}
6780 
6781 	private void recomputeChildLayoutHelper() {
6782 		if(sw is null) return;
6783 
6784 		bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll;
6785 		if(horizontalScrollBar && verticalScrollBar) {
6786 			horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0);
6787 			horizontalScrollBar.height = horizontalScrollBar.minHeight();
6788 			horizontalScrollBar.x = 0;
6789 			horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight();
6790 
6791 			verticalScrollBar.width = verticalScrollBar.minWidth();
6792 			verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2;
6793 			verticalScrollBar.x = this.width - verticalScrollBar.minWidth();
6794 			verticalScrollBar.y = 0 + 2;
6795 
6796 			sw.x = 0;
6797 			sw.y = 0;
6798 			sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0);
6799 			sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0);
6800 
6801 			if(sw.contentWidth_ <= this.width)
6802 				sw.scrollOrigin_.x = 0;
6803 			if(sw.contentHeight_ <= this.height)
6804 				sw.scrollOrigin_.y = 0;
6805 
6806 			horizontalScrollBar.recomputeChildLayout();
6807 			verticalScrollBar.recomputeChildLayout();
6808 			sw.recomputeChildLayout();
6809 		}
6810 
6811 		if(sw.contentWidth_ <= this.width)
6812 			sw.scrollOrigin_.x = 0;
6813 		if(sw.contentHeight_ <= this.height)
6814 			sw.scrollOrigin_.y = 0;
6815 
6816 		if(sw.showingHorizontalScroll())
6817 			horizontalScrollBar.showing(true, false);
6818 		else
6819 			horizontalScrollBar.showing(false, false);
6820 		if(sw.showingVerticalScroll())
6821 			verticalScrollBar.showing(true, false);
6822 		else
6823 			verticalScrollBar.showing(false, false);
6824 
6825 		verticalScrollBar.setViewableArea(sw.viewportHeight());
6826 		verticalScrollBar.setMax(sw.contentHeight);
6827 		verticalScrollBar.setPosition(sw.scrollOrigin.y);
6828 
6829 		horizontalScrollBar.setViewableArea(sw.viewportWidth());
6830 		horizontalScrollBar.setMax(sw.contentWidth);
6831 		horizontalScrollBar.setPosition(sw.scrollOrigin.x);
6832 	}
6833 }
6834 
6835 /*
6836 class ScrollableClientWidget : Widget {
6837 	this(Widget parent) {
6838 		super(parent);
6839 	}
6840 	override void paint(WidgetPainter p) {
6841 		parent.paint(p);
6842 	}
6843 }
6844 */
6845 
6846 /++
6847 	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.
6848 +/
6849 abstract class Slider : Widget {
6850 	this(int min, int max, int step, Widget parent) {
6851 		min_ = min;
6852 		max_ = max;
6853 		step_ = step;
6854 		page_ = step;
6855 		super(parent);
6856 	}
6857 
6858 	private int min_;
6859 	private int max_;
6860 	private int step_;
6861 	private int position_;
6862 	private int page_;
6863 
6864 	// selection start and selection end
6865 	// tics
6866 	// tooltip?
6867 	// some way to see and just type the value
6868 	// win32 buddy controls are labels
6869 
6870 	///
6871 	void setMin(int a) {
6872 		min_ = a;
6873 		version(custom_widgets)
6874 			redraw();
6875 		version(win32_widgets)
6876 			SendMessage(hwnd, TBM_SETRANGEMIN, true, a);
6877 	}
6878 	///
6879 	int min() {
6880 		return min_;
6881 	}
6882 	///
6883 	void setMax(int a) {
6884 		max_ = a;
6885 		version(custom_widgets)
6886 			redraw();
6887 		version(win32_widgets)
6888 			SendMessage(hwnd, TBM_SETRANGEMAX, true, a);
6889 	}
6890 	///
6891 	int max() {
6892 		return max_;
6893 	}
6894 	///
6895 	void setPosition(int a) {
6896 		if(a > max)
6897 			a = max;
6898 		if(a < min)
6899 			a = min;
6900 		position_ = a;
6901 		version(custom_widgets)
6902 			setPositionCustom(a);
6903 
6904 		version(win32_widgets)
6905 			setPositionWindows(a);
6906 	}
6907 	version(win32_widgets) {
6908 		protected abstract void setPositionWindows(int a);
6909 	}
6910 
6911 	protected abstract int win32direction();
6912 
6913 	/++
6914 		Alias for [position] for better compatibility with generic code.
6915 
6916 		History:
6917 			Added October 5, 2021
6918 	+/
6919 	@property int value() {
6920 		return position;
6921 	}
6922 
6923 	///
6924 	int position() {
6925 		return position_;
6926 	}
6927 	///
6928 	void setStep(int a) {
6929 		step_ = a;
6930 		version(win32_widgets)
6931 			SendMessage(hwnd, TBM_SETLINESIZE, 0, a);
6932 	}
6933 	///
6934 	int step() {
6935 		return step_;
6936 	}
6937 	///
6938 	void setPageSize(int a) {
6939 		page_ = a;
6940 		version(win32_widgets)
6941 			SendMessage(hwnd, TBM_SETPAGESIZE, 0, a);
6942 	}
6943 	///
6944 	int pageSize() {
6945 		return page_;
6946 	}
6947 
6948 	private void notify() {
6949 		auto event = new ChangeEvent!int(this, &this.position);
6950 		event.dispatch();
6951 	}
6952 
6953 	version(win32_widgets)
6954 	void win32Setup(int style) {
6955 		createWin32Window(this, TRACKBAR_CLASS, "",
6956 			0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0);
6957 
6958 		// the trackbar sends the same messages as scroll, which
6959 		// our other layer sends as these... just gonna translate
6960 		// here
6961 		this.addDirectEventListener("scrolltoposition", (Event event) {
6962 			event.stopPropagation();
6963 			this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue);
6964 			notify();
6965 		});
6966 		this.addDirectEventListener("scrolltonextline", (Event event) {
6967 			event.stopPropagation();
6968 			this.setPosition(this.position + this.step_ * this.win32direction);
6969 			notify();
6970 		});
6971 		this.addDirectEventListener("scrolltopreviousline", (Event event) {
6972 			event.stopPropagation();
6973 			this.setPosition(this.position - this.step_ * this.win32direction);
6974 			notify();
6975 		});
6976 		this.addDirectEventListener("scrolltonextpage", (Event event) {
6977 			event.stopPropagation();
6978 			this.setPosition(this.position + this.page_ * this.win32direction);
6979 			notify();
6980 		});
6981 		this.addDirectEventListener("scrolltopreviouspage", (Event event) {
6982 			event.stopPropagation();
6983 			this.setPosition(this.position - this.page_ * this.win32direction);
6984 			notify();
6985 		});
6986 
6987 		setMin(min_);
6988 		setMax(max_);
6989 		setStep(step_);
6990 		setPageSize(page_);
6991 	}
6992 
6993 	version(custom_widgets) {
6994 		protected MouseTrackingWidget thumb;
6995 
6996 		protected abstract void setPositionCustom(int a);
6997 
6998 		override void defaultEventHandler_keydown(KeyDownEvent event) {
6999 			switch(event.key) {
7000 				case Key.Up:
7001 				case Key.Right:
7002 					setPosition(position() - step() * win32direction);
7003 					changed();
7004 				break;
7005 				case Key.Down:
7006 				case Key.Left:
7007 					setPosition(position() + step() * win32direction);
7008 					changed();
7009 				break;
7010 				case Key.Home:
7011 					setPosition(win32direction > 0 ? min() : max());
7012 					changed();
7013 				break;
7014 				case Key.End:
7015 					setPosition(win32direction > 0 ? max() : min());
7016 					changed();
7017 				break;
7018 				case Key.PageUp:
7019 					setPosition(position() - pageSize() * win32direction);
7020 					changed();
7021 				break;
7022 				case Key.PageDown:
7023 					setPosition(position() + pageSize() * win32direction);
7024 					changed();
7025 				break;
7026 				default:
7027 			}
7028 			super.defaultEventHandler_keydown(event);
7029 		}
7030 
7031 		protected void changed() {
7032 			auto ev = new ChangeEvent!int(this, &position);
7033 			ev.dispatch();
7034 		}
7035 	}
7036 }
7037 
7038 /++
7039 
7040 +/
7041 class VerticalSlider : Slider {
7042 	this(int min, int max, int step, Widget parent) {
7043 		version(custom_widgets)
7044 			initialize();
7045 
7046 		super(min, max, step, parent);
7047 
7048 		version(win32_widgets)
7049 			win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */);
7050 	}
7051 
7052 	protected override int win32direction() {
7053 		return -1;
7054 	}
7055 
7056 	version(win32_widgets)
7057 	protected override void setPositionWindows(int a) {
7058 		// the windows thing makes the top 0 and i don't like that.
7059 		SendMessage(hwnd, TBM_SETPOS, true, max - a);
7060 	}
7061 
7062 	version(custom_widgets)
7063 	private void initialize() {
7064 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this);
7065 
7066 		thumb.tabStop = false;
7067 
7068 		thumb.thumbWidth = width;
7069 		thumb.thumbHeight = scaleWithDpi(16);
7070 
7071 		thumb.addEventListener(EventType.change, () {
7072 			auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16));
7073 			sx = max - sx;
7074 			//informProgramThatUserChangedPosition(sx);
7075 
7076 			position_ = sx;
7077 
7078 			changed();
7079 		});
7080 	}
7081 
7082 	version(custom_widgets)
7083 	override void recomputeChildLayout() {
7084 		thumb.thumbWidth = this.width;
7085 		super.recomputeChildLayout();
7086 		setPositionCustom(position_);
7087 	}
7088 
7089 	version(custom_widgets)
7090 	protected override void setPositionCustom(int a) {
7091 		if(max())
7092 			thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max();
7093 		redraw();
7094 	}
7095 }
7096 
7097 /++
7098 
7099 +/
7100 class HorizontalSlider : Slider {
7101 	this(int min, int max, int step, Widget parent) {
7102 		version(custom_widgets)
7103 			initialize();
7104 
7105 		super(min, max, step, parent);
7106 
7107 		version(win32_widgets)
7108 			win32Setup(TBS_HORZ);
7109 	}
7110 
7111 	version(win32_widgets)
7112 	protected override void setPositionWindows(int a) {
7113 		SendMessage(hwnd, TBM_SETPOS, true, a);
7114 	}
7115 
7116 	protected override int win32direction() {
7117 		return 1;
7118 	}
7119 
7120 	version(custom_widgets)
7121 	private void initialize() {
7122 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this);
7123 
7124 		thumb.tabStop = false;
7125 
7126 		thumb.thumbWidth = scaleWithDpi(16);
7127 		thumb.thumbHeight = height;
7128 
7129 		thumb.addEventListener(EventType.change, () {
7130 			auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16));
7131 			//informProgramThatUserChangedPosition(sx);
7132 
7133 			position_ = sx;
7134 
7135 			changed();
7136 		});
7137 	}
7138 
7139 	version(custom_widgets)
7140 	override void recomputeChildLayout() {
7141 		thumb.thumbHeight = this.height;
7142 		super.recomputeChildLayout();
7143 		setPositionCustom(position_);
7144 	}
7145 
7146 	version(custom_widgets)
7147 	protected override void setPositionCustom(int a) {
7148 		if(max())
7149 			thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max();
7150 		redraw();
7151 	}
7152 }
7153 
7154 
7155 ///
7156 abstract class ScrollbarBase : Widget {
7157 	///
7158 	this(Widget parent) {
7159 		super(parent);
7160 		tabStop = false;
7161 		step_ = scaleWithDpi(16);
7162 	}
7163 
7164 	private int viewableArea_;
7165 	private int max_;
7166 	private int step_;// = 16;
7167 	private int position_;
7168 
7169 	///
7170 	bool atEnd() {
7171 		return position_ + viewableArea_ >= max_;
7172 	}
7173 
7174 	///
7175 	bool atStart() {
7176 		return position_ == 0;
7177 	}
7178 
7179 	///
7180 	void setViewableArea(int a) {
7181 		viewableArea_ = a;
7182 		version(custom_widgets)
7183 			redraw();
7184 	}
7185 	///
7186 	void setMax(int a) {
7187 		max_ = a;
7188 		version(custom_widgets)
7189 			redraw();
7190 	}
7191 	///
7192 	int max() {
7193 		return max_;
7194 	}
7195 	///
7196 	void setPosition(int a) {
7197 		auto logicalMax = max_ - viewableArea_;
7198 		if(a == int.max)
7199 			a = logicalMax;
7200 
7201 		if(a > logicalMax)
7202 			a = logicalMax;
7203 		if(a < 0)
7204 			a = 0;
7205 
7206 		position_ = a;
7207 
7208 		version(custom_widgets)
7209 			redraw();
7210 	}
7211 	///
7212 	int position() {
7213 		return position_;
7214 	}
7215 	///
7216 	void setStep(int a) {
7217 		step_ = a;
7218 	}
7219 	///
7220 	int step() {
7221 		return step_;
7222 	}
7223 
7224 	// FIXME: remove this.... maybe
7225 	/+
7226 	protected void informProgramThatUserChangedPosition(int n) {
7227 		position_ = n;
7228 		auto evt = new Event(EventType.change, this);
7229 		evt.intValue = n;
7230 		evt.dispatch();
7231 	}
7232 	+/
7233 
7234 	version(custom_widgets) {
7235 		enum MIN_THUMB_SIZE = 8;
7236 
7237 		abstract protected int getBarDim();
7238 		int thumbSize() {
7239 			if(viewableArea_ >= max_ || max_ == 0)
7240 				return getBarDim();
7241 
7242 			int res = viewableArea_ * getBarDim() / max_;
7243 
7244 			if(res < scaleWithDpi(MIN_THUMB_SIZE))
7245 				res = scaleWithDpi(MIN_THUMB_SIZE);
7246 
7247 			return res;
7248 		}
7249 
7250 		int thumbPosition() {
7251 			/*
7252 				viewableArea_ is the viewport height/width
7253 				position_ is where we are
7254 			*/
7255 			//if(position_ + viewableArea_ >= max_)
7256 				//return getBarDim - thumbSize;
7257 
7258 			auto maximumPossibleValue = getBarDim() - thumbSize;
7259 			auto maximiumLogicalValue = max_ - viewableArea_;
7260 
7261 			auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0;
7262 
7263 			return p;
7264 		}
7265 	}
7266 }
7267 
7268 //public import mgt;
7269 
7270 /++
7271 	A mouse tracking widget is one that follows the mouse when dragged inside it.
7272 
7273 	Concrete subclasses may include a scrollbar thumb and a volume control.
7274 +/
7275 //version(custom_widgets)
7276 class MouseTrackingWidget : Widget {
7277 
7278 	///
7279 	int positionX() { return positionX_; }
7280 	///
7281 	int positionY() { return positionY_; }
7282 
7283 	///
7284 	void positionX(int p) { positionX_ = p; }
7285 	///
7286 	void positionY(int p) { positionY_ = p; }
7287 
7288 	private int positionX_;
7289 	private int positionY_;
7290 
7291 	///
7292 	enum Orientation {
7293 		horizontal, ///
7294 		vertical, ///
7295 		twoDimensional, ///
7296 	}
7297 
7298 	private int thumbWidth_;
7299 	private int thumbHeight_;
7300 
7301 	///
7302 	int thumbWidth() { return thumbWidth_; }
7303 	///
7304 	int thumbHeight() { return thumbHeight_; }
7305 	///
7306 	int thumbWidth(int a) { return thumbWidth_ = a; }
7307 	///
7308 	int thumbHeight(int a) { return thumbHeight_ = a; }
7309 
7310 	private bool dragging;
7311 	private bool hovering;
7312 	private int startMouseX, startMouseY;
7313 
7314 	///
7315 	this(Orientation orientation, Widget parent) {
7316 		super(parent);
7317 
7318 		//assert(parentWindow !is null);
7319 
7320 		addEventListener((MouseDownEvent event) {
7321 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
7322 				dragging = true;
7323 				startMouseX = event.clientX - positionX;
7324 				startMouseY = event.clientY - positionY;
7325 				parentWindow.captureMouse(this);
7326 			} else {
7327 				if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
7328 					positionX = event.clientX - thumbWidth / 2;
7329 				if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
7330 					positionY = event.clientY - thumbHeight / 2;
7331 
7332 				if(positionX + thumbWidth > this.width)
7333 					positionX = this.width - thumbWidth;
7334 				if(positionY + thumbHeight > this.height)
7335 					positionY = this.height - thumbHeight;
7336 
7337 				if(positionX < 0)
7338 					positionX = 0;
7339 				if(positionY < 0)
7340 					positionY = 0;
7341 
7342 
7343 				// this.emit!(ChangeEvent!void)();
7344 				auto evt = new Event(EventType.change, this);
7345 				evt.sendDirectly();
7346 
7347 				redraw();
7348 
7349 			}
7350 		});
7351 
7352 		addEventListener(EventType.mouseup, (Event event) {
7353 			dragging = false;
7354 			parentWindow.releaseMouseCapture();
7355 		});
7356 
7357 		addEventListener(EventType.mouseout, (Event event) {
7358 			if(!hovering)
7359 				return;
7360 			hovering = false;
7361 			redraw();
7362 		});
7363 
7364 		int lpx, lpy;
7365 
7366 		addEventListener((MouseMoveEvent event) {
7367 			auto oh = hovering;
7368 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
7369 				hovering = true;
7370 			} else {
7371 				hovering = false;
7372 			}
7373 			if(!dragging) {
7374 				if(hovering != oh)
7375 					redraw();
7376 				return;
7377 			}
7378 
7379 			if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
7380 				positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it
7381 			if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
7382 				positionY = event.clientY - startMouseY;
7383 
7384 			if(positionX + thumbWidth > this.width)
7385 				positionX = this.width - thumbWidth;
7386 			if(positionY + thumbHeight > this.height)
7387 				positionY = this.height - thumbHeight;
7388 
7389 			if(positionX < 0)
7390 				positionX = 0;
7391 			if(positionY < 0)
7392 				positionY = 0;
7393 
7394 			if(positionX != lpx || positionY != lpy) {
7395 				lpx = positionX;
7396 				lpy = positionY;
7397 
7398 				auto evt = new Event(EventType.change, this);
7399 				evt.sendDirectly();
7400 			}
7401 
7402 			redraw();
7403 		});
7404 	}
7405 
7406 	version(custom_widgets)
7407 	override void paint(WidgetPainter painter) {
7408 		auto cs = getComputedStyle();
7409 		auto c = darken(cs.windowBackgroundColor, 0.2);
7410 		painter.outlineColor = c;
7411 		painter.fillColor = c;
7412 		painter.drawRectangle(Point(0, 0), this.width, this.height);
7413 
7414 		auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor;
7415 		draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color);
7416 	}
7417 }
7418 
7419 //version(custom_widgets)
7420 //private
7421 class HorizontalScrollbar : ScrollbarBase {
7422 
7423 	version(custom_widgets) {
7424 		private MouseTrackingWidget thumb;
7425 
7426 		override int getBarDim() {
7427 			return thumb.width;
7428 		}
7429 	}
7430 
7431 	override void setViewableArea(int a) {
7432 		super.setViewableArea(a);
7433 
7434 		version(win32_widgets) {
7435 			SCROLLINFO info;
7436 			info.cbSize = info.sizeof;
7437 			info.nPage = a + 1;
7438 			info.fMask = SIF_PAGE;
7439 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7440 		} else version(custom_widgets) {
7441 			thumb.positionX = thumbPosition;
7442 			thumb.thumbWidth = thumbSize;
7443 			thumb.redraw();
7444 		} else static assert(0);
7445 
7446 	}
7447 
7448 	override void setMax(int a) {
7449 		super.setMax(a);
7450 		version(win32_widgets) {
7451 			SCROLLINFO info;
7452 			info.cbSize = info.sizeof;
7453 			info.nMin = 0;
7454 			info.nMax = max;
7455 			info.fMask = SIF_RANGE;
7456 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7457 		} else version(custom_widgets) {
7458 			thumb.positionX = thumbPosition;
7459 			thumb.thumbWidth = thumbSize;
7460 			thumb.redraw();
7461 		}
7462 	}
7463 
7464 	override void setPosition(int a) {
7465 		super.setPosition(a);
7466 		version(win32_widgets) {
7467 			SCROLLINFO info;
7468 			info.cbSize = info.sizeof;
7469 			info.fMask = SIF_POS;
7470 			info.nPos = position;
7471 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7472 		} else version(custom_widgets) {
7473 			thumb.positionX = thumbPosition();
7474 			thumb.thumbWidth = thumbSize;
7475 			thumb.redraw();
7476 		} else static assert(0);
7477 	}
7478 
7479 	this(Widget parent) {
7480 		super(parent);
7481 
7482 		version(win32_widgets) {
7483 			createWin32Window(this, "Scrollbar"w, "",
7484 				0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0);
7485 		} else version(custom_widgets) {
7486 			auto vl = new HorizontalLayout(this);
7487 			auto leftButton = new ArrowButton(ArrowDirection.left, vl);
7488 			leftButton.setClickRepeat(scrollClickRepeatInterval);
7489 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl);
7490 			auto rightButton = new ArrowButton(ArrowDirection.right, vl);
7491 			rightButton.setClickRepeat(scrollClickRepeatInterval);
7492 
7493 			leftButton.tabStop = false;
7494 			rightButton.tabStop = false;
7495 			thumb.tabStop = false;
7496 
7497 			leftButton.addEventListener(EventType.triggered, () {
7498 				this.emitCommand!"scrolltopreviousline"();
7499 				//informProgramThatUserChangedPosition(position - step());
7500 			});
7501 			rightButton.addEventListener(EventType.triggered, () {
7502 				this.emitCommand!"scrolltonextline"();
7503 				//informProgramThatUserChangedPosition(position + step());
7504 			});
7505 
7506 			thumb.thumbWidth = this.minWidth;
7507 			thumb.thumbHeight = scaleWithDpi(16);
7508 
7509 			thumb.addEventListener(EventType.change, () {
7510 				auto maximumPossibleValue = thumb.width - thumb.thumbWidth;
7511 				auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0;
7512 
7513 				//informProgramThatUserChangedPosition(sx);
7514 
7515 				auto ev = new ScrollToPositionEvent(this, sx);
7516 				ev.dispatch();
7517 			});
7518 		}
7519 	}
7520 
7521 	override int minHeight() { return scaleWithDpi(16); }
7522 	override int maxHeight() { return scaleWithDpi(16); }
7523 	override int minWidth() { return scaleWithDpi(48); }
7524 }
7525 
7526 final class ScrollToPositionEvent : Event {
7527 	enum EventString = "scrolltoposition";
7528 
7529 	this(Widget target, int value) {
7530 		this.value = value;
7531 		super(EventString, target);
7532 	}
7533 
7534 	immutable int value;
7535 
7536 	override @property int intValue() {
7537 		return value;
7538 	}
7539 }
7540 
7541 //version(custom_widgets)
7542 //private
7543 class VerticalScrollbar : ScrollbarBase {
7544 
7545 	version(custom_widgets) {
7546 		override int getBarDim() {
7547 			return thumb.height;
7548 		}
7549 
7550 		private MouseTrackingWidget thumb;
7551 	}
7552 
7553 	override void setViewableArea(int a) {
7554 		super.setViewableArea(a);
7555 
7556 		version(win32_widgets) {
7557 			SCROLLINFO info;
7558 			info.cbSize = info.sizeof;
7559 			info.nPage = a + 1;
7560 			info.fMask = SIF_PAGE;
7561 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7562 		} else version(custom_widgets) {
7563 			thumb.positionY = thumbPosition;
7564 			thumb.thumbHeight = thumbSize;
7565 			thumb.redraw();
7566 		} else static assert(0);
7567 
7568 	}
7569 
7570 	override void setMax(int a) {
7571 		super.setMax(a);
7572 		version(win32_widgets) {
7573 			SCROLLINFO info;
7574 			info.cbSize = info.sizeof;
7575 			info.nMin = 0;
7576 			info.nMax = max;
7577 			info.fMask = SIF_RANGE;
7578 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7579 		} else version(custom_widgets) {
7580 			thumb.positionY = thumbPosition;
7581 			thumb.thumbHeight = thumbSize;
7582 			thumb.redraw();
7583 		}
7584 	}
7585 
7586 	override void setPosition(int a) {
7587 		super.setPosition(a);
7588 		version(win32_widgets) {
7589 			SCROLLINFO info;
7590 			info.cbSize = info.sizeof;
7591 			info.fMask = SIF_POS;
7592 			info.nPos = position;
7593 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7594 		} else version(custom_widgets) {
7595 			thumb.positionY = thumbPosition;
7596 			thumb.thumbHeight = thumbSize;
7597 			thumb.redraw();
7598 		} else static assert(0);
7599 	}
7600 
7601 	this(Widget parent) {
7602 		super(parent);
7603 
7604 		version(win32_widgets) {
7605 			createWin32Window(this, "Scrollbar"w, "",
7606 				0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0);
7607 		} else version(custom_widgets) {
7608 			auto vl = new VerticalLayout(this);
7609 			auto upButton = new ArrowButton(ArrowDirection.up, vl);
7610 			upButton.setClickRepeat(scrollClickRepeatInterval);
7611 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl);
7612 			auto downButton = new ArrowButton(ArrowDirection.down, vl);
7613 			downButton.setClickRepeat(scrollClickRepeatInterval);
7614 
7615 			upButton.addEventListener(EventType.triggered, () {
7616 				this.emitCommand!"scrolltopreviousline"();
7617 				//informProgramThatUserChangedPosition(position - step());
7618 			});
7619 			downButton.addEventListener(EventType.triggered, () {
7620 				this.emitCommand!"scrolltonextline"();
7621 				//informProgramThatUserChangedPosition(position + step());
7622 			});
7623 
7624 			thumb.thumbWidth = this.minWidth;
7625 			thumb.thumbHeight = scaleWithDpi(16);
7626 
7627 			thumb.addEventListener(EventType.change, () {
7628 				auto maximumPossibleValue = thumb.height - thumb.thumbHeight;
7629 				auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0;
7630 
7631 				auto ev = new ScrollToPositionEvent(this, sy);
7632 				ev.dispatch();
7633 
7634 				//informProgramThatUserChangedPosition(sy);
7635 			});
7636 
7637 			upButton.tabStop = false;
7638 			downButton.tabStop = false;
7639 			thumb.tabStop = false;
7640 		}
7641 	}
7642 
7643 	override int minWidth() { return scaleWithDpi(16); }
7644 	override int maxWidth() { return scaleWithDpi(16); }
7645 	override int minHeight() { return scaleWithDpi(48); }
7646 }
7647 
7648 
7649 /++
7650 	EXPERIMENTAL
7651 
7652 	A widget specialized for being a container for other widgets.
7653 
7654 	History:
7655 		Added May 29, 2021. Not stabilized at this time.
7656 +/
7657 class WidgetContainer : Widget {
7658 	this(Widget parent) {
7659 		tabStop = false;
7660 		super(parent);
7661 	}
7662 
7663 	override int maxHeight() {
7664 		if(this.children.length == 1) {
7665 			return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom);
7666 		} else {
7667 			return int.max;
7668 		}
7669 	}
7670 
7671 	override int maxWidth() {
7672 		if(this.children.length == 1) {
7673 			return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight);
7674 		} else {
7675 			return int.max;
7676 		}
7677 	}
7678 
7679 	/+
7680 
7681 	override int minHeight() {
7682 		int largest = 0;
7683 		int margins = 0;
7684 		int lastMargin = 0;
7685 		foreach(child; children) {
7686 			auto mh = child.minHeight();
7687 			if(mh > largest)
7688 				largest = mh;
7689 			margins += mymax(lastMargin, child.marginTop());
7690 			lastMargin = child.marginBottom();
7691 		}
7692 		return largest + margins;
7693 	}
7694 
7695 	override int maxHeight() {
7696 		int largest = 0;
7697 		int margins = 0;
7698 		int lastMargin = 0;
7699 		foreach(child; children) {
7700 			auto mh = child.maxHeight();
7701 			if(mh == int.max)
7702 				return int.max;
7703 			if(mh > largest)
7704 				largest = mh;
7705 			margins += mymax(lastMargin, child.marginTop());
7706 			lastMargin = child.marginBottom();
7707 		}
7708 		return largest + margins;
7709 	}
7710 
7711 	override int minWidth() {
7712 		int min;
7713 		foreach(child; children) {
7714 			auto cm = child.minWidth;
7715 			if(cm > min)
7716 				min = cm;
7717 		}
7718 		return min + paddingLeft + paddingRight;
7719 	}
7720 
7721 	override int minHeight() {
7722 		int min;
7723 		foreach(child; children) {
7724 			auto cm = child.minHeight;
7725 			if(cm > min)
7726 				min = cm;
7727 		}
7728 		return min + paddingTop + paddingBottom;
7729 	}
7730 
7731 	override int maxHeight() {
7732 		int largest = 0;
7733 		int margins = 0;
7734 		int lastMargin = 0;
7735 		foreach(child; children) {
7736 			auto mh = child.maxHeight();
7737 			if(mh == int.max)
7738 				return int.max;
7739 			if(mh > largest)
7740 				largest = mh;
7741 			margins += mymax(lastMargin, child.marginTop());
7742 			lastMargin = child.marginBottom();
7743 		}
7744 		return largest + margins;
7745 	}
7746 
7747 	override int heightStretchiness() {
7748 		int max;
7749 		foreach(child; children) {
7750 			auto c = child.heightStretchiness;
7751 			if(c > max)
7752 				max = c;
7753 		}
7754 		return max;
7755 	}
7756 
7757 	override int marginTop() {
7758 		if(this.children.length)
7759 			return this.children[0].marginTop;
7760 		return 0;
7761 	}
7762 	+/
7763 }
7764 
7765 ///
7766 abstract class Layout : Widget {
7767 	this(Widget parent) {
7768 		tabStop = false;
7769 		super(parent);
7770 	}
7771 }
7772 
7773 /++
7774 	Makes all children minimum width and height, placing them down
7775 	left to right, top to bottom.
7776 
7777 	Useful if you want to make a list of buttons that automatically
7778 	wrap to a new line when necessary.
7779 +/
7780 class InlineBlockLayout : Layout {
7781 	///
7782 	this(Widget parent) { super(parent); }
7783 
7784 	override void recomputeChildLayout() {
7785 		registerMovement();
7786 
7787 		int x = this.paddingLeft, y = this.paddingTop;
7788 
7789 		int lineHeight;
7790 		int previousMargin = 0;
7791 		int previousMarginBottom = 0;
7792 
7793 		foreach(child; children) {
7794 			if(child.hidden)
7795 				continue;
7796 			if(cast(FixedPosition) child) {
7797 				child.recomputeChildLayout();
7798 				continue;
7799 			}
7800 			child.width = child.flexBasisWidth();
7801 			if(child.width == 0)
7802 				child.width = child.minWidth();
7803 			if(child.width == 0)
7804 				child.width = 32;
7805 
7806 			child.height = child.flexBasisHeight();
7807 			if(child.height == 0)
7808 				child.height = child.minHeight();
7809 			if(child.height == 0)
7810 				child.height = 32;
7811 
7812 			if(x + child.width + paddingRight > this.width) {
7813 				x = this.paddingLeft;
7814 				y += lineHeight;
7815 				lineHeight = 0;
7816 				previousMargin = 0;
7817 				previousMarginBottom = 0;
7818 			}
7819 
7820 			auto margin = child.marginLeft;
7821 			if(previousMargin > margin)
7822 				margin = previousMargin;
7823 
7824 			x += margin;
7825 
7826 			child.x = x;
7827 			child.y = y;
7828 
7829 			int marginTopApplied;
7830 			if(child.marginTop > previousMarginBottom) {
7831 				child.y += child.marginTop;
7832 				marginTopApplied = child.marginTop;
7833 			}
7834 
7835 			x += child.width;
7836 			previousMargin = child.marginRight;
7837 
7838 			if(child.marginBottom > previousMarginBottom)
7839 				previousMarginBottom = child.marginBottom;
7840 
7841 			auto h = child.height + previousMarginBottom + marginTopApplied;
7842 			if(h > lineHeight)
7843 				lineHeight = h;
7844 
7845 			child.recomputeChildLayout();
7846 		}
7847 
7848 	}
7849 
7850 	override int minWidth() {
7851 		int min;
7852 		foreach(child; children) {
7853 			auto cm = child.minWidth;
7854 			if(cm > min)
7855 				min = cm;
7856 		}
7857 		return min + paddingLeft + paddingRight;
7858 	}
7859 
7860 	override int minHeight() {
7861 		int min;
7862 		foreach(child; children) {
7863 			auto cm = child.minHeight;
7864 			if(cm > min)
7865 				min = cm;
7866 		}
7867 		return min + paddingTop + paddingBottom;
7868 	}
7869 }
7870 
7871 /++
7872 	A TabMessageWidget is a clickable row of tabs followed by a content area, very similar
7873 	to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas
7874 	the [TabWidget] will automatically change pages of child widgets.
7875 
7876 	This allows you to react to it however you see fit rather than having to
7877 	be tied to just the new sets of child widgets.
7878 
7879 	It sends the message in the form of `this.emitCommand!"changetab"();`.
7880 
7881 	History:
7882 		Added December 24, 2021 (dub v10.5)
7883 +/
7884 class TabMessageWidget : Widget {
7885 
7886 	protected void tabIndexClicked(int item) {
7887 		this.emitCommand!"changetab"();
7888 	}
7889 
7890 	/++
7891 		Adds the a new tab to the control with the given title.
7892 
7893 		Returns:
7894 			The index of the newly added tab. You will need to know
7895 			this index to refer to it later and to know which tab to
7896 			change to when you get a changetab message.
7897 	+/
7898 	int addTab(string title, int pos = int.max) {
7899 		version(win32_widgets) {
7900 			TCITEM item;
7901 			item.mask = TCIF_TEXT;
7902 			WCharzBuffer buf = WCharzBuffer(title);
7903 			item.pszText = buf.ptr;
7904 			return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item);
7905 		} else version(custom_widgets) {
7906 			if(pos >= tabs.length) {
7907 				tabs ~= title;
7908 				redraw();
7909 				return cast(int) tabs.length - 1;
7910 			} else if(pos <= 0) {
7911 				tabs = title ~ tabs;
7912 				redraw();
7913 				return 0;
7914 			} else {
7915 				tabs = tabs[0 .. pos] ~ title ~ title[pos .. $];
7916 				redraw();
7917 				return pos;
7918 			}
7919 		}
7920 	}
7921 
7922 	override void addChild(Widget child, int pos = int.max) {
7923 		if(container)
7924 			container.addChild(child, pos);
7925 		else
7926 			super.addChild(child, pos);
7927 	}
7928 
7929 	protected Widget makeContainer() {
7930 		return new Widget(this);
7931 	}
7932 
7933 	private Widget container;
7934 
7935 	override void recomputeChildLayout() {
7936 		version(win32_widgets) {
7937 			this.registerMovement();
7938 
7939 			RECT rect;
7940 			GetWindowRect(hwnd, &rect);
7941 
7942 			auto left = rect.left;
7943 			auto top = rect.top;
7944 
7945 			TabCtrl_AdjustRect(hwnd, false, &rect);
7946 			foreach(child; children) {
7947 				if(!child.showing) continue;
7948 				child.x = rect.left - left;
7949 				child.y = rect.top - top;
7950 				child.width = rect.right - rect.left;
7951 				child.height = rect.bottom - rect.top;
7952 				child.recomputeChildLayout();
7953 			}
7954 		} else version(custom_widgets) {
7955 			this.registerMovement();
7956 			foreach(child; children) {
7957 				if(!child.showing) continue;
7958 				child.x = 2;
7959 				child.y = tabBarHeight + 2; // for the border
7960 				child.width = width - 4; // for the border
7961 				child.height = height - tabBarHeight - 2 - 2; // for the border
7962 				child.recomputeChildLayout();
7963 			}
7964 		} else static assert(0);
7965 	}
7966 
7967 	version(custom_widgets)
7968 		string[] tabs;
7969 
7970 	this(Widget parent) {
7971 		super(parent);
7972 
7973 		tabStop = false;
7974 
7975 		version(win32_widgets) {
7976 			createWin32Window(this, WC_TABCONTROL, "", 0);
7977 		} else version(custom_widgets) {
7978 			addEventListener((ClickEvent event) {
7979 				if(event.target !is this)
7980 					return;
7981 				if(event.clientY >= 0 && event.clientY < tabBarHeight) {
7982 					auto t = (event.clientX / tabWidth);
7983 					if(t >= 0 && t < tabs.length) {
7984 						currentTab_ = t;
7985 						tabIndexClicked(t);
7986 						redraw();
7987 					}
7988 				}
7989 			});
7990 		} else static assert(0);
7991 
7992 		this.container = makeContainer();
7993 	}
7994 
7995 	override int marginTop() { return 4; }
7996 	override int paddingBottom() { return 4; }
7997 
7998 	override int minHeight() {
7999 		int max = 0;
8000 		foreach(child; children)
8001 			max = mymax(child.minHeight, max);
8002 
8003 
8004 		version(win32_widgets) {
8005 			RECT rect;
8006 			rect.right = this.width;
8007 			rect.bottom = max;
8008 			TabCtrl_AdjustRect(hwnd, true, &rect);
8009 
8010 			max = rect.bottom;
8011 		} else {
8012 			max += defaultLineHeight + 4;
8013 		}
8014 
8015 
8016 		return max;
8017 	}
8018 
8019 	version(win32_widgets)
8020 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
8021 		switch(code) {
8022 			case TCN_SELCHANGE:
8023 				auto sel = TabCtrl_GetCurSel(hwnd);
8024 				tabIndexClicked(sel);
8025 			break;
8026 			default:
8027 		}
8028 		return 0;
8029 	}
8030 
8031 	version(custom_widgets) {
8032 		private int currentTab_;
8033 		private int tabBarHeight() { return defaultLineHeight; }
8034 		int tabWidth() { return scaleWithDpi(80); }
8035 	}
8036 
8037 	version(win32_widgets)
8038 	override void paint(WidgetPainter painter) {}
8039 
8040 	version(custom_widgets)
8041 	override void paint(WidgetPainter painter) {
8042 		auto cs = getComputedStyle();
8043 
8044 		draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color);
8045 
8046 		int posX = 0;
8047 		foreach(idx, title; tabs) {
8048 			auto isCurrent = idx == getCurrentTab();
8049 
8050 			painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight);
8051 
8052 			draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1));
8053 			painter.outlineColor = cs.foregroundColor;
8054 			painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter);
8055 
8056 			if(isCurrent) {
8057 				painter.outlineColor = cs.windowBackgroundColor;
8058 				painter.fillColor = Color.transparent;
8059 				painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1));
8060 				painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2));
8061 
8062 				painter.outlineColor = Color.white;
8063 				painter.drawPixel(Point(posX + 1, tabBarHeight - 1));
8064 				painter.drawPixel(Point(posX + 1, tabBarHeight - 2));
8065 				painter.outlineColor = cs.activeTabColor;
8066 				painter.drawPixel(Point(posX, tabBarHeight - 1));
8067 			}
8068 
8069 			posX += tabWidth - 2;
8070 		}
8071 	}
8072 
8073 	///
8074 	@scriptable
8075 	void setCurrentTab(int item) {
8076 		version(win32_widgets)
8077 			TabCtrl_SetCurSel(hwnd, item);
8078 		else version(custom_widgets)
8079 			currentTab_ = item;
8080 		else static assert(0);
8081 
8082 		tabIndexClicked(item);
8083 	}
8084 
8085 	///
8086 	@scriptable
8087 	int getCurrentTab() {
8088 		version(win32_widgets)
8089 			return TabCtrl_GetCurSel(hwnd);
8090 		else version(custom_widgets)
8091 			return currentTab_; // FIXME
8092 		else static assert(0);
8093 	}
8094 
8095 	///
8096 	@scriptable
8097 	void removeTab(int item) {
8098 		if(item && item == getCurrentTab())
8099 			setCurrentTab(item - 1);
8100 
8101 		version(win32_widgets) {
8102 			TabCtrl_DeleteItem(hwnd, item);
8103 		}
8104 
8105 		for(int a = item; a < children.length - 1; a++)
8106 			this._children[a] = this._children[a + 1];
8107 		this._children = this._children[0 .. $-1];
8108 	}
8109 
8110 }
8111 
8112 
8113 /++
8114 	A tab widget is a set of clickable tab buttons followed by a content area.
8115 
8116 
8117 	Tabs can change existing content or can be new pages.
8118 
8119 	When the user picks a different tab, a `change` message is generated.
8120 +/
8121 class TabWidget : TabMessageWidget {
8122 	this(Widget parent) {
8123 		super(parent);
8124 	}
8125 
8126 	override protected Widget makeContainer() {
8127 		return null;
8128 	}
8129 
8130 	override void addChild(Widget child, int pos = int.max) {
8131 		if(auto twp = cast(TabWidgetPage) child) {
8132 			Widget.addChild(child, pos);
8133 			if(pos == int.max)
8134 				pos = cast(int) this.children.length - 1;
8135 
8136 			super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop...
8137 
8138 			if(pos != getCurrentTab) {
8139 				child.showing = false;
8140 			}
8141 		} else {
8142 			assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)");
8143 		}
8144 	}
8145 
8146 	// FIXME: add tab icons at some point, Windows supports them
8147 	/++
8148 		Adds a page and its associated tab with the given label to the widget.
8149 
8150 		Returns:
8151 			The added page object, to which you can add other widgets.
8152 	+/
8153 	@scriptable
8154 	TabWidgetPage addPage(string title) {
8155 		return new TabWidgetPage(title, this);
8156 	}
8157 
8158 	/++
8159 		Gets the page at the given tab index, or `null` if the index is bad.
8160 
8161 		History:
8162 			Added December 24, 2021.
8163 	+/
8164 	TabWidgetPage getPage(int index) {
8165 		if(index < this.children.length)
8166 			return null;
8167 		return cast(TabWidgetPage) this.children[index];
8168 	}
8169 
8170 	/++
8171 		While you can still use the addTab from the parent class,
8172 		*strongly* recommend you use [addPage] insteaad.
8173 
8174 		History:
8175 			Added December 24, 2021 to fulful the interface
8176 			requirement that came from adding [TabMessageWidget].
8177 
8178 			You should not use it though since the [addPage] function
8179 			is much easier to use here.
8180 	+/
8181 	override int addTab(string title, int pos = int.max) {
8182 		auto p = addPage(title);
8183 		foreach(idx, child; this.children)
8184 			if(child is p)
8185 				return cast(int) idx;
8186 		return -1;
8187 	}
8188 
8189 	protected override void tabIndexClicked(int item) {
8190 		foreach(idx, child; children) {
8191 			child.showing(false, false); // batch the recalculates for the end
8192 		}
8193 
8194 		foreach(idx, child; children) {
8195 			if(idx == item) {
8196 				child.showing(true, false);
8197 				if(parentWindow) {
8198 					auto f = parentWindow.getFirstFocusable(child);
8199 					if(f)
8200 						f.focus();
8201 				}
8202 				recomputeChildLayout();
8203 			}
8204 		}
8205 
8206 		version(win32_widgets) {
8207 			InvalidateRect(hwnd, null, true);
8208 		} else version(custom_widgets) {
8209 			this.redraw();
8210 		}
8211 	}
8212 
8213 }
8214 
8215 /++
8216 	A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget".
8217 
8218 	You add [TabWidgetPage]s to it.
8219 +/
8220 class PageWidget : Widget {
8221 	this(Widget parent) {
8222 		super(parent);
8223 	}
8224 
8225 	override int minHeight() {
8226 		int max = 0;
8227 		foreach(child; children)
8228 			max = mymax(child.minHeight, max);
8229 
8230 		return max;
8231 	}
8232 
8233 
8234 	override void addChild(Widget child, int pos = int.max) {
8235 		if(auto twp = cast(TabWidgetPage) child) {
8236 			super.addChild(child, pos);
8237 			if(pos == int.max)
8238 				pos = cast(int) this.children.length - 1;
8239 
8240 			if(pos != getCurrentTab) {
8241 				child.showing = false;
8242 			}
8243 		} else {
8244 			assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)");
8245 		}
8246 	}
8247 
8248 	override void recomputeChildLayout() {
8249 		this.registerMovement();
8250 		foreach(child; children) {
8251 			child.x = 0;
8252 			child.y = 0;
8253 			child.width = width;
8254 			child.height = height;
8255 			child.recomputeChildLayout();
8256 		}
8257 	}
8258 
8259 	private int currentTab_;
8260 
8261 	///
8262 	@scriptable
8263 	void setCurrentTab(int item) {
8264 		currentTab_ = item;
8265 
8266 		showOnly(item);
8267 	}
8268 
8269 	///
8270 	@scriptable
8271 	int getCurrentTab() {
8272 		return currentTab_;
8273 	}
8274 
8275 	///
8276 	@scriptable
8277 	void removeTab(int item) {
8278 		if(item && item == getCurrentTab())
8279 			setCurrentTab(item - 1);
8280 
8281 		for(int a = item; a < children.length - 1; a++)
8282 			this._children[a] = this._children[a + 1];
8283 		this._children = this._children[0 .. $-1];
8284 	}
8285 
8286 	///
8287 	@scriptable
8288 	TabWidgetPage addPage(string title) {
8289 		return new TabWidgetPage(title, this);
8290 	}
8291 
8292 	private void showOnly(int item) {
8293 		foreach(idx, child; children)
8294 			if(idx == item) {
8295 				child.show();
8296 				child.queueRecomputeChildLayout();
8297 			} else {
8298 				child.hide();
8299 			}
8300 	}
8301 }
8302 
8303 /++
8304 
8305 +/
8306 class TabWidgetPage : Widget {
8307 	string title;
8308 	this(string title, Widget parent) {
8309 		this.title = title;
8310 		this.tabStop = false;
8311 		super(parent);
8312 
8313 		///*
8314 		version(win32_widgets) {
8315 			createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0);
8316 		}
8317 		//*/
8318 	}
8319 
8320 	override int minHeight() {
8321 		int sum = 0;
8322 		foreach(child; children)
8323 			sum += child.minHeight();
8324 		return sum;
8325 	}
8326 }
8327 
8328 version(none)
8329 /++
8330 	A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button.
8331 
8332 	I think I need to modify the layout algorithms to support this.
8333 +/
8334 class CollapsableSidebar : Widget {
8335 
8336 }
8337 
8338 /// Stacks the widgets vertically, taking all the available width for each child.
8339 class VerticalLayout : Layout {
8340 	// most of this is intentionally blank - widget's default is vertical layout right now
8341 	///
8342 	this(Widget parent) { super(parent); }
8343 
8344 	/++
8345 		Sets a max width for the layout so you don't have to subclass. The max width
8346 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
8347 
8348 		History:
8349 			Added November 29, 2021 (dub v10.5)
8350 	+/
8351 	this(int maxWidth, Widget parent) {
8352 		this.mw = maxWidth;
8353 		super(parent);
8354 	}
8355 
8356 	private int mw = int.max;
8357 
8358 	override int maxWidth() { return scaleWithDpi(mw); }
8359 }
8360 
8361 /// Stacks the widgets horizontally, taking all the available height for each child.
8362 class HorizontalLayout : Layout {
8363 	///
8364 	this(Widget parent) { super(parent); }
8365 
8366 	/++
8367 		Sets a max height for the layout so you don't have to subclass. The max height
8368 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
8369 
8370 		History:
8371 			Added November 29, 2021 (dub v10.5)
8372 	+/
8373 	this(int maxHeight, Widget parent) {
8374 		this.mh = maxHeight;
8375 		super(parent);
8376 	}
8377 
8378 	private int mh = 0;
8379 
8380 
8381 
8382 	override void recomputeChildLayout() {
8383 		.recomputeChildLayout!"width"(this);
8384 	}
8385 
8386 	override int minHeight() {
8387 		int largest = 0;
8388 		int margins = 0;
8389 		int lastMargin = 0;
8390 		foreach(child; children) {
8391 			auto mh = child.minHeight();
8392 			if(mh > largest)
8393 				largest = mh;
8394 			margins += mymax(lastMargin, child.marginTop());
8395 			lastMargin = child.marginBottom();
8396 		}
8397 		return largest + margins;
8398 	}
8399 
8400 	override int maxHeight() {
8401 		if(mh != 0)
8402 			return mymax(minHeight, scaleWithDpi(mh));
8403 
8404 		int largest = 0;
8405 		int margins = 0;
8406 		int lastMargin = 0;
8407 		foreach(child; children) {
8408 			auto mh = child.maxHeight();
8409 			if(mh == int.max)
8410 				return int.max;
8411 			if(mh > largest)
8412 				largest = mh;
8413 			margins += mymax(lastMargin, child.marginTop());
8414 			lastMargin = child.marginBottom();
8415 		}
8416 		return largest + margins;
8417 	}
8418 
8419 	override int heightStretchiness() {
8420 		int max;
8421 		foreach(child; children) {
8422 			auto c = child.heightStretchiness;
8423 			if(c > max)
8424 				max = c;
8425 		}
8426 		return max;
8427 	}
8428 }
8429 
8430 version(win32_widgets)
8431 private
8432 extern(Windows)
8433 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow {
8434 	Widget* pwin = hwnd in Widget.nativeMapping;
8435 	if(pwin is null)
8436 		return DefWindowProc(hwnd, message, wparam, lparam);
8437 	SimpleWindow win = pwin.simpleWindowWrappingHwnd;
8438 	if(win is null)
8439 		return DefWindowProc(hwnd, message, wparam, lparam);
8440 
8441 	switch(message) {
8442 		case WM_SIZE:
8443 			auto width = LOWORD(lparam);
8444 			auto height = HIWORD(lparam);
8445 
8446 			auto hdc = GetDC(hwnd);
8447 			auto hdcBmp = CreateCompatibleDC(hdc);
8448 
8449 			// FIXME: could this be more efficient? it never relinquishes a large bitmap
8450 			if(width > win.bmpWidth || height > win.bmpHeight) {
8451 				auto oldBuffer = win.buffer;
8452 				win.buffer = CreateCompatibleBitmap(hdc, width, height);
8453 
8454 				if(oldBuffer)
8455 					DeleteObject(oldBuffer);
8456 
8457 				win.bmpWidth = width;
8458 				win.bmpHeight = height;
8459 			}
8460 
8461 			// just always erase it upon resizing so minigui can draw over with a clean slate
8462 			auto oldBmp = SelectObject(hdcBmp, win.buffer);
8463 
8464 			auto brush = GetSysColorBrush(COLOR_3DFACE);
8465 			RECT r;
8466 			r.left = 0;
8467 			r.top = 0;
8468 			r.right = width;
8469 			r.bottom = height;
8470 			FillRect(hdcBmp, &r, brush);
8471 
8472 			SelectObject(hdcBmp, oldBmp);
8473 			DeleteDC(hdcBmp);
8474 			ReleaseDC(hwnd, hdc);
8475 		break;
8476 		case WM_PAINT:
8477 			if(win.buffer is null)
8478 				goto default;
8479 
8480 			BITMAP bm;
8481 			PAINTSTRUCT ps;
8482 
8483 			HDC hdc = BeginPaint(hwnd, &ps);
8484 
8485 			HDC hdcMem = CreateCompatibleDC(hdc);
8486 			HBITMAP hbmOld = SelectObject(hdcMem, win.buffer);
8487 
8488 			GetObject(win.buffer, bm.sizeof, &bm);
8489 
8490 			BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
8491 
8492 			SelectObject(hdcMem, hbmOld);
8493 			DeleteDC(hdcMem);
8494 			EndPaint(hwnd, &ps);
8495 		break;
8496 		default:
8497 			return DefWindowProc(hwnd, message, wparam, lparam);
8498 	}
8499 
8500 	return 0;
8501 }
8502 
8503 private wstring Win32Class(wstring name)() {
8504 	static bool classRegistered;
8505 	if(!classRegistered) {
8506 		HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null);
8507 		WNDCLASSEX wc;
8508 		wc.cbSize = wc.sizeof;
8509 		wc.hInstance = hInstance;
8510 		wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH);
8511 		wc.lpfnWndProc = &DoubleBufferWndProc;
8512 		wc.lpszClassName = name.ptr;
8513 		if(!RegisterClassExW(&wc))
8514 			throw new Exception("RegisterClass ");// ~ to!string(GetLastError()));
8515 		classRegistered = true;
8516 	}
8517 
8518 		return name;
8519 }
8520 
8521 /+
8522 version(win32_widgets)
8523 extern(Windows)
8524 private
8525 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
8526 	switch(iMessage) {
8527 		case WM_PAINT:
8528 			if(auto te = hWnd in Widget.nativeMapping) {
8529 				try {
8530 					//te.redraw();
8531 					writeln(te, " drawing");
8532 				} catch(Exception) {}
8533 			}
8534 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
8535 		default:
8536 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
8537 	}
8538 }
8539 +/
8540 
8541 
8542 /++
8543 	A widget specifically designed to hold other widgets.
8544 
8545 	History:
8546 		Added July 1, 2021
8547 +/
8548 class ContainerWidget : Widget {
8549 	this(Widget parent) {
8550 		super(parent);
8551 		this.tabStop = false;
8552 
8553 		version(win32_widgets) {
8554 			createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0);
8555 		}
8556 	}
8557 }
8558 
8559 /++
8560 	A widget that takes your widget, puts scroll bars around it, and sends
8561 	messages to it when the user scrolls. Unlike [ScrollableWidget], it makes
8562 	no effort to automatically scroll or clip its child widgets - it just sends
8563 	the messages.
8564 
8565 
8566 	A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed.
8567 	The scroll coordinates are all given in a unit you interpret as you wish. One
8568 	of these units is moved on each press of the arrow buttons and represents the
8569 	smallest amount the user can scroll. The intention is for this to be one line,
8570 	one item in a list, one row in a table, etc. Whatever makes sense for your widget
8571 	in each direction that the user might be interested in.
8572 
8573 	You can set a "page size" with the [step] property. (Yes, I regret the name...)
8574 	This is the amount it jumps when the user pressed page up and page down, or clicks
8575 	in the exposed part of the scroll bar.
8576 
8577 	You should add child content to the ScrollMessageWidget. However, it is important to
8578 	note that the coordinates are always independent of the scroll position! It is YOUR
8579 	responsibility to do any necessary transforms, clipping, etc., while drawing the
8580 	content and interpreting mouse events if they are supposed to change with the scroll.
8581 	This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries
8582 	to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives
8583 	you more control (which can be considerably more efficient and adapted to your actual data)
8584 	at the expense of you also needing to be aware of its reality.
8585 
8586 	Please note that it does NOT react to mouse wheel events or various keyboard events as of
8587 	version 10.3. Maybe this will change in the future.... but for now you must call
8588 	[addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself.
8589 +/
8590 class ScrollMessageWidget : Widget {
8591 	this(Widget parent) {
8592 		super(parent);
8593 
8594 		container = new Widget(this);
8595 		hsb = new HorizontalScrollbar(this);
8596 		vsb = new VerticalScrollbar(this);
8597 
8598 		hsb.addEventListener("scrolltonextline", {
8599 			hsb.setPosition(hsb.position + movementPerButtonClickH_);
8600 			notify();
8601 		});
8602 		hsb.addEventListener("scrolltopreviousline", {
8603 			hsb.setPosition(hsb.position - movementPerButtonClickH_);
8604 			notify();
8605 		});
8606 		vsb.addEventListener("scrolltonextline", {
8607 			vsb.setPosition(vsb.position + movementPerButtonClickV_);
8608 			notify();
8609 		});
8610 		vsb.addEventListener("scrolltopreviousline", {
8611 			vsb.setPosition(vsb.position - movementPerButtonClickV_);
8612 			notify();
8613 		});
8614 		hsb.addEventListener("scrolltonextpage", {
8615 			hsb.setPosition(hsb.position + hsb.step_);
8616 			notify();
8617 		});
8618 		hsb.addEventListener("scrolltopreviouspage", {
8619 			hsb.setPosition(hsb.position - hsb.step_);
8620 			notify();
8621 		});
8622 		vsb.addEventListener("scrolltonextpage", {
8623 			vsb.setPosition(vsb.position + vsb.step_);
8624 			notify();
8625 		});
8626 		vsb.addEventListener("scrolltopreviouspage", {
8627 			vsb.setPosition(vsb.position - vsb.step_);
8628 			notify();
8629 		});
8630 		hsb.addEventListener("scrolltoposition", (Event event) {
8631 			hsb.setPosition(event.intValue);
8632 			notify();
8633 		});
8634 		vsb.addEventListener("scrolltoposition", (Event event) {
8635 			vsb.setPosition(event.intValue);
8636 			notify();
8637 		});
8638 
8639 
8640 		tabStop = false;
8641 		container.tabStop = false;
8642 		magic = true;
8643 	}
8644 
8645 	private int movementPerButtonClickH_ = 1;
8646 	private int movementPerButtonClickV_ = 1;
8647 	public void movementPerButtonClick(int h, int v) {
8648 		movementPerButtonClickH_ = h;
8649 		movementPerButtonClickV_ = v;
8650 	}
8651 
8652 	/++
8653 		Add default event listeners for keyboard and mouse wheel scrolling shortcuts.
8654 
8655 
8656 		The defaults for [addDefaultWheelListeners] are:
8657 
8658 			$(LIST
8659 				* Mouse wheel scrolls vertically
8660 				* Alt key + mouse wheel scrolls horiontally
8661 				* Shift + mouse wheel scrolls faster.
8662 				* Any mouse click or wheel event will focus the inner widget if it has `tabStop = true`
8663 			)
8664 
8665 		The defaults for [addDefaultKeyboardListeners] are:
8666 
8667 			$(LIST
8668 				* Arrow keys scroll by the given amounts
8669 				* Shift+arrow keys scroll by the given amounts times the given shiftMultiplier
8670 				* Page up and down scroll by the vertical viewable area
8671 				* Home and end scroll to the start and end of the verticle viewable area.
8672 				* Alt + page up / page down / home / end will horizonally scroll instead of vertical.
8673 			)
8674 
8675 		My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line.
8676 
8677 		Params:
8678 			horizontalArrowScrollAmount =
8679 			verticalArrowScrollAmount =
8680 			verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel
8681 			horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel
8682 			shiftMultiplier = multiplies the scroll amount by this when shift is held
8683 	+/
8684 	void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) {
8685 		defaultKeyboardListener_verticalArrowScrollAmount = verticalArrowScrollAmount;
8686 		defaultKeyboardListener_horizontalArrowScrollAmount = horizontalArrowScrollAmount;
8687 		defaultKeyboardListener_shiftMultiplier = shiftMultiplier;
8688 
8689 		container.addEventListener(&defaultKeyboardListener);
8690 	}
8691 
8692 	/// ditto
8693 	void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) {
8694 		auto _this = this;
8695 		container.addEventListener((scope ClickEvent ce) {
8696 
8697 			//if(ce.target && ce.target.tabStop)
8698 				//ce.target.focus();
8699 
8700 			// ctrl is reserved for the application
8701 			if(ce.ctrlKey)
8702 				return;
8703 
8704 			if(horizontalWheelScrollAmount == 0 && ce.altKey)
8705 				return;
8706 
8707 			if(shiftMultiplier == 0 && ce.shiftKey)
8708 				return;
8709 
8710 			if(ce.button == MouseButton.wheelDown) {
8711 				if(ce.altKey)
8712 					_this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8713 				else
8714 					_this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8715 			} else if(ce.button == MouseButton.wheelUp) {
8716 				if(ce.altKey)
8717 					_this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8718 				else
8719 					_this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8720 			}
8721 		});
8722 	}
8723 
8724 	int defaultKeyboardListener_verticalArrowScrollAmount = 1;
8725 	int defaultKeyboardListener_horizontalArrowScrollAmount = 1;
8726 	int defaultKeyboardListener_shiftMultiplier = 3;
8727 
8728 	void defaultKeyboardListener(scope KeyDownEvent ke) {
8729 		switch(ke.key) {
8730 			case Key.Left:
8731 				this.scrollLeft(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8732 			break;
8733 			case Key.Right:
8734 				this.scrollRight(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8735 			break;
8736 			case Key.Up:
8737 				this.scrollUp(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8738 			break;
8739 			case Key.Down:
8740 				this.scrollDown(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8741 			break;
8742 			case Key.PageUp:
8743 				if(ke.altKey)
8744 					this.scrollLeft(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8745 				else
8746 					this.scrollUp(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8747 			break;
8748 			case Key.PageDown:
8749 				if(ke.altKey)
8750 					this.scrollRight(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8751 				else
8752 					this.scrollDown(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8753 			break;
8754 			case Key.Home:
8755 				if(ke.altKey)
8756 					this.scrollLeft(short.max * 16);
8757 				else
8758 					this.scrollUp(short.max * 16);
8759 			break;
8760 			case Key.End:
8761 				if(ke.altKey)
8762 					this.scrollRight(short.max * 16);
8763 				else
8764 					this.scrollDown(short.max * 16);
8765 			break;
8766 
8767 			default:
8768 				// ignore, not for us.
8769 		}
8770 	}
8771 
8772 	/++
8773 		Scrolls the given amount.
8774 
8775 		History:
8776 			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.
8777 	+/
8778 	void scrollUp(int amount = 1) {
8779 		vsb.setPosition(vsb.position.NonOverflowingInt - amount);
8780 		notify();
8781 	}
8782 	/// ditto
8783 	void scrollDown(int amount = 1) {
8784 		vsb.setPosition(vsb.position.NonOverflowingInt + amount);
8785 		notify();
8786 	}
8787 	/// ditto
8788 	void scrollLeft(int amount = 1) {
8789 		hsb.setPosition(hsb.position.NonOverflowingInt - amount);
8790 		notify();
8791 	}
8792 	/// ditto
8793 	void scrollRight(int amount = 1) {
8794 		hsb.setPosition(hsb.position.NonOverflowingInt + amount);
8795 		notify();
8796 	}
8797 
8798 	///
8799 	VerticalScrollbar verticalScrollBar() { return vsb; }
8800 	///
8801 	HorizontalScrollbar horizontalScrollBar() { return hsb; }
8802 
8803 	void notify() {
8804 		static bool insideNotify;
8805 
8806 		if(insideNotify)
8807 			return; // avoid the recursive call, even if it isn't strictly correct
8808 
8809 		insideNotify = true;
8810 		scope(exit) insideNotify = false;
8811 
8812 		this.emit!ScrollEvent();
8813 	}
8814 
8815 	mixin Emits!ScrollEvent;
8816 
8817 	///
8818 	Point position() {
8819 		return Point(hsb.position, vsb.position);
8820 	}
8821 
8822 	///
8823 	void setPosition(int x, int y) {
8824 		hsb.setPosition(x);
8825 		vsb.setPosition(y);
8826 	}
8827 
8828 	///
8829 	void setPageSize(int unitsX, int unitsY) {
8830 		hsb.setStep(unitsX);
8831 		vsb.setStep(unitsY);
8832 	}
8833 
8834 	/// Always call this BEFORE setViewableArea
8835 	void setTotalArea(int width, int height) {
8836 		hsb.setMax(width);
8837 		vsb.setMax(height);
8838 	}
8839 
8840 	/++
8841 		Always set the viewable area AFTER setitng the total area if you are going to change both.
8842 		NEVER call this from inside a scroll event. This includes through recomputeChildLayout.
8843 		If you need to do that, use [queueRecomputeChildLayout].
8844 	+/
8845 	void setViewableArea(int width, int height) {
8846 
8847 		// actually there IS A need to dothis cuz the max might have changed since then
8848 		//if(width == hsb.viewableArea_ && height == vsb.viewableArea_)
8849 			//return; // no need to do what is already done
8850 		hsb.setViewableArea(width);
8851 		vsb.setViewableArea(height);
8852 
8853 		bool needsNotify = false;
8854 
8855 		// FIXME: if at any point the rhs is outside the scrollbar, we need
8856 		// to reset to 0. but it should remember the old position in case the
8857 		// window resizes again, so it can kinda return ot where it was.
8858 		//
8859 		// so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0)
8860 		if(width >= hsb.max) {
8861 			// there's plenty of room to display it all so we need to reset to zero
8862 			// FIXME: adjust so it matches the note above
8863 			hsb.setPosition(0);
8864 			needsNotify = true;
8865 		}
8866 		if(height >= vsb.max) {
8867 			// there's plenty of room to display it all so we need to reset to zero
8868 			// FIXME: adjust so it matches the note above
8869 			vsb.setPosition(0);
8870 			needsNotify = true;
8871 		}
8872 		if(needsNotify)
8873 			notify();
8874 	}
8875 
8876 	private bool magic;
8877 	override void addChild(Widget w, int position = int.max) {
8878 		if(magic)
8879 			container.addChild(w, position);
8880 		else
8881 			super.addChild(w, position);
8882 	}
8883 
8884 	override void recomputeChildLayout() {
8885 		if(hsb is null || vsb is null || container is null) return;
8886 
8887 		registerMovement();
8888 
8889 		enum BUTTON_SIZE = 16;
8890 
8891 		hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane?
8892 		hsb.x = 0;
8893 		hsb.y = this.height - hsb.height;
8894 
8895 		vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME?
8896 		vsb.x = this.width - vsb.width;
8897 		vsb.y = 0;
8898 
8899 		auto vsb_width = vsb.showing ? vsb.width : 0;
8900 		auto hsb_height = hsb.showing ? hsb.height : 0;
8901 
8902 		hsb.width = this.width - vsb_width;
8903 		vsb.height = this.height - hsb_height;
8904 
8905 		hsb.recomputeChildLayout();
8906 		vsb.recomputeChildLayout();
8907 
8908 		if(this.header is null) {
8909 			container.x = 0;
8910 			container.y = 0;
8911 			container.width = this.width - vsb_width;
8912 			container.height = this.height - hsb_height;
8913 			container.recomputeChildLayout();
8914 		} else {
8915 			header.x = 0;
8916 			header.y = 0;
8917 			header.width = this.width - vsb_width;
8918 			header.height = scaleWithDpi(BUTTON_SIZE); // size of the button
8919 			header.recomputeChildLayout();
8920 
8921 			container.x = 0;
8922 			container.y = scaleWithDpi(BUTTON_SIZE);
8923 			container.width = this.width - vsb_width;
8924 			container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE);
8925 			container.recomputeChildLayout();
8926 		}
8927 	}
8928 
8929 	private HorizontalScrollbar hsb;
8930 	private VerticalScrollbar vsb;
8931 	Widget container;
8932 	private Widget header;
8933 
8934 	/++
8935 		Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button.
8936 
8937 		History:
8938 			Added September 27, 2021 (dub v10.3)
8939 	+/
8940 	Widget getHeader() {
8941 		if(this.header is null) {
8942 			magic = false;
8943 			scope(exit) magic = true;
8944 			this.header = new Widget(this);
8945 			queueRecomputeChildLayout();
8946 		}
8947 		return this.header;
8948 	}
8949 
8950 	/++
8951 		Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary.
8952 
8953 		History:
8954 			Added January 3, 2023 (dub v11.0)
8955 	+/
8956 	void scrollIntoView(Rectangle rect) {
8957 		Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_));
8958 
8959 		// import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1)));
8960 
8961 		// the lower right is exclusive normally
8962 		auto test = rect.lowerRight;
8963 		if(test.x > 0) test.x--;
8964 		if(test.y > 0) test.y--;
8965 
8966 		if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) {
8967 			// try to scroll only one dimension at a time if we can
8968 			if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y)))
8969 				setPosition(rect.upperLeft.x, position.y);
8970 			if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y)))
8971 				setPosition(position.x, rect.upperLeft.y);
8972 		}
8973 
8974 	}
8975 
8976 	override int minHeight() {
8977 		int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0));
8978 		if(header !is null)
8979 			min += header.minHeight;
8980 		if(horizontalScrollBar.showing)
8981 			min += horizontalScrollBar.minHeight;
8982 		return min;
8983 	}
8984 
8985 	override int maxHeight() {
8986 		int max = container ? container.maxHeight : int.max;
8987 		if(max == int.max)
8988 			return max;
8989 		if(horizontalScrollBar.showing)
8990 			max += horizontalScrollBar.minHeight;
8991 		return max;
8992 	}
8993 
8994 	static class Style : Widget.Style {
8995 		override WidgetBackground background() {
8996 			return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
8997 		}
8998 	}
8999 	mixin OverrideStyle!Style;
9000 }
9001 
9002 /++
9003 	$(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")
9004 	$(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.)
9005 +/
9006 version(minigui_screenshots)
9007 @Screenshot("ScrollMessageWidget")
9008 unittest {
9009 	auto window = new Window("ScrollMessageWidget");
9010 
9011 	auto smw = new ScrollMessageWidget(window);
9012 	smw.addDefaultKeyboardListeners();
9013 	smw.addDefaultWheelListeners();
9014 
9015 	window.loop();
9016 }
9017 
9018 /++
9019 	Bypasses automatic layout for its children, using manual positioning and sizing only.
9020 	While you need to manually position them, you must ensure they are inside the StaticLayout's
9021 	bounding box to avoid undefined behavior.
9022 
9023 	You should almost never use this.
9024 +/
9025 class StaticLayout : Layout {
9026 	///
9027 	this(Widget parent) { super(parent); }
9028 	override void recomputeChildLayout() {
9029 		registerMovement();
9030 		foreach(child; children)
9031 			child.recomputeChildLayout();
9032 	}
9033 }
9034 
9035 /++
9036 	Bypasses automatic positioning when being laid out. It is your responsibility to make
9037 	room for this widget in the parent layout.
9038 
9039 	Its children are laid out normally, unless there is exactly one, in which case it takes
9040 	on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you
9041 	can do that with `padding`).
9042 +/
9043 class StaticPosition : Layout {
9044 	///
9045 	this(Widget parent) { super(parent); }
9046 
9047 	override void recomputeChildLayout() {
9048 		registerMovement();
9049 		if(this.children.length == 1) {
9050 			auto child = children[0];
9051 			child.x = 0;
9052 			child.y = 0;
9053 			child.width = this.width;
9054 			child.height = this.height;
9055 			child.recomputeChildLayout();
9056 		} else
9057 		foreach(child; children)
9058 			child.recomputeChildLayout();
9059 	}
9060 
9061 	alias width = typeof(super).width;
9062 	alias height = typeof(super).height;
9063 
9064 	@property int width(int w) @nogc pure @safe nothrow {
9065 		return this._width = w;
9066 	}
9067 
9068 	@property int height(int w) @nogc pure @safe nothrow {
9069 		return this._height = w;
9070 	}
9071 
9072 }
9073 
9074 /++
9075 	FixedPosition is like [StaticPosition], but its coordinates
9076 	are always relative to the viewport, meaning they do not scroll with
9077 	the parent content.
9078 +/
9079 class FixedPosition : StaticPosition {
9080 	///
9081 	this(Widget parent) { super(parent); }
9082 }
9083 
9084 version(win32_widgets)
9085 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) {
9086 	if(true) {
9087 		// cmd == 0 = menu, cmd == 1 = accelerator
9088 		if(auto item = idm in Action.mapping) {
9089 			foreach(handler; (*item).triggered)
9090 				handler();
9091 		/*
9092 			auto event = new Event("triggered", *item);
9093 			event.button = idm;
9094 			event.dispatch();
9095 		*/
9096 			return 0;
9097 		}
9098 	}
9099 	if(handle)
9100 	if(auto widgetp = handle in Widget.nativeMapping) {
9101 		(*widgetp).handleWmCommand(cmd, idm);
9102 		return 0;
9103 	}
9104 	return 1;
9105 }
9106 
9107 
9108 ///
9109 class Window : Widget {
9110 	Widget[] mouseCapturedBy;
9111 	void captureMouse(Widget byWhom) {
9112 		assert(byWhom !is null);
9113 		if(mouseCapturedBy.length > 0) {
9114 			auto cc = mouseCapturedBy[$-1];
9115 			if(cc is byWhom)
9116 				return; // or should it throw?
9117 			auto par = byWhom;
9118 			while(par) {
9119 				if(cc is par)
9120 					goto allowed;
9121 				par = par.parent;
9122 			}
9123 
9124 			throw new Exception("mouse is already captured by other widget");
9125 		}
9126 		allowed:
9127 		mouseCapturedBy ~= byWhom;
9128 		if(mouseCapturedBy.length == 1)
9129 			win.grabInput(false, true, false);
9130 		//void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) {
9131 	}
9132 	void releaseMouseCapture() {
9133 		if(mouseCapturedBy.length == 0)
9134 			return; // or should it throw?
9135 		mouseCapturedBy = mouseCapturedBy[0 .. $-1];
9136 		mouseCapturedBy.assumeSafeAppend();
9137 		if(mouseCapturedBy.length == 0)
9138 			win.releaseInputGrab();
9139 	}
9140 
9141 
9142 	/++
9143 
9144 	+/
9145 	MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
9146 		return .messageBox(this, title, message, style, icon);
9147 	}
9148 
9149 	/// ditto
9150 	int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
9151 		return messageBox(null, message, style, icon);
9152 	}
9153 
9154 
9155 	/++
9156 		Sets the window icon which is often seen in title bars and taskbars.
9157 
9158 		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.
9159 
9160 		History:
9161 			Added April 5, 2022 (dub v10.8)
9162 	+/
9163 	@property void icon(MemoryImage icon) {
9164 		if(win && icon)
9165 			win.icon = icon;
9166 	}
9167 
9168 	// 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
9169 	// this does NOT change the icon on the window! That's what the other overload is for
9170 	static @property .icon icon(GenericIcons i) {
9171 		return .icon(i);
9172 	}
9173 
9174 	///
9175 	@scriptable
9176 	@property bool focused() {
9177 		return win.focused;
9178 	}
9179 
9180 	static class Style : Widget.Style {
9181 		override WidgetBackground background() {
9182 			version(custom_widgets)
9183 				return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
9184 			else version(win32_widgets)
9185 				return WidgetBackground(Color.transparent);
9186 			else static assert(0);
9187 		}
9188 	}
9189 	mixin OverrideStyle!Style;
9190 
9191 	/++
9192 		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.
9193 	+/
9194 	deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() {
9195 		return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback();
9196 	}
9197 
9198 	private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() {
9199 		OperatingSystemFont font;
9200 		if(auto vt = WidgetPainter.visualTheme) {
9201 			font = vt.defaultFontCached(96); // FIXME
9202 		}
9203 
9204 		if(font is null) {
9205 			static int defaultHeightCache;
9206 			if(defaultHeightCache == 0) {
9207 				font = new OperatingSystemFont;
9208 				font.loadDefault;
9209 				defaultHeightCache = font.height();// * 5 / 4;
9210 			}
9211 			return defaultHeightCache;
9212 		}
9213 
9214 		return font.height();// * 5 / 4;
9215 	}
9216 
9217 	Widget focusedWidget;
9218 
9219 	private SimpleWindow win_;
9220 
9221 	@property {
9222 		/++
9223 			Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers.
9224 
9225 			History:
9226 				Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property.
9227 		+/
9228 		public SimpleWindow win() {
9229 			return win_;
9230 		}
9231 		///
9232 		protected void win(SimpleWindow w) {
9233 			win_ = w;
9234 		}
9235 	}
9236 
9237 	/// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing.
9238 	this(Widget p) {
9239 		tabStop = false;
9240 		super(p);
9241 	}
9242 
9243 	private void actualRedraw() {
9244 		if(recomputeChildLayoutRequired)
9245 			recomputeChildLayoutEntry();
9246 		if(!showing) return;
9247 
9248 		assert(parentWindow !is null);
9249 
9250 		auto w = drawableWindow;
9251 		if(w is null)
9252 			w = parentWindow.win;
9253 
9254 		if(w.closed())
9255 			return;
9256 
9257 		auto ugh = this.parent;
9258 		int lox, loy;
9259 		while(ugh) {
9260 			lox += ugh.x;
9261 			loy += ugh.y;
9262 			ugh = ugh.parent;
9263 		}
9264 		auto painter = w.draw(true);
9265 		privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw());
9266 	}
9267 
9268 
9269 	private bool skipNextChar = false;
9270 
9271 	/++
9272 		Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers.
9273 
9274 		This constructor is intended primarily for internal use and may be changed to `protected` later.
9275 	+/
9276 	this(SimpleWindow win) {
9277 
9278 		static if(UsingSimpledisplayX11) {
9279 			win.discardAdditionalConnectionState = &discardXConnectionState;
9280 			win.recreateAdditionalConnectionState = &recreateXConnectionState;
9281 		}
9282 
9283 		tabStop = false;
9284 		super(null);
9285 		this.win = win;
9286 
9287 		win.addEventListener((Widget.RedrawEvent) {
9288 			if(win.eventQueued!RecomputeEvent) {
9289 				// writeln("skipping");
9290 				return; // let the recompute event do the actual redraw
9291 			}
9292 			this.actualRedraw();
9293 		});
9294 
9295 		win.addEventListener((Widget.RecomputeEvent) {
9296 			recomputeChildLayoutEntry();
9297 			if(win.eventQueued!RedrawEvent)
9298 				return; // let the queued one do it
9299 			else {
9300 				// writeln("drawing");
9301 				this.actualRedraw(); // if not queued, it needs to be done now anyway
9302 			}
9303 		});
9304 
9305 		this.width = win.width;
9306 		this.height = win.height;
9307 		this.parentWindow = this;
9308 
9309 		win.closeQuery = () {
9310 			if(this.emit!ClosingEvent())
9311 				win.close();
9312 		};
9313 		win.onClosing = () {
9314 			this.emit!ClosedEvent();
9315 		};
9316 
9317 		win.windowResized = (int w, int h) {
9318 			this.width = w;
9319 			this.height = h;
9320 			queueRecomputeChildLayout();
9321 			// this causes a HUGE performance problem for no apparent benefit, hence the commenting
9322 			//version(win32_widgets)
9323 				//InvalidateRect(hwnd, null, true);
9324 			redraw();
9325 		};
9326 
9327 		win.onFocusChange = (bool getting) {
9328 			// sdpyPrintDebugString("onFocusChange ", getting, " ", this.toString);
9329 			if(this.focusedWidget) {
9330 				if(getting) {
9331 					this.focusedWidget.emit!FocusEvent();
9332 					this.focusedWidget.emit!FocusInEvent();
9333 				} else {
9334 					this.focusedWidget.emit!BlurEvent();
9335 					this.focusedWidget.emit!FocusOutEvent();
9336 				}
9337 			}
9338 
9339 			if(getting) {
9340 				this.emit!FocusEvent();
9341 				this.emit!FocusInEvent();
9342 			} else {
9343 				this.emit!BlurEvent();
9344 				this.emit!FocusOutEvent();
9345 			}
9346 		};
9347 
9348 		win.onDpiChanged = {
9349 			this.queueRecomputeChildLayout();
9350 			auto event = new DpiChangedEvent(this);
9351 			event.sendDirectly();
9352 
9353 			privateDpiChanged();
9354 		};
9355 
9356 		win.setEventHandlers(
9357 			(MouseEvent e) {
9358 				dispatchMouseEvent(e);
9359 			},
9360 			(KeyEvent e) {
9361 				//writefln("%x   %s", cast(uint) e.key, e.key);
9362 				dispatchKeyEvent(e);
9363 			},
9364 			(dchar e) {
9365 				if(e == 13) e = 10; // hack?
9366 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
9367 				dispatchCharEvent(e);
9368 			},
9369 		);
9370 
9371 		addEventListener("char", (Widget, Event ev) {
9372 			if(skipNextChar) {
9373 				ev.preventDefault();
9374 				skipNextChar = false;
9375 			}
9376 		});
9377 
9378 		version(win32_widgets)
9379 		win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
9380 			if(hwnd !is this.win.impl.hwnd)
9381 				return 1; // we don't care... pass it on
9382 			auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn);
9383 			if(mustReturn)
9384 				return ret;
9385 			return 1; // pass it on
9386 		};
9387 
9388 		if(Window.newWindowCreated)
9389 			Window.newWindowCreated(this);
9390 	}
9391 
9392 	version(custom_widgets)
9393 	override void defaultEventHandler_click(ClickEvent event) {
9394 		if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) {
9395 			if(event.target && event.target.tabStop)
9396 				event.target.focus();
9397 		}
9398 	}
9399 
9400 	private static void delegate(Window) newWindowCreated;
9401 
9402 	version(win32_widgets)
9403 	override void paint(WidgetPainter painter) {
9404 		/*
9405 		RECT rect;
9406 		rect.right = this.width;
9407 		rect.bottom = this.height;
9408 		DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null);
9409 		*/
9410 		// 3dface is used as window backgrounds by Windows too, so that's why I'm using it here
9411 		auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
9412 		auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
9413 		// since the pen is null, to fill the whole space, we need the +1 on both.
9414 		gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
9415 		SelectObject(painter.impl.hdc, p);
9416 		SelectObject(painter.impl.hdc, b);
9417 	}
9418 	version(custom_widgets)
9419 	override void paint(WidgetPainter painter) {
9420 		auto cs = getComputedStyle();
9421 		painter.fillColor = cs.windowBackgroundColor;
9422 		painter.outlineColor = cs.windowBackgroundColor;
9423 		painter.drawRectangle(Point(0, 0), this.width, this.height);
9424 	}
9425 
9426 
9427 	override void defaultEventHandler_keydown(KeyDownEvent event) {
9428 		Widget _this = event.target;
9429 
9430 		if(event.key == Key.Tab) {
9431 			/* Window tab ordering is a recursive thingy with each group */
9432 
9433 			// FIXME inefficient
9434 			Widget[] helper(Widget p) {
9435 				if(p.hidden)
9436 					return null;
9437 				Widget[] childOrdering;
9438 
9439 				auto children = p.children.dup;
9440 
9441 				while(true) {
9442 					// UIs should be generally small, so gonna brute force it a little
9443 					// note that it must be a stable sort here; if all are index 0, it should be in order of declaration
9444 
9445 					Widget smallestTab;
9446 					foreach(ref c; children) {
9447 						if(c is null) continue;
9448 						if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) {
9449 							smallestTab = c;
9450 							c = null;
9451 						}
9452 					}
9453 					if(smallestTab !is null) {
9454 						if(smallestTab.tabStop && !smallestTab.hidden)
9455 							childOrdering ~= smallestTab;
9456 						if(!smallestTab.hidden)
9457 							childOrdering ~= helper(smallestTab);
9458 					} else
9459 						break;
9460 
9461 				}
9462 
9463 				return childOrdering;
9464 			}
9465 
9466 			Widget[] tabOrdering = helper(this);
9467 
9468 			Widget recipient;
9469 
9470 			if(tabOrdering.length) {
9471 				bool seenThis = false;
9472 				Widget previous;
9473 				foreach(idx, child; tabOrdering) {
9474 					if(child is focusedWidget) {
9475 
9476 						if(event.shiftKey) {
9477 							if(idx == 0)
9478 								recipient = tabOrdering[$-1];
9479 							else
9480 								recipient = tabOrdering[idx - 1];
9481 							break;
9482 						}
9483 
9484 						seenThis = true;
9485 						if(idx + 1 == tabOrdering.length) {
9486 							// we're at the end, either move to the next group
9487 							// or start back over
9488 							recipient = tabOrdering[0];
9489 						}
9490 						continue;
9491 					}
9492 					if(seenThis) {
9493 						recipient = child;
9494 						break;
9495 					}
9496 					previous = child;
9497 				}
9498 			}
9499 
9500 			if(recipient !is null) {
9501 				//  writeln(typeid(recipient));
9502 				recipient.focus();
9503 
9504 				skipNextChar = true;
9505 			}
9506 		}
9507 
9508 		debug if(event.key == Key.F12) {
9509 			if(devTools) {
9510 				devTools.close();
9511 				devTools = null;
9512 			} else {
9513 				devTools = new DevToolWindow(this);
9514 				devTools.show();
9515 			}
9516 		}
9517 	}
9518 
9519 	debug DevToolWindow devTools;
9520 
9521 
9522 	/++
9523 		Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display.
9524 
9525 		History:
9526 			Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead.
9527 
9528 			The width and height arguments were added to the overload that takes `string` first on June 21, 2021.
9529 	+/
9530 	this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
9531 		if(title is null) {
9532 			import core.runtime;
9533 			if(Runtime.args.length)
9534 				title = Runtime.args[0];
9535 		}
9536 		win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent);
9537 
9538 		static if(UsingSimpledisplayX11)
9539 		if(windowFlags & WindowFlags.managesChildWindowFocus) {
9540 		///+
9541 		// for input proxy
9542 		auto display = XDisplayConnection.get;
9543 		auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0);
9544 		XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask);
9545 		XMapWindow(display, inputProxy);
9546 		// writefln("input proxy: 0x%0x", inputProxy);
9547 		this.inputProxy = new SimpleWindow(inputProxy);
9548 
9549 		/+
9550 		this.inputProxy.onFocusChange = (bool getting) {
9551 			sdpyPrintDebugString("input proxy focus change ", getting);
9552 		};
9553 		+/
9554 
9555 		XEvent lastEvent;
9556 		this.inputProxy.handleNativeEvent = (XEvent ev) {
9557 			lastEvent = ev;
9558 			return 1;
9559 		};
9560 		this.inputProxy.setEventHandlers(
9561 			(MouseEvent e) {
9562 				dispatchMouseEvent(e);
9563 			},
9564 			(KeyEvent e) {
9565 				//writefln("%x   %s", cast(uint) e.key, e.key);
9566 				if(dispatchKeyEvent(e)) {
9567 					// FIXME: i should trap error
9568 					if(auto nw = cast(NestedChildWindowWidget) focusedWidget) {
9569 						auto thing = nw.focusableWindow();
9570 						if(thing && thing.window) {
9571 							lastEvent.xkey.window = thing.window;
9572 							// writeln("sending event ", lastEvent.xkey);
9573 							trapXErrors( {
9574 								XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent);
9575 							});
9576 						}
9577 					}
9578 				}
9579 			},
9580 			(dchar e) {
9581 				if(e == 13) e = 10; // hack?
9582 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
9583 				dispatchCharEvent(e);
9584 			},
9585 		);
9586 
9587 		this.inputProxy.populateXic();
9588 		// done
9589 		//+/
9590 		}
9591 
9592 
9593 
9594 		win.setRequestedInputFocus = &this.setRequestedInputFocus;
9595 
9596 		this(win);
9597 	}
9598 
9599 	SimpleWindow inputProxy;
9600 
9601 	private SimpleWindow setRequestedInputFocus() {
9602 		return inputProxy;
9603 	}
9604 
9605 	/// ditto
9606 	this(string title, int width = 500, int height = 500) {
9607 		this(width, height, title);
9608 	}
9609 
9610 	///
9611 	@property string title() { return parentWindow.win.title; }
9612 	///
9613 	@property void title(string title) { parentWindow.win.title = title; }
9614 
9615 	///
9616 	@scriptable
9617 	void close() {
9618 		win.close();
9619 		// I synchronize here upon window closing to ensure all child windows
9620 		// get updated too before the event loop. This avoids some random X errors.
9621 		static if(UsingSimpledisplayX11) {
9622 			runInGuiThread( {
9623 				XSync(XDisplayConnection.get, false);
9624 			});
9625 		}
9626 	}
9627 
9628 	bool dispatchKeyEvent(KeyEvent ev) {
9629 		auto wid = focusedWidget;
9630 		if(wid is null)
9631 			wid = this;
9632 		KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid);
9633 		event.originalKeyEvent = ev;
9634 		event.key = ev.key;
9635 		event.state = ev.modifierState;
9636 		event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
9637 		event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
9638 		event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
9639 		event.dispatch();
9640 
9641 		return !event.propagationStopped;
9642 	}
9643 
9644 	// returns true if propagation should continue into nested things.... prolly not a great thing to do.
9645 	bool dispatchCharEvent(dchar ch) {
9646 		if(focusedWidget) {
9647 			auto event = new CharEvent(focusedWidget, ch);
9648 			event.dispatch();
9649 			return !event.propagationStopped;
9650 		}
9651 		return true;
9652 	}
9653 
9654 	Widget mouseLastOver;
9655 	Widget mouseLastDownOn;
9656 	bool lastWasDoubleClick;
9657 	bool dispatchMouseEvent(MouseEvent ev) {
9658 		auto eleR = widgetAtPoint(this, ev.x, ev.y);
9659 		auto ele = eleR.widget;
9660 
9661 		auto captureEle = ele;
9662 
9663 		auto mouseCapturedBy = this.mouseCapturedBy.length ? this.mouseCapturedBy[$-1] : null;
9664 		if(mouseCapturedBy !is null) {
9665 			if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele))
9666 				captureEle = mouseCapturedBy;
9667 		}
9668 
9669 		// a hack to get it relative to the widget.
9670 		eleR.x = ev.x;
9671 		eleR.y = ev.y;
9672 		auto pain = captureEle;
9673 
9674 		auto vpx = eleR.x;
9675 		auto vpy = eleR.y;
9676 
9677 		while(pain) {
9678 			eleR.x -= pain.x;
9679 			eleR.y -= pain.y;
9680 			pain.addScrollPosition(eleR.x, eleR.y);
9681 
9682 			vpx -= pain.x;
9683 			vpy -= pain.y;
9684 
9685 			pain = pain.parent;
9686 		}
9687 
9688 		void populateMouseEventBase(MouseEventBase event) {
9689 			event.button = ev.button;
9690 			event.buttonLinear = ev.buttonLinear;
9691 			event.state = ev.modifierState;
9692 			event.clientX = eleR.x;
9693 			event.clientY = eleR.y;
9694 
9695 			event.viewportX = vpx;
9696 			event.viewportY = vpy;
9697 
9698 			event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
9699 			event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
9700 			event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
9701 		}
9702 
9703 		if(ev.type == MouseEventType.buttonPressed) {
9704 			{
9705 				auto event = new MouseDownEvent(captureEle);
9706 				populateMouseEventBase(event);
9707 				event.dispatch();
9708 			}
9709 
9710 			if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) {
9711 				auto event = new DoubleClickEvent(captureEle);
9712 				populateMouseEventBase(event);
9713 				event.dispatch();
9714 				lastWasDoubleClick = ev.doubleClick;
9715 			} else {
9716 				lastWasDoubleClick = false;
9717 			}
9718 
9719 			mouseLastDownOn = ele;
9720 		} else if(ev.type == MouseEventType.buttonReleased) {
9721 			{
9722 				auto event = new MouseUpEvent(captureEle);
9723 				populateMouseEventBase(event);
9724 				event.dispatch();
9725 			}
9726 			if(!lastWasDoubleClick && mouseLastDownOn is ele) {
9727 				auto event = new ClickEvent(captureEle);
9728 				populateMouseEventBase(event);
9729 				event.dispatch();
9730 			}
9731 		} else if(ev.type == MouseEventType.motion) {
9732 			// motion
9733 			{
9734 				auto event = new MouseMoveEvent(captureEle);
9735 				populateMouseEventBase(event); // fills in button which is meaningless but meh
9736 				event.dispatch();
9737 			}
9738 
9739 			if(mouseLastOver !is ele) {
9740 				if(ele !is null) {
9741 					if(!isAParentOf(ele, mouseLastOver)) {
9742 						ele.setDynamicState(DynamicState.hover, true);
9743 						auto event = new MouseEnterEvent(ele);
9744 						event.relatedTarget = mouseLastOver;
9745 						event.sendDirectly();
9746 
9747 						ele.useStyleProperties((scope Widget.Style s) {
9748 							ele.parentWindow.win.cursor = s.cursor;
9749 						});
9750 					}
9751 				}
9752 
9753 				if(mouseLastOver !is null) {
9754 					if(!isAParentOf(mouseLastOver, ele)) {
9755 						mouseLastOver.setDynamicState(DynamicState.hover, false);
9756 						auto event = new MouseLeaveEvent(mouseLastOver);
9757 						event.relatedTarget = ele;
9758 						event.sendDirectly();
9759 					}
9760 				}
9761 
9762 				if(ele !is null) {
9763 					auto event = new MouseOverEvent(ele);
9764 					event.relatedTarget = mouseLastOver;
9765 					event.dispatch();
9766 				}
9767 
9768 				if(mouseLastOver !is null) {
9769 					auto event = new MouseOutEvent(mouseLastOver);
9770 					event.relatedTarget = ele;
9771 					event.dispatch();
9772 				}
9773 
9774 				mouseLastOver = ele;
9775 			}
9776 		}
9777 
9778 		return true; // FIXME: the event default prevented?
9779 	}
9780 
9781 	/++
9782 		Shows the window and runs the application event loop.
9783 
9784 		Blocks until this window is closed.
9785 
9786 		Bugs:
9787 
9788 		$(PITFALL
9789 			You should always have one event loop live for your application.
9790 			If you make two windows in sequence, the second call to loop (or
9791 			simpledisplay's [SimpleWindow.eventLoop], upon which this is built)
9792 			might fail:
9793 
9794 			---
9795 			// don't do this!
9796 			auto window = new Window();
9797 			window.loop();
9798 
9799 			// or new Window or new MainWindow, all the same
9800 			auto window2 = new SimpleWindow();
9801 			window2.eventLoop(0); // problematic! might crash
9802 			---
9803 
9804 			simpledisplay's current implementation assumes that final cleanup is
9805 			done when the event loop refcount reaches zero. So after the first
9806 			eventLoop returns, when there isn't already another one active, it assumes
9807 			the program will exit soon and cleans up.
9808 
9809 			This is arguably a bug that it doesn't reinitialize, and I'll probably change
9810 			it eventually, but in the mean time, there's an easy solution:
9811 
9812 			---
9813 			// do this
9814 			EventLoop mainEventLoop = EventLoop.get; // just add this line
9815 
9816 			auto window = new Window();
9817 			window.loop();
9818 
9819 			// or any other type of Window etc.
9820 			auto window2 = new Window();
9821 			window2.loop(); // perfectly fine since mainEventLoop still alive
9822 			---
9823 
9824 			By adding a top-level reference to the event loop, it ensures the final cleanup
9825 			is not performed until it goes out of scope too, letting the individual window loops
9826 			work without trouble despite the bug.
9827 		)
9828 
9829 		History:
9830 			The [BlockingMode] parameter was added on December 8, 2021.
9831 			The default behavior is to block until the application quits
9832 			(so all windows have been closed), unless another minigui or
9833 			simpledisplay event loop is already running, in which case it
9834 			will block until this window closes specifically.
9835 	+/
9836 	@scriptable
9837 	void loop(BlockingMode bm = BlockingMode.automatic) {
9838 		if(win.closed)
9839 			return; // otherwise show will throw
9840 		show();
9841 		win.eventLoopWithBlockingMode(bm, 0);
9842 	}
9843 
9844 	private bool firstShow = true;
9845 
9846 	@scriptable
9847 	override void show() {
9848 		bool rd = false;
9849 		if(firstShow) {
9850 			firstShow = false;
9851 			queueRecomputeChildLayout();
9852 			// unless the programmer already called focus on something, pick something ourselves
9853 			auto f = focusedWidget is null ? getFirstFocusable(this) : focusedWidget; // FIXME: autofocus?
9854 			if(f)
9855 				f.focus();
9856 			redraw();
9857 		}
9858 		win.show();
9859 		super.show();
9860 	}
9861 	@scriptable
9862 	override void hide() {
9863 		win.hide();
9864 		super.hide();
9865 	}
9866 
9867 	static Widget getFirstFocusable(Widget start) {
9868 		if(start is null)
9869 			return null;
9870 
9871 		foreach(widget; &start.focusableWidgets) {
9872 			return widget;
9873 		}
9874 
9875 		return null;
9876 	}
9877 
9878 	static Widget getLastFocusable(Widget start) {
9879 		if(start is null)
9880 			return null;
9881 
9882 		Widget last;
9883 		foreach(widget; &start.focusableWidgets) {
9884 			last = widget;
9885 		}
9886 
9887 		return last;
9888 	}
9889 
9890 
9891 	mixin Emits!ClosingEvent;
9892 	mixin Emits!ClosedEvent;
9893 }
9894 
9895 /++
9896 	History:
9897 		Added January 12, 2022
9898 
9899 		Made `final` on January 3, 2025
9900 +/
9901 final class DpiChangedEvent : Event {
9902 	enum EventString = "dpichanged";
9903 
9904 	this(Widget target) {
9905 		super(EventString, target);
9906 	}
9907 }
9908 
9909 debug private class DevToolWindow : Window {
9910 	Window p;
9911 
9912 	TextEdit parentList;
9913 	TextEdit logWindow;
9914 	TextLabel clickX, clickY;
9915 
9916 	this(Window p) {
9917 		this.p = p;
9918 		super(400, 300, "Developer Toolbox");
9919 
9920 		logWindow = new TextEdit(this);
9921 		parentList = new TextEdit(this);
9922 
9923 		auto hl = new HorizontalLayout(this);
9924 		clickX = new TextLabel("", TextAlignment.Right, hl);
9925 		clickY = new TextLabel("", TextAlignment.Right, hl);
9926 
9927 		parentListeners ~= p.addEventListener("*", (Event ev) {
9928 			log(typeid(ev.source).name, " emitted ", typeid(ev).name);
9929 		});
9930 
9931 		parentListeners ~= p.addEventListener((ClickEvent ev) {
9932 			auto s = ev.srcElement;
9933 
9934 			string list;
9935 
9936 			void addInfo(Widget s) {
9937 				list ~= s.toString();
9938 				list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight);
9939 				list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight);
9940 				list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness);
9941 				list ~= "\n\theight: " ~ toInternal!string(s.height);
9942 				list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth);
9943 				list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth);
9944 				list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness);
9945 				list ~= "\n\twidth: " ~ toInternal!string(s.width);
9946 				list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop);
9947 				list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom);
9948 			}
9949 
9950 			addInfo(s);
9951 
9952 			s = s.parent;
9953 			while(s) {
9954 				list ~= "\n";
9955 				addInfo(s);
9956 				s = s.parent;
9957 			}
9958 			parentList.content = list;
9959 
9960 			clickX.label = toInternal!string(ev.clientX);
9961 			clickY.label = toInternal!string(ev.clientY);
9962 		});
9963 	}
9964 
9965 	EventListener[] parentListeners;
9966 
9967 	override void close() {
9968 		assert(p !is null);
9969 		foreach(p; parentListeners)
9970 			p.disconnect();
9971 		parentListeners = null;
9972 		p.devTools = null;
9973 		p = null;
9974 		super.close();
9975 	}
9976 
9977 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
9978 		if(ev.key == Key.F12) {
9979 			this.close();
9980 			if(p)
9981 				p.devTools = null;
9982 		} else {
9983 			super.defaultEventHandler_keydown(ev);
9984 		}
9985 	}
9986 
9987 	void log(T...)(T t) {
9988 		string str;
9989 		import std.conv;
9990 		foreach(i; t)
9991 			str ~= to!string(i);
9992 		str ~= "\n";
9993 		logWindow.addText(str);
9994 		logWindow.scrollToBottom();
9995 
9996 		//version(custom_widgets)
9997 		//logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox());
9998 	}
9999 }
10000 
10001 /++
10002 	A dialog is a transient window that intends to get information from
10003 	the user before being dismissed.
10004 +/
10005 class Dialog : Window {
10006 	///
10007 	this(Window parent, int width, int height, string title = null) {
10008 		super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win);
10009 
10010 		// this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
10011 	}
10012 
10013 	///
10014 	this(Window parent, string title, int width, int height) {
10015 		this(parent, width, height, title);
10016 	}
10017 
10018 	deprecated("Pass an explicit parent window, even if it is `null`")
10019 	this(int width, int height, string title = null) {
10020 		this(null, width, height, title);
10021 	}
10022 
10023 	///
10024 	void OK() {
10025 
10026 	}
10027 
10028 	///
10029 	void Cancel() {
10030 		this.close();
10031 	}
10032 }
10033 
10034 /++
10035 	A custom widget similar to the HTML5 <details> tag.
10036 +/
10037 version(none)
10038 class DetailsView : Widget {
10039 
10040 }
10041 
10042 // FIXME: maybe i should expose the other list views Windows offers too
10043 
10044 /++
10045 	A TableView is a widget made to display a table of data strings.
10046 
10047 
10048 	Future_Directions:
10049 		Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers.
10050 
10051 		I will add a selection changed event at some point, as well as item clicked events.
10052 	History:
10053 		Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet.
10054 	See_Also:
10055 		[ListWidget] which displays a list of strings without additional columns.
10056 +/
10057 class TableView : Widget {
10058 	/++
10059 
10060 	+/
10061 	this(Widget parent) {
10062 		super(parent);
10063 
10064 		version(win32_widgets) {
10065 			// LVS_EX_LABELTIP might be worth too
10066 			// LVS_OWNERDRAWFIXED
10067 			createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//, LVS_EX_TRACKSELECT); // ex style for for LVN_HOTTRACK
10068 		} else version(custom_widgets) {
10069 			auto smw = new ScrollMessageWidget(this);
10070 			smw.addDefaultKeyboardListeners();
10071 			smw.addDefaultWheelListeners(1, scaleWithDpi(16));
10072 			tvwi = new TableViewWidgetInner(this, smw);
10073 		}
10074 	}
10075 
10076 	// FIXME: auto-size columns on double click of header thing like in Windows
10077 	// it need only make the currently displayed things fit well.
10078 
10079 
10080 	private ColumnInfo[] columns;
10081 	private int itemCount;
10082 
10083 	version(custom_widgets) private {
10084 		TableViewWidgetInner tvwi;
10085 	}
10086 
10087 	/// Passed to [setColumnInfo]
10088 	static struct ColumnInfo {
10089 		const(char)[] name; /// the name displayed in the header
10090 		/++
10091 			The default width, in pixels. As a special case, you can set this to -1
10092 			if you want the system to try to automatically size the width to fit visible
10093 			content. If it can't, it will try to pick a sensible default size.
10094 
10095 			Any other negative value is not allowed and may lead to unpredictable results.
10096 
10097 			History:
10098 				The -1 behavior was specified on December 3, 2021. It actually worked before
10099 				anyway on Win32 but now it is a formal feature with partial Linux support.
10100 
10101 			Bugs:
10102 				It doesn't actually attempt to calculate a best-fit width on Linux as of
10103 				December 3, 2021. I do plan to fix this in the future, but Windows is the
10104 				priority right now. At least it doesn't break things when you use it now.
10105 		+/
10106 		int width;
10107 
10108 		/++
10109 			Alignment of the text in the cell. Applies to the header as well as all data in this
10110 			column.
10111 
10112 			Bugs:
10113 				On Windows, the first column ignores this member and is always left aligned.
10114 				You can work around this by inserting a dummy first column with width = 0
10115 				then putting your actual data in the second column, which does respect the
10116 				alignment.
10117 
10118 				This is a quirk of the operating system's implementation going back a very
10119 				long time and is unlikely to ever be fixed.
10120 		+/
10121 		TextAlignment alignment;
10122 
10123 		/++
10124 			After all the pixel widths have been assigned, any left over
10125 			space is divided up among all columns and distributed to according
10126 			to the widthPercent field.
10127 
10128 
10129 			For example, if you have two fields, both with width 50 and one with
10130 			widthPercent of 25 and the other with widthPercent of 75, and the
10131 			container is 200 pixels wide, first both get their width of 50.
10132 			then the 100 remaining pixels are split up, so the one gets a total
10133 			of 75 pixels and the other gets a total of 125.
10134 
10135 			This is automatically applied as the window is resized.
10136 
10137 			If there is not enough space - that is, when a horizontal scrollbar
10138 			needs to appear - there are 0 pixels divided up, and thus everyone
10139 			gets 0. This can cause a column to shrink out of proportion when
10140 			passing the scroll threshold.
10141 
10142 			It is important to still set a fixed width (that is, to populate the
10143 			`width` field) even if you use the percents because that will be the
10144 			default minimum in the event of a scroll bar appearing.
10145 
10146 			The percents total in the column can never exceed 100 or be less than 0.
10147 			Doing this will trigger an assert error.
10148 
10149 			Implementation note:
10150 
10151 			Please note that percentages are only recalculated 1) upon original
10152 			construction and 2) upon resizing the control. If the user adjusts the
10153 			width of a column, the percentage items will not be updated.
10154 
10155 			On the other hand, if the user adjusts the width of a percentage column
10156 			then resizes the window, it is recalculated, meaning their hand adjustment
10157 			is discarded. This specific behavior may change in the future as it is
10158 			arguably a bug, but I'm not certain yet.
10159 
10160 			History:
10161 				Added November 10, 2021 (dub v10.4)
10162 		+/
10163 		int widthPercent;
10164 
10165 
10166 		private int calculatedWidth;
10167 	}
10168 	/++
10169 		Sets the number of columns along with information about the headers.
10170 
10171 		Please note: on Windows, the first column ignores your alignment preference
10172 		and is always left aligned.
10173 	+/
10174 	void setColumnInfo(ColumnInfo[] columns...) {
10175 
10176 		foreach(ref c; columns) {
10177 			c.name = c.name.idup;
10178 		}
10179 		this.columns = columns.dup;
10180 
10181 		updateCalculatedWidth(false);
10182 
10183 		version(custom_widgets) {
10184 			tvwi.header.updateHeaders();
10185 			tvwi.updateScrolls();
10186 		} else version(win32_widgets)
10187 		foreach(i, column; this.columns) {
10188 			LVCOLUMN lvColumn;
10189 			lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
10190 			lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth;
10191 
10192 			auto bfr = WCharzBuffer(column.name);
10193 			lvColumn.pszText = bfr.ptr;
10194 
10195 			if(column.alignment & TextAlignment.Center)
10196 				lvColumn.fmt = LVCFMT_CENTER;
10197 			else if(column.alignment & TextAlignment.Right)
10198 				lvColumn.fmt = LVCFMT_RIGHT;
10199 			else
10200 				lvColumn.fmt = LVCFMT_LEFT;
10201 
10202 			if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1)
10203 				throw new WindowsApiException("Insert Column Fail", GetLastError());
10204 		}
10205 	}
10206 
10207 	version(custom_widgets)
10208 	private int getColumnSizeForContent(size_t columnIndex) {
10209 		// FIXME: idk where the problem is but with a 2x scale the horizontal scroll is insuffiicent. i think the SMW is doing it wrong.
10210 		// might also want a user-defined max size too
10211 		int padding = scaleWithDpi(6);
10212 		int m = this.defaultTextWidth(this.columns[columnIndex].name) + padding;
10213 
10214 		if(getData !is null)
10215 		foreach(row; 0 .. itemCount)
10216 			getData(row, cast(int) columnIndex, (txt) {
10217 				m = mymax(m, this.defaultTextWidth(txt) + padding);
10218 			});
10219 
10220 		if(m < 32)
10221 			m = 32;
10222 
10223 		return m;
10224 	}
10225 
10226 	/++
10227 		History:
10228 			Added February 26, 2025
10229 	+/
10230 	void autoSizeColumnsToContent() {
10231 		version(custom_widgets) {
10232 			foreach(idx, ref c; columns) {
10233 				c.width = getColumnSizeForContent(idx);
10234 			}
10235 			updateCalculatedWidth(false);
10236 			tvwi.updateScrolls();
10237 		} else version(win32_widgets) {
10238 			foreach(i, c; columns)
10239 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, LVSCW_AUTOSIZE); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
10240 		}
10241 	}
10242 
10243 	/++
10244 		History:
10245 			Added March 1, 2025
10246 	+/
10247 	bool supportsPerCellAlignment() {
10248 		version(custom_widgets)
10249 			return true;
10250 		else version(win32_widgets)
10251 			return false;
10252 		return false;
10253 	}
10254 
10255 	private int getActualSetSize(size_t i, bool askWindows) {
10256 		version(win32_widgets)
10257 			if(askWindows)
10258 				return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0);
10259 		auto w = columns[i].width;
10260 		if(w == -1)
10261 			return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME
10262 		return w;
10263 	}
10264 
10265 	private void updateCalculatedWidth(bool informWindows) {
10266 		int padding;
10267 		version(win32_widgets)
10268 			padding = 4;
10269 		int remaining = this.width;
10270 		foreach(i, column; columns)
10271 			remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding;
10272 		remaining -= padding;
10273 		if(remaining < 0)
10274 			remaining = 0;
10275 
10276 		int percentTotal;
10277 		foreach(i, ref column; columns) {
10278 			percentTotal += column.widthPercent;
10279 
10280 			auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100;
10281 
10282 			column.calculatedWidth = c;
10283 
10284 			version(win32_widgets)
10285 			if(informWindows)
10286 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
10287 		}
10288 
10289 		assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100.");
10290 		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).");
10291 
10292 
10293 	}
10294 
10295 	override void registerMovement() {
10296 		super.registerMovement();
10297 
10298 		updateCalculatedWidth(true);
10299 	}
10300 
10301 	/++
10302 		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.
10303 	+/
10304 	void setItemCount(int count) {
10305 		this.itemCount = count;
10306 		version(custom_widgets) {
10307 			tvwi.updateScrolls();
10308 			redraw();
10309 		} else version(win32_widgets) {
10310 			SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0);
10311 		}
10312 	}
10313 
10314 	/++
10315 		Clears all items;
10316 	+/
10317 	void clear() {
10318 		this.itemCount = 0;
10319 		this.columns = null;
10320 		version(custom_widgets) {
10321 			tvwi.header.updateHeaders();
10322 			tvwi.updateScrolls();
10323 			redraw();
10324 		} else version(win32_widgets) {
10325 			SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0);
10326 		}
10327 	}
10328 
10329 	/+
10330 	version(win32_widgets)
10331 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis)
10332 		auto itemId = dis.itemID;
10333 		auto hdc = dis.hDC;
10334 		auto rect = dis.rcItem;
10335 		switch(dis.itemAction) {
10336 			case ODA_DRAWENTIRE:
10337 
10338 				// FIXME: do other items
10339 				// FIXME: do the focus rectangle i guess
10340 				// FIXME: alignment
10341 				// FIXME: column width
10342 				// FIXME: padding left
10343 				// FIXME: check dpi scaling
10344 				// FIXME: don't owner draw unless it is necessary.
10345 
10346 				auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi
10347 				RECT itemRect;
10348 				itemRect.top = 1; // subitem idx, 1-based
10349 				itemRect.left = LVIR_BOUNDS;
10350 
10351 				SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect);
10352 				itemRect.left += padding;
10353 
10354 				getData(itemId, 0, (in char[] data) {
10355 					auto wdata = WCharzBuffer(data);
10356 					DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS);
10357 
10358 				});
10359 			goto case;
10360 			case ODA_FOCUS:
10361 				if(dis.itemState & ODS_FOCUS)
10362 					DrawFocusRect(hdc, &rect);
10363 			break;
10364 			case ODA_SELECT:
10365 				// itemState & ODS_SELECTED
10366 			break;
10367 			default:
10368 		}
10369 		return 1;
10370 	}
10371 	+/
10372 
10373 	version(win32_widgets) {
10374 		CellStyle last;
10375 		COLORREF defaultColor;
10376 		COLORREF defaultBackground;
10377 	}
10378 
10379 	version(win32_widgets)
10380 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
10381 		switch(code) {
10382 			case NM_CUSTOMDRAW:
10383 				auto s = cast(NMLVCUSTOMDRAW*) hdr;
10384 				switch(s.nmcd.dwDrawStage) {
10385 					case CDDS_PREPAINT:
10386 						if(getCellStyle is null)
10387 							return 0;
10388 
10389 						mustReturn = true;
10390 						return CDRF_NOTIFYITEMDRAW;
10391 					case CDDS_ITEMPREPAINT:
10392 						mustReturn = true;
10393 						return CDRF_NOTIFYSUBITEMDRAW;
10394 					case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
10395 						mustReturn = true;
10396 
10397 						if(getCellStyle is null) // this SHOULD never happen...
10398 							return 0;
10399 
10400 						if(s.iSubItem == 0) {
10401 							// Windows resets it per row so we'll use item 0 as a chance
10402 							// to capture these for later
10403 							defaultColor = s.clrText;
10404 							defaultBackground = s.clrTextBk;
10405 						}
10406 
10407 						auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem);
10408 						// if no special style and no reset needed...
10409 						if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init))
10410 							return 0; // allow default processing to continue
10411 
10412 						last = style;
10413 
10414 						// might still need to reset or use the preference.
10415 
10416 						if(style.flags & CellStyle.Flags.textColorSet)
10417 							s.clrText = style.textColor.asWindowsColorRef;
10418 						else
10419 							s.clrText = defaultColor; // reset in case it was set from last iteration not a fan
10420 						if(style.flags & CellStyle.Flags.backgroundColorSet)
10421 							s.clrTextBk = style.backgroundColor.asWindowsColorRef;
10422 						else
10423 							s.clrTextBk = defaultBackground; // need to reset it... not a fan of this
10424 
10425 						return CDRF_NEWFONT;
10426 					default:
10427 						return 0;
10428 
10429 				}
10430 			case NM_RETURN: // no need since i subclass keydown
10431 			break;
10432 			case LVN_COLUMNCLICK:
10433 				auto info = cast(LPNMLISTVIEW) hdr;
10434 				this.emit!HeaderClickedEvent(info.iSubItem);
10435 			break;
10436 			case (LVN_FIRST-21) /* LVN_HOTTRACK */:
10437 				// requires LVS_EX_TRACKSELECT
10438 				// sdpyPrintDebugString("here");
10439 				mustReturn = 1; // override Windows' auto selection
10440 			break;
10441 			case NM_CLICK:
10442 				NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr;
10443 				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);
10444 			break;
10445 			case NM_DBLCLK:
10446 				NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr;
10447 				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);
10448 			break;
10449 			case NM_RCLICK:
10450 				NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr;
10451 				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);
10452 			break;
10453 			case NM_RDBLCLK:
10454 				NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr;
10455 				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);
10456 			break;
10457 			case LVN_GETDISPINFO:
10458 				LV_DISPINFO* info = cast(LV_DISPINFO*) hdr;
10459 				if(info.item.mask & LVIF_TEXT) {
10460 					if(getData) {
10461 						getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) {
10462 							auto bfr = WCharzBuffer(dataReceived);
10463 							auto len = info.item.cchTextMax;
10464 							if(bfr.length < len)
10465 								len = cast(typeof(len)) bfr.length;
10466 							info.item.pszText[0 .. len] = bfr.ptr[0 .. len];
10467 							info.item.pszText[len] = 0;
10468 						});
10469 					} else {
10470 						info.item.pszText[0] = 0;
10471 					}
10472 					//info.item.iItem
10473 					//if(info.item.iSubItem)
10474 				}
10475 			break;
10476 			default:
10477 		}
10478 		return 0;
10479 	}
10480 
10481 	// FIXME: this throws off mouse calculations, it should only happen when we're at the top level or something idk
10482 	override bool encapsulatedChildren() {
10483 		return true;
10484 	}
10485 
10486 	/++
10487 		Informs the control that content has changed.
10488 
10489 		History:
10490 			Added November 10, 2021 (dub v10.4)
10491 	+/
10492 	void update() {
10493 		version(custom_widgets)
10494 			redraw();
10495 		else {
10496 			SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0));
10497 			UpdateWindow(hwnd);
10498 		}
10499 
10500 
10501 	}
10502 
10503 	/++
10504 		Called by the system to request the text content of an individual cell. You
10505 		should pass the text into the provided `sink` delegate. This function will be
10506 		called for each visible cell as-needed when drawing.
10507 	+/
10508 	void delegate(int row, int column, scope void delegate(in char[]) sink) getData;
10509 
10510 	/++
10511 		Available per-cell style customization options. Use one of the constructors
10512 		provided to set the values conveniently, or default construct it and set individual
10513 		values yourself. Just remember to set the `flags` so your values are actually used.
10514 		If the flag isn't set, the field is ignored and the system default is used instead.
10515 
10516 		This is returned by the [getCellStyle] delegate.
10517 
10518 		Examples:
10519 			---
10520 			// assumes you have a variables called `my_data` which is an array of arrays of numbers
10521 			auto table = new TableView(window);
10522 			// snip: you would set up columns here
10523 
10524 			// this is how you provide data to the table view class
10525 			table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) {
10526 				import std.conv;
10527 				sink(to!string(my_data[row][column]));
10528 			};
10529 
10530 			// and this is how you customize the colors
10531 			table.getCellStyle = delegate(int row, int column) {
10532 				return (my_data[row][column] < 0) ?
10533 					TableView.CellStyle(Color.red); // make negative numbers red
10534 					: TableView.CellStyle.init; // leave the rest alone
10535 			};
10536 			// snip: you would call table.setItemCount here then continue with the rest of your window setup work
10537 			---
10538 
10539 		History:
10540 			Added November 27, 2021 (dub v10.4)
10541 	+/
10542 	struct CellStyle {
10543 		/// 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.
10544 		this(Color textColor) {
10545 			this.textColor = textColor;
10546 			this.flags |= Flags.textColorSet;
10547 		}
10548 		/// Sets a custom text and background color.
10549 		this(Color textColor, Color backgroundColor) {
10550 			this.textColor = textColor;
10551 			this.backgroundColor = backgroundColor;
10552 			this.flags |= Flags.textColorSet | Flags.backgroundColorSet;
10553 		}
10554 		/++
10555 			Alignment is only supported on some platforms.
10556 		+/
10557 		this(TextAlignment alignment) {
10558 			this.alignment = alignment;
10559 			this.flags |= Flags.alignmentSet;
10560 		}
10561 		/// ditto
10562 		this(TextAlignment alignment, Color textColor) {
10563 			this.alignment = alignment;
10564 			this.textColor = textColor;
10565 			this.flags |= Flags.alignmentSet | Flags.textColorSet;
10566 		}
10567 		/// ditto
10568 		this(TextAlignment alignment, Color textColor, Color backgroundColor) {
10569 			this.alignment = alignment;
10570 			this.textColor = textColor;
10571 			this.backgroundColor = backgroundColor;
10572 			this.flags |= Flags.alignmentSet | Flags.textColorSet | Flags.backgroundColorSet;
10573 		}
10574 
10575 		TextAlignment alignment;
10576 		Color textColor;
10577 		Color backgroundColor;
10578 		int flags; /// bitmask of [Flags]
10579 		/// available options to combine into [flags]
10580 		enum Flags {
10581 			textColorSet = 1 << 0,
10582 			backgroundColorSet = 1 << 1,
10583 			alignmentSet = 1 << 2,
10584 		}
10585 	}
10586 	/++
10587 		Companion delegate to [getData] that allows you to custom style each
10588 		cell of the table.
10589 
10590 		Returns:
10591 			A [CellStyle] structure that describes the desired style for the
10592 			given cell. `return CellStyle.init` if you want the default style.
10593 
10594 		History:
10595 			Added November 27, 2021 (dub v10.4)
10596 	+/
10597 	CellStyle delegate(int row, int column) getCellStyle;
10598 
10599 	// i want to be able to do things like draw little colored things to show red for negative numbers
10600 	// or background color indicators or even in-cell charts
10601 	// void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell;
10602 
10603 	/++
10604 		When the user clicks on a header, this event is emitted. It has a member to identify which header (by index) was clicked.
10605 	+/
10606 	mixin Emits!HeaderClickedEvent;
10607 
10608 	/++
10609 		History:
10610 			Added March 2, 2025
10611 	+/
10612 	mixin Emits!CellClickedEvent;
10613 }
10614 
10615 /++
10616 	This is emitted by the [TableView] when a user clicks on a column header.
10617 
10618 	Its member `columnIndex` has the zero-based index of the column that was clicked.
10619 
10620 	The default behavior of this event is to do nothing, so `preventDefault` has no effect.
10621 
10622 	History:
10623 		Added November 27, 2021 (dub v10.4)
10624 
10625 		Made `final` on January 3, 2025
10626 +/
10627 final class HeaderClickedEvent : Event {
10628 	enum EventString = "HeaderClicked";
10629 	this(Widget target, int columnIndex) {
10630 		this.columnIndex = columnIndex;
10631 		super(EventString, target);
10632 	}
10633 
10634 	/// The index of the column
10635 	int columnIndex;
10636 
10637 	///
10638 	override @property int intValue() {
10639 		return columnIndex;
10640 	}
10641 }
10642 
10643 /++
10644 	History:
10645 		Added March 2, 2025
10646 +/
10647 final class CellClickedEvent : MouseEventBase {
10648 	enum EventString = "CellClicked";
10649 	this(Widget target, int rowIndex, int columnIndex, MouseButton button, MouseButtonLinear mouseButtonLinear, int x, int y, bool altKey, bool ctrlKey, bool shiftKey, bool isDoubleClick) {
10650 		this.rowIndex = rowIndex;
10651 		this.columnIndex = columnIndex;
10652 		this.button = button;
10653 		this.buttonLinear = mouseButtonLinear;
10654 		this.isDoubleClick = isDoubleClick;
10655 		this.clientX = x;
10656 		this.clientY = y;
10657 
10658 		this.altKey = altKey;
10659 		this.ctrlKey = ctrlKey;
10660 		this.shiftKey = shiftKey;
10661 
10662 		// import std.stdio; std.stdio.writeln(rowIndex, "x", columnIndex, " @ ", x, ",", y, " ", button, " ", isDoubleClick, " ", altKey, " ", ctrlKey, " ", shiftKey);
10663 
10664 		// FIXME: x, y, state, altButton etc?
10665 		super(EventString, target);
10666 	}
10667 
10668 	/++
10669 		See also: [button] inherited from the base class.
10670 
10671 		clientX and clientY are irrespective of scrolling - FIXME is that sane?
10672 	+/
10673 	int columnIndex;
10674 
10675 	/// ditto
10676 	int rowIndex;
10677 
10678 	/// ditto
10679 	bool isDoubleClick;
10680 
10681 	/+
10682 	// i could do intValue as a linear index if we know the width
10683 	// and a stringValue with the string in the cell. but idk if worth.
10684 	override @property int intValue() {
10685 		return columnIndex;
10686 	}
10687 	+/
10688 
10689 }
10690 
10691 version(custom_widgets)
10692 private class TableViewWidgetInner : Widget {
10693 
10694 // wrap this thing in a ScrollMessageWidget
10695 
10696 	TableView tvw;
10697 	ScrollMessageWidget smw;
10698 	HeaderWidget header;
10699 
10700 	this(TableView tvw, ScrollMessageWidget smw) {
10701 		this.tvw = tvw;
10702 		this.smw = smw;
10703 		super(smw);
10704 
10705 		this.tabStop = true;
10706 
10707 		header = new HeaderWidget(this, smw.getHeader());
10708 
10709 		smw.addEventListener("scroll", () {
10710 			this.redraw();
10711 			header.redraw();
10712 		});
10713 
10714 
10715 		// I need headers outside the scroll area but rendered on the same line as the up arrow
10716 		// FIXME: add a fixed header to the SMW
10717 	}
10718 
10719 	enum padding = 3;
10720 
10721 	void updateScrolls() {
10722 		int w;
10723 		foreach(idx, column; tvw.columns) {
10724 			w += column.calculatedWidth;
10725 		}
10726 		smw.setTotalArea(w, tvw.itemCount);
10727 		columnsWidth = w;
10728 	}
10729 
10730 	private int columnsWidth;
10731 
10732 	private int lh() { return scaleWithDpi(16); } // FIXME lineHeight
10733 
10734 	override void registerMovement() {
10735 		super.registerMovement();
10736 		// FIXME: actual column width. it might need to be done per-pixel instead of per-column
10737 		smw.setViewableArea(this.width, this.height / lh);
10738 	}
10739 
10740 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
10741 		int x;
10742 		int y;
10743 
10744 		int row = smw.position.y;
10745 
10746 		foreach(lol; 0 .. this.height / lh) {
10747 			if(row >= tvw.itemCount)
10748 				break;
10749 			x = 0;
10750 			foreach(columnNumber, column; tvw.columns) {
10751 				auto x2 = x + column.calculatedWidth;
10752 				auto smwx = smw.position.x;
10753 
10754 				if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) {
10755 					auto startX = x;
10756 					auto endX = x + column.calculatedWidth;
10757 					switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) {
10758 						case TextAlignment.Left: startX += padding; break;
10759 						case TextAlignment.Center: startX += padding; endX -= padding; break;
10760 						case TextAlignment.Right: endX -= padding; break;
10761 						default: /* broken */ break;
10762 					}
10763 					if(column.width != 0) // no point drawing an invisible column
10764 					tvw.getData(row, cast(int) columnNumber, (in char[] info) {
10765 						auto endClip = endX - smw.position.x;
10766 						if(endClip > this.width - padding)
10767 							endClip = this.width - padding;
10768 						auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endClip, y + lh)));
10769 
10770 						void dotext(WidgetPainter painter, TextAlignment alignment) {
10771 							painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x - padding, y + lh), alignment);
10772 						}
10773 
10774 						if(tvw.getCellStyle !is null) {
10775 							auto style = tvw.getCellStyle(row, cast(int) columnNumber);
10776 
10777 							if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) {
10778 								auto tempPainter = painter;
10779 								tempPainter.fillColor = style.backgroundColor;
10780 								tempPainter.outlineColor = style.backgroundColor;
10781 
10782 								tempPainter.drawRectangle(Point(startX - smw.position.x, y),
10783 									Point(endX - smw.position.x, y + lh));
10784 							}
10785 							auto tempPainter = painter;
10786 							if(style.flags & TableView.CellStyle.Flags.textColorSet)
10787 								tempPainter.outlineColor = style.textColor;
10788 
10789 							auto alignment = column.alignment;
10790 							if(style.flags & TableView.CellStyle.Flags.alignmentSet)
10791 								alignment = style.alignment;
10792 							dotext(tempPainter, alignment);
10793 						} else {
10794 							dotext(painter, column.alignment);
10795 						}
10796 					});
10797 				}
10798 
10799 				x += column.calculatedWidth;
10800 			}
10801 			row++;
10802 			y += lh;
10803 		}
10804 		return bounds;
10805 	}
10806 
10807 	static class Style : Widget.Style {
10808 		override WidgetBackground background() {
10809 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
10810 		}
10811 	}
10812 	mixin OverrideStyle!Style;
10813 
10814 	private static class HeaderWidget : Widget {
10815 		/+
10816 			maybe i should do a splitter thing on top of the other widgets
10817 			so the splitter itself isn't really drawn but still replies to mouse events?
10818 		+/
10819 		this(TableViewWidgetInner tvw, Widget parent) {
10820 			super(parent);
10821 			this.tvw = tvw;
10822 
10823 			this.remainder = new Button("", this);
10824 
10825 			this.addEventListener((scope ClickEvent ev) {
10826 				int header = -1;
10827 				foreach(idx, child; this.children[1 .. $]) {
10828 					if(child is ev.target) {
10829 						header = cast(int) idx;
10830 						break;
10831 					}
10832 				}
10833 
10834 				if(header != -1) {
10835 					auto hce = new HeaderClickedEvent(tvw.tvw, header);
10836 					hce.dispatch();
10837 				}
10838 
10839 			});
10840 		}
10841 
10842 		override int minHeight() {
10843 			return defaultLineHeight + 4; // same as Button
10844 		}
10845 
10846 		void updateHeaders() {
10847 			foreach(child; children[1 .. $])
10848 				child.removeWidget();
10849 
10850 			foreach(column; tvw.tvw.columns) {
10851 				// the cast is ok because I dup it above, just the type is never changed.
10852 				// all this is private so it should never get messed up.
10853 				new Button(ImageLabel(cast(string) column.name, column.alignment), this);
10854 			}
10855 		}
10856 
10857 		Button remainder;
10858 		TableViewWidgetInner tvw;
10859 
10860 		override void recomputeChildLayout() {
10861 			registerMovement();
10862 			int pos;
10863 			foreach(idx, child; children[1 .. $]) {
10864 				if(idx >= tvw.tvw.columns.length)
10865 					continue;
10866 				child.x = pos;
10867 				child.y = 0;
10868 				child.width = tvw.tvw.columns[idx].calculatedWidth;
10869 				child.height = scaleWithDpi(16);// this.height;
10870 				pos += child.width;
10871 
10872 				child.recomputeChildLayout();
10873 			}
10874 
10875 			if(remainder is null)
10876 				return;
10877 
10878 			remainder.x = pos;
10879 			remainder.y = 0;
10880 			if(pos < this.width)
10881 				remainder.width = this.width - pos;// + 4;
10882 			else
10883 				remainder.width = 0;
10884 			remainder.height = scaleWithDpi(16);
10885 
10886 			remainder.recomputeChildLayout();
10887 		}
10888 
10889 		// for the scrollable children mixin
10890 		Point scrollOrigin() {
10891 			return Point(tvw.smw.position.x, 0);
10892 		}
10893 		void paintFrameAndBackground(WidgetPainter painter) { }
10894 
10895 		// for mouse event dispatching
10896 		override protected void addScrollPosition(ref int x, ref int y) {
10897 			x += scrollOrigin.x;
10898 			y += scrollOrigin.y;
10899 		}
10900 
10901 		mixin ScrollableChildren;
10902 	}
10903 
10904 	private void emitCellClickedEvent(scope MouseEventBase event, bool isDoubleClick) {
10905 		int mx = event.clientX + smw.position.x;
10906 		int my = event.clientY;
10907 
10908 		Widget par = this;
10909 		while(par && !par.encapsulatedChildren) {
10910 			my -= par.y; // to undo the encapsulatedChildren adjustClientCoordinates effect
10911 			par = par.parent;
10912 		}
10913 		if(par is null)
10914 			my = event.clientY; // encapsulatedChildren not present?
10915 
10916 		int row = my / lh + smw.position.y; // scrolling here is done per-item, not per pixel
10917 		if(row > tvw.itemCount)
10918 			row = -1;
10919 
10920 		int column = -1;
10921 		if(row != -1) {
10922 			int pos;
10923 			foreach(idx, col; tvw.columns) {
10924 				pos += col.calculatedWidth;
10925 				if(mx < pos) {
10926 					column = cast(int) idx;
10927 					break;
10928 				}
10929 			}
10930 		}
10931 
10932 		// wtf are these casts about?
10933 		tvw.emit!CellClickedEvent(row, column, cast(MouseButton) event.button, cast(MouseButtonLinear) event.buttonLinear, event.clientX, event.clientY, event.altKey, event.ctrlKey, event.shiftKey, isDoubleClick);
10934 	}
10935 
10936 	override void defaultEventHandler_click(scope ClickEvent ce) {
10937 		// FIXME: should i filter mouse wheel events? Windows doesn't send them but i can.
10938 		emitCellClickedEvent(ce, false);
10939 	}
10940 
10941 	override void defaultEventHandler_dblclick(scope DoubleClickEvent ce) {
10942 		emitCellClickedEvent(ce, true);
10943 	}
10944 }
10945 
10946 /+
10947 
10948 // given struct / array / number / string / etc, make it viewable and editable
10949 class DataViewerWidget : Widget {
10950 
10951 }
10952 +/
10953 
10954 /++
10955 	A line edit box with an associated label.
10956 
10957 	History:
10958 		On May 17, 2021, the default internal layout was changed from horizontal to vertical.
10959 
10960 		```
10961 		Old: ________
10962 
10963 		New:
10964 		____________
10965 		```
10966 
10967 		To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);`
10968 
10969 		You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a
10970 		horizontal label but left aligned. You may also consider a [GridLayout].
10971 +/
10972 alias LabeledLineEdit = Labeled!LineEdit;
10973 
10974 private int widthThatWouldFitChildLabels(Widget w) {
10975 	if(w is null)
10976 		return 0;
10977 
10978 	int max;
10979 
10980 	if(auto label = cast(TextLabel) w) {
10981 		return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight();
10982 	} else {
10983 		foreach(child; w.children) {
10984 			max = mymax(max, widthThatWouldFitChildLabels(child));
10985 		}
10986 	}
10987 
10988 	return max;
10989 }
10990 
10991 /++
10992 	History:
10993 		Added May 19, 2021
10994 +/
10995 class Labeled(T) : Widget {
10996 	///
10997 	this(string label, Widget parent) {
10998 		super(parent);
10999 		initialize!VerticalLayout(label, TextAlignment.Left, parent);
11000 	}
11001 
11002 	/++
11003 		History:
11004 			The alignment parameter was added May 17, 2021
11005 	+/
11006 	this(string label, TextAlignment alignment, Widget parent) {
11007 		super(parent);
11008 		initialize!HorizontalLayout(label, alignment, parent);
11009 	}
11010 
11011 	private void initialize(L)(string label, TextAlignment alignment, Widget parent) {
11012 		tabStop = false;
11013 		horizontal = is(L == HorizontalLayout);
11014 		auto hl = new L(this);
11015 		if(horizontal) {
11016 			static class SpecialTextLabel : TextLabel {
11017 				Widget outerParent;
11018 
11019 				this(string label, TextAlignment alignment, Widget outerParent, Widget parent) {
11020 					this.outerParent = outerParent;
11021 					super(label, alignment, parent);
11022 				}
11023 
11024 				override int flexBasisWidth() {
11025 					return widthThatWouldFitChildLabels(outerParent);
11026 				}
11027 				/+
11028 				override int widthShrinkiness() { return 0; }
11029 				override int widthStretchiness() { return 1; }
11030 				+/
11031 
11032 				override int paddingRight() { return 6; }
11033 				override int paddingLeft() { return 9; }
11034 
11035 				override int paddingTop() { return 3; }
11036 			}
11037 			this.label = new SpecialTextLabel(label, alignment, parent, hl);
11038 		} else
11039 			this.label = new TextLabel(label, alignment, hl);
11040 		this.lineEdit = new T(hl);
11041 
11042 		this.label.labelFor = this.lineEdit;
11043 	}
11044 
11045 	private bool horizontal;
11046 
11047 	TextLabel label; ///
11048 	T lineEdit; ///
11049 
11050 	override int flexBasisWidth() { return 250; }
11051 	override int widthShrinkiness() { return 1; }
11052 
11053 	override int minHeight() {
11054 		return this.children[0].minHeight;
11055 	}
11056 	override int maxHeight() { return minHeight(); }
11057 	override int marginTop() { return 4; }
11058 	override int marginBottom() { return 4; }
11059 
11060 	// FIXME: i should prolly call it value as well as content tbh
11061 
11062 	///
11063 	@property string content() {
11064 		return lineEdit.content;
11065 	}
11066 	///
11067 	@property void content(string c) {
11068 		return lineEdit.content(c);
11069 	}
11070 
11071 	///
11072 	void selectAll() {
11073 		lineEdit.selectAll();
11074 	}
11075 
11076 	override void focus() {
11077 		lineEdit.focus();
11078 	}
11079 }
11080 
11081 /++
11082 	A labeled password edit.
11083 
11084 	History:
11085 		Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021
11086 
11087 		The default parameters for the constructors were also removed on May 19, 2021
11088 +/
11089 alias LabeledPasswordEdit = Labeled!PasswordEdit;
11090 
11091 private string toMenuLabel(string s) {
11092 	string n;
11093 	n.reserve(s.length);
11094 	foreach(c; s)
11095 		if(c == '_')
11096 			n ~= ' ';
11097 		else
11098 			n ~= c;
11099 	return n;
11100 }
11101 
11102 private void autoExceptionHandler(Exception e) {
11103 	messageBox(e.msg);
11104 }
11105 
11106 void callAsIfClickedFromMenu(alias fn)(auto ref __traits(parent, fn) _this, Window window) {
11107 	makeAutomaticHandler!(fn)(window, &__traits(child, _this, fn))();
11108 }
11109 
11110 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) {
11111 	static if(is(T : void delegate())) {
11112 		return () {
11113 			try
11114 				t();
11115 			catch(Exception e)
11116 				autoExceptionHandler(e);
11117 		};
11118 	} else static if(is(typeof(fn) Params == __parameters)) {
11119 		static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) {
11120 			return () {
11121 				void onOK(string s) {
11122 					member = s;
11123 					try
11124 						t(Params[0](s));
11125 					catch(Exception e)
11126 						autoExceptionHandler(e);
11127 				}
11128 
11129 				if(
11130 					(type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export")))
11131 					|| type == FileDialogType.Save)
11132 				{
11133 					getSaveFileName(window, &onOK, member, filters, null);
11134 				} else
11135 					getOpenFileName(window, &onOK, member, filters, null);
11136 			};
11137 		} else {
11138 			struct S {
11139 				static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) {
11140 					pragma(msg, "warning: automatic handler of params not yet implemented on your compiler");
11141 				} else mixin(q{
11142 				static foreach(idx, ignore; Params) {
11143 					mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";");
11144 				}
11145 				});
11146 			}
11147 			return () {
11148 				dialog(window, (S s) {
11149 					try {
11150 						static if(is(typeof(t) Ret == return)) {
11151 							static if(is(Ret == void)) {
11152 								t(s.tupleof);
11153 							} else {
11154 								auto ret = t(s.tupleof);
11155 								import std.conv;
11156 								messageBox(to!string(ret), "Returned Value");
11157 							}
11158 						}
11159 					} catch(Exception e)
11160 						autoExceptionHandler(e);
11161 				}, null, __traits(identifier, fn));
11162 			};
11163 		}
11164 	}
11165 }
11166 
11167 private template hasAnyRelevantAnnotations(a...) {
11168 	bool helper() {
11169 		bool any;
11170 		foreach(attr; a) {
11171 			static if(is(typeof(attr) == .menu))
11172 				any = true;
11173 			else static if(is(typeof(attr) == .toolbar))
11174 				any = true;
11175 			else static if(is(attr == .separator))
11176 				any = true;
11177 			else static if(is(typeof(attr) == .accelerator))
11178 				any = true;
11179 			else static if(is(typeof(attr) == .hotkey))
11180 				any = true;
11181 			else static if(is(typeof(attr) == .icon))
11182 				any = true;
11183 			else static if(is(typeof(attr) == .label))
11184 				any = true;
11185 			else static if(is(typeof(attr) == .tip))
11186 				any = true;
11187 		}
11188 		return any;
11189 	}
11190 
11191 	enum bool hasAnyRelevantAnnotations = helper();
11192 }
11193 
11194 /++
11195 	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.
11196 +/
11197 class MainWindow : Window {
11198 	///
11199 	this(string title = null, int initialWidth = 500, int initialHeight = 500) {
11200 		super(initialWidth, initialHeight, title);
11201 
11202 		_clientArea = new ClientAreaWidget();
11203 		_clientArea.x = 0;
11204 		_clientArea.y = 0;
11205 		_clientArea.width = this.width;
11206 		_clientArea.height = this.height;
11207 		_clientArea.tabStop = false;
11208 
11209 		super.addChild(_clientArea);
11210 
11211 		statusBar = new StatusBar(this);
11212 	}
11213 
11214 	/++
11215 		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).
11216 
11217 		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')`.
11218 
11219 		You can also use `@separator` to put a separating line in the menu before the function.
11220 
11221 		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.
11222 
11223 		Let's look at a complete example:
11224 
11225 	---
11226 	import arsd.minigui;
11227 
11228 	void main() {
11229 		auto window = new MainWindow();
11230 
11231 		// we can add widgets before or after setting the menu, either way is fine.
11232 		// i'll do it before here so the local variables are available to the commands.
11233 
11234 		auto textEdit = new TextEdit(window);
11235 
11236 		// Remember, in D, you can define structs inside of functions
11237 		// and those structs can access the function's local variables.
11238 		//
11239 		// Of course, you might also want to do this separately, and if you
11240 		// do, make sure you keep a reference to the window as a struct data
11241 		// member so you can refer to it in cases like this Exit function.
11242 		struct Commands {
11243 			// the & in the string indicates that the next letter is the hotkey
11244 			// to access it from the keyboard (so here, alt+f will open the
11245 			// file menu)
11246 			@menu("&File") {
11247 				@accelerator("Ctrl+N")
11248 				@hotkey('n')
11249 				@icon(GenericIcons.New) // add an icon to the action
11250 				@toolbar("File") // adds it to a toolbar.
11251 				// The toolbar name is never visible to the user, but is used to group icons.
11252 				void New() {
11253 					previousFileReferenced = null;
11254 					textEdit.content = "";
11255 				}
11256 
11257 				@icon(GenericIcons.Open)
11258 				@toolbar("File")
11259 				@hotkey('s')
11260 				@accelerator("Ctrl+O")
11261 				void Open(FileName!() filename) {
11262 					import std.file;
11263 					textEdit.content = std.file.readText(filename);
11264 				}
11265 
11266 				@icon(GenericIcons.Save)
11267 				@toolbar("File")
11268 				@accelerator("Ctrl+S")
11269 				@hotkey('s')
11270 				void Save() {
11271 					// these are still functions, so of course you can
11272 					// still call them yourself too
11273 					Save_As(previousFileReferenced);
11274 				}
11275 
11276 				// underscores translate to spaces in the visible name
11277 				@hotkey('a')
11278 				void Save_As(FileName!() filename) {
11279 					import std.file;
11280 					std.file.write(previousFileReferenced, textEdit.content);
11281 				}
11282 
11283 				// you can put the annotations before or after the function name+args and it works the same way
11284 				@separator
11285 				void Exit() @accelerator("Alt+F4") @hotkey('x') {
11286 					window.close();
11287 				}
11288 			}
11289 
11290 			@menu("&Edit") {
11291 				// not putting accelerators here because the text edit widget
11292 				// does it locally, so no need to duplicate it globally.
11293 
11294 				@icon(GenericIcons.Undo)
11295 				void Undo() @toolbar("Undo") {
11296 					textEdit.undo();
11297 				}
11298 
11299 				@separator
11300 
11301 				@icon(GenericIcons.Cut)
11302 				void Cut() @toolbar("Edit") {
11303 					textEdit.cut();
11304 				}
11305 				@icon(GenericIcons.Copy)
11306 				void Copy() @toolbar("Edit") {
11307 					textEdit.copy();
11308 				}
11309 				@icon(GenericIcons.Paste)
11310 				void Paste() @toolbar("Edit") {
11311 					textEdit.paste();
11312 				}
11313 
11314 				@separator
11315 				void Select_All() {
11316 					textEdit.selectAll();
11317 				}
11318 			}
11319 
11320 			@menu("Help") {
11321 				void About() @accelerator("F1") {
11322 					window.messageBox("A minigui sample program.");
11323 				}
11324 
11325 				// @label changes the name in the menu from what is in the code
11326 				@label("In Menu Name")
11327 				void otherNameInCode() {}
11328 			}
11329 		}
11330 
11331 		// declare the object that holds the commands, and set
11332 		// and members you want from it
11333 		Commands commands;
11334 
11335 		// and now tell minigui to do its magic and create the ui for it!
11336 		window.setMenuAndToolbarFromAnnotatedCode(commands);
11337 
11338 		// then, loop the window normally;
11339 		window.loop();
11340 
11341 		// important to note that the `commands` variable must live through the window's whole life cycle,
11342 		// or you can have crashes. If you declare the variable and loop in different functions, make sure
11343 		// you do `new Commands` so the garbage collector can take over management of it for you.
11344 	}
11345 	---
11346 
11347 	Note that you can call this function multiple times and it will add the items in order to the given items.
11348 
11349 	+/
11350 	void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) {
11351 		setMenuAndToolbarFromAnnotatedCode_internal(t);
11352 	}
11353 	/// ditto
11354 	void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) {
11355 		setMenuAndToolbarFromAnnotatedCode_internal(t);
11356 	}
11357 	void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
11358 		auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
11359 		Menu[string] mcs;
11360 
11361 		alias ToolbarSection = ToolBar.ToolbarSection;
11362 		ToolbarSection[] toolbarSections;
11363 
11364 		foreach(menu; menuBar.subMenus) {
11365 			mcs[menu.label] = menu;
11366 		}
11367 
11368 		foreach(memberName; __traits(derivedMembers, T)) {
11369 			static if(memberName != "this")
11370 			static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
11371 				.menu menu;
11372 				.toolbar toolbar;
11373 				bool separator;
11374 				.accelerator accelerator;
11375 				.hotkey hotkey;
11376 				.icon icon;
11377 				string label;
11378 				string tip;
11379 				foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
11380 					static if(is(typeof(attr) == .menu))
11381 						menu = attr;
11382 					else static if(is(typeof(attr) == .toolbar))
11383 						toolbar = attr;
11384 					else static if(is(attr == .separator))
11385 						separator = true;
11386 					else static if(is(typeof(attr) == .accelerator))
11387 						accelerator = attr;
11388 					else static if(is(typeof(attr) == .hotkey))
11389 						hotkey = attr;
11390 					else static if(is(typeof(attr) == .icon))
11391 						icon = attr;
11392 					else static if(is(typeof(attr) == .label))
11393 						label = attr.label;
11394 					else static if(is(typeof(attr) == .tip))
11395 						tip = attr.tip;
11396 				}
11397 
11398 				if(menu !is .menu.init || toolbar !is .toolbar.init) {
11399 					ushort correctIcon = icon.id; // FIXME
11400 					if(label.length == 0)
11401 						label = memberName.toMenuLabel;
11402 
11403 					auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName));
11404 
11405 					auto action = new Action(label, correctIcon, handler);
11406 
11407 					if(accelerator.keyString.length) {
11408 						auto ke = KeyEvent.parse(accelerator.keyString);
11409 						action.accelerator = ke;
11410 						accelerators[ke.toStr] = handler;
11411 					}
11412 
11413 					if(toolbar !is .toolbar.init) {
11414 						bool found;
11415 						foreach(ref section; toolbarSections)
11416 							if(section.name == toolbar.groupName) {
11417 								section.actions ~= action;
11418 								found = true;
11419 								break;
11420 							}
11421 						if(!found) {
11422 							toolbarSections ~= ToolbarSection(toolbar.groupName, [action]);
11423 						}
11424 					}
11425 					if(menu !is .menu.init) {
11426 						Menu mc;
11427 						if(menu.name in mcs) {
11428 							mc = mcs[menu.name];
11429 						} else {
11430 							mc = new Menu(menu.name, this);
11431 							menuBar.addItem(mc);
11432 							mcs[menu.name] = mc;
11433 						}
11434 
11435 						if(separator)
11436 							mc.addSeparator();
11437 						auto mi = mc.addItem(new MenuItem(action));
11438 
11439 						if(hotkey !is .hotkey.init)
11440 							mi.hotkey = hotkey.ch;
11441 					}
11442 				}
11443 			}
11444 		}
11445 
11446 		this.menuBar = menuBar;
11447 
11448 		if(toolbarSections.length) {
11449 			auto tb = new ToolBar(toolbarSections, this);
11450 		}
11451 	}
11452 
11453 	void delegate()[string] accelerators;
11454 
11455 	override void defaultEventHandler_keydown(KeyDownEvent event) {
11456 		auto str = event.originalKeyEvent.toStr;
11457 		if(auto acl = str in accelerators)
11458 			(*acl)();
11459 
11460 		// Windows this this automatically so only on custom need we implement it
11461 		version(custom_widgets) {
11462 			if(event.altKey && this.menuBar) {
11463 				foreach(item; this.menuBar.items) {
11464 					if(item.hotkey == keyToLetterCharAssumingLotsOfThingsThatYouMightBetterNotAssume(event.key)) {
11465 						// FIXME this kinda sucks but meh just pretending to click on it to trigger other existing mediocre code
11466 						item.dynamicState = DynamicState.hover | DynamicState.depressed;
11467 						item.redraw();
11468 						auto e = new MouseDownEvent(item);
11469 						e.dispatch();
11470 						break;
11471 					}
11472 				}
11473 			}
11474 
11475 			if(event.key == Key.Menu) {
11476 				showContextMenu(-1, -1);
11477 			}
11478 		}
11479 
11480 		super.defaultEventHandler_keydown(event);
11481 	}
11482 
11483 	override void defaultEventHandler_mouseover(MouseOverEvent event) {
11484 		super.defaultEventHandler_mouseover(event);
11485 		if(this.statusBar !is null && event.target.statusTip.length)
11486 			this.statusBar.parts[0].content = event.target.statusTip;
11487 		else if(this.statusBar !is null && this.statusTip.length)
11488 			this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString();
11489 	}
11490 
11491 	override void addChild(Widget c, int position = int.max) {
11492 		if(auto tb = cast(ToolBar) c)
11493 			version(win32_widgets)
11494 				super.addChild(c, 0);
11495 			else version(custom_widgets)
11496 				super.addChild(c, menuBar ? 1 : 0);
11497 			else static assert(0);
11498 		else
11499 			clientArea.addChild(c, position);
11500 	}
11501 
11502 	ToolBar _toolBar;
11503 	///
11504 	ToolBar toolBar() { return _toolBar; }
11505 	///
11506 	ToolBar toolBar(ToolBar t) {
11507 		_toolBar = t;
11508 		foreach(child; this.children)
11509 			if(child is t)
11510 				return t;
11511 		version(win32_widgets)
11512 			super.addChild(t, 0);
11513 		else version(custom_widgets)
11514 			super.addChild(t, menuBar ? 1 : 0);
11515 		else static assert(0);
11516 		return t;
11517 	}
11518 
11519 	MenuBar _menu;
11520 	///
11521 	MenuBar menuBar() { return _menu; }
11522 	///
11523 	MenuBar menuBar(MenuBar m) {
11524 		if(m is _menu) {
11525 			version(custom_widgets)
11526 				queueRecomputeChildLayout();
11527 			return m;
11528 		}
11529 
11530 		if(_menu !is null) {
11531 			// make sure it is sanely removed
11532 			// FIXME
11533 		}
11534 
11535 		_menu = m;
11536 
11537 		version(win32_widgets) {
11538 			SetMenu(parentWindow.win.impl.hwnd, m.handle);
11539 		} else version(custom_widgets) {
11540 			super.addChild(m, 0);
11541 
11542 		//	clientArea.y = menu.height;
11543 		//	clientArea.height = this.height - menu.height;
11544 
11545 			queueRecomputeChildLayout();
11546 		} else static assert(false);
11547 
11548 		return _menu;
11549 	}
11550 	private Widget _clientArea;
11551 	///
11552 	@property Widget clientArea() { return _clientArea; }
11553 	protected @property void clientArea(Widget wid) {
11554 		_clientArea = wid;
11555 	}
11556 
11557 	private StatusBar _statusBar;
11558 	/++
11559 		Returns the window's [StatusBar]. Be warned it may be `null`.
11560 	+/
11561 	@property StatusBar statusBar() { return _statusBar; }
11562 	/// ditto
11563 	@property void statusBar(StatusBar bar) {
11564 		if(_statusBar !is null)
11565 			_statusBar.removeWidget();
11566 		_statusBar = bar;
11567 		if(bar !is null)
11568 			super.addChild(_statusBar);
11569 	}
11570 }
11571 
11572 /+
11573 	This is really an implementation detail of [MainWindow]
11574 +/
11575 private class ClientAreaWidget : Widget {
11576 	this() {
11577 		this.tabStop = false;
11578 		super(null);
11579 		//sa = new ScrollableWidget(this);
11580 	}
11581 	/*
11582 	ScrollableWidget sa;
11583 	override void addChild(Widget w, int position) {
11584 		if(sa is null)
11585 			super.addChild(w, position);
11586 		else {
11587 			sa.addChild(w, position);
11588 			sa.setContentSize(this.minWidth + 1, this.minHeight);
11589 			writeln(sa.contentWidth, "x", sa.contentHeight);
11590 		}
11591 	}
11592 	*/
11593 }
11594 
11595 /**
11596 	Toolbars are lists of buttons (typically icons) that appear under the menu.
11597 	Each button ought to correspond to a menu item, represented by [Action] objects.
11598 */
11599 class ToolBar : Widget {
11600 	version(win32_widgets) {
11601 		private int idealHeight;
11602 		override int minHeight() { return idealHeight; }
11603 		override int maxHeight() { return idealHeight; }
11604 	} else version(custom_widgets) {
11605 		override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; }
11606 		override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; }
11607 	} else static assert(false);
11608 	override int heightStretchiness() { return 0; }
11609 
11610 	static struct ToolbarSection {
11611 		string name;
11612 		Action[] actions;
11613 	}
11614 
11615 	version(win32_widgets) {
11616 		HIMAGELIST imageListSmall;
11617 		HIMAGELIST imageListLarge;
11618 	}
11619 
11620 	this(Widget parent) {
11621 		this(cast(ToolbarSection[]) null, parent);
11622 	}
11623 
11624 	version(win32_widgets)
11625 	void changeIconSize(bool useLarge) {
11626 		SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall));
11627 
11628 		/+
11629 		SIZE size;
11630 		import core.sys.windows.commctrl;
11631 		SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size);
11632 		idealHeight = size.cy + 4; // the plus 4 is a hack
11633 		+/
11634 
11635 		idealHeight = useLarge ? 34 : 26;
11636 
11637 		if(parent) {
11638 			parent.queueRecomputeChildLayout();
11639 			parent.redraw();
11640 		}
11641 
11642 		SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4));
11643 		SendMessageW(hwnd, TB_AUTOSIZE, 0, 0);
11644 	}
11645 
11646 	/++
11647 		History:
11648 			The `ToolbarSection` overload was added December 31, 2024
11649 	+/
11650 	this(Action[] actions, Widget parent) {
11651 		this([ToolbarSection(null, actions)], parent);
11652 	}
11653 
11654 	/// ditto
11655 	this(ToolbarSection[] sections, Widget parent) {
11656 		super(parent);
11657 
11658 		tabStop = false;
11659 
11660 		version(win32_widgets) {
11661 			// so i like how the flat thing looks on windows, but not on wine
11662 			// and eh, with windows visual styles enabled it looks cool anyway soooo gonna
11663 			// leave it commented
11664 			createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS);
11665 
11666 			SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/);
11667 
11668 			imageListSmall = ImageList_Create(
11669 				// width, height
11670 				16, 16,
11671 				ILC_COLOR16 | ILC_MASK,
11672 				16 /*numberOfButtons*/, 0);
11673 
11674 			imageListLarge = ImageList_Create(
11675 				// width, height
11676 				24, 24,
11677 				ILC_COLOR16 | ILC_MASK,
11678 				16 /*numberOfButtons*/, 0);
11679 
11680 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall);
11681 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL);
11682 
11683 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge);
11684 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL);
11685 
11686 			SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0);
11687 
11688 			TBBUTTON[] buttons;
11689 
11690 			// FIXME: I_IMAGENONE is if here is no icon
11691 			foreach(sidx, section; sections) {
11692 				if(sidx)
11693 					buttons ~= TBBUTTON(
11694 						scaleWithDpi(4),
11695 						0,
11696 						TBSTATE_ENABLED, // state
11697 						TBSTYLE_SEP | BTNS_SEP, // style
11698 						0, // reserved array, just zero it out
11699 						0, // dwData
11700 						-1
11701 					);
11702 
11703 				foreach(action; section.actions)
11704 					buttons ~= TBBUTTON(
11705 						MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0),
11706 						action.id,
11707 						TBSTATE_ENABLED, // state
11708 						0, // style
11709 						0, // reserved array, just zero it out
11710 						0, // dwData
11711 						cast(size_t) toWstringzInternal(action.label) // INT_PTR
11712 					);
11713 			}
11714 
11715 			SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0);
11716 			SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr);
11717 
11718 			/*
11719 			RECT rect;
11720 			GetWindowRect(hwnd, &rect);
11721 			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
11722 			*/
11723 
11724 			dpiChanged(); // to load the things calling changeIconSize the first time
11725 
11726 			assert(idealHeight);
11727 		} else version(custom_widgets) {
11728 			foreach(sidx, section; sections) {
11729 				if(sidx)
11730 					new HorizontalSpacer(4, this);
11731 				foreach(action; section.actions)
11732 					new ToolButton(action, this);
11733 			}
11734 		} else static assert(false);
11735 	}
11736 
11737 	override void recomputeChildLayout() {
11738 		.recomputeChildLayout!"width"(this);
11739 	}
11740 
11741 
11742 	version(win32_widgets)
11743 	override protected void dpiChanged() {
11744 		auto sz = scaleWithDpi(16);
11745 		if(sz >= 20)
11746 			changeIconSize(true);
11747 		else
11748 			changeIconSize(false);
11749 	}
11750 }
11751 
11752 /// 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.
11753 class ToolButton : Button {
11754 	///
11755 	this(Action action, Widget parent) {
11756 		super(action.label, parent);
11757 		tabStop = false;
11758 		this.action = action;
11759 	}
11760 
11761 	version(custom_widgets)
11762 	override void defaultEventHandler_click(ClickEvent event) {
11763 		foreach(handler; action.triggered)
11764 			handler();
11765 	}
11766 
11767 	Action action;
11768 
11769 	override int maxWidth() { return toolbarIconSize; }
11770 	override int minWidth() { return toolbarIconSize; }
11771 	override int maxHeight() { return toolbarIconSize; }
11772 	override int minHeight() { return toolbarIconSize; }
11773 
11774 	version(custom_widgets)
11775 	override void paint(WidgetPainter painter) {
11776 	painter.drawThemed(delegate Rectangle (const Rectangle bounds) {
11777 		painter.outlineColor = Color.black;
11778 
11779 		immutable multiplier = toolbarIconSize / 4;
11780 		immutable divisor = 16 / 4;
11781 
11782 		int ScaledNumber(int n) {
11783 			// return n * multiplier / divisor;
11784 			auto s = n * multiplier;
11785 			auto it = s / divisor;
11786 			auto rem = s % divisor;
11787 			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
11788 				it++;
11789 			return it;
11790 		}
11791 
11792 		arsd.color.Point Point(int x, int y) {
11793 			return arsd.color.Point(ScaledNumber(x), ScaledNumber(y));
11794 		}
11795 
11796 		switch(action.iconId) {
11797 			case GenericIcons.New:
11798 				painter.fillColor = Color.white;
11799 				painter.drawPolygon(
11800 					Point(3, 2), Point(3, 13), Point(12, 13), Point(12, 6),
11801 					Point(8, 2), Point(8, 6), Point(12, 6), Point(8, 2),
11802 					Point(3, 2), Point(3, 13)
11803 				);
11804 			break;
11805 			case GenericIcons.Save:
11806 				painter.fillColor = Color.white;
11807 				painter.outlineColor = Color.black;
11808 				painter.drawRectangle(Point(2, 2), Point(13, 13));
11809 
11810 				// the label
11811 				painter.drawRectangle(Point(4, 8), Point(11, 13));
11812 
11813 				// the slider
11814 				painter.fillColor = Color.black;
11815 				painter.outlineColor = Color.black;
11816 				painter.drawRectangle(Point(4, 3), Point(10, 6));
11817 
11818 				painter.fillColor = Color.white;
11819 				painter.outlineColor = Color.white;
11820 				// the disc window
11821 				painter.drawRectangle(Point(5, 3), Point(6, 5));
11822 			break;
11823 			case GenericIcons.Open:
11824 				painter.fillColor = Color.white;
11825 				painter.drawPolygon(
11826 					Point(4, 4), Point(4, 12), Point(13, 12), Point(13, 3),
11827 					Point(9, 3), Point(9, 4), Point(4, 4));
11828 				painter.drawPolygon(
11829 					Point(2, 6), Point(11, 6),
11830 					Point(12, 12), Point(4, 12),
11831 					Point(2, 6));
11832 				//painter.drawLine(Point(9, 6), Point(13, 7));
11833 			break;
11834 			case GenericIcons.Copy:
11835 				painter.fillColor = Color.white;
11836 				painter.drawRectangle(Point(3, 2), Point(9, 10));
11837 				painter.drawRectangle(Point(6, 5), Point(12, 13));
11838 			break;
11839 			case GenericIcons.Cut:
11840 				painter.fillColor = Color.transparent;
11841 				painter.outlineColor = getComputedStyle.foregroundColor();
11842 				painter.drawLine(Point(3, 2), Point(10, 9));
11843 				painter.drawLine(Point(4, 9), Point(11, 2));
11844 				painter.drawRectangle(Point(3, 9), Point(5, 13));
11845 				painter.drawRectangle(Point(9, 9), Point(11, 12));
11846 			break;
11847 			case GenericIcons.Paste:
11848 				painter.fillColor = Color.white;
11849 				painter.drawRectangle(Point(2, 3), Point(11, 11));
11850 				painter.drawRectangle(Point(6, 8), Point(13, 13));
11851 				painter.drawLine(Point(6, 2), Point(4, 5));
11852 				painter.drawLine(Point(6, 2), Point(9, 5));
11853 				painter.fillColor = Color.black;
11854 				painter.drawRectangle(Point(4, 5), Point(9, 6));
11855 			break;
11856 			case GenericIcons.Help:
11857 				painter.outlineColor = getComputedStyle.foregroundColor();
11858 				painter.drawText(arsd.color.Point(0, 0), "?", arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
11859 			break;
11860 			case GenericIcons.Undo:
11861 				painter.fillColor = Color.transparent;
11862 				painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64);
11863 				painter.outlineColor = Color.black;
11864 				painter.fillColor = Color.black;
11865 				painter.drawPolygon(
11866 					Point(4, 4),
11867 					Point(8, 2),
11868 					Point(8, 6),
11869 					Point(4, 4),
11870 				);
11871 			break;
11872 			case GenericIcons.Redo:
11873 				painter.fillColor = Color.transparent;
11874 				painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64);
11875 				painter.outlineColor = Color.black;
11876 				painter.fillColor = Color.black;
11877 				painter.drawPolygon(
11878 					Point(10, 4),
11879 					Point(6, 2),
11880 					Point(6, 6),
11881 					Point(10, 4),
11882 				);
11883 			break;
11884 			default:
11885 				painter.outlineColor = getComputedStyle.foregroundColor;
11886 				painter.drawText(arsd.color.Point(0, 0), action.label, arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
11887 		}
11888 		return bounds;
11889 		});
11890 	}
11891 
11892 }
11893 
11894 
11895 /++
11896 	You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode].
11897 +/
11898 class MenuBar : Widget {
11899 	MenuItem[] items;
11900 	Menu[] subMenus;
11901 
11902 	version(win32_widgets) {
11903 		HMENU handle;
11904 		///
11905 		this(Widget parent = null) {
11906 			super(parent);
11907 
11908 			handle = CreateMenu();
11909 			tabStop = false;
11910 		}
11911 	} else version(custom_widgets) {
11912 		///
11913 		this(Widget parent = null) {
11914 			tabStop = false; // these are selected some other way
11915 			super(parent);
11916 		}
11917 
11918 		mixin Padding!q{2};
11919 	} else static assert(false);
11920 
11921 	version(custom_widgets)
11922 	override void paint(WidgetPainter painter) {
11923 		draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color);
11924 	}
11925 
11926 	///
11927 	MenuItem addItem(MenuItem item) {
11928 		this.addChild(item);
11929 		items ~= item;
11930 		version(win32_widgets) {
11931 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
11932 		}
11933 		return item;
11934 	}
11935 
11936 
11937 	///
11938 	Menu addItem(Menu item) {
11939 
11940 		subMenus ~= item;
11941 
11942 		auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane
11943 
11944 		addChild(mbItem);
11945 		items ~= mbItem;
11946 
11947 		version(win32_widgets) {
11948 			AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label));
11949 		} else version(custom_widgets) {
11950 			mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) {
11951 				item.popup(mbItem);
11952 			};
11953 		} else static assert(false);
11954 
11955 		return item;
11956 	}
11957 
11958 	override void recomputeChildLayout() {
11959 		.recomputeChildLayout!"width"(this);
11960 	}
11961 
11962 	override int maxHeight() { return defaultLineHeight + 4; }
11963 	override int minHeight() { return defaultLineHeight + 4; }
11964 }
11965 
11966 
11967 /**
11968 	Status bars appear at the bottom of a MainWindow.
11969 	They are made out of Parts, with a width and content.
11970 
11971 	They can have multiple parts or be in simple mode. FIXME: implement simple mode.
11972 
11973 
11974 	sb.parts[0].content = "Status bar text!";
11975 */
11976 // https://learn.microsoft.com/en-us/windows/win32/controls/status-bars#owner-drawn-status-bars
11977 class StatusBar : Widget {
11978 	private Part[] partsArray;
11979 	///
11980 	struct Parts {
11981 		@disable this();
11982 		this(StatusBar owner) { this.owner = owner; }
11983 		//@disable this(this);
11984 		///
11985 		@property int length() { return cast(int) owner.partsArray.length; }
11986 		private StatusBar owner;
11987 		private this(StatusBar owner, Part[] parts) {
11988 			this.owner.partsArray = parts;
11989 			this.owner = owner;
11990 		}
11991 		///
11992 		Part opIndex(int p) {
11993 			if(owner.partsArray.length == 0)
11994 				this ~= new StatusBar.Part(0);
11995 			return owner.partsArray[p];
11996 		}
11997 
11998 		///
11999 		Part opOpAssign(string op : "~" )(Part p) {
12000 			assert(owner.partsArray.length < 255);
12001 			p.owner = this.owner;
12002 			p.idx = cast(int) owner.partsArray.length;
12003 			owner.partsArray ~= p;
12004 
12005 			owner.queueRecomputeChildLayout();
12006 
12007 			version(win32_widgets) {
12008 				int[256] pos;
12009 				int cpos;
12010 				foreach(idx, part; owner.partsArray) {
12011 					if(idx + 1 == owner.partsArray.length)
12012 						pos[idx] = -1;
12013 					else {
12014 						cpos += part.currentlyAssignedWidth;
12015 						pos[idx] = cpos;
12016 					}
12017 				}
12018 				SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr);
12019 			} else version(custom_widgets) {
12020 				owner.redraw();
12021 			} else static assert(false);
12022 
12023 			return p;
12024 		}
12025 
12026 		/++
12027 			Sets up proportional parts in one function call. You can use negative numbers to indicate device-independent pixels, and positive numbers to indicate proportions.
12028 
12029 			No given item should be 0.
12030 
12031 			History:
12032 				Added December 31, 2024
12033 		+/
12034 		void setSizes(int[] proportions...) {
12035 			assert(this.owner);
12036 			this.owner.partsArray = null;
12037 
12038 			foreach(n; proportions) {
12039 				assert(n, "do not give 0 to statusBar.parts.set, it would make an invisible part. Try 1 instead.");
12040 
12041 				this.opOpAssign!"~"(new StatusBar.Part(n > 0 ? n : -n, n > 0 ? StatusBar.Part.WidthUnits.Proportional : StatusBar.Part.WidthUnits.DeviceIndependentPixels));
12042 			}
12043 
12044 		}
12045 	}
12046 
12047 	private Parts _parts;
12048 	///
12049 	final @property Parts parts() {
12050 		return _parts;
12051 	}
12052 
12053 	/++
12054 
12055 	+/
12056 	static class Part {
12057 		/++
12058 			History:
12059 				Added September 1, 2023 (dub v11.1)
12060 		+/
12061 		enum WidthUnits {
12062 			/++
12063 				Unscaled pixels as they appear on screen.
12064 
12065 				If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui.
12066 			+/
12067 			DeviceDependentPixels,
12068 			/++
12069 				Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui.
12070 			+/
12071 			DeviceIndependentPixels,
12072 			/++
12073 				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`).
12074 			+/
12075 			ApproximateCharacters,
12076 			/++
12077 				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.
12078 
12079 				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.
12080 			+/
12081 			Proportional
12082 		}
12083 		private WidthUnits units;
12084 		private int width;
12085 		private StatusBar owner;
12086 
12087 		private int currentlyAssignedWidth;
12088 
12089 		/++
12090 			History:
12091 				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.
12092 
12093 				It now allows you to provide your own value for [WidthUnits].
12094 
12095 				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`.
12096 		+/
12097 		this(int w, WidthUnits units = WidthUnits.Proportional) {
12098 			this.units = units;
12099 			this.width = w;
12100 		}
12101 
12102 		/// ditto
12103 		this(int w = 0) {
12104 			if(w == 0)
12105 				this(w, WidthUnits.Proportional);
12106 			else
12107 				this(w, WidthUnits.DeviceDependentPixels);
12108 		}
12109 
12110 		private int idx;
12111 		private string _content;
12112 		///
12113 		@property string content() { return _content; }
12114 		///
12115 		@property void content(string s) {
12116 			version(win32_widgets) {
12117 				_content = s;
12118 				WCharzBuffer bfr = WCharzBuffer(s);
12119 				SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr);
12120 			} else version(custom_widgets) {
12121 				if(_content != s) {
12122 					_content = s;
12123 					owner.redraw();
12124 				}
12125 			} else static assert(false);
12126 		}
12127 	}
12128 	string simpleModeContent;
12129 	bool inSimpleMode;
12130 
12131 
12132 	///
12133 	this(Widget parent) {
12134 		super(null); // FIXME
12135 		_parts = Parts(this);
12136 		tabStop = false;
12137 		version(win32_widgets) {
12138 			parentWindow = parent.parentWindow;
12139 			createWin32Window(this, "msctls_statusbar32"w, "", 0);
12140 
12141 			RECT rect;
12142 			GetWindowRect(hwnd, &rect);
12143 			idealHeight = rect.bottom - rect.top;
12144 			assert(idealHeight);
12145 		} else version(custom_widgets) {
12146 		} else static assert(false);
12147 	}
12148 
12149 	override void recomputeChildLayout() {
12150 		int remainingLength = this.width;
12151 
12152 		int proportionalSum;
12153 		int proportionalCount;
12154 		foreach(idx, part; this.partsArray) {
12155 			with(Part.WidthUnits)
12156 			final switch(part.units) {
12157 				case DeviceDependentPixels:
12158 					part.currentlyAssignedWidth = part.width;
12159 					remainingLength -= part.currentlyAssignedWidth;
12160 				break;
12161 				case DeviceIndependentPixels:
12162 					part.currentlyAssignedWidth = scaleWithDpi(part.width);
12163 					remainingLength -= part.currentlyAssignedWidth;
12164 				break;
12165 				case ApproximateCharacters:
12166 					auto cs = getComputedStyle();
12167 					auto font = cs.font;
12168 
12169 					part.currentlyAssignedWidth = font.averageWidth * this.width;
12170 					remainingLength -= part.currentlyAssignedWidth;
12171 				break;
12172 				case Proportional:
12173 					proportionalSum += part.width;
12174 					proportionalCount ++;
12175 				break;
12176 			}
12177 		}
12178 
12179 		foreach(part; this.partsArray) {
12180 			if(part.units == Part.WidthUnits.Proportional) {
12181 				auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width;
12182 				if(proportion == 0)
12183 					proportion = 1;
12184 
12185 				if(proportionalSum == 0)
12186 					proportionalSum = proportionalCount;
12187 
12188 				part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum;
12189 			}
12190 		}
12191 
12192 		super.recomputeChildLayout();
12193 	}
12194 
12195 	version(win32_widgets)
12196 	override protected void dpiChanged() {
12197 		RECT rect;
12198 		GetWindowRect(hwnd, &rect);
12199 		idealHeight = rect.bottom - rect.top;
12200 		assert(idealHeight);
12201 	}
12202 
12203 	version(custom_widgets)
12204 	override void paint(WidgetPainter painter) {
12205 		auto cs = getComputedStyle();
12206 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
12207 		int cpos = 0;
12208 		foreach(idx, part; this.partsArray) {
12209 			auto partWidth = part.currentlyAssignedWidth;
12210 			// part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100);
12211 			painter.setClipRectangle(Point(cpos, 0), partWidth, height);
12212 			draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color);
12213 			painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4);
12214 
12215 			painter.outlineColor = cs.foregroundColor();
12216 			painter.fillColor = cs.foregroundColor();
12217 
12218 			painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter);
12219 			cpos += partWidth;
12220 		}
12221 	}
12222 
12223 
12224 	version(win32_widgets) {
12225 		private int idealHeight;
12226 		override int maxHeight() { return idealHeight; }
12227 		override int minHeight() { return idealHeight; }
12228 	} else version(custom_widgets) {
12229 		override int maxHeight() { return defaultLineHeight + 4; }
12230 		override int minHeight() { return defaultLineHeight + 4; }
12231 	} else static assert(false);
12232 }
12233 
12234 /// Displays an in-progress indicator without known values
12235 version(none)
12236 class IndefiniteProgressBar : Widget {
12237 	version(win32_widgets)
12238 	this(Widget parent) {
12239 		super(parent);
12240 		createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */);
12241 		tabStop = false;
12242 	}
12243 	override int minHeight() { return 10; }
12244 }
12245 
12246 /// A progress bar with a known endpoint and completion amount
12247 class ProgressBar : Widget {
12248 	/++
12249 		History:
12250 			Added March 16, 2022 (dub v10.7)
12251 	+/
12252 	this(int min, int max, Widget parent) {
12253 		this(parent);
12254 		setRange(cast(ushort) min, cast(ushort) max); // FIXME
12255 	}
12256 	this(Widget parent) {
12257 		version(win32_widgets) {
12258 			super(parent);
12259 			createWin32Window(this, "msctls_progress32"w, "", 0);
12260 			tabStop = false;
12261 		} else version(custom_widgets) {
12262 			super(parent);
12263 			max = 100;
12264 			step = 10;
12265 			tabStop = false;
12266 		} else static assert(0);
12267 	}
12268 
12269 	version(custom_widgets)
12270 	override void paint(WidgetPainter painter) {
12271 		auto cs = getComputedStyle();
12272 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
12273 		painter.fillColor = cs.progressBarColor;
12274 		painter.drawRectangle(Point(0, 0), width * current / max, height);
12275 	}
12276 
12277 
12278 	version(custom_widgets) {
12279 		int current;
12280 		int max;
12281 		int step;
12282 	}
12283 
12284 	///
12285 	void advanceOneStep() {
12286 		version(win32_widgets)
12287 			SendMessageW(hwnd, PBM_STEPIT, 0, 0);
12288 		else version(custom_widgets)
12289 			addToPosition(step);
12290 		else static assert(false);
12291 	}
12292 
12293 	///
12294 	void setStepIncrement(int increment) {
12295 		version(win32_widgets)
12296 			SendMessageW(hwnd, PBM_SETSTEP, increment, 0);
12297 		else version(custom_widgets)
12298 			step = increment;
12299 		else static assert(false);
12300 	}
12301 
12302 	///
12303 	void addToPosition(int amount) {
12304 		version(win32_widgets)
12305 			SendMessageW(hwnd, PBM_DELTAPOS, amount, 0);
12306 		else version(custom_widgets)
12307 			setPosition(current + amount);
12308 		else static assert(false);
12309 	}
12310 
12311 	///
12312 	void setPosition(int pos) {
12313 		version(win32_widgets)
12314 			SendMessageW(hwnd, PBM_SETPOS, pos, 0);
12315 		else version(custom_widgets) {
12316 			current = pos;
12317 			if(current > max)
12318 				current = max;
12319 			redraw();
12320 		}
12321 		else static assert(false);
12322 	}
12323 
12324 	///
12325 	void setRange(ushort min, ushort max) {
12326 		version(win32_widgets)
12327 			SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max));
12328 		else version(custom_widgets) {
12329 			this.max = max;
12330 		}
12331 		else static assert(false);
12332 	}
12333 
12334 	override int minHeight() { return 10; }
12335 }
12336 
12337 version(custom_widgets)
12338 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) {
12339 	thisLabel.reserve(label.length);
12340 	bool justSawAmpersand;
12341 	foreach(ch; label) {
12342 		if(justSawAmpersand) {
12343 			justSawAmpersand = false;
12344 			if(ch == '&') {
12345 				goto plain;
12346 			}
12347 			thisAccelerator = ch;
12348 		} else {
12349 			if(ch == '&') {
12350 				justSawAmpersand = true;
12351 				continue;
12352 			}
12353 			plain:
12354 			thisLabel ~= ch;
12355 		}
12356 	}
12357 }
12358 
12359 /++
12360 	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.
12361 
12362 
12363 	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
12364 
12365 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
12366 
12367 	History:
12368 		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.
12369 +/
12370 class Fieldset : Widget {
12371 	// FIXME: on Windows,it doesn't draw the background on the label
12372 	// on X, it doesn't fix the clipping rectangle for it
12373 	version(win32_widgets)
12374 		override int paddingTop() { return defaultLineHeight; }
12375 	else version(custom_widgets)
12376 		override int paddingTop() { return defaultLineHeight + 2; }
12377 	else static assert(false);
12378 	override int paddingBottom() { return 6; }
12379 	override int paddingLeft() { return 6; }
12380 	override int paddingRight() { return 6; }
12381 
12382 	override int marginLeft() { return 6; }
12383 	override int marginRight() { return 6; }
12384 	override int marginTop() { return 2; }
12385 	override int marginBottom() { return 2; }
12386 
12387 	string legend;
12388 
12389 	version(custom_widgets) private dchar accelerator;
12390 
12391 	this(string legend, Widget parent) {
12392 		version(win32_widgets) {
12393 			super(parent);
12394 			this.legend = legend;
12395 			createWin32Window(this, "button"w, legend, BS_GROUPBOX);
12396 			tabStop = false;
12397 		} else version(custom_widgets) {
12398 			super(parent);
12399 			tabStop = false;
12400 
12401 			legend.extractWindowsStyleLabel(this.legend, this.accelerator);
12402 		} else static assert(0);
12403 	}
12404 
12405 	version(custom_widgets)
12406 	override void paint(WidgetPainter painter) {
12407 		auto dlh = defaultLineHeight;
12408 
12409 		painter.fillColor = Color.transparent;
12410 		auto cs = getComputedStyle();
12411 		painter.pen = Pen(cs.foregroundColor, 1);
12412 		painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2);
12413 
12414 		auto tx = painter.textSize(legend);
12415 		painter.outlineColor = Color.transparent;
12416 
12417 		version(Windows) {
12418 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
12419 			painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height);
12420 			SelectObject(painter.impl.hdc, b);
12421 		} else static if(UsingSimpledisplayX11) {
12422 			painter.fillColor = getComputedStyle().windowBackgroundColor;
12423 			painter.drawRectangle(Point(8, 0), tx.width, tx.height);
12424 		}
12425 		painter.outlineColor = cs.foregroundColor;
12426 		painter.drawText(Point(8, 0), legend);
12427 	}
12428 
12429 	override int maxHeight() {
12430 		auto m = paddingTop() + paddingBottom();
12431 		foreach(child; children) {
12432 			auto mh = child.maxHeight();
12433 			if(mh == int.max)
12434 				return int.max;
12435 			m += mh;
12436 			m += child.marginBottom();
12437 			m += child.marginTop();
12438 		}
12439 		m += 6;
12440 		if(m < minHeight)
12441 			return minHeight;
12442 		return m;
12443 	}
12444 
12445 	override int minHeight() {
12446 		auto m = paddingTop() + paddingBottom();
12447 		foreach(child; children) {
12448 			m += child.minHeight();
12449 			m += child.marginBottom();
12450 			m += child.marginTop();
12451 		}
12452 		return m + 6;
12453 	}
12454 
12455 	override int minWidth() {
12456 		return 6 + cast(int) this.legend.length * 7;
12457 	}
12458 }
12459 
12460 /++
12461 	$(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")
12462 	$(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.)
12463 +/
12464 version(minigui_screenshots)
12465 @Screenshot("Fieldset")
12466 unittest {
12467 	auto window = new Window(200, 100);
12468 	auto set = new Fieldset("Baby will", window);
12469 	auto option1 = new Radiobox("Eat", set);
12470 	auto option2 = new Radiobox("Cry", set);
12471 	auto option3 = new Radiobox("Sleep", set);
12472 	window.loop();
12473 }
12474 
12475 /// Draws a line
12476 class HorizontalRule : Widget {
12477 	mixin Margin!q{ 2 };
12478 	override int minHeight() { return 2; }
12479 	override int maxHeight() { return 2; }
12480 
12481 	///
12482 	this(Widget parent) {
12483 		super(parent);
12484 	}
12485 
12486 	override void paint(WidgetPainter painter) {
12487 		auto cs = getComputedStyle();
12488 		painter.outlineColor = cs.darkAccentColor;
12489 		painter.drawLine(Point(0, 0), Point(width, 0));
12490 		painter.outlineColor = cs.lightAccentColor;
12491 		painter.drawLine(Point(0, 1), Point(width, 1));
12492 	}
12493 }
12494 
12495 version(minigui_screenshots)
12496 @Screenshot("HorizontalRule")
12497 /++
12498 	$(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.)
12499 
12500 +/
12501 unittest {
12502 	auto window = new Window(200, 100);
12503 	auto above = new TextLabel("Above the line", TextAlignment.Left, window);
12504 	new HorizontalRule(window);
12505 	auto below = new TextLabel("Below the line", TextAlignment.Left, window);
12506 	window.loop();
12507 }
12508 
12509 /// ditto
12510 class VerticalRule : Widget {
12511 	mixin Margin!q{ 2 };
12512 	override int minWidth() { return 2; }
12513 	override int maxWidth() { return 2; }
12514 
12515 	///
12516 	this(Widget parent) {
12517 		super(parent);
12518 	}
12519 
12520 	override void paint(WidgetPainter painter) {
12521 		auto cs = getComputedStyle();
12522 		painter.outlineColor = cs.darkAccentColor;
12523 		painter.drawLine(Point(0, 0), Point(0, height));
12524 		painter.outlineColor = cs.lightAccentColor;
12525 		painter.drawLine(Point(1, 0), Point(1, height));
12526 	}
12527 }
12528 
12529 
12530 ///
12531 class Menu : Window {
12532 	void remove() {
12533 		foreach(i, child; parentWindow.children)
12534 			if(child is this) {
12535 				parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $];
12536 				break;
12537 			}
12538 		parentWindow.redraw();
12539 
12540 		parentWindow.releaseMouseCapture();
12541 	}
12542 
12543 	///
12544 	void addSeparator() {
12545 		version(win32_widgets)
12546 			AppendMenu(handle, MF_SEPARATOR, 0, null);
12547 		else version(custom_widgets)
12548 			auto hr = new HorizontalRule(this);
12549 		else static assert(0);
12550 	}
12551 
12552 	override int paddingTop() { return 4; }
12553 	override int paddingBottom() { return 4; }
12554 	override int paddingLeft() { return 2; }
12555 	override int paddingRight() { return 2; }
12556 
12557 	version(win32_widgets) {}
12558 	else version(custom_widgets) {
12559 
12560 		Widget previouslyFocusedWidget;
12561 		Widget* previouslyFocusedWidgetBelongsIn;
12562 
12563 		SimpleWindow dropDown;
12564 		Widget menuParent;
12565 		void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
12566 			this.menuParent = parent;
12567 
12568 			previouslyFocusedWidget = parent.parentWindow.focusedWidget;
12569 			previouslyFocusedWidgetBelongsIn = &parent.parentWindow.focusedWidget;
12570 			parent.parentWindow.focusedWidget = this;
12571 
12572 			int w = 150;
12573 			int h = paddingTop + paddingBottom;
12574 			if(this.children.length) {
12575 				// hacking it to get the ideal height out of recomputeChildLayout
12576 				this.width = w;
12577 				this.height = h;
12578 				this.recomputeChildLayoutEntry();
12579 				h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
12580 				h += paddingBottom;
12581 
12582 				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
12583 			}
12584 
12585 			if(offsetY == int.min)
12586 				offsetY = parent.defaultLineHeight;
12587 
12588 			auto coord = parent.globalCoordinates();
12589 			dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
12590 			this.x = 0;
12591 			this.y = 0;
12592 			this.width = dropDown.width;
12593 			this.height = dropDown.height;
12594 			this.drawableWindow = dropDown;
12595 			this.recomputeChildLayoutEntry();
12596 
12597 			static if(UsingSimpledisplayX11)
12598 				XSync(XDisplayConnection.get, 0);
12599 
12600 			dropDown.visibilityChanged = (bool visible) {
12601 				if(visible) {
12602 					this.redraw();
12603 					dropDown.grabInput();
12604 				} else {
12605 					dropDown.releaseInputGrab();
12606 				}
12607 			};
12608 
12609 			dropDown.show();
12610 
12611 			clickListener = this.addEventListener((scope ClickEvent ev) {
12612 				unpopup();
12613 				// need to unlock asap just in case other user handlers block...
12614 				static if(UsingSimpledisplayX11)
12615 					flushGui();
12616 			}, true /* again for asap action */);
12617 		}
12618 
12619 		EventListener clickListener;
12620 	}
12621 	else static assert(false);
12622 
12623 	version(custom_widgets)
12624 	void unpopup() {
12625 		mouseLastOver = mouseLastDownOn = null;
12626 		dropDown.hide();
12627 		if(!menuParent.parentWindow.win.closed) {
12628 			if(auto maw = cast(MouseActivatedWidget) menuParent) {
12629 				maw.setDynamicState(DynamicState.depressed, false);
12630 				maw.setDynamicState(DynamicState.hover, false);
12631 				maw.redraw();
12632 			}
12633 			// menuParent.parentWindow.win.focus();
12634 		}
12635 		clickListener.disconnect();
12636 
12637 		if(previouslyFocusedWidgetBelongsIn)
12638 			*previouslyFocusedWidgetBelongsIn = previouslyFocusedWidget;
12639 	}
12640 
12641 	MenuItem[] items;
12642 
12643 	///
12644 	MenuItem addItem(MenuItem item) {
12645 		addChild(item);
12646 		items ~= item;
12647 		version(win32_widgets) {
12648 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
12649 		}
12650 		return item;
12651 	}
12652 
12653 	string label;
12654 
12655 	version(win32_widgets) {
12656 		HMENU handle;
12657 		///
12658 		this(string label, Widget parent) {
12659 			// not actually passing the parent since it effs up the drawing
12660 			super(cast(Widget) null);// parent);
12661 			this.label = label;
12662 			handle = CreatePopupMenu();
12663 		}
12664 	} else version(custom_widgets) {
12665 		///
12666 		this(string label, Widget parent) {
12667 
12668 			if(dropDown) {
12669 				dropDown.close();
12670 			}
12671 			dropDown = new SimpleWindow(
12672 				150, 4,
12673 				// FIXME: what if it is a popupMenu ?
12674 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null);
12675 
12676 			this.label = label;
12677 
12678 			super(dropDown);
12679 		}
12680 	} else static assert(false);
12681 
12682 	override int maxHeight() { return defaultLineHeight; }
12683 	override int minHeight() { return defaultLineHeight; }
12684 
12685 	version(custom_widgets) {
12686 		Widget currentPlace;
12687 
12688 		void changeCurrentPlace(Widget n) {
12689 			if(currentPlace) {
12690 				currentPlace.dynamicState = 0;
12691 			}
12692 
12693 			if(n) {
12694 				n.dynamicState = DynamicState.hover;
12695 			}
12696 
12697 			currentPlace = n;
12698 		}
12699 
12700 		override void paint(WidgetPainter painter) {
12701 			this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color);
12702 		}
12703 
12704 		override void defaultEventHandler_keydown(KeyDownEvent ke) {
12705 			switch(ke.key) {
12706 				case Key.Down:
12707 					Widget next;
12708 					Widget first;
12709 					foreach(w; this.children) {
12710 						if((cast(MenuItem) w) is null)
12711 							continue;
12712 
12713 						if(first is null)
12714 							first = w;
12715 
12716 						if(next !is null) {
12717 							next = w;
12718 							break;
12719 						}
12720 
12721 						if(currentPlace is null) {
12722 							next = w;
12723 							break;
12724 						}
12725 
12726 						if(w is currentPlace) {
12727 							next = w;
12728 						}
12729 					}
12730 
12731 					if(next is currentPlace)
12732 						next = first;
12733 
12734 					changeCurrentPlace(next);
12735 					break;
12736 				case Key.Up:
12737 					Widget prev;
12738 					foreach(w; this.children) {
12739 						if((cast(MenuItem) w) is null)
12740 							continue;
12741 						if(w is currentPlace) {
12742 							if(prev is null) {
12743 								foreach_reverse(c; this.children) {
12744 									if((cast(MenuItem) c) !is null) {
12745 										prev = c;
12746 										break;
12747 									}
12748 								}
12749 							}
12750 							break;
12751 						}
12752 						prev = w;
12753 					}
12754 					changeCurrentPlace(prev);
12755 					break;
12756 				case Key.Left:
12757 				case Key.Right:
12758 					if(menuParent) {
12759 						Menu first;
12760 						Menu last;
12761 						Menu prev;
12762 						Menu next;
12763 						bool found;
12764 
12765 						size_t prev_idx;
12766 						size_t next_idx;
12767 
12768 						MenuBar mb = cast(MenuBar) menuParent.parent;
12769 
12770 						if(mb) {
12771 							foreach(idx, menu; mb.subMenus) {
12772 								if(first is null)
12773 									first = menu;
12774 								last = menu;
12775 								if(found && next is null) {
12776 									next = menu;
12777 									next_idx = idx;
12778 								}
12779 								if(menu is this)
12780 									found = true;
12781 								if(!found) {
12782 									prev = menu;
12783 									prev_idx = idx;
12784 								}
12785 							}
12786 
12787 							Menu nextMenu;
12788 							size_t nextMenuIdx;
12789 							if(ke.key == Key.Left) {
12790 								nextMenu = prev ? prev : last;
12791 								nextMenuIdx = prev ? prev_idx : mb.subMenus.length - 1;
12792 							} else {
12793 								nextMenu = next ? next : first;
12794 								nextMenuIdx = next ? next_idx : 0;
12795 							}
12796 
12797 							unpopup();
12798 
12799 							auto rent = mb.children[nextMenuIdx]; // FIXME thsi is not necessarily right
12800 							rent.dynamicState = DynamicState.depressed | DynamicState.hover;
12801 							nextMenu.popup(rent);
12802 						}
12803 					}
12804 					break;
12805 				case Key.Enter:
12806 				case Key.PadEnter:
12807 					// because the key up and char events will go back to the other window after we unpopup!
12808 					// we will wait for the char event to come (in the following method)
12809 					break;
12810 				case Key.Escape:
12811 					unpopup();
12812 					break;
12813 				default:
12814 			}
12815 		}
12816 		override void defaultEventHandler_char(CharEvent ke) {
12817 			// if one is selected, enter activates it
12818 			if(currentPlace) {
12819 				if(ke.character == '\n') {
12820 					// enter selects
12821 					auto event = new Event(EventType.triggered, currentPlace);
12822 					event.dispatch();
12823 					unpopup();
12824 					return;
12825 				}
12826 			}
12827 
12828 			// otherwise search for a hotkey
12829 			foreach(item; items) {
12830 				if(item.hotkey == ke.character) {
12831 					auto event = new Event(EventType.triggered, item);
12832 					event.dispatch();
12833 					unpopup();
12834 					return;
12835 				}
12836 			}
12837 		}
12838 		override void defaultEventHandler_mouseover(MouseOverEvent moe) {
12839 			if(moe.target && moe.target.parent is this)
12840 				changeCurrentPlace(moe.target);
12841 		}
12842 	}
12843 }
12844 
12845 /++
12846 	A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked.
12847 +/
12848 class MenuItem : MouseActivatedWidget {
12849 	Menu submenu;
12850 
12851 	Action action;
12852 	string label;
12853 	dchar hotkey;
12854 
12855 	override int paddingLeft() { return 4; }
12856 
12857 	override int maxHeight() { return defaultLineHeight + 4; }
12858 	override int minHeight() { return defaultLineHeight + 4; }
12859 	override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); }
12860 	override int maxWidth() {
12861 		if(cast(MenuBar) parent) {
12862 			return minWidth();
12863 		}
12864 		return int.max;
12865 	}
12866 	/// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu.
12867 	this(string lbl, Widget parent = null) {
12868 		super(parent);
12869 		//label = lbl; // FIXME
12870 		foreach(idx, char ch; lbl) // FIXME
12871 			if(ch != '&') { // FIXME
12872 				label ~= ch; // FIXME
12873 			} else {
12874 				if(idx + 1 < lbl.length) {
12875 					hotkey = lbl[idx + 1];
12876 					if(hotkey >= 'A' && hotkey <= 'Z')
12877 						hotkey += 32;
12878 				}
12879 			}
12880 		tabStop = false; // these are selected some other way
12881 	}
12882 
12883 	///
12884 	this(Action action, Widget parent = null) {
12885 		assert(action !is null);
12886 		this(action.label, parent);
12887 		this.action = action;
12888 		tabStop = false; // these are selected some other way
12889 	}
12890 
12891 	version(custom_widgets)
12892 	override void paint(WidgetPainter painter) {
12893 		auto cs = getComputedStyle();
12894 		if(dynamicState & DynamicState.depressed)
12895 			this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
12896 		else {
12897 			if(dynamicState & DynamicState.hover) {
12898 				painter.fillColor = cs.hoveringColor;
12899 				painter.outlineColor = Color.transparent;
12900 			} else {
12901 				painter.fillColor = cs.background.color;
12902 				painter.outlineColor = Color.transparent;
12903 			}
12904 
12905 			painter.drawRectangle(Point(0, 0), Size(this.width, this.height));
12906 		}
12907 
12908 		if(dynamicState & DynamicState.hover)
12909 			painter.outlineColor = cs.activeMenuItemColor;
12910 		else
12911 			painter.outlineColor = cs.foregroundColor;
12912 		painter.fillColor = Color.transparent;
12913 		painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
12914 		if(action && action.accelerator !is KeyEvent.init) {
12915 			painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter);
12916 
12917 		}
12918 	}
12919 
12920 	static class Style : Widget.Style {
12921 		override bool variesWithState(ulong dynamicStateFlags) {
12922 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
12923 		}
12924 	}
12925 	mixin OverrideStyle!Style;
12926 
12927 	override void defaultEventHandler_triggered(Event event) {
12928 		if(action)
12929 		foreach(handler; action.triggered)
12930 			handler();
12931 
12932 		if(auto pmenu = cast(Menu) this.parent)
12933 			pmenu.remove();
12934 
12935 		super.defaultEventHandler_triggered(event);
12936 	}
12937 }
12938 
12939 version(win32_widgets)
12940 /// A "mouse activiated widget" is really just an abstract variant of button.
12941 class MouseActivatedWidget : Widget {
12942 	@property bool isChecked() {
12943 		assert(hwnd);
12944 		return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED;
12945 
12946 	}
12947 	@property void isChecked(bool state) {
12948 		assert(hwnd);
12949 		SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0);
12950 
12951 	}
12952 
12953 	override void handleWmCommand(ushort cmd, ushort id) {
12954 		if(cmd == 0) {
12955 			auto event = new Event(EventType.triggered, this);
12956 			event.dispatch();
12957 		}
12958 	}
12959 
12960 	this(Widget parent) {
12961 		super(parent);
12962 	}
12963 }
12964 else version(custom_widgets)
12965 /// ditto
12966 class MouseActivatedWidget : Widget {
12967 	@property bool isChecked() { return isChecked_; }
12968 	@property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;}
12969 
12970 	private bool isChecked_;
12971 
12972 	this(Widget parent) {
12973 		super(parent);
12974 
12975 		addEventListener((MouseDownEvent ev) {
12976 			if(ev.button == MouseButton.left) {
12977 				setDynamicState(DynamicState.depressed, true);
12978 				setDynamicState(DynamicState.hover, true);
12979 				redraw();
12980 			}
12981 		});
12982 
12983 		addEventListener((MouseUpEvent ev) {
12984 			if(ev.button == MouseButton.left) {
12985 				setDynamicState(DynamicState.depressed, false);
12986 				setDynamicState(DynamicState.hover, false);
12987 				redraw();
12988 			}
12989 		});
12990 
12991 		addEventListener((MouseMoveEvent mme) {
12992 			if(!(mme.state & ModifierState.leftButtonDown)) {
12993 				if(dynamicState_ & DynamicState.depressed) {
12994 					setDynamicState(DynamicState.depressed, false);
12995 					redraw();
12996 				}
12997 			}
12998 		});
12999 	}
13000 
13001 	override void defaultEventHandler_focus(FocusEvent ev) {
13002 		super.defaultEventHandler_focus(ev);
13003 		this.redraw();
13004 	}
13005 	override void defaultEventHandler_blur(BlurEvent ev) {
13006 		super.defaultEventHandler_blur(ev);
13007 		setDynamicState(DynamicState.depressed, false);
13008 		this.redraw();
13009 	}
13010 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
13011 		super.defaultEventHandler_keydown(ev);
13012 		if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) {
13013 			setDynamicState(DynamicState.depressed, true);
13014 			setDynamicState(DynamicState.hover, true);
13015 			this.redraw();
13016 		}
13017 	}
13018 	override void defaultEventHandler_keyup(KeyUpEvent ev) {
13019 		super.defaultEventHandler_keyup(ev);
13020 		if(!(dynamicState & DynamicState.depressed))
13021 			return;
13022 		setDynamicState(DynamicState.depressed, false);
13023 		setDynamicState(DynamicState.hover, false);
13024 		this.redraw();
13025 
13026 		auto event = new Event(EventType.triggered, this);
13027 		event.sendDirectly();
13028 	}
13029 	override void defaultEventHandler_click(ClickEvent ev) {
13030 		super.defaultEventHandler_click(ev);
13031 		if(ev.button == MouseButton.left) {
13032 			auto event = new Event(EventType.triggered, this);
13033 			event.sendDirectly();
13034 		}
13035 	}
13036 
13037 }
13038 else static assert(false);
13039 
13040 /*
13041 /++
13042 	Like the tablet thing, it would have a label, a description, and a switch slider thingy.
13043 
13044 	Basically the same as a checkbox.
13045 +/
13046 class OnOffSwitch : MouseActivatedWidget {
13047 
13048 }
13049 */
13050 
13051 /++
13052 	History:
13053 		Added June 15, 2021 (dub v10.1)
13054 +/
13055 struct ImageLabel {
13056 	/++
13057 		Defines a label+image combo used by some widgets.
13058 
13059 		If you provide just a text label, that is all the widget will try to
13060 		display. Or just an image will display just that. If you provide both,
13061 		it may display both text and image side by side or display the image
13062 		and offer text on an input event depending on the widget.
13063 
13064 		History:
13065 			The `alignment` parameter was added on September 27, 2021
13066 	+/
13067 	this(string label, TextAlignment alignment = TextAlignment.Center) {
13068 		this.label = label;
13069 		this.displayFlags = DisplayFlags.displayText;
13070 		this.alignment = alignment;
13071 	}
13072 
13073 	/// ditto
13074 	this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
13075 		this.label = label;
13076 		this.image = image;
13077 		this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
13078 		this.alignment = alignment;
13079 	}
13080 
13081 	/// ditto
13082 	this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
13083 		this.image = image;
13084 		this.displayFlags = DisplayFlags.displayImage;
13085 		this.alignment = alignment;
13086 	}
13087 
13088 	/// ditto
13089 	this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) {
13090 		this.label = label;
13091 		this.image = image;
13092 		this.alignment = alignment;
13093 		this.displayFlags = displayFlags;
13094 	}
13095 
13096 	string label;
13097 	MemoryImage image;
13098 
13099 	enum DisplayFlags {
13100 		displayText = 1 << 0,
13101 		displayImage = 1 << 1,
13102 	}
13103 
13104 	int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
13105 
13106 	TextAlignment alignment;
13107 }
13108 
13109 /++
13110 	A basic checked or not checked box with an attached label.
13111 
13112 
13113 	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
13114 
13115 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
13116 
13117 	History:
13118 		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.
13119 +/
13120 class Checkbox : MouseActivatedWidget {
13121 	version(win32_widgets) {
13122 		override int maxHeight() { return scaleWithDpi(16); }
13123 		override int minHeight() { return scaleWithDpi(16); }
13124 	} else version(custom_widgets) {
13125 		private enum buttonSize = 16;
13126 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
13127 		override int minHeight() { return maxHeight(); }
13128 	} else static assert(0);
13129 
13130 	override int marginLeft() { return 4; }
13131 
13132 	override int flexBasisWidth() { return 24 + cast(int) label.length * 7; }
13133 
13134 	/++
13135 		Just an alias because I keep typing checked out of web habit.
13136 
13137 		History:
13138 			Added May 31, 2021
13139 	+/
13140 	alias checked = isChecked;
13141 
13142 	private string label;
13143 	private dchar accelerator;
13144 
13145 	/++
13146 	+/
13147 	this(string label, Widget parent) {
13148 		this(ImageLabel(label), Appearance.checkbox, parent);
13149 	}
13150 
13151 	/// ditto
13152 	this(string label, Appearance appearance, Widget parent) {
13153 		this(ImageLabel(label), appearance, parent);
13154 	}
13155 
13156 	/++
13157 		Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific.
13158 
13159 		History:
13160 			Added June 29, 2021 (dub v10.2)
13161 	+/
13162 	enum Appearance {
13163 		checkbox, /// a normal checkbox
13164 		pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad.
13165 		//sliderswitch,
13166 	}
13167 	private Appearance appearance;
13168 
13169 	/// ditto
13170 	private this(ImageLabel label, Appearance appearance, Widget parent) {
13171 		super(parent);
13172 		version(win32_widgets) {
13173 			this.label = label.label;
13174 
13175 			uint extraStyle;
13176 			final switch(appearance) {
13177 				case Appearance.checkbox:
13178 				break;
13179 				case Appearance.pushbutton:
13180 					extraStyle |= BS_PUSHLIKE;
13181 				break;
13182 			}
13183 
13184 			createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle);
13185 		} else version(custom_widgets) {
13186 			label.label.extractWindowsStyleLabel(this.label, this.accelerator);
13187 		} else static assert(0);
13188 	}
13189 
13190 	version(custom_widgets)
13191 	override void paint(WidgetPainter painter) {
13192 		auto cs = getComputedStyle();
13193 		if(isFocused()) {
13194 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
13195 			painter.fillColor = cs.windowBackgroundColor;
13196 			painter.drawRectangle(Point(0, 0), width, height);
13197 			painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
13198 		} else {
13199 			painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid);
13200 			painter.fillColor = cs.windowBackgroundColor;
13201 			painter.drawRectangle(Point(0, 0), width, height);
13202 		}
13203 
13204 
13205 		painter.outlineColor = Color.black;
13206 		painter.fillColor = Color.white;
13207 		enum rectOffset = 2;
13208 		painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset));
13209 
13210 		if(isChecked) {
13211 			auto size = scaleWithDpi(2);
13212 			painter.pen = Pen(Color.black, size);
13213 			// I'm using height so the checkbox is square
13214 			enum padding = 3;
13215 			painter.drawLine(
13216 				scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)),
13217 				scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2)
13218 			);
13219 			painter.drawLine(
13220 				scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0),
13221 				scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 -  size % 2)
13222 			);
13223 
13224 			painter.pen = Pen(Color.black, 1);
13225 		}
13226 
13227 		if(label !is null) {
13228 			painter.outlineColor = cs.foregroundColor();
13229 			painter.fillColor = cs.foregroundColor();
13230 
13231 			// i want the centerline of the text to be aligned with the centerline of the checkbox
13232 			/+
13233 			auto font = cs.font();
13234 			auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2;
13235 			painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label);
13236 			+/
13237 			painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter);
13238 		}
13239 	}
13240 
13241 	override void defaultEventHandler_triggered(Event ev) {
13242 		isChecked = !isChecked;
13243 
13244 		this.emit!(ChangeEvent!bool)(&isChecked);
13245 
13246 		redraw();
13247 	}
13248 
13249 	/// Emits a change event with the checked state
13250 	mixin Emits!(ChangeEvent!bool);
13251 }
13252 
13253 /// Adds empty space to a layout.
13254 class VerticalSpacer : Widget {
13255 	private int mh;
13256 
13257 	/++
13258 		History:
13259 			The overload with `maxHeight` was added on December 31, 2024
13260 	+/
13261 	this(Widget parent) {
13262 		this(0, parent);
13263 	}
13264 
13265 	/// ditto
13266 	this(int maxHeight, Widget parent) {
13267 		this.mh = maxHeight;
13268 		super(parent);
13269 		this.tabStop = false;
13270 	}
13271 
13272 	override int maxHeight() {
13273 		return mh ? scaleWithDpi(mh) : super.maxHeight();
13274 	}
13275 }
13276 
13277 
13278 /// ditto
13279 class HorizontalSpacer : Widget {
13280 	private int mw;
13281 
13282 	/++
13283 		History:
13284 			The overload with `maxWidth` was added on December 31, 2024
13285 	+/
13286 	this(Widget parent) {
13287 		this(0, parent);
13288 	}
13289 
13290 	/// ditto
13291 	this(int maxWidth, Widget parent) {
13292 		this.mw = maxWidth;
13293 		super(parent);
13294 		this.tabStop = false;
13295 	}
13296 
13297 	override int maxWidth() {
13298 		return mw ? scaleWithDpi(mw) : super.maxWidth();
13299 	}
13300 }
13301 
13302 
13303 /++
13304 	Creates a radio button with an associated label. These are usually put inside a [Fieldset].
13305 
13306 
13307 	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
13308 
13309 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
13310 
13311 	History:
13312 		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.
13313 +/
13314 class Radiobox : MouseActivatedWidget {
13315 
13316 	version(win32_widgets) {
13317 		override int maxHeight() { return scaleWithDpi(16); }
13318 		override int minHeight() { return scaleWithDpi(16); }
13319 	} else version(custom_widgets) {
13320 		private enum buttonSize = 16;
13321 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
13322 		override int minHeight() { return maxHeight(); }
13323 	} else static assert(0);
13324 
13325 	override int marginLeft() { return 4; }
13326 
13327 	// FIXME: make a label getter
13328 	private string label;
13329 	private dchar accelerator;
13330 
13331 	/++
13332 
13333 	+/
13334 	this(string label, Widget parent) {
13335 		super(parent);
13336 		version(win32_widgets) {
13337 			this.label = label;
13338 			createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON);
13339 		} else version(custom_widgets) {
13340 			label.extractWindowsStyleLabel(this.label, this.accelerator);
13341 			height = 16;
13342 			width = height + 4 + cast(int) label.length * 16;
13343 		}
13344 	}
13345 
13346 	version(custom_widgets)
13347 	override void paint(WidgetPainter painter) {
13348 		auto cs = getComputedStyle();
13349 
13350 		if(isFocused) {
13351 			painter.fillColor = cs.windowBackgroundColor;
13352 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
13353 		} else {
13354 			painter.fillColor = cs.windowBackgroundColor;
13355 			painter.outlineColor = cs.windowBackgroundColor;
13356 		}
13357 		painter.drawRectangle(Point(0, 0), width, height);
13358 
13359 		painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
13360 
13361 		painter.outlineColor = Color.black;
13362 		painter.fillColor = Color.white;
13363 		painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2)));
13364 		if(isChecked) {
13365 			painter.outlineColor = Color.black;
13366 			painter.fillColor = Color.black;
13367 			// I'm using height so the checkbox is square
13368 			auto size = scaleWithDpi(2);
13369 			painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2));
13370 		}
13371 
13372 		painter.outlineColor = cs.foregroundColor();
13373 		painter.fillColor = cs.foregroundColor();
13374 
13375 		painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
13376 	}
13377 
13378 
13379 	override void defaultEventHandler_triggered(Event ev) {
13380 		isChecked = true;
13381 
13382 		if(this.parent) {
13383 			foreach(child; this.parent.children) {
13384 				if(child is this) continue;
13385 				if(auto rb = cast(Radiobox) child) {
13386 					rb.isChecked = false;
13387 					rb.emit!(ChangeEvent!bool)(&rb.isChecked);
13388 					rb.redraw();
13389 				}
13390 			}
13391 		}
13392 
13393 		this.emit!(ChangeEvent!bool)(&this.isChecked);
13394 
13395 		redraw();
13396 	}
13397 
13398 	/// 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.
13399 	mixin Emits!(ChangeEvent!bool);
13400 }
13401 
13402 
13403 /++
13404 	Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event.
13405 
13406 
13407 	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
13408 
13409 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
13410 
13411 	History:
13412 		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.
13413 +/
13414 class Button : MouseActivatedWidget {
13415 	override int heightStretchiness() { return 3; }
13416 	override int widthStretchiness() { return 3; }
13417 
13418 	/++
13419 		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.
13420 
13421 		History:
13422 			Added July 2, 2021
13423 	+/
13424 	public bool triggersOnMultiClick;
13425 
13426 	private string label_;
13427 	private TextAlignment alignment;
13428 	private dchar accelerator;
13429 
13430 	///
13431 	string label() { return label_; }
13432 	///
13433 	void label(string l) {
13434 		label_ = l;
13435 		version(win32_widgets) {
13436 			WCharzBuffer bfr = WCharzBuffer(l);
13437 			SetWindowTextW(hwnd, bfr.ptr);
13438 		} else version(custom_widgets) {
13439 			redraw();
13440 		}
13441 	}
13442 
13443 	override void defaultEventHandler_dblclick(DoubleClickEvent ev) {
13444 		super.defaultEventHandler_dblclick(ev);
13445 		if(triggersOnMultiClick) {
13446 			if(ev.button == MouseButton.left) {
13447 				auto event = new Event(EventType.triggered, this);
13448 				event.sendDirectly();
13449 			}
13450 		}
13451 	}
13452 
13453 	private Sprite sprite;
13454 	private int displayFlags;
13455 
13456 	protected bool needsOwnerDraw() {
13457 		return &this.paint !is &Button.paint || &this.useStyleProperties !is &Button.useStyleProperties || &this.paintContent !is &Button.paintContent;
13458 	}
13459 
13460 	version(win32_widgets)
13461 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis) {
13462 		auto itemId = dis.itemID;
13463 		auto hdc = dis.hDC;
13464 		auto rect = dis.rcItem;
13465 		switch(dis.itemAction) {
13466 			// skipping setDynamicState because i don't want to queue the redraw unnecessarily
13467 			case ODA_SELECT:
13468 				dynamicState_ &= ~DynamicState.depressed;
13469 				if(dis.itemState & ODS_SELECTED)
13470 					dynamicState_ |= DynamicState.depressed;
13471 			goto case;
13472 			case ODA_FOCUS:
13473 				dynamicState_ &= ~DynamicState.focus;
13474 				if(dis.itemState & ODS_FOCUS)
13475 					dynamicState_ |= DynamicState.focus;
13476 			goto case;
13477 			case ODA_DRAWENTIRE:
13478 				auto painter = WidgetPainter(this.simpleWindowWrappingHwnd.draw(true), this);
13479 				//painter.impl.hdc = hdc;
13480 				paint(painter);
13481 			break;
13482 			default:
13483 		}
13484 		return 1;
13485 
13486 	}
13487 
13488 	/++
13489 		Creates a push button with the given label, which may be an image or some text.
13490 
13491 		Bugs:
13492 			If the image is bigger than the button, it may not be displayed in the right position on Linux.
13493 
13494 		History:
13495 			The [ImageLabel] overload was added on June 21, 2021 (dub v10.1).
13496 
13497 			The button with label and image will respect requests to show both on Windows as
13498 			of March 28, 2022 iff you provide a manifest file to opt into common controls v6.
13499 	+/
13500 	this(string label, Widget parent) {
13501 		this(ImageLabel(label), parent);
13502 	}
13503 
13504 	/// ditto
13505 	this(ImageLabel label, Widget parent) {
13506 		bool needsImage;
13507 		version(win32_widgets) {
13508 			super(parent);
13509 
13510 			// BS_BITMAP is set when we want image only, so checking for exactly that combination
13511 			enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText;
13512 			auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0;
13513 
13514 			// could also do a virtual method needsOwnerDraw which default returns true and we control it here. typeid(this) == typeid(Button) for override check.
13515 
13516 			if(needsOwnerDraw) {
13517 				extraStyle |= BS_OWNERDRAW;
13518 				needsImage = true;
13519 			}
13520 
13521 			// the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter
13522 			createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 );
13523 
13524 			if(label.image) {
13525 				sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true);
13526 
13527 				SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle);
13528 			}
13529 
13530 			this.label = label.label;
13531 		} else version(custom_widgets) {
13532 			super(parent);
13533 
13534 			label.label.extractWindowsStyleLabel(this.label_, this.accelerator);
13535 			needsImage = true;
13536 		}
13537 
13538 
13539 		if(needsImage && label.image) {
13540 			this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image);
13541 			this.displayFlags = label.displayFlags;
13542 		}
13543 
13544 		this.alignment = label.alignment;
13545 	}
13546 
13547 	override int minHeight() { return defaultLineHeight + 4; }
13548 
13549 	static class Style : Widget.Style {
13550 		override WidgetBackground background() {
13551 			auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive
13552 
13553 			auto pressed = DynamicState.depressed | DynamicState.hover;
13554 			if((widget.dynamicState & pressed) == pressed) {
13555 				return WidgetBackground(cs.depressedButtonColor());
13556 			} else if(widget.dynamicState & DynamicState.hover) {
13557 				return WidgetBackground(cs.hoveringColor());
13558 			} else {
13559 				return WidgetBackground(cs.buttonColor());
13560 			}
13561 		}
13562 
13563 		override FrameStyle borderStyle() {
13564 			auto pressed = DynamicState.depressed | DynamicState.hover;
13565 			if((widget.dynamicState & pressed) == pressed) {
13566 				return FrameStyle.sunk;
13567 			} else {
13568 				return FrameStyle.risen;
13569 			}
13570 
13571 		}
13572 
13573 		override bool variesWithState(ulong dynamicStateFlags) {
13574 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
13575 		}
13576 	}
13577 	mixin OverrideStyle!Style;
13578 
13579 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
13580 		if(sprite) {
13581 			sprite.drawAt(
13582 				painter,
13583 				bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2),
13584 				Point(0, 0)
13585 			);
13586 		} else {
13587 			Point pos = bounds.upperLeft;
13588 			if(this.height == 16)
13589 				pos.y -= 2; // total hack omg
13590 			painter.drawText(pos, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter);
13591 		}
13592 		return bounds;
13593 	}
13594 
13595 	override int flexBasisWidth() {
13596 		version(win32_widgets) {
13597 			SIZE size;
13598 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
13599 			if(size.cx == 0)
13600 				goto fallback;
13601 			return size.cx + scaleWithDpi(16);
13602 		}
13603 		fallback:
13604 			return scaleWithDpi(cast(int) label.length * 8 + 16);
13605 	}
13606 
13607 	override int flexBasisHeight() {
13608 		version(win32_widgets) {
13609 			SIZE size;
13610 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
13611 			if(size.cy == 0)
13612 				goto fallback;
13613 			return size.cy + scaleWithDpi(6);
13614 		}
13615 		fallback:
13616 			return defaultLineHeight + 4;
13617 	}
13618 }
13619 
13620 /++
13621 	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.
13622 
13623 	History:
13624 		Added January 14, 2024
13625 +/
13626 class CustomButton : Button {
13627 	this(ImageLabel label, Widget parent) {
13628 		super(label, parent);
13629 	}
13630 
13631 	this(string label, Widget parent) {
13632 		super(label, parent);
13633 	}
13634 
13635 	version(win32_widgets)
13636 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
13637 		// paint is driven by handleWmDrawItem instead of minigui's redraw events
13638 		if(hwnd)
13639 			InvalidateRect(hwnd, null, false); // get Windows to trigger the actual redraw
13640 		return;
13641 	}
13642 
13643 	override void paint(WidgetPainter painter) {
13644 		// the parent does `if(hwnd) return;` because
13645 		// normally we don't want to draw on standard controls,
13646 		// but this is an exception if it is an owner drawn button
13647 		// (which is determined in the constructor by testing,
13648 		// at runtime, for the existence of an overridden paint
13649 		// member anyway, so this needed to trigger BS_OWNERDRAW)
13650 		// sdpyPrintDebugString("drawing");
13651 		painter.drawThemed(&paintContent);
13652 	}
13653 }
13654 
13655 /++
13656 	A button with a consistent size, suitable for user commands like OK and CANCEL.
13657 +/
13658 class CommandButton : Button {
13659 	this(string label, Widget parent) {
13660 		super(label, parent);
13661 	}
13662 
13663 	// FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there
13664 
13665 	override int maxHeight() {
13666 		return defaultLineHeight + 4;
13667 	}
13668 
13669 	override int maxWidth() {
13670 		return defaultLineHeight * 4;
13671 	}
13672 
13673 	override int marginLeft() { return 12; }
13674 	override int marginRight() { return 12; }
13675 	override int marginTop() { return 12; }
13676 	override int marginBottom() { return 12; }
13677 }
13678 
13679 ///
13680 enum ArrowDirection {
13681 	left, ///
13682 	right, ///
13683 	up, ///
13684 	down ///
13685 }
13686 
13687 ///
13688 version(custom_widgets)
13689 class ArrowButton : Button {
13690 	///
13691 	this(ArrowDirection direction, Widget parent) {
13692 		super("", parent);
13693 		this.direction = direction;
13694 		triggersOnMultiClick = true;
13695 	}
13696 
13697 	private ArrowDirection direction;
13698 
13699 	override int minHeight() { return scaleWithDpi(16); }
13700 	override int maxHeight() { return scaleWithDpi(16); }
13701 	override int minWidth() { return scaleWithDpi(16); }
13702 	override int maxWidth() { return scaleWithDpi(16); }
13703 
13704 	override void paint(WidgetPainter painter) {
13705 		super.paint(painter);
13706 
13707 		auto cs = getComputedStyle();
13708 
13709 		painter.outlineColor = cs.foregroundColor;
13710 		painter.fillColor = cs.foregroundColor;
13711 
13712 		auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2);
13713 
13714 		final switch(direction) {
13715 			case ArrowDirection.up:
13716 				painter.drawPolygon(
13717 					scaleWithDpi(Point(2, 10) + offset),
13718 					scaleWithDpi(Point(7, 5) + offset),
13719 					scaleWithDpi(Point(12, 10) + offset),
13720 					scaleWithDpi(Point(2, 10) + offset)
13721 				);
13722 			break;
13723 			case ArrowDirection.down:
13724 				painter.drawPolygon(
13725 					scaleWithDpi(Point(2, 6) + offset),
13726 					scaleWithDpi(Point(7, 11) + offset),
13727 					scaleWithDpi(Point(12, 6) + offset),
13728 					scaleWithDpi(Point(2, 6) + offset)
13729 				);
13730 			break;
13731 			case ArrowDirection.left:
13732 				painter.drawPolygon(
13733 					scaleWithDpi(Point(10, 2) + offset),
13734 					scaleWithDpi(Point(5, 7) + offset),
13735 					scaleWithDpi(Point(10, 12) + offset),
13736 					scaleWithDpi(Point(10, 2) + offset)
13737 				);
13738 			break;
13739 			case ArrowDirection.right:
13740 				painter.drawPolygon(
13741 					scaleWithDpi(Point(6, 2) + offset),
13742 					scaleWithDpi(Point(11, 7) + offset),
13743 					scaleWithDpi(Point(6, 12) + offset),
13744 					scaleWithDpi(Point(6, 2) + offset)
13745 				);
13746 			break;
13747 		}
13748 	}
13749 }
13750 
13751 private
13752 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow {
13753 	int x, y;
13754 	Widget par = c;
13755 	while(par) {
13756 		x += par.x;
13757 		y += par.y;
13758 		par = par.parent;
13759 	}
13760 	return [x, y];
13761 }
13762 
13763 version(win32_widgets)
13764 private
13765 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow {
13766 // MapWindowPoints?
13767 	int x, y;
13768 	Widget par = c;
13769 	while(par) {
13770 		x += par.x;
13771 		y += par.y;
13772 		par = par.parent;
13773 		if(par !is null && par.useNativeDrawing())
13774 			break;
13775 	}
13776 	return [x, y];
13777 }
13778 
13779 ///
13780 class ImageBox : Widget {
13781 	private MemoryImage image_;
13782 
13783 	override int widthStretchiness() { return 1; }
13784 	override int heightStretchiness() { return 1; }
13785 	override int widthShrinkiness() { return 1; }
13786 	override int heightShrinkiness() { return 1; }
13787 
13788 	override int flexBasisHeight() {
13789 		return image_.height;
13790 	}
13791 
13792 	override int flexBasisWidth() {
13793 		return image_.width;
13794 	}
13795 
13796 	///
13797 	public void setImage(MemoryImage image){
13798 		this.image_ = image;
13799 		if(this.parentWindow && this.parentWindow.win) {
13800 			if(sprite)
13801 				sprite.dispose();
13802 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
13803 		}
13804 		redraw();
13805 	}
13806 
13807 	/// How to fit the image in the box if they aren't an exact match in size?
13808 	enum HowToFit {
13809 		center, /// centers the image, cropping around all the edges as needed
13810 		crop, /// always draws the image in the upper left, cropping the lower right if needed
13811 		// stretch, /// not implemented
13812 	}
13813 
13814 	private Sprite sprite;
13815 	private HowToFit howToFit_;
13816 
13817 	private Color backgroundColor_;
13818 
13819 	///
13820 	this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) {
13821 		this.image_ = image;
13822 		this.tabStop = false;
13823 		this.howToFit_ = howToFit;
13824 		this.backgroundColor_ = backgroundColor;
13825 		super(parent);
13826 		updateSprite();
13827 	}
13828 
13829 	/// ditto
13830 	this(MemoryImage image, HowToFit howToFit, Widget parent) {
13831 		this(image, howToFit, Color.transparent, parent);
13832 	}
13833 
13834 	private void updateSprite() {
13835 		if(sprite is null && this.parentWindow && this.parentWindow.win) {
13836 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
13837 		}
13838 	}
13839 
13840 	override void paint(WidgetPainter painter) {
13841 		updateSprite();
13842 		if(backgroundColor_.a) {
13843 			painter.fillColor = backgroundColor_;
13844 			painter.drawRectangle(Point(0, 0), width, height);
13845 		}
13846 		if(howToFit_ == HowToFit.crop)
13847 			sprite.drawAt(painter, Point(0, 0));
13848 		else if(howToFit_ == HowToFit.center) {
13849 			sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2));
13850 		}
13851 	}
13852 }
13853 
13854 ///
13855 class TextLabel : Widget {
13856 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; }
13857 	override int maxHeight() { return minHeight; }
13858 	override int minWidth() { return 32; }
13859 
13860 	override int flexBasisHeight() { return minHeight(); }
13861 	override int flexBasisWidth() { return defaultTextWidth(label); }
13862 
13863 	string label_;
13864 
13865 	/++
13866 		Indicates which other control this label is here for. Similar to HTML `for` attribute.
13867 
13868 		In practice this means a click on the label will focus the `labelFor`. In future versions
13869 		it will also set screen reader hints but that is not yet implemented.
13870 
13871 		History:
13872 			Added October 3, 2021 (dub v10.4)
13873 	+/
13874 	Widget labelFor;
13875 
13876 	///
13877 	@scriptable
13878 	string label() { return label_; }
13879 
13880 	///
13881 	@scriptable
13882 	void label(string l) {
13883 		label_ = l;
13884 		version(win32_widgets) {
13885 			WCharzBuffer bfr = WCharzBuffer(l);
13886 			SetWindowTextW(hwnd, bfr.ptr);
13887 		} else version(custom_widgets)
13888 			redraw();
13889 	}
13890 
13891 	override void defaultEventHandler_click(scope ClickEvent ce) {
13892 		if(this.labelFor !is null)
13893 			this.labelFor.focus();
13894 	}
13895 
13896 	/++
13897 		WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version.
13898 		For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly.
13899 	+/
13900 	this(string label, TextAlignment alignment, Widget parent) {
13901 		this.label_ = label;
13902 		this.alignment = alignment;
13903 		this.tabStop = false;
13904 		super(parent);
13905 
13906 		version(win32_widgets)
13907 		createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT);
13908 	}
13909 
13910 	/// ditto
13911 	this(string label, Widget parent) {
13912 		this(label, TextAlignment.Right, parent);
13913 	}
13914 
13915 	TextAlignment alignment;
13916 
13917 	version(custom_widgets)
13918 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
13919 		painter.outlineColor = getComputedStyle().foregroundColor;
13920 		painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment);
13921 		return bounds;
13922 	}
13923 }
13924 
13925 class TextDisplayHelper : Widget {
13926 	protected TextLayouter l;
13927 	protected ScrollMessageWidget smw;
13928 
13929 	private const(TextLayouter.State)*[] undoStack;
13930 	private const(TextLayouter.State)*[] redoStack;
13931 
13932 	private string preservedPrimaryText;
13933 	protected void selectionChanged() {
13934 		// sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString());
13935 		static if(UsingSimpledisplayX11)
13936 		with(l.selection()) {
13937 			if(!isEmpty()) {
13938 				//sdpyPrintDebugString("!isEmpty");
13939 
13940 				getPrimarySelection(parentWindow.win, (in char[] txt) {
13941 					// sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")");
13942 					// import std.stdio; writeln("txt: ", txt, " sel: ", getContentString);
13943 					if(txt.length) {
13944 						preservedPrimaryText = txt.idup;
13945 						// writeln(preservedPrimaryText);
13946 					}
13947 
13948 					setPrimarySelection(parentWindow.win, getContentString());
13949 				});
13950 			}
13951 		}
13952 	}
13953 
13954 	final TextLayouter layouter() {
13955 		return l;
13956 	}
13957 
13958 	bool readonly;
13959 	bool caretNavigation; // scroll lock can flip this
13960 	bool singleLine;
13961 	bool acceptsTabInput;
13962 
13963 	private Menu ctx;
13964 	override Menu contextMenu(int x, int y) {
13965 		if(ctx is null) {
13966 			ctx = new Menu("Actions", this);
13967 			if(!readonly) {
13968 				ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo)));
13969 				ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo)));
13970 				ctx.addSeparator();
13971 			}
13972 			if(!readonly)
13973 				ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut)));
13974 			ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, &copy)));
13975 			if(!readonly)
13976 				ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste)));
13977 			if(!readonly)
13978 				ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection)));
13979 			ctx.addSeparator();
13980 			ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll)));
13981 		}
13982 		return ctx;
13983 	}
13984 
13985 	override void defaultEventHandler_blur(BlurEvent ev) {
13986 		super.defaultEventHandler_blur(ev);
13987 		if(l.wasMutated()) {
13988 			auto evt = new ChangeEvent!string(this, &this.content);
13989 			evt.dispatch();
13990 			l.clearWasMutatedFlag();
13991 		}
13992 	}
13993 
13994 	private string content() {
13995 		return l.getTextString();
13996 	}
13997 
13998 	void undo() {
13999 		if(readonly) return;
14000 		if(undoStack.length) {
14001 			auto state = undoStack[$-1];
14002 			undoStack = undoStack[0 .. $-1];
14003 			undoStack.assumeSafeAppend();
14004 			redoStack ~= l.saveState();
14005 			l.restoreState(state);
14006 			adjustScrollbarSizes();
14007 			scrollForCaret();
14008 			redraw();
14009 			stateCheckpoint = true;
14010 		}
14011 	}
14012 
14013 	void redo() {
14014 		if(readonly) return;
14015 		if(redoStack.length) {
14016 			doStateCheckpoint();
14017 			auto state = redoStack[$-1];
14018 			redoStack = redoStack[0 .. $-1];
14019 			redoStack.assumeSafeAppend();
14020 			l.restoreState(state);
14021 			adjustScrollbarSizes();
14022 			scrollForCaret();
14023 			redraw();
14024 			stateCheckpoint = true;
14025 		}
14026 	}
14027 
14028 	void cut() {
14029 		if(readonly) return;
14030 		with(l.selection()) {
14031 			if(!isEmpty()) {
14032 				setClipboardText(parentWindow.win, getContentString());
14033 				doStateCheckpoint();
14034 				replaceContent("");
14035 				adjustScrollbarSizes();
14036 				scrollForCaret();
14037 				this.redraw();
14038 			}
14039 		}
14040 
14041 	}
14042 
14043 	void copy() {
14044 		with(l.selection()) {
14045 			if(!isEmpty()) {
14046 				setClipboardText(parentWindow.win, getContentString());
14047 				this.redraw();
14048 			}
14049 		}
14050 	}
14051 
14052 	void paste() {
14053 		if(readonly) return;
14054 		getClipboardText(parentWindow.win, (txt) {
14055 			doStateCheckpoint();
14056 			if(singleLine)
14057 				l.selection.replaceContent(txt.stripInternal());
14058 			else
14059 				l.selection.replaceContent(txt);
14060 			adjustScrollbarSizes();
14061 			scrollForCaret();
14062 			this.redraw();
14063 		});
14064 	}
14065 
14066 	void deleteContentOfSelection() {
14067 		if(readonly) return;
14068 		doStateCheckpoint();
14069 		l.selection.replaceContent("");
14070 		l.selection.setUserXCoordinate();
14071 		adjustScrollbarSizes();
14072 		scrollForCaret();
14073 		redraw();
14074 	}
14075 
14076 	void selectAll() {
14077 		with(l.selection) {
14078 			moveToStartOfDocument();
14079 			setAnchor();
14080 			moveToEndOfDocument();
14081 			setFocus();
14082 
14083 			selectionChanged();
14084 		}
14085 		redraw();
14086 	}
14087 
14088 	protected bool stateCheckpoint = true;
14089 
14090 	protected void doStateCheckpoint() {
14091 		if(stateCheckpoint) {
14092 			undoStack ~= l.saveState();
14093 			stateCheckpoint = false;
14094 		}
14095 	}
14096 
14097 	protected void adjustScrollbarSizes() {
14098 		// FIXME: will want a content area helper function instead of doing all these subtractions myself
14099 		auto borderWidth = 2;
14100 		this.smw.setTotalArea(l.width, l.height);
14101 		this.smw.setViewableArea(
14102 			this.width - this.paddingLeft - this.paddingRight - borderWidth * 2,
14103 			this.height - this.paddingTop - this.paddingBottom - borderWidth * 2);
14104 	}
14105 
14106 	protected void scrollForCaret() {
14107 		// writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom);
14108 		smw.scrollIntoView(l.selection.focusBoundingBox());
14109 	}
14110 
14111 	// FIXME: this should be a theme changed event listener instead
14112 	private BaseVisualTheme currentTheme;
14113 	override void recomputeChildLayout() {
14114 		if(currentTheme is null)
14115 			currentTheme = WidgetPainter.visualTheme;
14116 		if(WidgetPainter.visualTheme !is currentTheme) {
14117 			currentTheme = WidgetPainter.visualTheme;
14118 			auto ds = this.l.defaultStyle;
14119 			if(auto ms = cast(MyTextStyle) ds) {
14120 				auto cs = getComputedStyle();
14121 				auto font = cs.font();
14122 				if(font !is null)
14123 					ms.font_ = font;
14124 				else {
14125 					auto osc = new OperatingSystemFont();
14126 					osc.loadDefault;
14127 					ms.font_ = osc;
14128 				}
14129 			}
14130 		}
14131 		super.recomputeChildLayout();
14132 	}
14133 
14134 	private Point adjustForSingleLine(Point p) {
14135 		if(singleLine)
14136 			return Point(p.x, this.height / 2);
14137 		else
14138 			return p;
14139 	}
14140 
14141 	private bool wordWrapEnabled_;
14142 
14143 	this(TextLayouter l, ScrollMessageWidget parent) {
14144 		this.smw = parent;
14145 
14146 		smw.addDefaultWheelListeners(16, 16, 8);
14147 		smw.movementPerButtonClick(16, 16);
14148 
14149 		this.defaultPadding = Rectangle(2, 2, 2, 2);
14150 
14151 		this.l = l;
14152 		super(parent);
14153 
14154 		smw.addEventListener((scope ScrollEvent se) {
14155 			this.redraw();
14156 		});
14157 
14158 		this.addEventListener((scope ResizeEvent re) {
14159 			// FIXME: I should add a method to give this client area width thing
14160 			if(wordWrapEnabled_)
14161 				this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight;
14162 
14163 			adjustScrollbarSizes();
14164 			scrollForCaret();
14165 
14166 			this.redraw();
14167 		});
14168 
14169 	}
14170 
14171 	private {
14172 		bool mouseDown;
14173 		bool mouseActuallyMoved;
14174 
14175 		Point downAt;
14176 
14177 		Timer autoscrollTimer;
14178 		int autoscrollDirection;
14179 		int autoscrollAmount;
14180 
14181 		void autoscroll() {
14182 			switch(autoscrollDirection) {
14183 				case 0: smw.scrollUp(autoscrollAmount); break;
14184 				case 1: smw.scrollDown(autoscrollAmount); break;
14185 				case 2: smw.scrollLeft(autoscrollAmount); break;
14186 				case 3: smw.scrollRight(autoscrollAmount); break;
14187 				default: assert(0);
14188 			}
14189 
14190 			this.redraw();
14191 		}
14192 
14193 		void setAutoscrollTimer(int direction, int amount) {
14194 			if(autoscrollTimer is null) {
14195 				autoscrollTimer = new Timer(1000 / 60, &autoscroll);
14196 			}
14197 
14198 			autoscrollDirection = direction;
14199 			autoscrollAmount = amount;
14200 		}
14201 
14202 		void stopAutoscrollTimer() {
14203 			if(autoscrollTimer !is null) {
14204 				autoscrollTimer.dispose();
14205 				autoscrollTimer = null;
14206 			}
14207 			autoscrollAmount = 0;
14208 			autoscrollDirection = 0;
14209 		}
14210 	}
14211 
14212 	override void defaultEventHandler_mousemove(scope MouseMoveEvent ce) {
14213 		if(mouseDown) {
14214 			auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
14215 
14216 			// FIXME: when scrolling i actually do want a timer.
14217 			// i also want a zone near the sides of the window where i can auto scroll
14218 
14219 			auto scrollMultiplier = scaleWithDpi(16);
14220 			auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster
14221 
14222 			if(!singleLine && movedTo.y < 4) {
14223 				setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor);
14224 			} else
14225 			if(!singleLine && (movedTo.y + 6) > this.height) {
14226 				setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor);
14227 			} else
14228 			if(movedTo.x < 4) {
14229 				setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor);
14230 			} else
14231 			if((movedTo.x + 6) > this.width) {
14232 				setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor);
14233 			} else
14234 				stopAutoscrollTimer();
14235 
14236 			l.selection.moveTo(adjustForSingleLine(smw.position + movedTo));
14237 			l.selection.setFocus();
14238 			mouseActuallyMoved = true;
14239 			this.redraw();
14240 		}
14241 
14242 		super.defaultEventHandler_mousemove(ce);
14243 	}
14244 
14245 	override void defaultEventHandler_mouseup(scope MouseUpEvent ce) {
14246 		// FIXME: assert primary selection
14247 		if(mouseDown && ce.button == MouseButton.left) {
14248 			stateCheckpoint = true;
14249 			//l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop)));
14250 			//l.selection.setFocus();
14251 			mouseDown = false;
14252 			parentWindow.releaseMouseCapture();
14253 			stopAutoscrollTimer();
14254 			this.redraw();
14255 
14256 			if(mouseActuallyMoved)
14257 				selectionChanged();
14258 		}
14259 		//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
14260 
14261 		super.defaultEventHandler_mouseup(ce);
14262 	}
14263 
14264 	static if(UsingSimpledisplayX11)
14265 	override void defaultEventHandler_click(scope ClickEvent ce) {
14266 		if(ce.button == MouseButton.middle) {
14267 			parentWindow.win.getPrimarySelection((txt) {
14268 				doStateCheckpoint();
14269 
14270 				// import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText);
14271 
14272 				if(txt == l.selection.getContentString && preservedPrimaryText.length)
14273 					l.selection.replaceContent(preservedPrimaryText);
14274 				else
14275 					l.selection.replaceContent(txt);
14276 				redraw();
14277 			});
14278 		}
14279 
14280 		super.defaultEventHandler_click(ce);
14281 	}
14282 
14283 	override void defaultEventHandler_dblclick(scope DoubleClickEvent dce) {
14284 		if(dce.button == MouseButton.left) {
14285 			with(l.selection()) {
14286 				// FIXME: for a url or file picker i might wanna use / as a separator intead
14287 				scope dg = delegate const(char)[] (scope return const(char)[] ch) {
14288 					if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r")
14289 						return ch;
14290 					return null;
14291 				};
14292 				find(dg, 1, true).moveToEnd.setAnchor;
14293 				find(dg, 1, false).moveTo.setFocus;
14294 				selectionChanged();
14295 				redraw();
14296 			}
14297 		}
14298 
14299 		super.defaultEventHandler_dblclick(dce);
14300 	}
14301 
14302 	override void defaultEventHandler_mousedown(scope MouseDownEvent ce) {
14303 		if(ce.button == MouseButton.left) {
14304 			downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
14305 			l.selection.moveTo(adjustForSingleLine(smw.position + downAt));
14306 			if(ce.shiftKey)
14307 				l.selection.setFocus();
14308 			else
14309 				l.selection.setAnchor();
14310 			mouseDown = true;
14311 			mouseActuallyMoved = false;
14312 			parentWindow.captureMouse(this);
14313 			this.redraw();
14314 		}
14315 		//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
14316 
14317 		super.defaultEventHandler_mousedown(ce);
14318 	}
14319 
14320 	override void defaultEventHandler_char(scope CharEvent ce) {
14321 		super.defaultEventHandler_char(ce);
14322 
14323 		if(readonly)
14324 			return;
14325 		if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b')
14326 			return; // skip the ctrl+x characters we don't care about as plain text
14327 
14328 		if(singleLine && ce.character == '\n')
14329 			return;
14330 		if(!acceptsTabInput && ce.character == '\t')
14331 			return;
14332 
14333 		doStateCheckpoint();
14334 
14335 		char[4] buffer;
14336 		import arsd.core;
14337 		auto stride = encodeUtf8(buffer, ce.character);
14338 		l.selection.replaceContent(buffer[0 .. stride]);
14339 		l.selection.setUserXCoordinate();
14340 		adjustScrollbarSizes();
14341 		scrollForCaret();
14342 		redraw();
14343 
14344 	}
14345 
14346 	override void defaultEventHandler_keydown(scope KeyDownEvent kde) {
14347 		switch(kde.key) {
14348 			case Key.Up, Key.Down, Key.Left, Key.Right:
14349 			case Key.Home, Key.End:
14350 				stateCheckpoint = true;
14351 				bool setPosition = false;
14352 				switch(kde.key) {
14353 					case Key.Up: l.selection.moveUp(); break;
14354 					case Key.Down: l.selection.moveDown(); break;
14355 					case Key.Left: l.selection.moveLeft(); setPosition = true; break;
14356 					case Key.Right: l.selection.moveRight(); setPosition = true; break;
14357 					case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break;
14358 					case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break;
14359 					default: assert(0);
14360 				}
14361 
14362 				if(kde.shiftKey)
14363 					l.selection.setFocus();
14364 				else
14365 					l.selection.setAnchor();
14366 
14367 				selectionChanged();
14368 
14369 				if(setPosition)
14370 					l.selection.setUserXCoordinate();
14371 				scrollForCaret();
14372 				redraw();
14373 			break;
14374 			case Key.PageUp, Key.PageDown:
14375 				// want to act like the user clicked on the caret again
14376 				// after the scroll operation completed, so it would remain at
14377 				// about the same place on the viewport
14378 				auto oldY = smw.vsb.position;
14379 				smw.defaultKeyboardListener(kde);
14380 				auto newY = smw.vsb.position;
14381 				with(l.selection) {
14382 					auto uc = getUserCoordinate();
14383 					uc.y += newY - oldY;
14384 					moveTo(uc);
14385 
14386 					if(kde.shiftKey)
14387 						setFocus();
14388 					else
14389 						setAnchor();
14390 				}
14391 			break;
14392 			case Key.Delete:
14393 				if(l.selection.isEmpty()) {
14394 					l.selection.setAnchor();
14395 					l.selection.moveRight();
14396 					l.selection.setFocus();
14397 				}
14398 				deleteContentOfSelection();
14399 				adjustScrollbarSizes();
14400 				scrollForCaret();
14401 			break;
14402 			case Key.Insert:
14403 			break;
14404 			case Key.A:
14405 				if(kde.ctrlKey)
14406 					selectAll();
14407 			break;
14408 			case Key.F:
14409 				// find
14410 			break;
14411 			case Key.Z:
14412 				if(kde.ctrlKey)
14413 					undo();
14414 			break;
14415 			case Key.R:
14416 				if(kde.ctrlKey)
14417 					redo();
14418 			break;
14419 			case Key.X:
14420 				if(kde.ctrlKey)
14421 					cut();
14422 			break;
14423 			case Key.C:
14424 				if(kde.ctrlKey)
14425 					copy();
14426 			break;
14427 			case Key.V:
14428 				if(kde.ctrlKey)
14429 					paste();
14430 			break;
14431 			case Key.F1:
14432 				with(l.selection()) {
14433 					moveToStartOfLine();
14434 					setAnchor();
14435 					moveToEndOfLine();
14436 					moveToIncludeAdjacentEndOfLineMarker();
14437 					setFocus();
14438 					replaceContent("");
14439 				}
14440 
14441 				redraw();
14442 			break;
14443 			/*
14444 			case Key.F2:
14445 				l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle(
14446 					//(cast(MyTextStyle) old).font,
14447 					font2,
14448 					Color.red)));
14449 				redraw();
14450 			break;
14451 			*/
14452 			case Key.Tab:
14453 				// we process the char event, so don't want to change focus on it, unless the user overrides that with ctrl
14454 				if(acceptsTabInput && !kde.ctrlKey)
14455 					kde.preventDefault();
14456 			break;
14457 			default:
14458 		}
14459 
14460 		if(!kde.defaultPrevented)
14461 			super.defaultEventHandler_keydown(kde);
14462 	}
14463 
14464 	// we want to delegate all the Widget.Style stuff up to the other class that the user can see
14465 	override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) {
14466 		// this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidget Parent
14467 		if(parent && parent.parent && parent.parent.parent)
14468 			parent.parent.parent.useStyleProperties(dg);
14469 		else
14470 			super.useStyleProperties(dg);
14471 	}
14472 
14473 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; }
14474 	override int maxHeight() {
14475 		if(singleLine)
14476 			return minHeight;
14477 		else
14478 			return super.maxHeight();
14479 	}
14480 
14481 	void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
14482 		painter.setFont(myStyle.font);
14483 		painter.drawText(upperLeft, text);
14484 	}
14485 
14486 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
14487 		//painter.setFont(font);
14488 
14489 		auto cs = getComputedStyle();
14490 		auto defaultColor = cs.foregroundColor;
14491 
14492 		auto old = painter.setClipRectangleForWidget(bounds.upperLeft, bounds.width, bounds.height);
14493 		scope(exit) painter.setClipRectangleForWidget(old.upperLeft, old.width, old.height);
14494 
14495 		l.getDrawableText(delegate bool(txt, style, info, carets...) {
14496 			//writeln("Segment: ", txt);
14497 			assert(style !is null);
14498 
14499 			if(info.selections && info.boundingBox.width > 0) {
14500 				auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode
14501 				painter.fillColor = color;
14502 				painter.outlineColor = color;
14503 				painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size));
14504 				painter.outlineColor = cs.selectionForegroundColor;
14505 				//painter.fillColor = Color.white;
14506 			} else {
14507 				painter.outlineColor = defaultColor;
14508 			}
14509 
14510 			if(this.isFocused)
14511 			foreach(idx, caret; carets) {
14512 				if(idx == 0)
14513 					painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height);
14514 				painter.drawLine(
14515 					caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(),
14516 					bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position()
14517 				);
14518 			}
14519 
14520 			if(txt.stripInternal.length) {
14521 				// defaultColor = myStyle.color; // FIXME: so wrong
14522 				if(auto myStyle = cast(MyTextStyle) style)
14523 					drawTextSegment(myStyle, painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
14524 				else if(auto myStyle = cast(MyImageStyle) style)
14525 					myStyle.draw(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
14526 			}
14527 
14528 			if(info.boundingBox.upperLeft.y - smw.position().y > this.height) {
14529 				return false;
14530 			} else {
14531 				return true;
14532 			}
14533 		}, Rectangle(smw.position(), bounds.size));
14534 
14535 		/+
14536 		int place = 0;
14537 		int y = 75;
14538 		foreach(width; widths) {
14539 			painter.fillColor = Color.red;
14540 			painter.drawRectangle(Point(place, y), Size(width, 75));
14541 			//y += 15;
14542 			place += width;
14543 		}
14544 		+/
14545 
14546 		return bounds;
14547 	}
14548 
14549 	static class MyTextStyle : TextStyle {
14550 		OperatingSystemFont font_;
14551 		this(OperatingSystemFont font, bool passwordMode = false) {
14552 			this.font_ = font;
14553 		}
14554 
14555 		override OperatingSystemFont font() {
14556 			return font_;
14557 		}
14558 
14559 		bool foregroundColorOverridden;
14560 		bool backgroundColorOverridden;
14561 		Color foregroundColor;
14562 		Color backgroundColor; // should this be inline segment or the whole paragraph block?
14563 		bool italic;
14564 		bool bold;
14565 		bool underline;
14566 		bool strikeout;
14567 		bool subscript;
14568 		bool superscript;
14569 	}
14570 
14571 	static class MyImageStyle : TextStyle, MeasurableFont {
14572 		MemoryImage image_;
14573 		Image converted;
14574 		this(MemoryImage image) {
14575 			this.image_ =  image;
14576 			this.converted = Image.fromMemoryImage(image);
14577 		}
14578 
14579 		bool isMonospace() { return false; }
14580 		int averageWidth() { return image_.width; }
14581 		int height() { return image_.height; }
14582 		int ascent() { return image_.height; }
14583 		int descent() { return 0; }
14584 
14585 		int stringWidth(scope const(char)[] s, SimpleWindow window = null) {
14586 			return image_.width;
14587 		}
14588 
14589 		override MeasurableFont font() {
14590 			return this;
14591 		}
14592 
14593 		void draw(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
14594 			painter.drawImage(upperLeft, converted);
14595 		}
14596 	}
14597 }
14598 
14599 /+
14600 class TextWidget : Widget {
14601 	TextLayouter l;
14602 	ScrollMessageWidget smw;
14603 	TextDisplayHelper helper;
14604 	this(TextLayouter l, Widget parent) {
14605 		this.l = l;
14606 		super(parent);
14607 
14608 		smw = new ScrollMessageWidget(this);
14609 		//smw.horizontalScrollBar.hide;
14610 		//smw.verticalScrollBar.hide;
14611 		smw.addDefaultWheelListeners(16, 16, 8);
14612 		smw.movementPerButtonClick(16, 16);
14613 		helper = new TextDisplayHelper(l, smw);
14614 
14615 		// no need to do this here since there's gonna be a resize
14616 		// event immediately before any drawing
14617 		// smw.setTotalArea(l.width, l.height);
14618 		smw.setViewableArea(
14619 			this.width - this.paddingLeft - this.paddingRight,
14620 			this.height - this.paddingTop - this.paddingBottom);
14621 
14622 		/+
14623 		writeln(l.width, "x", l.height);
14624 		+/
14625 	}
14626 }
14627 +/
14628 
14629 
14630 
14631 
14632 /+
14633 	make sure it calls parentWindow.inputProxy.setIMEPopupLocation too
14634 +/
14635 
14636 /++
14637 	Contains the implementation of text editing and shared basic api. You should construct one of the child classes instead, like [TextEdit], [LineEdit], or [PasswordEdit].
14638 +/
14639 abstract class EditableTextWidget : Widget {
14640 	protected this(Widget parent) {
14641 		version(custom_widgets)
14642 			this(true, parent);
14643 		else
14644 			this(false, parent);
14645 	}
14646 
14647 	private bool useCustomWidget;
14648 
14649 	protected this(bool useCustomWidget, Widget parent) {
14650 		this.useCustomWidget = useCustomWidget;
14651 
14652 		super(parent);
14653 
14654 		if(useCustomWidget)
14655 			setupCustomTextEditing();
14656 	}
14657 
14658 	private bool wordWrapEnabled_;
14659 	/++
14660 		Enables or disables wrapping of long lines on word boundaries.
14661 	+/
14662 	void wordWrapEnabled(bool enabled) {
14663 		if(useCustomWidget) {
14664 			wordWrapEnabled_ = enabled;
14665 			if(tdh)
14666 				tdh.wordWrapEnabled_ = true;
14667 			textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME
14668 		} else version(win32_widgets) {
14669 			SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0);
14670 		}
14671 	}
14672 
14673 	override int minWidth() { return scaleWithDpi(16); }
14674 	override int widthStretchiness() { return 7; }
14675 	override int widthShrinkiness() { return 1; }
14676 
14677 	override int maxHeight() {
14678 		if(useCustomWidget)
14679 			return tdh.maxHeight;
14680 		else
14681 			return super.maxHeight();
14682 	}
14683 
14684 	override void focus() {
14685 		if(useCustomWidget && tdh)
14686 			tdh.focus();
14687 		else
14688 			super.focus();
14689 	}
14690 
14691 	override void defaultEventHandler_focusout(FocusOutEvent foe) {
14692 		if(tdh !is null && foe.target is tdh)
14693 			tdh.redraw();
14694 	}
14695 
14696 	override void defaultEventHandler_focusin(FocusInEvent foe) {
14697 		if(tdh !is null && foe.target is tdh)
14698 			tdh.redraw();
14699 	}
14700 
14701 
14702 	/++
14703 		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.
14704 	+/
14705 	void selectAll() {
14706 		if(useCustomWidget) {
14707 			tdh.selectAll();
14708 		} else version(win32_widgets) {
14709 			SendMessage(hwnd, EM_SETSEL, 0, -1);
14710 		}
14711 	}
14712 
14713 	/++
14714 		Basic clipboard operations.
14715 
14716 		History:
14717 			Added December 31, 2024
14718 	+/
14719 	void copy() {
14720 		if(useCustomWidget) {
14721 			tdh.copy();
14722 		} else version(win32_widgets) {
14723 			SendMessage(hwnd, WM_COPY, 0, 0);
14724 		}
14725 	}
14726 
14727 	/// ditto
14728 	void cut() {
14729 		if(useCustomWidget) {
14730 			tdh.cut();
14731 		} else version(win32_widgets) {
14732 			SendMessage(hwnd, WM_CUT, 0, 0);
14733 		}
14734 	}
14735 
14736 	/// ditto
14737 	void paste() {
14738 		if(useCustomWidget) {
14739 			tdh.paste();
14740 		} else version(win32_widgets) {
14741 			SendMessage(hwnd, WM_PASTE, 0, 0);
14742 		}
14743 	}
14744 
14745 	///
14746 	void undo() {
14747 		if(useCustomWidget) {
14748 			tdh.undo();
14749 		} else version(win32_widgets) {
14750 			SendMessage(hwnd, EM_UNDO, 0, 0);
14751 		}
14752 	}
14753 
14754 	// note that WM_CLEAR deletes the selection without copying it to the clipboard
14755 	// also windows supports margins, modified flag, and much more
14756 
14757 	// EM_UNDO and EM_CANUNDO. EM_REDO is only supported in rich text boxes here
14758 
14759 	// EM_GETSEL, EM_REPLACESEL, and EM_SETSEL might be usable for find etc.
14760 
14761 
14762 
14763 	/*protected*/ TextDisplayHelper tdh;
14764 	/*protected*/ TextLayouter textLayout;
14765 
14766 	/++
14767 		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.
14768 	+/
14769 	@property string content() {
14770 		if(useCustomWidget) {
14771 			return textLayout.getTextString();
14772 		} else version(win32_widgets) {
14773 			wchar[4096] bufferstack;
14774 			wchar[] buffer;
14775 			auto len = GetWindowTextLength(hwnd);
14776 			if(len < bufferstack.length)
14777 				buffer = bufferstack[0 .. len + 1];
14778 			else
14779 				buffer = new wchar[](len + 1);
14780 
14781 			auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length);
14782 			if(l >= 0)
14783 				return makeUtf8StringFromWindowsString(buffer[0 .. l]);
14784 			else
14785 				return null;
14786 		}
14787 
14788 		assert(0);
14789 	}
14790 	/// ditto
14791 	@property void content(string s) {
14792 		if(useCustomWidget) {
14793 			with(textLayout.selection) {
14794 				moveToStartOfDocument();
14795 				setAnchor();
14796 				moveToEndOfDocument();
14797 				setFocus();
14798 				replaceContent(s);
14799 			}
14800 
14801 			tdh.adjustScrollbarSizes();
14802 			// these don't seem to help
14803 			// tdh.smw.setPosition(0, 0);
14804 			// tdh.scrollForCaret();
14805 
14806 			redraw();
14807 		} else version(win32_widgets) {
14808 			WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines);
14809 			SetWindowTextW(hwnd, bfr.ptr);
14810 		}
14811 	}
14812 
14813 	/++
14814 		Appends some text to the widget at the end, without affecting the user selection or cursor position.
14815 	+/
14816 	void addText(string txt) {
14817 		if(useCustomWidget) {
14818 			textLayout.appendText(txt);
14819 			tdh.adjustScrollbarSizes();
14820 			redraw();
14821 		} else version(win32_widgets) {
14822 			// get the current selection
14823 			DWORD StartPos, EndPos;
14824 			SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) );
14825 
14826 			// move the caret to the end of the text
14827 			int outLength = GetWindowTextLengthW(hwnd);
14828 			SendMessageW( hwnd, EM_SETSEL, outLength, outLength );
14829 
14830 			// insert the text at the new caret position
14831 			WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines);
14832 			SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr );
14833 
14834 			// restore the previous selection
14835 			SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos );
14836 		}
14837 	}
14838 
14839 	// EM_SCROLLCARET scrolls the caret into view
14840 
14841 	void scrollToBottom() {
14842 		if(useCustomWidget) {
14843 			tdh.smw.scrollDown(int.max);
14844 		} else version(win32_widgets) {
14845 			SendMessageW( hwnd, EM_LINESCROLL, 0, int.max );
14846 		}
14847 	}
14848 
14849 	protected TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
14850 		return new TextDisplayHelper(textLayout, smw);
14851 	}
14852 
14853 	protected TextStyle defaultTextStyle() {
14854 		return new TextDisplayHelper.MyTextStyle(getUsedFont());
14855 	}
14856 
14857 	private OperatingSystemFont getUsedFont() {
14858 		auto cs = getComputedStyle();
14859 		auto font = cs.font;
14860 		if(font is null) {
14861 			font = new OperatingSystemFont;
14862 			font.loadDefault();
14863 		}
14864 		return font;
14865 	}
14866 
14867 	protected void setupCustomTextEditing() {
14868 		textLayout = new TextLayouter(defaultTextStyle());
14869 
14870 		auto smw = new ScrollMessageWidget(this);
14871 		if(!showingHorizontalScroll)
14872 			smw.horizontalScrollBar.hide();
14873 		if(!showingVerticalScroll)
14874 			smw.verticalScrollBar.hide();
14875 		this.tabStop = false;
14876 		smw.tabStop = false;
14877 		tdh = textDisplayHelperFactory(textLayout, smw);
14878 	}
14879 
14880 	override void newParentWindow(Window old, Window n) {
14881 		if(n is null) return;
14882 		this.parentWindow.addEventListener((scope DpiChangedEvent dce) {
14883 			if(textLayout) {
14884 				if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) {
14885 					// the dpi change can change the font, so this informs the layouter that it has changed too
14886 					style.font_ = getUsedFont();
14887 
14888 					// arsd.core.writeln(this.parentWindow.win.actualDpi);
14889 				}
14890 			}
14891 		});
14892 	}
14893 
14894 	static class Style : Widget.Style {
14895 		override WidgetBackground background() {
14896 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
14897 		}
14898 
14899 		override Color foregroundColor() {
14900 			return WidgetPainter.visualTheme.foregroundColor;
14901 		}
14902 
14903 		override FrameStyle borderStyle() {
14904 			return FrameStyle.sunk;
14905 		}
14906 
14907 		override MouseCursor cursor() {
14908 			return GenericCursor.Text;
14909 		}
14910 	}
14911 	mixin OverrideStyle!Style;
14912 
14913 	version(win32_widgets) {
14914 		private string lastContentBlur;
14915 
14916 		override void defaultEventHandler_blur(BlurEvent ev) {
14917 			super.defaultEventHandler_blur(ev);
14918 
14919 			if(!useCustomWidget)
14920 			if(this.content != lastContentBlur) {
14921 				auto evt = new ChangeEvent!string(this, &this.content);
14922 				evt.dispatch();
14923 				lastContentBlur = this.content;
14924 			}
14925 		}
14926 	}
14927 
14928 
14929 	bool showingVerticalScroll() { return true; }
14930 	bool showingHorizontalScroll() { return true; }
14931 }
14932 
14933 /++
14934 	A `LineEdit` is an editor of a single line of text, comparable to a HTML `<input type="text" />`.
14935 
14936 	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.
14937 
14938 	See_Also:
14939 		[PasswordEdit] for a `LineEdit` that obscures its input.
14940 
14941 		[TextEdit] for a multi-line plain text editor widget.
14942 
14943 		[TextLabel] for a single line piece of static text.
14944 
14945 		[TextDisplay] for a read-only display of a larger piece of plain text.
14946 +/
14947 class LineEdit : EditableTextWidget {
14948 	override bool showingVerticalScroll() { return false; }
14949 	override bool showingHorizontalScroll() { return false; }
14950 
14951 	override int flexBasisWidth() { return 250; }
14952 	override int widthShrinkiness() { return 10; }
14953 
14954 	///
14955 	this(Widget parent) {
14956 		super(parent);
14957 		version(win32_widgets) {
14958 			createWin32Window(this, "edit"w, "",
14959 				0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
14960 		} else version(custom_widgets) {
14961 		} else static assert(false);
14962 	}
14963 
14964 	private this(bool useCustomWidget, Widget parent) {
14965 		if(!useCustomWidget)
14966 			this(parent);
14967 		else
14968 			super(true, parent);
14969 	}
14970 
14971 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
14972 		auto tdh = new TextDisplayHelper(textLayout, smw);
14973 		tdh.singleLine = true;
14974 		return tdh;
14975 	}
14976 
14977 	version(win32_widgets) {
14978 		mixin Padding!q{0};
14979 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
14980 		override int maxHeight() { return minHeight; }
14981 	}
14982 
14983 	/+
14984 	@property void passwordMode(bool p) {
14985 		SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD);
14986 	}
14987 	+/
14988 }
14989 
14990 /// ditto
14991 class CustomLineEdit : LineEdit {
14992 	this(Widget parent) {
14993 		super(true, parent);
14994 	}
14995 }
14996 
14997 /++
14998 	A [LineEdit] that displays `*` in place of the actual characters.
14999 
15000 	Alas, Windows requires the window to be created differently to use this style,
15001 	so it had to be a new class instead of a toggle on and off on an existing object.
15002 
15003 	History:
15004 		Added January 24, 2021
15005 
15006 		Implemented on Linux on January 31, 2023.
15007 +/
15008 class PasswordEdit : EditableTextWidget {
15009 	override bool showingVerticalScroll() { return false; }
15010 	override bool showingHorizontalScroll() { return false; }
15011 
15012 	override int flexBasisWidth() { return 250; }
15013 
15014 	override TextStyle defaultTextStyle() {
15015 		auto cs = getComputedStyle();
15016 
15017 		auto osf = new class OperatingSystemFont {
15018 			this() {
15019 				super(cs.font);
15020 			}
15021 			override int stringWidth(scope const(char)[] text, SimpleWindow window = null) {
15022 				int count = 0;
15023 				foreach(dchar ch; text)
15024 					count++;
15025 				return count * super.stringWidth("*", window);
15026 			}
15027 		};
15028 
15029 		return new TextDisplayHelper.MyTextStyle(osf);
15030 	}
15031 
15032 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
15033 		static class TDH : TextDisplayHelper {
15034 			this(TextLayouter textLayout, ScrollMessageWidget smw) {
15035 				singleLine = true;
15036 				super(textLayout, smw);
15037 			}
15038 
15039 			override void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
15040 				char[256] buffer = void;
15041 				int bufferLength = 0;
15042 				foreach(dchar ch; text)
15043 					buffer[bufferLength++] = '*';
15044 				painter.setFont(myStyle.font);
15045 				painter.drawText(upperLeft, buffer[0..bufferLength]);
15046 			}
15047 		}
15048 
15049 		return new TDH(textLayout, smw);
15050 	}
15051 
15052 	///
15053 	this(Widget parent) {
15054 		super(parent);
15055 		version(win32_widgets) {
15056 			createWin32Window(this, "edit"w, "",
15057 				ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
15058 		} else version(custom_widgets) {
15059 		} else static assert(false);
15060 	}
15061 
15062 	private this(bool useCustomWidget, Widget parent) {
15063 		if(!useCustomWidget)
15064 			this(parent);
15065 		else
15066 			super(true, parent);
15067 	}
15068 
15069 	version(win32_widgets) {
15070 		mixin Padding!q{2};
15071 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
15072 		override int maxHeight() { return minHeight; }
15073 	}
15074 }
15075 
15076 /// ditto
15077 class CustomPasswordEdit : PasswordEdit {
15078 	this(Widget parent) {
15079 		super(true, parent);
15080 	}
15081 }
15082 
15083 
15084 /++
15085 	A `TextEdit` is a multi-line plain text editor, comparable to a HTML `<textarea>`.
15086 
15087 	See_Also:
15088 		[TextDisplay] for a read-only text display.
15089 
15090 		[LineEdit] for a single line text editor.
15091 
15092 		[PasswordEdit] for a single line text editor that obscures its input.
15093 +/
15094 class TextEdit : EditableTextWidget {
15095 	///
15096 	this(Widget parent) {
15097 		super(parent);
15098 		version(win32_widgets) {
15099 			createWin32Window(this, "edit"w, "",
15100 				0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE);
15101 		} else version(custom_widgets) {
15102 		} else static assert(false);
15103 	}
15104 
15105 	private this(bool useCustomWidget, Widget parent) {
15106 		if(!useCustomWidget)
15107 			this(parent);
15108 		else
15109 			super(true, parent);
15110 	}
15111 
15112 	override int maxHeight() { return int.max; }
15113 	override int heightStretchiness() { return 7; }
15114 
15115 	override int flexBasisWidth() { return 250; }
15116 	override int flexBasisHeight() { return 25; }
15117 }
15118 
15119 /// ditto
15120 class CustomTextEdit : TextEdit {
15121 	this(Widget parent) {
15122 		super(true, parent);
15123 	}
15124 }
15125 
15126 /+
15127 /++
15128 
15129 +/
15130 version(none)
15131 class RichTextDisplay : Widget {
15132 	@property void content(string c) {}
15133 	void appendContent(string c) {}
15134 }
15135 +/
15136 
15137 /++
15138 	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.
15139 
15140 	History:
15141 		Added October 31, 2023 (dub v11.3)
15142 +/
15143 class TextDisplay : EditableTextWidget {
15144 	this(string text, Widget parent) {
15145 		super(true, parent);
15146 		this.content = text;
15147 	}
15148 
15149 	override int maxHeight() { return int.max; }
15150 	override int minHeight() { return Window.defaultLineHeight; }
15151 	override int heightStretchiness() { return 7; }
15152 	override int heightShrinkiness() { return 2; }
15153 
15154 	override int flexBasisWidth() {
15155 		return scaleWithDpi(250);
15156 	}
15157 	override int flexBasisHeight() {
15158 		if(textLayout is null || this.tdh is null)
15159 			return Window.defaultLineHeight;
15160 
15161 		auto textHeight = borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textLayout.height))).height;
15162 		return this.tdh.borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textHeight))).height;
15163 	}
15164 
15165 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
15166 		return new MyTextDisplayHelper(textLayout, smw);
15167 	}
15168 
15169 	override void registerMovement() {
15170 		super.registerMovement();
15171 		this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally
15172 	}
15173 
15174 	static class MyTextDisplayHelper : TextDisplayHelper {
15175 		this(TextLayouter textLayout, ScrollMessageWidget smw) {
15176 			smw.verticalScrollBar.hide();
15177 			smw.horizontalScrollBar.hide();
15178 			super(textLayout, smw);
15179 			this.readonly = true;
15180 		}
15181 
15182 		override void registerMovement() {
15183 			super.registerMovement();
15184 
15185 			// FIXME: do the horizontal one too as needed and make sure that it does
15186 			// wordwrapping again
15187 			if(l.height + smw.horizontalScrollBar.height > this.height)
15188 				smw.verticalScrollBar.show();
15189 			else
15190 				smw.verticalScrollBar.hide();
15191 
15192 			l.wordWrapWidth = this.width;
15193 
15194 			smw.verticalScrollBar.setPosition = 0;
15195 		}
15196 	}
15197 
15198 	class Style : Widget.Style {
15199 		// just want the generic look for these
15200 	}
15201 
15202 	mixin OverrideStyle!Style;
15203 }
15204 
15205 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active
15206 /++
15207 	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.
15208 
15209 
15210 	When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`.
15211 
15212 	Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand.
15213 
15214 	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.
15215 
15216 	History:
15217 		Added August 12, 2024 (dub v11.6)
15218 +/
15219 abstract class GenericListViewWidget : Widget {
15220 	/++
15221 
15222 	+/
15223 	this(Widget parent) {
15224 		super(parent);
15225 
15226 		smw = new ScrollMessageWidget(this);
15227 		smw.addDefaultKeyboardListeners(itemSize.height, itemSize.width);
15228 		smw.addDefaultWheelListeners(itemSize.height, itemSize.width);
15229 		smw.hsb.hide(); // FIXME: this might actually be useful but we can't really communicate that yet
15230 
15231 		inner = new GenericListViewWidgetInner(this, smw, new GenericListViewInnerContainer(smw));
15232 		inner.tabStop = this.tabStop;
15233 		this.tabStop = false;
15234 	}
15235 
15236 	private ScrollMessageWidget smw;
15237 	private GenericListViewWidgetInner inner;
15238 
15239 	/++
15240 
15241 	+/
15242 	abstract GenericListViewItem itemFactory(Widget parent);
15243 	// in device-dependent pixels
15244 	/++
15245 
15246 	+/
15247 	abstract Size itemSize(); // use 0 to indicate it can stretch?
15248 
15249 	enum LayoutMode {
15250 		rows,
15251 		columns,
15252 		gridRowsFirst,
15253 		gridColumnsFirst
15254 	}
15255 	LayoutMode layoutMode() {
15256 		return LayoutMode.rows;
15257 	}
15258 
15259 	private int itemCount_;
15260 
15261 	/++
15262 		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.
15263 	+/
15264 	void setItemCount(int count) {
15265 		smw.setTotalArea(inner.width, count * itemSize().height);
15266 		smw.setViewableArea(inner.width, inner.height);
15267 		this.itemCount_ = count;
15268 	}
15269 
15270 	/++
15271 		Returns the current count of items expected to available in the list.
15272 	+/
15273 	int itemCount() {
15274 		return this.itemCount_;
15275 	}
15276 
15277 	/++
15278 		Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data.
15279 
15280 		Note you must $(I also) call [setItemCount] if the total item count has changed.
15281 	+/
15282 	void notifyItemsChanged(int index, int count = 1) {
15283 	}
15284 	/// ditto
15285 	void notifyItemsInserted(int index, int count = 1) {
15286 	}
15287 	/// ditto
15288 	void notifyItemsRemoved(int index, int count = 1) {
15289 	}
15290 	/// ditto
15291 	void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) {
15292 	}
15293 
15294 	/++
15295 		History:
15296 			Added January 1, 2025
15297 	+/
15298 	void ensureItemVisibleInScroll(int index) {
15299 		auto itemPos = index * itemSize().height;
15300 		auto vsb = smw.verticalScrollBar;
15301 		auto viewable = vsb.viewableArea_;
15302 
15303 		if(viewable == 0) {
15304 			// viewable == 0 isn't actually supposed to happen, this means
15305 			// this method is being called before having our size assigned, it should
15306 			// probably just queue it up for later.
15307 			queuedScroll = index;
15308 			return;
15309 		}
15310 
15311 		queuedScroll = int.min;
15312 
15313 		if(itemPos < vsb.position) {
15314 			// scroll up to it
15315 			vsb.setPosition(itemPos);
15316 			smw.notify();
15317 		} else if(itemPos + itemSize().height > (vsb.position + viewable)) {
15318 			// scroll down to it, so it is at the bottom
15319 
15320 			auto lastViewableItemPosition = (viewable - itemSize.height) / itemSize.height * itemSize.height;
15321 			// need the itemPos to be at the lastViewableItemPosition after scrolling, so subtraction does it
15322 
15323 			vsb.setPosition(itemPos - lastViewableItemPosition);
15324 			smw.notify();
15325 		}
15326 	}
15327 
15328 	/++
15329 		History:
15330 			Added January 1, 2025;
15331 	+/
15332 	int numberOfCurrentlyFullyVisibleItems() {
15333 		return smw.verticalScrollBar.viewableArea_ / itemSize.height;
15334 	}
15335 
15336 	private int queuedScroll = int.min;
15337 
15338 	override void recomputeChildLayout() {
15339 		super.recomputeChildLayout();
15340 		if(queuedScroll != int.min)
15341 			ensureItemVisibleInScroll(queuedScroll);
15342 	}
15343 
15344 	private GenericListViewItem[] items;
15345 
15346 	override void paint(WidgetPainter painter) {}
15347 }
15348 
15349 /// ditto
15350 abstract class GenericListViewItem : Widget {
15351 	/++
15352 	+/
15353 	this(Widget parent) {
15354 		super(parent);
15355 	}
15356 
15357 	private int _currentIndex = -1;
15358 
15359 	private void showItemPrivate(int idx) {
15360 		showItem(idx);
15361 		_currentIndex = idx;
15362 	}
15363 
15364 	/++
15365 		Implement this to show an item from your data backing to the list.
15366 
15367 		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.
15368 	+/
15369 	abstract void showItem(int idx);
15370 
15371 	/++
15372 		Maintained by the library after calling [showItem] so the object knows which data index it currently has.
15373 
15374 		It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent).
15375 
15376 		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.
15377 	+/
15378 	final int currentIndexLoaded() {
15379 		return _currentIndex;
15380 	}
15381 }
15382 
15383 ///
15384 unittest {
15385 	import arsd.minigui;
15386 
15387 	import std.conv;
15388 
15389 	void main() {
15390 		auto mw = new MainWindow();
15391 
15392 		static class MyListViewItem : GenericListViewItem {
15393 			this(Widget parent) {
15394 				super(parent);
15395 
15396 				label = new TextLabel("unloaded", TextAlignment.Left, this);
15397 				button = new Button("Click", this);
15398 
15399 				button.addEventListener("triggered", (){
15400 					messageBox(text("clicked ", currentIndexLoaded()));
15401 				});
15402 			}
15403 			override void showItem(int idx) {
15404 				label.label = "Item " ~ to!string(idx);
15405 			}
15406 
15407 			TextLabel label;
15408 			Button button;
15409 		}
15410 
15411 		auto widget = new class GenericListViewWidget {
15412 			this() {
15413 				super(mw);
15414 			}
15415 			override GenericListViewItem itemFactory(Widget parent) {
15416 				return new MyListViewItem(parent);
15417 			}
15418 			override Size itemSize() {
15419 				return Size(0, scaleWithDpi(80));
15420 			}
15421 		};
15422 
15423 		widget.setItemCount(5000);
15424 
15425 		mw.loop();
15426 	}
15427 }
15428 
15429 // this exists just to wrap the actual GenericListViewWidgetInner so borders
15430 // and padding and stuff can work
15431 private class GenericListViewInnerContainer : Widget {
15432 	this(Widget parent) {
15433 		super(parent);
15434 		this.tabStop = false;
15435 	}
15436 
15437 	override void recomputeChildLayout() {
15438 		registerMovement();
15439 
15440 		auto cs = getComputedStyle();
15441 		auto bw = getBorderWidth(cs.borderStyle);
15442 
15443 		assert(children.length < 2);
15444 		foreach(child; children) {
15445 			child.x = bw + paddingLeft();
15446 			child.y = bw + paddingTop();
15447 			child.width = this.width.NonOverflowingUint - bw - bw - paddingLeft() - paddingRight();
15448 			child.height = this.height.NonOverflowingUint - bw - bw - paddingTop() - paddingBottom();
15449 
15450 			child.recomputeChildLayout();
15451 		}
15452 	}
15453 
15454 	override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) {
15455 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15456 			return parent.parent.parent.useStyleProperties(dg);
15457 		else
15458 			return super.useStyleProperties(dg);
15459 	}
15460 
15461 	override int paddingTop() {
15462 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15463 			return parent.parent.parent.paddingTop();
15464 		else
15465 			return super.paddingTop();
15466 	}
15467 
15468 	override int paddingBottom() {
15469 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15470 			return parent.parent.parent.paddingBottom();
15471 		else
15472 			return super.paddingBottom();
15473 	}
15474 
15475 	override int paddingLeft() {
15476 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15477 			return parent.parent.parent.paddingLeft();
15478 		else
15479 			return super.paddingLeft();
15480 	}
15481 
15482 	override int paddingRight() {
15483 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15484 			return parent.parent.parent.paddingRight();
15485 		else
15486 			return super.paddingRight();
15487 	}
15488 
15489 
15490 }
15491 
15492 private class GenericListViewWidgetInner : Widget {
15493 	this(GenericListViewWidget glvw, ScrollMessageWidget smw, GenericListViewInnerContainer parent) {
15494 		super(parent);
15495 		this.glvw = glvw;
15496 
15497 		reloadVisible();
15498 
15499 		smw.addEventListener("scroll", () {
15500 			reloadVisible();
15501 		});
15502 	}
15503 
15504 	override void registerMovement() {
15505 		super.registerMovement();
15506 		if(glvw && glvw.smw)
15507 			glvw.smw.setViewableArea(this.width, this.height);
15508 	}
15509 
15510 	void reloadVisible() {
15511 		auto y = glvw.smw.position.y / glvw.itemSize.height;
15512 
15513 		// idk why i had this here it doesn't seem to be ueful and actually made last items diasppear
15514 		//int offset = glvw.smw.position.y % glvw.itemSize.height;
15515 		//if(offset || y >= glvw.itemCount())
15516 			//y--;
15517 
15518 		if(y < 0)
15519 			y = 0;
15520 
15521 		recomputeChildLayout();
15522 
15523 		foreach(item; glvw.items) {
15524 			if(y < glvw.itemCount()) {
15525 				item.showItemPrivate(y);
15526 				item.show();
15527 			} else {
15528 				item.hide();
15529 			}
15530 			y++;
15531 		}
15532 
15533 		this.redraw();
15534 	}
15535 
15536 	private GenericListViewWidget glvw;
15537 
15538 	private bool inRcl;
15539 	override void recomputeChildLayout() {
15540 		if(inRcl)
15541 			return;
15542 		inRcl = true;
15543 		scope(exit)
15544 			inRcl = false;
15545 
15546 		registerMovement();
15547 
15548 		auto ih = glvw.itemSize().height;
15549 
15550 		auto itemCount = this.height / ih + 2; // extra for partial display before and after
15551 		bool hadNew;
15552 		while(glvw.items.length < itemCount) {
15553 			// FIXME: free the old items? maybe just set length
15554 			glvw.items ~= glvw.itemFactory(this);
15555 			hadNew = true;
15556 		}
15557 
15558 		if(hadNew)
15559 			reloadVisible();
15560 
15561 		int y = -(glvw.smw.position.y % ih) + this.paddingTop();
15562 		foreach(child; children) {
15563 			child.x = this.paddingLeft();
15564 			child.y = y;
15565 			y += glvw.itemSize().height;
15566 			child.width = this.width.NonOverflowingUint - this.paddingLeft() - this.paddingRight();
15567 			child.height = ih;
15568 
15569 			child.recomputeChildLayout();
15570 		}
15571 	}
15572 }
15573 
15574 
15575 
15576 /++
15577 	History:
15578 		It was a child of Window before, but as of September 29, 2024, it is now a child of `Dialog`.
15579 +/
15580 class MessageBox : Dialog {
15581 	private string message;
15582 	MessageBoxButton buttonPressed = MessageBoxButton.None;
15583 	/++
15584 
15585 		History:
15586 		The overload that takes `Window originator` was added on September 29, 2024.
15587 	+/
15588 	this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
15589 		this(null, message, buttons, buttonIds);
15590 	}
15591 	/// ditto
15592 	this(Window originator, string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
15593 		message = message.stripRightInternal;
15594 		int mainWidth;
15595 
15596 		// estimate longest line
15597 		int count;
15598 		foreach(ch; message) {
15599 			if(ch == '\n') {
15600 				if(count > mainWidth)
15601 					mainWidth = count;
15602 				count = 0;
15603 			} else {
15604 				count++;
15605 			}
15606 		}
15607 		mainWidth *= 8;
15608 		if(mainWidth < 300)
15609 			mainWidth = 300;
15610 		if(mainWidth > 600)
15611 			mainWidth = 600;
15612 
15613 		super(originator, mainWidth, 100);
15614 
15615 		assert(buttons.length);
15616 		assert(buttons.length ==  buttonIds.length);
15617 
15618 		this.message = message;
15619 
15620 		auto label = new TextDisplay(message, this);
15621 
15622 		auto hl = new HorizontalLayout(this);
15623 		auto spacer = new HorizontalSpacer(hl); // to right align
15624 
15625 		foreach(idx, buttonText; buttons) {
15626 			auto button = new CommandButton(buttonText, hl);
15627 
15628 			button.addEventListener(EventType.triggered, ((size_t idx) { return () {
15629 				this.buttonPressed = buttonIds[idx];
15630 				win.close();
15631 			}; })(idx));
15632 
15633 			if(idx == 0)
15634 				button.focus();
15635 		}
15636 
15637 		if(buttons.length == 1)
15638 			auto spacer2 = new HorizontalSpacer(hl); // to center it
15639 
15640 		auto size = label.flexBasisHeight() + hl.minHeight() + this.paddingTop + this.paddingBottom;
15641 		auto max = scaleWithDpi(600); // random max height
15642 		if(size > max)
15643 			size = max;
15644 
15645 		win.resize(scaleWithDpi(mainWidth), size);
15646 
15647 		win.show();
15648 		redraw();
15649 	}
15650 
15651 	override void OK() {
15652 		this.win.close();
15653 	}
15654 
15655 	mixin Padding!q{16};
15656 }
15657 
15658 ///
15659 enum MessageBoxStyle {
15660 	OK, ///
15661 	OKCancel, ///
15662 	RetryCancel, ///
15663 	YesNo, ///
15664 	YesNoCancel, ///
15665 	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.
15666 }
15667 
15668 ///
15669 enum MessageBoxIcon {
15670 	None, ///
15671 	Info, ///
15672 	Warning, ///
15673 	Error ///
15674 }
15675 
15676 /// Identifies the button the user pressed on a message box.
15677 enum MessageBoxButton {
15678 	None, /// The user closed the message box without clicking any of the buttons.
15679 	OK, ///
15680 	Cancel, ///
15681 	Retry, ///
15682 	Yes, ///
15683 	No, ///
15684 	Continue ///
15685 }
15686 
15687 
15688 /++
15689 	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.
15690 
15691 	Returns: the button pressed.
15692 +/
15693 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15694 	return messageBox(null, title, message, style, icon);
15695 }
15696 
15697 /// ditto
15698 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15699 	return messageBox(null, null, message, style, icon);
15700 }
15701 
15702 /++
15703 
15704 +/
15705 MessageBoxButton messageBox(Window originator, string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15706 	version(win32_widgets) {
15707 		WCharzBuffer t = WCharzBuffer(title);
15708 		WCharzBuffer m = WCharzBuffer(message);
15709 		UINT type;
15710 		with(MessageBoxStyle)
15711 		final switch(style) {
15712 			case OK: type |= MB_OK; break;
15713 			case OKCancel: type |= MB_OKCANCEL; break;
15714 			case RetryCancel: type |= MB_RETRYCANCEL; break;
15715 			case YesNo: type |= MB_YESNO; break;
15716 			case YesNoCancel: type |= MB_YESNOCANCEL; break;
15717 			case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break;
15718 		}
15719 		with(MessageBoxIcon)
15720 		final switch(icon) {
15721 			case None: break;
15722 			case Info: type |= MB_ICONINFORMATION; break;
15723 			case Warning: type |= MB_ICONWARNING; break;
15724 			case Error: type |= MB_ICONERROR; break;
15725 		}
15726 		switch(MessageBoxW(originator is null ? null : originator.win.hwnd, m.ptr, t.ptr, type)) {
15727 			case IDOK: return MessageBoxButton.OK;
15728 			case IDCANCEL: return MessageBoxButton.Cancel;
15729 			case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry;
15730 			case IDYES: return MessageBoxButton.Yes;
15731 			case IDNO: return MessageBoxButton.No;
15732 			case IDCONTINUE: return MessageBoxButton.Continue;
15733 			default: return MessageBoxButton.None;
15734 		}
15735 	} else {
15736 		string[] buttons;
15737 		MessageBoxButton[] buttonIds;
15738 		with(MessageBoxStyle)
15739 		final switch(style) {
15740 			case OK:
15741 				buttons = ["OK"];
15742 				buttonIds = [MessageBoxButton.OK];
15743 			break;
15744 			case OKCancel:
15745 				buttons = ["OK", "Cancel"];
15746 				buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel];
15747 			break;
15748 			case RetryCancel:
15749 				buttons = ["Retry", "Cancel"];
15750 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel];
15751 			break;
15752 			case YesNo:
15753 				buttons = ["Yes", "No"];
15754 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No];
15755 			break;
15756 			case YesNoCancel:
15757 				buttons = ["Yes", "No", "Cancel"];
15758 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel];
15759 			break;
15760 			case RetryCancelContinue:
15761 				buttons = ["Try Again", "Cancel", "Continue"];
15762 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue];
15763 			break;
15764 		}
15765 		auto mb = new MessageBox(originator, message, buttons, buttonIds);
15766 		EventLoop el = EventLoop.get;
15767 		el.run(() { return !mb.win.closed; });
15768 		return mb.buttonPressed;
15769 	}
15770 
15771 }
15772 
15773 /// ditto
15774 int messageBox(Window originator, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15775 	return messageBox(originator, null, message, style, icon);
15776 }
15777 
15778 
15779 ///
15780 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler;
15781 
15782 /++
15783 	This is an opaque type you can use to disconnect an event handler when you're no longer interested.
15784 
15785 	History:
15786 		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.
15787 +/
15788 struct EventListener {
15789 	private Widget widget;
15790 	private string event;
15791 	private EventHandler handler;
15792 	private bool useCapture;
15793 
15794 	///
15795 	void disconnect() {
15796 		if(widget !is null && handler !is null)
15797 			widget.removeEventListener(this);
15798 	}
15799 }
15800 
15801 /++
15802 	The purpose of this enum was to give a compile-time checked version of various standard event strings.
15803 
15804 	Now, I recommend you use a statically typed event object instead.
15805 
15806 	See_Also: [Event]
15807 +/
15808 enum EventType : string {
15809 	click = "click", ///
15810 
15811 	mouseenter = "mouseenter", ///
15812 	mouseleave = "mouseleave", ///
15813 	mousein = "mousein", ///
15814 	mouseout = "mouseout", ///
15815 	mouseup = "mouseup", ///
15816 	mousedown = "mousedown", ///
15817 	mousemove = "mousemove", ///
15818 
15819 	keydown = "keydown", ///
15820 	keyup = "keyup", ///
15821 	char_ = "char", ///
15822 
15823 	focus = "focus", ///
15824 	blur = "blur", ///
15825 
15826 	triggered = "triggered", ///
15827 
15828 	change = "change", ///
15829 }
15830 
15831 /++
15832 	Represents an event that is currently being processed.
15833 
15834 
15835 	Minigui's event model is based on the web browser. An event has a name, a target,
15836 	and an associated data object. It starts from the window and works its way down through
15837 	the target through all intermediate [Widget]s, triggering capture phase handlers as it goes,
15838 	then goes back up again all the way back to the window, triggering bubble phase handlers. At
15839 	the end, if [Event.preventDefault] has not been called, it calls the target widget's default
15840 	handlers for the event (please note that default handlers will be called even if [Event.stopPropagation]
15841 	was called; that just stops it from calling other handlers in the widget tree, but the default happens
15842 	whenever propagation is done, not only if it gets to the end of the chain).
15843 
15844 	This model has several nice points:
15845 
15846 	$(LIST
15847 		* It is easy to delegate dynamic handlers to a parent. You can have a parent container
15848 		  with event handlers set, then add/remove children as much as you want without needing
15849 		  to manage the event handlers on them - the parent alone can manage everything.
15850 
15851 		* It is easy to create new custom events in your application.
15852 
15853 		* It is familiar to many web developers.
15854 	)
15855 
15856 	There's a few downsides though:
15857 
15858 	$(LIST
15859 		* There's not a lot of type safety.
15860 
15861 		* You don't get a static list of what events a widget can emit.
15862 
15863 		* Tracing where an event got cancelled along the chain can get difficult; the downside of
15864 		  the central delegation benefit is it can be lead to debugging of action at a distance.
15865 	)
15866 
15867 	In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript
15868 	while keeping the benefits - and most compatibility with - the existing model. The main idea is
15869 	to simply use a D object type which provides a static interface as well as a built-in event name.
15870 	Then, a new static interface allows you to see what an event can emit and attach handlers to it
15871 	similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still
15872 	delegate to a parent and use custom events as well as using the runtime dynamic access, in addition
15873 	to having a little more help from the D compiler and documentation generator.
15874 
15875 	Your code would change like this:
15876 
15877 	---
15878 	// old
15879 	widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture );
15880 
15881 	// new
15882 	widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture );
15883 	---
15884 
15885 	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.
15886 
15887 	All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class.
15888 
15889 	Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you!
15890 
15891 	Thus the family of functions are:
15892 
15893 	[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.
15894 
15895 	[Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate.
15896 
15897 	[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.
15898 
15899 	Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property:
15900 
15901 	---
15902 	class MyCheckbox : Widget {
15903 		/// This gives a chance to document it and generates a convenience function to send it and attach handlers.
15904 		/// It is NOT actually required but should be used whenever possible.
15905 		mixin Emits!(ChangeEvent!bool);
15906 
15907 		this(Widget parent) {
15908 			super(parent);
15909 			setDefaultEventHandler((ClickEvent) { checked = !checked; });
15910 		}
15911 
15912 		private bool _checked;
15913 		@property bool checked() { return _checked; }
15914 		@property void checked(bool set) {
15915 			_checked = set;
15916 			emit!(ChangeEvent!bool)(&checked);
15917 		}
15918 	}
15919 	---
15920 
15921 	## Creating Your Own Events
15922 
15923 	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.
15924 
15925 	---
15926 	final class MyEvent : Event {
15927 		this(Widget target) { super(EventString, target); }
15928 		mixin Register; // adds EventString and other reflection information
15929 	}
15930 	---
15931 
15932 	Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it.
15933 
15934 	History:
15935 		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.
15936 
15937 		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.
15938 +/
15939 /+
15940 
15941 	## General Conventions
15942 
15943 	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.
15944 
15945 
15946 	## Qt-style signals and slots
15947 
15948 	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.
15949 
15950 	The intention is for events to be used when
15951 
15952 	---
15953 	class Demo : Widget {
15954 		this() {
15955 			myPropertyChanged = Signal!int(this);
15956 		}
15957 		@property myProperty(int v) {
15958 			myPropertyChanged.emit(v);
15959 		}
15960 
15961 		Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name...
15962 		// but it can just genuinely not care about `this` since that's not really passed.
15963 	}
15964 
15965 	class Foo : Widget {
15966 		// the slot uda is not necessary, but it helps the script and ui builder find it.
15967 		@slot void setValue(int v) { ... }
15968 	}
15969 
15970 	demo.myPropertyChanged.connect(&foo.setValue);
15971 	---
15972 
15973 	The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor.
15974 
15975 	Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically.
15976 
15977 	class StringChangeEvent : ChangeEvent, Signal!string {
15978 		mixin SignalImpl
15979 	}
15980 
15981 +/
15982 class Event : ReflectableProperties {
15983 	/// Creates an event without populating any members and without sending it. See [dispatch]
15984 	this(string eventName, Widget emittedBy) {
15985 		this.eventName = eventName;
15986 		this.srcElement = emittedBy;
15987 	}
15988 
15989 
15990 	/// Implementations for the [ReflectableProperties] interface/
15991 	void getPropertiesList(scope void delegate(string name) sink) const {}
15992 	/// ditto
15993 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { }
15994 	/// ditto
15995 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) {
15996 		return SetPropertyResult.notPermitted;
15997 	}
15998 
15999 
16000 	/+
16001 	/++
16002 		This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice.
16003 
16004 		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.
16005 	+/
16006 	protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) {
16007 		if(value.length == 0) {
16008 			finalSink(memberName, `""`);
16009 			return;
16010 		}
16011 
16012 		char[1024] bufferBacking;
16013 		char[] buffer = bufferBacking;
16014 		int bufferPosition;
16015 
16016 		void sink(char ch) {
16017 			if(bufferPosition >= buffer.length)
16018 				buffer.length = buffer.length + 1024;
16019 			buffer[bufferPosition++] = ch;
16020 		}
16021 
16022 		sink('"');
16023 
16024 		foreach(ch; value) {
16025 			switch(ch) {
16026 				case '\\':
16027 					sink('\\'); sink('\\');
16028 				break;
16029 				case '"':
16030 					sink('\\'); sink('"');
16031 				break;
16032 				case '\n':
16033 					sink('\\'); sink('n');
16034 				break;
16035 				case '\r':
16036 					sink('\\'); sink('r');
16037 				break;
16038 				case '\t':
16039 					sink('\\'); sink('t');
16040 				break;
16041 				default:
16042 					sink(ch);
16043 			}
16044 		}
16045 
16046 		sink('"');
16047 
16048 		finalSink(memberName, buffer[0 .. bufferPosition]);
16049 	}
16050 	+/
16051 
16052 	/+
16053 	enum EventInitiator {
16054 		system,
16055 		minigui,
16056 		user
16057 	}
16058 
16059 	immutable EventInitiator; initiatedBy;
16060 	+/
16061 
16062 	/++
16063 		Events should generally follow the propagation model, but there's some exceptions
16064 		to that rule. If so, they should override this to return false. In that case, only
16065 		bubbling event handlers on the target itself and capturing event handlers on the containing
16066 		window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal
16067 		capture -> target -> bubble process.)
16068 
16069 		History:
16070 			Added May 12, 2021
16071 	+/
16072 	bool propagates() const pure nothrow @nogc @safe {
16073 		return true;
16074 	}
16075 
16076 	/++
16077 		hints as to whether preventDefault will actually do anything. not entirely reliable.
16078 
16079 		History:
16080 			Added May 14, 2021
16081 	+/
16082 	bool cancelable() const pure nothrow @nogc @safe {
16083 		return true;
16084 	}
16085 
16086 	/++
16087 		You can mix this into child class to register some boilerplate. It includes the `EventString`
16088 		member, a constructor, and implementations of the dynamic get data interfaces.
16089 
16090 		If you fail to do this, your event will probably not have full compatibility but it might still work for you.
16091 
16092 
16093 		You can override the default EventString by simply providing your own in the form of
16094 		`enum string EventString = "some.name";` The default is the name of your class and its parent entity
16095 		which provides some namespace protection against conflicts in other libraries while still being fairly
16096 		easy to use.
16097 
16098 		If you provide your own constructor, it will override the default constructor provided here. A constructor
16099 		must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the
16100 		first argument to your constructor.
16101 
16102 		History:
16103 			Added May 13, 2021.
16104 	+/
16105 	protected static mixin template Register() {
16106 		public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~  __traits(identifier, typeof(this));
16107 		this(Widget target) { super(EventString, target); }
16108 
16109 		mixin ReflectableProperties.RegisterGetters;
16110 	}
16111 
16112 	/++
16113 		This is the widget that emitted the event.
16114 
16115 
16116 		The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms.
16117 
16118 		History:
16119 			The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms,
16120 			but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web
16121 			so I don't intend to remove these aliases.
16122 	+/
16123 	Widget source;
16124 	/// ditto
16125 	alias source target;
16126 	/// ditto
16127 	alias source srcElement;
16128 
16129 	Widget relatedTarget; /// Note: likely to be deprecated at some point.
16130 
16131 	/// Prevents the default event handler (if there is one) from being called
16132 	void preventDefault() {
16133 		lastDefaultPrevented = true;
16134 		defaultPrevented = true;
16135 	}
16136 
16137 	/// Stops the event propagation immediately.
16138 	void stopPropagation() {
16139 		propagationStopped = true;
16140 	}
16141 
16142 	private bool defaultPrevented;
16143 	private bool propagationStopped;
16144 	private string eventName;
16145 
16146 	private bool isBubbling;
16147 
16148 	/// 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.
16149 	protected void adjustClientCoordinates(int deltaX, int deltaY) { }
16150 
16151 	/++
16152 		this sends it only to the target. If you want propagation, use dispatch() instead.
16153 
16154 		This should be made private!!!
16155 
16156 	+/
16157 	void sendDirectly() {
16158 		if(srcElement is null)
16159 			return;
16160 
16161 		// 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.
16162 
16163 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
16164 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement);
16165 
16166 		if(auto e = target.parentWindow) {
16167 			if(auto handlers = "*" in e.capturingEventHandlers)
16168 			foreach(handler; *handlers)
16169 				if(handler) handler(e, this);
16170 			if(auto handlers = eventName in e.capturingEventHandlers)
16171 			foreach(handler; *handlers)
16172 				if(handler) handler(e, this);
16173 		}
16174 
16175 		auto e = srcElement;
16176 
16177 		if(auto handlers = eventName in e.bubblingEventHandlers)
16178 		foreach(handler; *handlers)
16179 			if(handler) handler(e, this);
16180 
16181 		if(auto handlers = "*" in e.bubblingEventHandlers)
16182 		foreach(handler; *handlers)
16183 			if(handler) handler(e, this);
16184 
16185 		// there's never a default for a catch-all event
16186 		if(!defaultPrevented)
16187 			if(eventName in e.defaultEventHandlers)
16188 				e.defaultEventHandlers[eventName](e, this);
16189 	}
16190 
16191 	/// this dispatches the element using the capture -> target -> bubble process
16192 	void dispatch() {
16193 		if(srcElement is null)
16194 			return;
16195 
16196 		if(!propagates) {
16197 			sendDirectly;
16198 			return;
16199 		}
16200 
16201 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
16202 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement);
16203 
16204 		// first capture, then bubble
16205 
16206 		Widget[] chain;
16207 		Widget curr = srcElement;
16208 		while(curr) {
16209 			auto l = curr;
16210 			chain ~= l;
16211 			curr = curr.parent;
16212 		}
16213 
16214 		isBubbling = false;
16215 
16216 		foreach_reverse(e; chain) {
16217 			if(auto handlers = "*" in e.capturingEventHandlers)
16218 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
16219 
16220 			if(propagationStopped)
16221 				break;
16222 
16223 			if(auto handlers = eventName in e.capturingEventHandlers)
16224 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
16225 
16226 			// the default on capture should really be to always do nothing
16227 
16228 			//if(!defaultPrevented)
16229 			//	if(eventName in e.defaultEventHandlers)
16230 			//		e.defaultEventHandlers[eventName](e.element, this);
16231 
16232 			if(propagationStopped)
16233 				break;
16234 		}
16235 
16236 		int adjustX;
16237 		int adjustY;
16238 
16239 		isBubbling = true;
16240 		if(!propagationStopped)
16241 		foreach(e; chain) {
16242 			if(auto handlers = eventName in e.bubblingEventHandlers)
16243 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
16244 
16245 			if(propagationStopped)
16246 				break;
16247 
16248 			if(auto handlers = "*" in e.bubblingEventHandlers)
16249 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
16250 
16251 			if(propagationStopped)
16252 				break;
16253 
16254 			if(e.encapsulatedChildren()) {
16255 				adjustClientCoordinates(adjustX, adjustY);
16256 				target = e;
16257 			} else {
16258 				adjustX += e.x;
16259 				adjustY += e.y;
16260 			}
16261 		}
16262 
16263 		if(!defaultPrevented)
16264 		foreach(e; chain) {
16265 			if(eventName in e.defaultEventHandlers)
16266 				e.defaultEventHandlers[eventName](e, this);
16267 		}
16268 	}
16269 
16270 
16271 	/* old compatibility things */
16272 	deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!")
16273 	final @property {
16274 		Key key() { return (cast(KeyEventBase) this).key; }
16275 		KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; }
16276 
16277 		bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; }
16278 		bool altKey() { return (cast(KeyEventBase) this).altKey; }
16279 		bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; }
16280 	}
16281 
16282 	deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!")
16283 	final @property {
16284 		int clientX() { return (cast(MouseEventBase) this).clientX; }
16285 		int clientY() { return (cast(MouseEventBase) this).clientY; }
16286 
16287 		int viewportX() { return (cast(MouseEventBase) this).viewportX; }
16288 		int viewportY() { return (cast(MouseEventBase) this).viewportY; }
16289 
16290 		int button() { return (cast(MouseEventBase) this).button; }
16291 		int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; }
16292 	}
16293 
16294 	deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward")
16295 	final @property {
16296 		int state() {
16297 			if(auto meb = cast(MouseEventBase) this)
16298 				return meb.state;
16299 			if(auto keb = cast(KeyEventBase) this)
16300 				return keb.state;
16301 			assert(0);
16302 		}
16303 	}
16304 
16305 	deprecated("Use a CharEvent instead of Event in your handler going forward")
16306 	final @property {
16307 		dchar character() {
16308 			if(auto ce = cast(CharEvent) this)
16309 				return ce.character;
16310 			return dchar.init;
16311 		}
16312 	}
16313 
16314 	// for change events
16315 	@property {
16316 		///
16317 		int intValue() { return 0; }
16318 		///
16319 		string stringValue() { return null; }
16320 	}
16321 }
16322 
16323 /++
16324 	This lets you statically verify you send the events you claim you send and gives you a hook to document them.
16325 
16326 	Please note that a widget may send events not listed as Emits. You can always construct and dispatch
16327 	dynamic and custom events, but the static list helps ensure you get them right.
16328 
16329 	If this is declared, you can use [Widget.emit] to send the event.
16330 
16331 	All events work the same way though, following the capture->widget->bubble model described under [Event].
16332 
16333 	History:
16334 		Added May 4, 2021
16335 +/
16336 mixin template Emits(EventType) {
16337 	import arsd.minigui : EventString;
16338 	static if(is(EventType : Event) && !is(EventType == Event))
16339 		mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";");
16340 	else
16341 		static assert(0, "You can only emit subclasses of Event");
16342 }
16343 
16344 /// ditto
16345 mixin template Emits(string eventString) {
16346 	mixin("private Event[0] emits_" ~ eventString ~";");
16347 }
16348 
16349 /*
16350 class SignalEvent(string name) : Event {
16351 
16352 }
16353 */
16354 
16355 /++
16356 	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".
16357 
16358 
16359 	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.
16360 
16361 	History:
16362 		Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things.
16363 +/
16364 class CommandEvent : Event {
16365 	enum EventString = "command";
16366 	this(Widget source, string CommandString = EventString) {
16367 		super(CommandString, source);
16368 	}
16369 }
16370 
16371 /++
16372 	A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments.
16373 +/
16374 class CommandEventWithArgs(Args...) : CommandEvent {
16375 	this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; }
16376 	Args args;
16377 }
16378 
16379 /++
16380 	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.
16381 
16382 	See [CommandEvent] for more information.
16383 
16384 	Returns:
16385 		The [EventListener] you can use to remove the handler.
16386 +/
16387 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) {
16388 	return w.addEventListener(CommandString, (Event ev) {
16389 		if(ev.target is w)
16390 			return; // it does not consume its own commands!
16391 		if(auto cev = cast(CommandEventWithArgs!Args) ev) {
16392 			handler(cev.args);
16393 			ev.stopPropagation();
16394 		}
16395 	});
16396 }
16397 
16398 /++
16399 	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.
16400 +/
16401 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) {
16402 	auto event = new CommandEventWithArgs!Args(w, CommandString, args);
16403 	event.dispatch();
16404 }
16405 
16406 /++
16407 	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.
16408 
16409 	If you need to know the old size, you need to store it yourself.
16410 
16411 	History:
16412 		Made final on January 3, 2025 (dub v12.0)
16413 +/
16414 final class ResizeEvent : Event {
16415 	enum EventString = "resize";
16416 
16417 	this(Widget target) { super(EventString, target); }
16418 
16419 	override bool propagates() const { return false; }
16420 }
16421 
16422 /++
16423 	ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close.
16424 
16425 	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.
16426 
16427 	History:
16428 		Added June 21, 2021 (dub v10.1)
16429 
16430 		Made final on January 3, 2025 (dub v12.0)
16431 +/
16432 final class ClosingEvent : Event {
16433 	enum EventString = "closing";
16434 
16435 	this(Widget target) { super(EventString, target); }
16436 
16437 	override bool propagates() const { return false; }
16438 	override bool cancelable() const { return true; }
16439 }
16440 
16441 /// ditto
16442 final class ClosedEvent : Event {
16443 	enum EventString = "closed";
16444 
16445 	this(Widget target) { super(EventString, target); }
16446 
16447 	override bool propagates() const { return false; }
16448 	override bool cancelable() const { return false; }
16449 }
16450 
16451 ///
16452 final class BlurEvent : Event {
16453 	enum EventString = "blur";
16454 
16455 	// FIXME: related target?
16456 	this(Widget target) { super(EventString, target); }
16457 
16458 	override bool propagates() const { return false; }
16459 }
16460 
16461 ///
16462 final class FocusEvent : Event {
16463 	enum EventString = "focus";
16464 
16465 	// FIXME: related target?
16466 	this(Widget target) { super(EventString, target); }
16467 
16468 	override bool propagates() const { return false; }
16469 }
16470 
16471 /++
16472 	FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates.
16473 
16474 	History:
16475 		Added July 3, 2021
16476 +/
16477 final class FocusInEvent : Event {
16478 	enum EventString = "focusin";
16479 
16480 	// FIXME: related target?
16481 	this(Widget target) { super(EventString, target); }
16482 
16483 	override bool cancelable() const { return false; }
16484 }
16485 
16486 /// ditto
16487 final class FocusOutEvent : Event {
16488 	enum EventString = "focusout";
16489 
16490 	// FIXME: related target?
16491 	this(Widget target) { super(EventString, target); }
16492 
16493 	override bool cancelable() const { return false; }
16494 }
16495 
16496 ///
16497 final class ScrollEvent : Event {
16498 	enum EventString = "scroll";
16499 	this(Widget target) { super(EventString, target); }
16500 
16501 	override bool cancelable() const { return false; }
16502 }
16503 
16504 /++
16505 	Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget.
16506 
16507 	History:
16508 		Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class.
16509 +/
16510 final class CharEvent : Event {
16511 	enum EventString = "char";
16512 	this(Widget target, dchar ch) {
16513 		character = ch;
16514 		super(EventString, target);
16515 	}
16516 
16517 	immutable dchar character;
16518 }
16519 
16520 /++
16521 	You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information.
16522 +/
16523 abstract class ChangeEventBase : Event {
16524 	enum EventString = "change";
16525 	this(Widget target) {
16526 		super(EventString, target);
16527 	}
16528 
16529 	/+
16530 		// idk where or how exactly i want to do this.
16531 		// i might come back to it later.
16532 
16533 	// If a widget itself broadcasts one of theses itself, it stops propagation going down
16534 	// this way the source doesn't get too confused (think of a nested scroll widget)
16535 	//
16536 	// the idea is like the scroll bar emits a command event saying like "scroll left one line"
16537 	// then you consume that command and change you scroll x position to whatever. then you do
16538 	// some kind of change event that is broadcast back to the children and any horizontal scroll
16539 	// listeners are now able to update, without having an explicit connection between them.
16540 	void broadcastToChildren(string fieldName) {
16541 
16542 	}
16543 	+/
16544 }
16545 
16546 /++
16547 	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.
16548 
16549 
16550 	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).
16551 
16552 	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);`
16553 
16554 	Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything.
16555 
16556 	History:
16557 		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.
16558 +/
16559 final class ChangeEvent(T) : ChangeEventBase {
16560 	this(Widget target, T delegate() getNewValue) {
16561 		assert(getNewValue !is null);
16562 		this.getNewValue = getNewValue;
16563 		super(target);
16564 	}
16565 
16566 	private T delegate() getNewValue;
16567 
16568 	/++
16569 		Gets the new value that just changed.
16570 	+/
16571 	@property T value() {
16572 		return getNewValue();
16573 	}
16574 
16575 	/// compatibility method for old generic Events
16576 	static if(is(immutable T == immutable int))
16577 		override int intValue() { return value; }
16578 	/// ditto
16579 	static if(is(immutable T == immutable string))
16580 		override string stringValue() { return value; }
16581 }
16582 
16583 /++
16584 	Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s.
16585 
16586 
16587 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16588 
16589 	History:
16590 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
16591 +/
16592 abstract class KeyEventBase : Event {
16593 	this(string name, Widget target) {
16594 		super(name, target);
16595 	}
16596 
16597 	// for key events
16598 	Key key; ///
16599 
16600 	KeyEvent originalKeyEvent;
16601 
16602 	/++
16603 		Indicates the current state of the given keyboard modifier keys.
16604 
16605 		History:
16606 			Added to events on April 15, 2020.
16607 	+/
16608 	bool ctrlKey;
16609 
16610 	/// ditto
16611 	bool altKey;
16612 
16613 	/// ditto
16614 	bool shiftKey;
16615 
16616 	/++
16617 		The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey].
16618 
16619 		See [arsd.simpledisplay.ModifierState] for other possible flags.
16620 	+/
16621 	int state;
16622 
16623 	mixin Register;
16624 }
16625 
16626 /++
16627 	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].
16628 
16629 
16630 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16631 
16632 	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.
16633 
16634 	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.
16635 
16636 	See_Also: [KeyUpEvent], [CharEvent]
16637 
16638 	History:
16639 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners.
16640 +/
16641 final class KeyDownEvent : KeyEventBase {
16642 	enum EventString = "keydown";
16643 	this(Widget target) { super(EventString, target); }
16644 }
16645 
16646 /++
16647 	Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase].
16648 
16649 
16650 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16651 
16652 	See_Also: [KeyDownEvent], [CharEvent]
16653 
16654 	History:
16655 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners.
16656 +/
16657 final class KeyUpEvent : KeyEventBase {
16658 	enum EventString = "keyup";
16659 	this(Widget target) { super(EventString, target); }
16660 }
16661 
16662 /++
16663 	Contains shared properties for various mouse events;
16664 
16665 
16666 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16667 
16668 	History:
16669 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
16670 +/
16671 abstract class MouseEventBase : Event {
16672 	this(string name, Widget target) {
16673 		super(name, target);
16674 	}
16675 
16676 	// for mouse events
16677 	int clientX; /// The mouse event location relative to the target widget
16678 	int clientY; /// ditto
16679 
16680 	int viewportX; /// The mouse event location relative to the window origin
16681 	int viewportY; /// ditto
16682 
16683 	int button; /// See: [MouseEvent.button]
16684 	int buttonLinear; /// See: [MouseEvent.buttonLinear]
16685 
16686 	/++
16687 		Indicates the current state of the given keyboard modifier keys.
16688 
16689 		History:
16690 			Added to mouse events on September 28, 2010.
16691 	+/
16692 	bool ctrlKey;
16693 
16694 	/// ditto
16695 	bool altKey;
16696 
16697 	/// ditto
16698 	bool shiftKey;
16699 
16700 
16701 
16702 	int state; ///
16703 
16704 	/++
16705 		for consistent names with key event.
16706 
16707 		History:
16708 			Added September 28, 2021 (dub v10.3)
16709 	+/
16710 	alias modifierState = state;
16711 
16712 	/++
16713 		Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out.
16714 
16715 		History:
16716 			Added May 15, 2021
16717 	+/
16718 	bool isMouseWheel() {
16719 		return button == MouseButton.wheelUp || button == MouseButton.wheelDown;
16720 	}
16721 
16722 	// private
16723 	override void adjustClientCoordinates(int deltaX, int deltaY) {
16724 		clientX += deltaX;
16725 		clientY += deltaY;
16726 	}
16727 
16728 	mixin Register;
16729 }
16730 
16731 /++
16732 	Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase].
16733 
16734 
16735 	$(WARNING
16736 		Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and
16737 		for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct
16738 		behavior.
16739 
16740 		Use [MouseEventBase.isMouseWheel] to filter wheel events while keeping others.
16741 	)
16742 
16743 	[MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement.
16744 
16745 	[MouseUpEvent] is sent when the user releases a mouse button.
16746 
16747 	[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.)
16748 
16749 	[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.
16750 
16751 	[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.
16752 
16753 	[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.
16754 
16755 	[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.
16756 
16757 	[MouseEnterEvent] is sent when the mouse enters the bounding box of a widget.
16758 
16759 	[MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget.
16760 
16761 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16762 
16763 	Rationale:
16764 
16765 		If you only want to do drag, mousedown/up works just fine being consistently sent.
16766 
16767 		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).
16768 
16769 		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.
16770 
16771 	History:
16772 		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.
16773 +/
16774 final class MouseUpEvent : MouseEventBase {
16775 	enum EventString = "mouseup"; ///
16776 	this(Widget target) { super(EventString, target); }
16777 }
16778 /// ditto
16779 final class MouseDownEvent : MouseEventBase {
16780 	enum EventString = "mousedown"; ///
16781 	this(Widget target) { super(EventString, target); }
16782 }
16783 /// ditto
16784 final class MouseMoveEvent : MouseEventBase {
16785 	enum EventString = "mousemove"; ///
16786 	this(Widget target) { super(EventString, target); }
16787 }
16788 /// ditto
16789 final class ClickEvent : MouseEventBase {
16790 	enum EventString = "click"; ///
16791 	this(Widget target) { super(EventString, target); }
16792 }
16793 /// ditto
16794 final class DoubleClickEvent : MouseEventBase {
16795 	enum EventString = "dblclick"; ///
16796 	this(Widget target) { super(EventString, target); }
16797 }
16798 /// ditto
16799 final class MouseOverEvent : Event {
16800 	enum EventString = "mouseover"; ///
16801 	this(Widget target) { super(EventString, target); }
16802 }
16803 /// ditto
16804 final class MouseOutEvent : Event {
16805 	enum EventString = "mouseout"; ///
16806 	this(Widget target) { super(EventString, target); }
16807 }
16808 /// ditto
16809 final class MouseEnterEvent : Event {
16810 	enum EventString = "mouseenter"; ///
16811 	this(Widget target) { super(EventString, target); }
16812 
16813 	override bool propagates() const { return false; }
16814 }
16815 /// ditto
16816 final class MouseLeaveEvent : Event {
16817 	enum EventString = "mouseleave"; ///
16818 	this(Widget target) { super(EventString, target); }
16819 
16820 	override bool propagates() const { return false; }
16821 }
16822 
16823 private bool isAParentOf(Widget a, Widget b) {
16824 	if(a is null || b is null)
16825 		return false;
16826 
16827 	while(b !is null) {
16828 		if(a is b)
16829 			return true;
16830 		b = b.parent;
16831 	}
16832 
16833 	return false;
16834 }
16835 
16836 private struct WidgetAtPointResponse {
16837 	Widget widget;
16838 
16839 	// x, y relative to the widget in the response.
16840 	int x;
16841 	int y;
16842 }
16843 
16844 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) {
16845 	assert(starting !is null);
16846 
16847 	starting.addScrollPosition(x, y);
16848 
16849 	auto child = starting.getChildAtPosition(x, y);
16850 	while(child) {
16851 		if(child.hidden)
16852 			continue;
16853 		starting = child;
16854 		x -= child.x;
16855 		y -= child.y;
16856 		auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y);
16857 		child = r.widget;
16858 		if(child is starting)
16859 			break;
16860 	}
16861 	return WidgetAtPointResponse(starting, x, y);
16862 }
16863 
16864 version(win32_widgets) {
16865 private:
16866 	import core.sys.windows.commctrl;
16867 
16868 	pragma(lib, "comctl32");
16869 	shared static this() {
16870 		// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx
16871 		INITCOMMONCONTROLSEX ic;
16872 		ic.dwSize = cast(DWORD) ic.sizeof;
16873 		ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES;
16874 		if(!InitCommonControlsEx(&ic)) {
16875 			//writeln("ICC failed");
16876 		}
16877 	}
16878 
16879 
16880 	// everything from here is just win32 headers copy pasta
16881 private:
16882 extern(Windows):
16883 
16884 	alias HANDLE HMENU;
16885 	HMENU CreateMenu();
16886 	bool SetMenu(HWND, HMENU);
16887 	HMENU CreatePopupMenu();
16888 	enum MF_POPUP = 0x10;
16889 	enum MF_STRING = 0;
16890 
16891 
16892 	BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*);
16893 	struct INITCOMMONCONTROLSEX {
16894 		DWORD dwSize;
16895 		DWORD dwICC;
16896 	}
16897 	enum HINST_COMMCTRL = cast(HINSTANCE) (-1);
16898 enum {
16899         IDB_STD_SMALL_COLOR,
16900         IDB_STD_LARGE_COLOR,
16901         IDB_VIEW_SMALL_COLOR = 4,
16902         IDB_VIEW_LARGE_COLOR = 5
16903 }
16904 enum {
16905         STD_CUT,
16906         STD_COPY,
16907         STD_PASTE,
16908         STD_UNDO,
16909         STD_REDOW,
16910         STD_DELETE,
16911         STD_FILENEW,
16912         STD_FILEOPEN,
16913         STD_FILESAVE,
16914         STD_PRINTPRE,
16915         STD_PROPERTIES,
16916         STD_HELP,
16917         STD_FIND,
16918         STD_REPLACE,
16919         STD_PRINT // = 14
16920 }
16921 
16922 alias HANDLE HIMAGELIST;
16923 	HIMAGELIST ImageList_Create(int, int, UINT, int, int);
16924 	int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP);
16925         BOOL ImageList_Destroy(HIMAGELIST);
16926 
16927 uint MAKELONG(ushort a, ushort b) {
16928         return cast(uint) ((b << 16) | a);
16929 }
16930 
16931 
16932 struct TBBUTTON {
16933 	int   iBitmap;
16934 	int   idCommand;
16935 	BYTE  fsState;
16936 	BYTE  fsStyle;
16937 	version(Win64)
16938 	BYTE[6] bReserved;
16939 	else
16940 	BYTE[2]  bReserved;
16941 	DWORD dwData;
16942 	INT_PTR   iString;
16943 }
16944 
16945 	enum {
16946 		TB_ADDBUTTONSA   = WM_USER + 20,
16947 		TB_INSERTBUTTONA = WM_USER + 21,
16948 		TB_GETIDEALSIZE = WM_USER + 99,
16949 	}
16950 
16951 struct SIZE {
16952 	LONG cx;
16953 	LONG cy;
16954 }
16955 
16956 
16957 enum {
16958 	TBSTATE_CHECKED       = 1,
16959 	TBSTATE_PRESSED       = 2,
16960 	TBSTATE_ENABLED       = 4,
16961 	TBSTATE_HIDDEN        = 8,
16962 	TBSTATE_INDETERMINATE = 16,
16963 	TBSTATE_WRAP          = 32
16964 }
16965 
16966 
16967 
16968 enum {
16969 	ILC_COLOR    = 0,
16970 	ILC_COLOR4   = 4,
16971 	ILC_COLOR8   = 8,
16972 	ILC_COLOR16  = 16,
16973 	ILC_COLOR24  = 24,
16974 	ILC_COLOR32  = 32,
16975 	ILC_COLORDDB = 254,
16976 	ILC_MASK     = 1,
16977 	ILC_PALETTE  = 2048
16978 }
16979 
16980 
16981 alias TBBUTTON*       PTBBUTTON, LPTBBUTTON;
16982 
16983 
16984 enum {
16985 	TB_ENABLEBUTTON          = WM_USER + 1,
16986 	TB_CHECKBUTTON,
16987 	TB_PRESSBUTTON,
16988 	TB_HIDEBUTTON,
16989 	TB_INDETERMINATE, //     = WM_USER + 5,
16990 	TB_ISBUTTONENABLED       = WM_USER + 9,
16991 	TB_ISBUTTONCHECKED,
16992 	TB_ISBUTTONPRESSED,
16993 	TB_ISBUTTONHIDDEN,
16994 	TB_ISBUTTONINDETERMINATE, // = WM_USER + 13,
16995 	TB_SETSTATE              = WM_USER + 17,
16996 	TB_GETSTATE              = WM_USER + 18,
16997 	TB_ADDBITMAP             = WM_USER + 19,
16998 	TB_DELETEBUTTON          = WM_USER + 22,
16999 	TB_GETBUTTON,
17000 	TB_BUTTONCOUNT,
17001 	TB_COMMANDTOINDEX,
17002 	TB_SAVERESTOREA,
17003 	TB_CUSTOMIZE,
17004 	TB_ADDSTRINGA,
17005 	TB_GETITEMRECT,
17006 	TB_BUTTONSTRUCTSIZE,
17007 	TB_SETBUTTONSIZE,
17008 	TB_SETBITMAPSIZE,
17009 	TB_AUTOSIZE, //          = WM_USER + 33,
17010 	TB_GETTOOLTIPS           = WM_USER + 35,
17011 	TB_SETTOOLTIPS           = WM_USER + 36,
17012 	TB_SETPARENT             = WM_USER + 37,
17013 	TB_SETROWS               = WM_USER + 39,
17014 	TB_GETROWS,
17015 	TB_GETBITMAPFLAGS,
17016 	TB_SETCMDID,
17017 	TB_CHANGEBITMAP,
17018 	TB_GETBITMAP,
17019 	TB_GETBUTTONTEXTA,
17020 	TB_REPLACEBITMAP, //     = WM_USER + 46,
17021 	TB_GETBUTTONSIZE         = WM_USER + 58,
17022 	TB_SETBUTTONWIDTH        = WM_USER + 59,
17023 	TB_GETBUTTONTEXTW        = WM_USER + 75,
17024 	TB_SAVERESTOREW          = WM_USER + 76,
17025 	TB_ADDSTRINGW            = WM_USER + 77,
17026 }
17027 
17028 extern(Windows)
17029 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM);
17030 
17031 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC;
17032 
17033 
17034 	enum {
17035 		TB_SETINDENT = WM_USER + 47,
17036 		TB_SETIMAGELIST,
17037 		TB_GETIMAGELIST,
17038 		TB_LOADIMAGES,
17039 		TB_GETRECT,
17040 		TB_SETHOTIMAGELIST,
17041 		TB_GETHOTIMAGELIST,
17042 		TB_SETDISABLEDIMAGELIST,
17043 		TB_GETDISABLEDIMAGELIST,
17044 		TB_SETSTYLE,
17045 		TB_GETSTYLE,
17046 		//TB_GETBUTTONSIZE,
17047 		//TB_SETBUTTONWIDTH,
17048 		TB_SETMAXTEXTROWS,
17049 		TB_GETTEXTROWS // = WM_USER + 61
17050 	}
17051 
17052 enum {
17053 	CCM_FIRST            = 0x2000,
17054 	CCM_LAST             = CCM_FIRST + 0x200,
17055 	CCM_SETBKCOLOR       = 8193,
17056 	CCM_SETCOLORSCHEME   = 8194,
17057 	CCM_GETCOLORSCHEME   = 8195,
17058 	CCM_GETDROPTARGET    = 8196,
17059 	CCM_SETUNICODEFORMAT = 8197,
17060 	CCM_GETUNICODEFORMAT = 8198,
17061 	CCM_SETVERSION       = 0x2007,
17062 	CCM_GETVERSION       = 0x2008,
17063 	CCM_SETNOTIFYWINDOW  = 0x2009
17064 }
17065 
17066 
17067 enum {
17068 	PBM_SETRANGE     = WM_USER + 1,
17069 	PBM_SETPOS,
17070 	PBM_DELTAPOS,
17071 	PBM_SETSTEP,
17072 	PBM_STEPIT,   // = WM_USER + 5
17073 	PBM_SETRANGE32   = 1030,
17074 	PBM_GETRANGE,
17075 	PBM_GETPOS,
17076 	PBM_SETBARCOLOR, // = 1033
17077 	PBM_SETBKCOLOR   = CCM_SETBKCOLOR
17078 }
17079 
17080 enum {
17081 	PBS_SMOOTH   = 1,
17082 	PBS_VERTICAL = 4
17083 }
17084 
17085 enum {
17086         ICC_LISTVIEW_CLASSES = 1,
17087         ICC_TREEVIEW_CLASSES = 2,
17088         ICC_BAR_CLASSES      = 4,
17089         ICC_TAB_CLASSES      = 8,
17090         ICC_UPDOWN_CLASS     = 16,
17091         ICC_PROGRESS_CLASS   = 32,
17092         ICC_HOTKEY_CLASS     = 64,
17093         ICC_ANIMATE_CLASS    = 128,
17094         ICC_WIN95_CLASSES    = 255,
17095         ICC_DATE_CLASSES     = 256,
17096         ICC_USEREX_CLASSES   = 512,
17097         ICC_COOL_CLASSES     = 1024,
17098 	ICC_STANDARD_CLASSES = 0x00004000,
17099 }
17100 
17101 	enum WM_USER = 1024;
17102 }
17103 
17104 version(win32_widgets)
17105 	pragma(lib, "comdlg32");
17106 
17107 
17108 ///
17109 enum GenericIcons : ushort {
17110 	None, ///
17111 	// these happen to match the win32 std icons numerically if you just subtract one from the value
17112 	Cut, ///
17113 	Copy, ///
17114 	Paste, ///
17115 	Undo, ///
17116 	Redo, ///
17117 	Delete, ///
17118 	New, ///
17119 	Open, ///
17120 	Save, ///
17121 	PrintPreview, ///
17122 	Properties, ///
17123 	Help, ///
17124 	Find, ///
17125 	Replace, ///
17126 	Print, ///
17127 }
17128 
17129 enum FileDialogType {
17130 	Automatic,
17131 	Open,
17132 	Save
17133 }
17134 
17135 /++
17136 	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.
17137 +/
17138 string previousFileReferenced;
17139 
17140 /++
17141 	Used in automatic menu functions to indicate that the user should be able to browse for a file.
17142 
17143 	Params:
17144 		storage = an alias to a `static string` variable that stores the last file referenced. It will
17145 		use this to pre-fill the dialog with a suggestion.
17146 
17147 		Please note that it MUST be `static` or you will get compile errors.
17148 
17149 		filters = the filters param to [getFileName]
17150 
17151 		type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will
17152 		guess based on the function name. If it has the word "Save" or "Export" in it, it will show
17153 		a save dialog box. Otherwise, it will show an open dialog box.
17154 +/
17155 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) {
17156 	string name;
17157 	alias name this;
17158 
17159 	@implicit this(string name) {
17160 		this.name = name;
17161 	}
17162 }
17163 
17164 /++
17165 	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.
17166 
17167 	History:
17168 		onCancel was added November 6, 2021.
17169 
17170 		The dialog itself on Linux was modified on December 2, 2021 to include
17171 		a directory picker in addition to the command line completion view.
17172 
17173 		The `initialDirectory` argument was added November 9, 2022 (dub v10.10)
17174 
17175 		The `owner` argument was added September 29, 2024. The overloads without this argument are likely to be deprecated in the next major version.
17176 	Future_directions:
17177 		I want to add some kind of custom preview and maybe thumbnail thing in the future,
17178 		at least on Linux, maybe on Windows too.
17179 +/
17180 void getOpenFileName(
17181 	Window owner,
17182 	void delegate(string) onOK,
17183 	string prefilledName = null,
17184 	string[] filters = null,
17185 	void delegate() onCancel = null,
17186 	string initialDirectory = null,
17187 )
17188 {
17189 	return getFileName(owner, true, onOK, prefilledName, filters, onCancel, initialDirectory);
17190 }
17191 
17192 /// ditto
17193 void getSaveFileName(
17194 	Window owner,
17195 	void delegate(string) onOK,
17196 	string prefilledName = null,
17197 	string[] filters = null,
17198 	void delegate() onCancel = null,
17199 	string initialDirectory = null,
17200 )
17201 {
17202 	return getFileName(owner, false, onOK, prefilledName, filters, onCancel, initialDirectory);
17203 }
17204 
17205 // 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.")
17206 /// ditto
17207 void getOpenFileName(
17208 	void delegate(string) onOK,
17209 	string prefilledName = null,
17210 	string[] filters = null,
17211 	void delegate() onCancel = null,
17212 	string initialDirectory = null,
17213 )
17214 {
17215 	return getFileName(null, true, onOK, prefilledName, filters, onCancel, initialDirectory);
17216 }
17217 
17218 /// ditto
17219 void getSaveFileName(
17220 	void delegate(string) onOK,
17221 	string prefilledName = null,
17222 	string[] filters = null,
17223 	void delegate() onCancel = null,
17224 	string initialDirectory = null,
17225 )
17226 {
17227 	return getFileName(null, false, onOK, prefilledName, filters, onCancel, initialDirectory);
17228 }
17229 
17230 /++
17231 	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.
17232 
17233 	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.
17234 
17235 	History:
17236 		Added January 1, 2025
17237 +/
17238 class FileDialogDelegate {
17239 
17240 	/++
17241 
17242 	+/
17243 	static abstract class PreviewWidget : Widget {
17244 		/// Call this from your subclass' constructor
17245 		this(Widget parent) {
17246 			super(parent);
17247 		}
17248 
17249 		/// Load the file given to you and show its preview inside the widget here
17250 		abstract void previewFile(string filename);
17251 	}
17252 
17253 	/++
17254 		Override this to add preview capabilities to the dialog for certain files.
17255 	+/
17256 	protected PreviewWidget makePreviewWidget(Widget parent) {
17257 		return null;
17258 	}
17259 
17260 	/++
17261 		Override this to change the dialog entirely.
17262 
17263 		This function IS allowed to block, but is NOT required to.
17264 	+/
17265 	protected void getFileName(
17266 		Window owner,
17267 		bool openOrSave, // true if open, false if save
17268 		void delegate(string) onOK,
17269 		string prefilledName,
17270 		string[] filters, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
17271 		void delegate() onCancel,
17272 		string initialDirectory,
17273 	)
17274 	{
17275 
17276 		version(win32_widgets) {
17277 			import core.sys.windows.commdlg;
17278 		/*
17279 		Ofn.lStructSize = sizeof(OPENFILENAME);
17280 		Ofn.hwndOwner = hWnd;
17281 		Ofn.lpstrFilter = szFilter;
17282 		Ofn.lpstrFile= szFile;
17283 		Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile);
17284 		Ofn.lpstrFileTitle = szFileTitle;
17285 		Ofn.nMaxFileTitle = sizeof(szFileTitle);
17286 		Ofn.lpstrInitialDir = (LPSTR)NULL;
17287 		Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT;
17288 		Ofn.lpstrTitle = szTitle;
17289 		 */
17290 
17291 
17292 			wchar[1024] file = 0;
17293 			wchar[1024] filterBuffer = 0;
17294 			makeWindowsString(prefilledName, file[]);
17295 			OPENFILENAME ofn;
17296 			ofn.lStructSize = ofn.sizeof;
17297 			ofn.hwndOwner = owner is null ? null : owner.win.hwnd;
17298 			if(filters.length) {
17299 				string filter;
17300 				foreach(i, f; filters) {
17301 					filter ~= f;
17302 					filter ~= "\0";
17303 				}
17304 				filter ~= "\0";
17305 				ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr;
17306 			}
17307 			ofn.lpstrFile = file.ptr;
17308 			ofn.nMaxFile = file.length;
17309 
17310 			wchar[1024] initialDir = 0;
17311 			if(initialDirectory !is null) {
17312 				makeWindowsString(initialDirectory, initialDir[]);
17313 				ofn.lpstrInitialDir = file.ptr;
17314 			}
17315 
17316 			if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn))
17317 			{
17318 				string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile);
17319 				if(okString.length && okString[$-1] == '\0')
17320 					okString = okString[0..$-1];
17321 				onOK(okString);
17322 			} else {
17323 				if(onCancel)
17324 					onCancel();
17325 			}
17326 		} else version(custom_widgets) {
17327 			filters ~= ["All Files\0*.*"];
17328 			auto picker = new FilePicker(openOrSave, prefilledName, filters, initialDirectory, owner);
17329 			picker.onOK = onOK;
17330 			picker.onCancel = onCancel;
17331 			picker.show();
17332 		}
17333 	}
17334 
17335 }
17336 
17337 /// ditto
17338 FileDialogDelegate fileDialogDelegate() {
17339 	if(fileDialogDelegate_ is null)
17340 		fileDialogDelegate_ = new FileDialogDelegate();
17341 	return fileDialogDelegate_;
17342 }
17343 
17344 /// ditto
17345 void fileDialogDelegate(FileDialogDelegate replacement) {
17346 	fileDialogDelegate_ = replacement;
17347 }
17348 
17349 private FileDialogDelegate fileDialogDelegate_;
17350 
17351 struct FileNameFilter {
17352 	string description;
17353 	string[] globPatterns;
17354 
17355 	string toString() {
17356 		string ret;
17357 		ret ~= description;
17358 		ret ~= " (";
17359 		foreach(idx, pattern; globPatterns) {
17360 			if(idx)
17361 				ret ~= "; ";
17362 			ret ~= pattern;
17363 		}
17364 		ret ~= ")";
17365 
17366 		return ret;
17367 	}
17368 
17369 	static FileNameFilter fromString(string s) {
17370 		size_t end = s.length;
17371 		size_t start = 0;
17372 		foreach_reverse(idx, ch; s) {
17373 			if(ch == ')' && end == s.length)
17374 				end = idx;
17375 			else if(ch == '(' && end != s.length) {
17376 				start = idx + 1;
17377 				break;
17378 			}
17379 		}
17380 
17381 		FileNameFilter fnf;
17382 		fnf.description = s[0 .. start ? start - 1 : 0];
17383 		size_t globStart = 0;
17384 		s = s[start .. end];
17385 		foreach(idx, ch; s)
17386 			if(ch == ';') {
17387 				auto ptn = stripInternal(s[globStart .. idx]);
17388 				if(ptn.length)
17389 					fnf.globPatterns ~= ptn;
17390 				globStart = idx + 1;
17391 
17392 			}
17393 		auto ptn = stripInternal(s[globStart .. $]);
17394 		if(ptn.length)
17395 			fnf.globPatterns ~= ptn;
17396 		return fnf;
17397 	}
17398 }
17399 
17400 struct FileNameFilterSet {
17401 	FileNameFilter[] filters;
17402 
17403 	static FileNameFilterSet fromWindowsFileNameFilterDescription(string[] filters) {
17404 		FileNameFilter[] ret;
17405 
17406 		foreach(filter; filters) {
17407 			FileNameFilter fnf;
17408 			size_t filterStartPoint;
17409 			foreach(idx, ch; filter) {
17410 				if(ch == 0) {
17411 					fnf.description = filter[0 .. idx];
17412 					filterStartPoint = idx + 1;
17413 				} else if(filterStartPoint && ch == ';') {
17414 					fnf.globPatterns ~= filter[filterStartPoint .. idx];
17415 					filterStartPoint = idx + 1;
17416 				}
17417 			}
17418 			fnf.globPatterns ~= filter[filterStartPoint .. $];
17419 
17420 			ret ~= fnf;
17421 		}
17422 
17423 		return FileNameFilterSet(ret);
17424 	}
17425 }
17426 
17427 void getFileName(
17428 	Window owner,
17429 	bool openOrSave,
17430 	void delegate(string) onOK,
17431 	string prefilledName = null,
17432 	string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
17433 	void delegate() onCancel = null,
17434 	string initialDirectory = null,
17435 )
17436 {
17437 	return fileDialogDelegate().getFileName(owner, openOrSave, onOK, prefilledName, filters, onCancel, initialDirectory);
17438 }
17439 
17440 version(custom_widgets)
17441 private
17442 class FilePicker : Dialog {
17443 	void delegate(string) onOK;
17444 	void delegate() onCancel;
17445 	LabeledLineEdit lineEdit;
17446 	bool isOpenDialogInsteadOfSave;
17447 
17448 	static struct HistoryItem {
17449 		string cwd;
17450 		FileNameFilter filters;
17451 	}
17452 	HistoryItem[] historyStack;
17453 	size_t historyStackPosition;
17454 
17455 	void back() {
17456 		if(historyStackPosition) {
17457 			historyStackPosition--;
17458 			currentDirectory = historyStack[historyStackPosition].cwd;
17459 			currentFilter = historyStack[historyStackPosition].filters;
17460 			filesOfType.content = currentFilter.toString();
17461 			loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true);
17462 			lineEdit.focus();
17463 		}
17464 	}
17465 
17466 	void forward() {
17467 		if(historyStackPosition + 1 < historyStack.length) {
17468 			historyStackPosition++;
17469 			currentDirectory = historyStack[historyStackPosition].cwd;
17470 			currentFilter = historyStack[historyStackPosition].filters;
17471 			filesOfType.content = currentFilter.toString();
17472 			loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true);
17473 			lineEdit.focus();
17474 		}
17475 	}
17476 
17477 	void up() {
17478 		currentDirectory = currentDirectory ~ "..";
17479 		loadFiles(currentDirectory, currentFilter);
17480 		lineEdit.focus();
17481 	}
17482 
17483 	void refresh() {
17484 		loadFiles(currentDirectory, currentFilter);
17485 		lineEdit.focus();
17486 	}
17487 
17488 	// returns common prefix
17489 	static struct CommonPrefixInfo {
17490 		string commonPrefix;
17491 		int fileCount;
17492 		string exactMatch;
17493 	}
17494 	CommonPrefixInfo loadFiles(string cwd, FileNameFilter filters, bool comingFromHistory = false) {
17495 
17496 		if(!comingFromHistory) {
17497 			if(historyStack.length) {
17498 				historyStack = historyStack[0 .. historyStackPosition + 1];
17499 				historyStack.assumeSafeAppend();
17500 			}
17501 			historyStack ~= HistoryItem(cwd, filters);
17502 			historyStackPosition = historyStack.length - 1;
17503 		}
17504 
17505 		string[] files;
17506 		string[] dirs;
17507 
17508 		dirs ~= "$HOME";
17509 		dirs ~= "$PWD";
17510 
17511 		string commonPrefix;
17512 		int commonPrefixCount;
17513 		string exactMatch;
17514 
17515 		bool matchesFilter(string name) {
17516 			foreach(filter; filters.globPatterns) {
17517 			if(
17518 				filter.length <= 1 ||
17519 				filter == "*.*" || // we always treat *.* the same as *, but it is a bit different than .*
17520 				(filter[0] == '*' && name.endsWith(filter[1 .. $])) ||
17521 				(filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1]))
17522 			)
17523 			{
17524 				if(name.length > 1 && name[0] == '.')
17525 					if(filter.length == 0 || filter[0] != '.')
17526 						return false;
17527 
17528 				return true;
17529 			}
17530 			}
17531 
17532 			return false;
17533 		}
17534 
17535 		void considerCommonPrefix(string name, bool prefiltered) {
17536 			if(!prefiltered && !matchesFilter(name))
17537 				return;
17538 
17539 			if(commonPrefix is null) {
17540 				commonPrefix = name;
17541 				commonPrefixCount = 1;
17542 				exactMatch = commonPrefix;
17543 			} else {
17544 				foreach(idx, char i; name) {
17545 					if(idx >= commonPrefix.length || i != commonPrefix[idx]) {
17546 						commonPrefix = commonPrefix[0 .. idx];
17547 						commonPrefixCount ++;
17548 						exactMatch = null;
17549 						break;
17550 					}
17551 				}
17552 			}
17553 		}
17554 
17555 		bool applyFilterToDirectories = true;
17556 		bool showDotFiles = false;
17557 		foreach(filter; filters.globPatterns) {
17558 			if(filter == ".*")
17559 				showDotFiles = true;
17560 			else foreach(ch; filter)
17561 				if(ch == '.') {
17562 					// a filter like *.exe should not apply to the directory
17563 					applyFilterToDirectories = false;
17564 					break;
17565 				}
17566 		}
17567 
17568 		try
17569 		getFiles(cwd, (string name, bool isDirectory) {
17570 			if(name == ".")
17571 				return; // skip this as unnecessary
17572 			if(isDirectory) {
17573 				if(applyFilterToDirectories) {
17574 					if(matchesFilter(name)) {
17575 						dirs ~= name;
17576 						considerCommonPrefix(name, false);
17577 					}
17578 				} else if(name != ".." && name.length > 1 && name[0] == '.') {
17579 					if(showDotFiles) {
17580 						dirs ~= name;
17581 						considerCommonPrefix(name, false);
17582 					}
17583 				} else {
17584 					dirs ~= name;
17585 					considerCommonPrefix(name, false);
17586 				}
17587 			} else {
17588 				if(matchesFilter(name)) {
17589 					files ~= name;
17590 
17591 					//if(filter.length > 0 && filter[$-1] == '*') {
17592 						considerCommonPrefix(name, true);
17593 					//}
17594 				}
17595 			}
17596 		});
17597 		catch(ArsdExceptionBase e) {
17598 			messageBox("Unable to read requested directory");
17599 			// FIXME: give them a chance to create it? or at least go back?
17600 			/+
17601 			comingFromHistory = true;
17602 			back();
17603 			return null;
17604 			+/
17605 		}
17606 
17607 		extern(C) static int comparator(scope const void* a, scope const void* b) {
17608 			auto sa = *cast(string*) a;
17609 			auto sb = *cast(string*) b;
17610 
17611 			/+
17612 				Goal here:
17613 
17614 				Dot first. This puts `foo.d` before `foo2.d`
17615 				Then numbers , natural sort order (so 9 comes before 10) for positive numbers
17616 				Then letters, in order Aa, Bb, Cc
17617 				Then other symbols in ascii order
17618 			+/
17619 			static int nextPiece(ref string whole) {
17620 				if(whole.length == 0)
17621 					return -1;
17622 
17623 				enum specialZoneSize = 1;
17624 
17625 				char current = whole[0];
17626 				if(current >= '0' && current <= '9') {
17627 					int accumulator;
17628 					do {
17629 						whole = whole[1 .. $];
17630 						accumulator *= 10;
17631 						accumulator += current - '0';
17632 						current = whole.length ? whole[0] : 0;
17633 					} while (current >= '0' && current <= '9');
17634 
17635 					return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols
17636 				} else {
17637 					whole = whole[1 .. $];
17638 
17639 					if(current == '.')
17640 						return 0; // the special case to put it before numbers
17641 
17642 					// anything above should be < specialZoneSize
17643 
17644 					int letterZoneSize = 26 * 2;
17645 					int base = int.max - letterZoneSize - char.max; // leaves space at end for symbols too if we want them after chars
17646 
17647 					if(current >= 'A' && current <= 'Z')
17648 						return base + (current - 'A') * 2;
17649 					if(current >= 'a' && current <= 'z')
17650 						return base + (current - 'a') * 2 + 1;
17651 					// return base + letterZoneSize + current; // would put symbols after numbers and letters
17652 					return specialZoneSize + current; // puts symbols before numbers and letters, but after the special zone
17653 				}
17654 			}
17655 
17656 			while(sa.length || sb.length) {
17657 				auto pa = nextPiece(sa);
17658 				auto pb = nextPiece(sb);
17659 
17660 				auto diff = pa - pb;
17661 				if(diff)
17662 					return diff;
17663 			}
17664 
17665 			return 0;
17666 		}
17667 
17668 		nonPhobosSort(files, &comparator);
17669 		nonPhobosSort(dirs, &comparator);
17670 
17671 		listWidget.clear();
17672 		dirWidget.clear();
17673 		foreach(name; dirs)
17674 			dirWidget.addOption(name);
17675 		foreach(name; files)
17676 			listWidget.addOption(name);
17677 
17678 		return CommonPrefixInfo(commonPrefix, commonPrefixCount, exactMatch);
17679 	}
17680 
17681 	ListWidget listWidget;
17682 	ListWidget dirWidget;
17683 
17684 	FreeEntrySelection filesOfType;
17685 	LineEdit directoryHolder;
17686 
17687 	string currentDirectory_;
17688 	FileNameFilter currentNonTabFilter;
17689 	FileNameFilter currentFilter;
17690 	FileNameFilterSet filterOptions;
17691 
17692 	void currentDirectory(string s) {
17693 		currentDirectory_ = FilePath(s).makeAbsolute(getCurrentWorkingDirectory()).toString();
17694 		directoryHolder.content = currentDirectory_;
17695 	}
17696 	string currentDirectory() {
17697 		return currentDirectory_;
17698 	}
17699 
17700 	private string getUserHomeDir() {
17701 		import core.stdc.stdlib;
17702 		version(Windows)
17703 			return (stringz(getenv("HOMEDRIVE")).borrow ~ stringz(getenv("HOMEPATH")).borrow).idup;
17704 		else
17705 			return (stringz(getenv("HOME")).borrow).idup;
17706 	}
17707 
17708 	private string expandTilde(string s) {
17709 		// FIXME: cannot look up other user dirs
17710 		if(s.length == 1 && s == "~")
17711 			return getUserHomeDir();
17712 		if(s.length > 1 && s[0] == '~' && s[1] == '/')
17713 			return getUserHomeDir() ~ s[1 .. $];
17714 		return s;
17715 	}
17716 
17717 	// FIXME: allow many files to be picked too sometimes
17718 
17719 	//string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
17720 	this(bool isOpenDialogInsteadOfSave, string prefilledName, string[] filtersInWindowsFormat, string initialDirectory, Window owner = null) {
17721 		this.filterOptions = FileNameFilterSet.fromWindowsFileNameFilterDescription(filtersInWindowsFormat);
17722 		this.isOpenDialogInsteadOfSave = isOpenDialogInsteadOfSave;
17723 		super(owner, 500, 400, "Choose File..."); // owner);
17724 
17725 		{
17726 			auto navbar = new HorizontalLayout(24, this);
17727 			auto backButton = new ToolButton(new Action("<", 0, &this.back), navbar);
17728 			auto forwardButton = new ToolButton(new Action(">", 0, &this.forward), navbar);
17729 			auto upButton = new ToolButton(new Action("^", 0, &this.up), navbar); // hmm with .. in the dir list we don't really need an up button
17730 
17731 			directoryHolder = new LineEdit(navbar);
17732 
17733 			directoryHolder.addEventListener(delegate(scope KeyDownEvent kde) {
17734 				if(kde.key == Key.Enter || kde.key == Key.PadEnter) {
17735 					kde.stopPropagation();
17736 
17737 					currentDirectory = directoryHolder.content;
17738 					loadFiles(currentDirectory, currentFilter);
17739 
17740 					lineEdit.focus();
17741 				}
17742 			});
17743 
17744 			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.
17745 
17746 			/+
17747 			auto newDirectoryButton = new ToolButton(new Action("N"), navbar);
17748 
17749 			// FIXME: make sure putting `.` in the dir filter goes back to the CWD
17750 			// and that ~ goes back to the home dir
17751 			// and blanking it goes back to the suggested dir
17752 
17753 			auto homeButton = new ToolButton(new Action("H"), navbar);
17754 			auto cwdButton = new ToolButton(new Action("."), navbar);
17755 			auto suggestedDirectoryButton = new ToolButton(new Action("*"), navbar);
17756 			+/
17757 
17758 			filesOfType = new class FreeEntrySelection {
17759 				this() {
17760 					string[] opt;
17761 					foreach(option; filterOptions.filters)
17762 						opt ~=  option.toString;
17763 					super(opt, navbar);
17764 				}
17765 				override int flexBasisWidth() {
17766 					return scaleWithDpi(150);
17767 				}
17768 				override int widthStretchiness() {
17769 					return 1;//super.widthStretchiness() / 2;
17770 				}
17771 			};
17772 			filesOfType.setSelection(0);
17773 			currentFilter = filterOptions.filters[0];
17774 			currentNonTabFilter = currentFilter;
17775 		}
17776 
17777 		{
17778 			auto mainGrid = new GridLayout(4, 1, this);
17779 
17780 			dirWidget = new ListWidget(mainGrid);
17781 			listWidget = new ListWidget(mainGrid);
17782 			listWidget.tabStop = false;
17783 			dirWidget.tabStop = false;
17784 
17785 			FileDialogDelegate.PreviewWidget previewWidget = fileDialogDelegate.makePreviewWidget(mainGrid);
17786 
17787 			mainGrid.setChildPosition(dirWidget, 0, 0, 1, 1);
17788 			mainGrid.setChildPosition(listWidget, 1, 0, previewWidget !is null ? 2 : 3, 1);
17789 			if(previewWidget)
17790 				mainGrid.setChildPosition(previewWidget, 2, 0, 1, 1);
17791 
17792 			// double click events normally trigger something else but
17793 			// here user might be clicking kinda fast and we'd rather just
17794 			// keep it
17795 			dirWidget.addEventListener((scope DoubleClickEvent dev) {
17796 				auto ce = new ChangeEvent!void(dirWidget, () {});
17797 				ce.dispatch();
17798 				lineEdit.focus();
17799 			});
17800 
17801 			dirWidget.addEventListener((scope ChangeEvent!void sce) {
17802 				string v;
17803 				foreach(o; dirWidget.options)
17804 					if(o.selected) {
17805 						v = o.label;
17806 						break;
17807 					}
17808 				if(v.length) {
17809 					if(v == "$HOME")
17810 						currentDirectory = getUserHomeDir();
17811 					else if(v == "$PWD")
17812 						currentDirectory = ".";
17813 					else
17814 						currentDirectory = currentDirectory ~ "/" ~ v;
17815 					loadFiles(currentDirectory, currentFilter);
17816 				}
17817 
17818 				dirWidget.focusOn = -1;
17819 				lineEdit.focus();
17820 			});
17821 
17822 			// double click here, on the other hand, selects the file
17823 			// and moves on
17824 			listWidget.addEventListener((scope DoubleClickEvent dev) {
17825 				OK();
17826 			});
17827 		}
17828 
17829 		lineEdit = new LabeledLineEdit("File name:", TextAlignment.Right, this);
17830 		lineEdit.focus();
17831 		lineEdit.addEventListener(delegate(CharEvent event) {
17832 			if(event.character == '\t' || event.character == '\n')
17833 				event.preventDefault();
17834 		});
17835 
17836 		listWidget.addEventListener(EventType.change, () {
17837 			foreach(o; listWidget.options)
17838 				if(o.selected)
17839 					lineEdit.content = o.label;
17840 		});
17841 
17842 		currentDirectory = initialDirectory is null ? "." : initialDirectory;
17843 
17844 		auto prefilledPath = FilePath(expandTilde(prefilledName)).makeAbsolute(FilePath(currentDirectory));
17845 		currentDirectory = prefilledPath.directoryName;
17846 		prefilledName = prefilledPath.filename;
17847 		loadFiles(currentDirectory, currentFilter);
17848 
17849 		filesOfType.addEventListener(delegate (FreeEntrySelection.SelectionChangedEvent ce) {
17850 			currentFilter = FileNameFilter.fromString(ce.stringValue);
17851 			currentNonTabFilter = currentFilter;
17852 			loadFiles(currentDirectory, currentFilter);
17853 			// lineEdit.focus(); // this causes a recursive crash.....
17854 		});
17855 
17856 		filesOfType.addEventListener(delegate(KeyDownEvent event) {
17857 			if(event.key == Key.Enter) {
17858 				currentFilter = FileNameFilter.fromString(filesOfType.content);
17859 				currentNonTabFilter = currentFilter;
17860 				loadFiles(currentDirectory, currentFilter);
17861 				event.stopPropagation();
17862 				// FIXME: refocus on the line edit
17863 			}
17864 		});
17865 
17866 		lineEdit.addEventListener((KeyDownEvent event) {
17867 			if(event.key == Key.Tab && !event.ctrlKey && !event.shiftKey) {
17868 
17869 				auto path = FilePath(expandTilde(lineEdit.content)).makeAbsolute(FilePath(currentDirectory));
17870 				currentDirectory = path.directoryName;
17871 				auto current = path.filename;
17872 
17873 				auto newFilter = current;
17874 				if(current.length && current[0] != '*' && current[$-1] != '*')
17875 					newFilter ~= "*";
17876 				else if(newFilter.length == 0)
17877 					newFilter = "*";
17878 
17879 				auto newFilterObj = FileNameFilter("Custom filter", [newFilter]);
17880 
17881 				CommonPrefixInfo commonPrefix = loadFiles(currentDirectory, newFilterObj);
17882 				if(commonPrefix.fileCount == 1) {
17883 					// exactly one file, let's see what it is
17884 					auto specificFile = FilePath(commonPrefix.exactMatch).makeAbsolute(FilePath(currentDirectory));
17885 					if(getFileType(specificFile.toString) == FileType.dir) {
17886 						// a directory means we should change to it and keep the old filter
17887 						currentDirectory = specificFile.toString();
17888 						lineEdit.content = specificFile.toString() ~ "/";
17889 						loadFiles(currentDirectory, currentFilter);
17890 					} else {
17891 						// any other file should be selected in the list
17892 						currentDirectory = specificFile.directoryName;
17893 						current = specificFile.filename;
17894 						lineEdit.content = current;
17895 						loadFiles(currentDirectory, currentFilter);
17896 					}
17897 				} else if(commonPrefix.fileCount > 1) {
17898 					currentFilter = newFilterObj;
17899 					filesOfType.content = currentFilter.toString();
17900 					lineEdit.content = commonPrefix.commonPrefix;
17901 				} else {
17902 					// if there were no files, we don't really want to change the filter..
17903 					//sdpyPrintDebugString("no files");
17904 				}
17905 
17906 				// FIXME: if that is a directory, add the slash? or even go inside?
17907 
17908 				event.preventDefault();
17909 			}
17910 			else if(event.key == Key.Left && event.altKey) {
17911 				this.back();
17912 				event.preventDefault();
17913 			}
17914 			else if(event.key == Key.Right && event.altKey) {
17915 				this.forward();
17916 				event.preventDefault();
17917 			}
17918 		});
17919 
17920 
17921 		lineEdit.content = prefilledName;
17922 
17923 		auto hl = new HorizontalLayout(60, this);
17924 		auto cancelButton = new Button("Cancel", hl);
17925 		auto okButton = new Button(isOpenDialogInsteadOfSave ? "Open" : "Save"/*"OK"*/, hl);
17926 
17927 		cancelButton.addEventListener(EventType.triggered, &Cancel);
17928 		okButton.addEventListener(EventType.triggered, &OK);
17929 
17930 		this.addEventListener((KeyDownEvent event) {
17931 			if(event.key == Key.Enter || event.key == Key.PadEnter) {
17932 				event.preventDefault();
17933 				OK();
17934 			}
17935 			else if(event.key == Key.Escape)
17936 				Cancel();
17937 			else if(event.key == Key.F5)
17938 				refresh();
17939 			else if(event.key == Key.Up && event.altKey)
17940 				up(); // ditto
17941 			else if(event.key == Key.Left && event.altKey)
17942 				back(); // FIXME: it sends the key to the line edit too
17943 			else if(event.key == Key.Right && event.altKey)
17944 				forward(); // ditto
17945 			else if(event.key == Key.Up)
17946 				listWidget.setSelection(listWidget.getSelection() - 1);
17947 			else if(event.key == Key.Down)
17948 				listWidget.setSelection(listWidget.getSelection() + 1);
17949 		});
17950 
17951 		// FIXME: set the list view's focusOn to -1 on most interactions so it doesn't keep a thing highlighted
17952 		// FIXME: button to create new directory
17953 		// FIXME: show dirs in the files list too? idk.
17954 
17955 		// FIXME: support ~ as alias for home in the input
17956 		// FIXME: tab complete ought to be able to change+complete dir too
17957 	}
17958 
17959 	override void OK() {
17960 		if(lineEdit.content.length) {
17961 			auto c = expandTilde(lineEdit.content);
17962 
17963 			FilePath accepted = FilePath(c).makeAbsolute(FilePath(currentDirectory));
17964 
17965 			auto ft = getFileType(accepted.toString);
17966 
17967 			if(ft == FileType.error && isOpenDialogInsteadOfSave) {
17968 				// FIXME: tell the user why
17969 				messageBox("Cannot open file: " ~ accepted.toString ~ "\nTry another or cancel.");
17970 				lineEdit.focus();
17971 				return;
17972 
17973 			}
17974 
17975 			// FIXME: symlinks to dirs should prolly also get this behavior
17976 			if(ft == FileType.dir) {
17977 				currentDirectory = accepted.toString;
17978 
17979 				currentFilter = currentNonTabFilter;
17980 				filesOfType.content = currentFilter.toString();
17981 
17982 				loadFiles(currentDirectory, currentFilter);
17983 				lineEdit.content = "";
17984 
17985 				lineEdit.focus();
17986 
17987 				return;
17988 			}
17989 
17990 			if(onOK)
17991 				onOK(accepted.toString);
17992 		}
17993 		close();
17994 	}
17995 
17996 	override void Cancel() {
17997 		if(onCancel)
17998 			onCancel();
17999 		close();
18000 	}
18001 }
18002 
18003 private enum FileType {
18004 	error,
18005 	dir,
18006 	other
18007 }
18008 
18009 private FileType getFileType(string name) {
18010 	version(Windows) {
18011 		auto ws = WCharzBuffer(name);
18012 		auto ret = GetFileAttributesW(ws.ptr);
18013 		if(ret == INVALID_FILE_ATTRIBUTES)
18014 			return FileType.error;
18015 		return ((ret & FILE_ATTRIBUTE_DIRECTORY) != 0) ? FileType.dir : FileType.other;
18016 	} else version(Posix) {
18017 		import core.sys.posix.sys.stat;
18018 		stat_t buf;
18019 		auto ret = stat((name ~ '\0').ptr, &buf);
18020 		if(ret == -1)
18021 			return FileType.error;
18022 		return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other;
18023 	} else assert(0, "Not implemented");
18024 }
18025 
18026 /*
18027 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes
18028 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx
18029 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx
18030 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx
18031 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx
18032 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box
18033 http://www.sbin.org/doc/Xlib/chapt_03.html
18034 
18035 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx
18036 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx
18037 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx
18038 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx
18039 */
18040 
18041 
18042 // These are all for setMenuAndToolbarFromAnnotatedCode
18043 /// This item in the menu will be preceded by a separator line
18044 /// Group: generating_from_code
18045 struct separator {}
18046 deprecated("It was misspelled, use separator instead") alias seperator = separator;
18047 /// Program-wide keyboard shortcut to trigger the action
18048 /// Group: generating_from_code
18049 struct accelerator { string keyString; } // FIXME: allow multiple aliases here
18050 /// tells which menu the action will be on
18051 /// Group: generating_from_code
18052 struct menu { string name; }
18053 /// Describes which toolbar section the action appears on
18054 /// Group: generating_from_code
18055 struct toolbar { string groupName; }
18056 ///
18057 /// Group: generating_from_code
18058 struct icon { ushort id; }
18059 ///
18060 /// Group: generating_from_code
18061 struct label { string label; }
18062 ///
18063 /// Group: generating_from_code
18064 struct hotkey { dchar ch; }
18065 ///
18066 /// Group: generating_from_code
18067 struct tip { string tip; }
18068 ///
18069 /// Group: generating_from_code
18070 enum context_menu = menu.init;
18071 /++
18072 	// FIXME: the options should have both a label and a value
18073 
18074 	if label is null, it will try to just stringify value.
18075 
18076 	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.
18077 +/
18078 /// Group: generating_from_code
18079 Choices!T choices(T)(T[] options, bool allowCustom = false, bool allowReordering = true, bool allowDuplicates = true) {
18080 	return Choices!T(() => options, allowCustom, allowReordering, allowDuplicates);
18081 }
18082 /// ditto
18083 Choices!T choices(T)(T[] delegate() options, bool allowCustom = false, bool allowReordering = true, bool allowDuplicates = true) {
18084 	return Choices!T(options, allowCustom, allowReordering, allowDuplicates);
18085 }
18086 /// ditto
18087 struct Choices(T) {
18088 	///
18089 	T[] delegate() options;
18090 	bool allowCustom = false;
18091 	/// only relevant if attached to an array
18092 	bool allowReordering = true;
18093 	/// ditto
18094 	bool allowDuplicates = true;
18095 	/// makes no sense on a set
18096 	bool requireAll = false;
18097 }
18098 
18099 
18100 /++
18101 	Observes and allows inspection of an object via automatic gui
18102 +/
18103 /// Group: generating_from_code
18104 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) {
18105 	return new ObjectInspectionWindowImpl!(T)(t);
18106 }
18107 
18108 class ObjectInspectionWindow : Window {
18109 	this(int a, int b, string c) {
18110 		super(a, b, c);
18111 	}
18112 
18113 	abstract void readUpdatesFromObject();
18114 }
18115 
18116 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow {
18117 	T t;
18118 	this(T t) {
18119 		this.t = t;
18120 
18121 		super(300, 400, "ObjectInspectionWindow - " ~ T.stringof);
18122 
18123 		foreach(memberName; __traits(derivedMembers, T)) {{
18124 			alias member = I!(__traits(getMember, t, memberName))[0];
18125 			alias type = typeof(member);
18126 			static if(is(type == int)) {
18127 				auto le = new LabeledLineEdit(memberName ~ ": ", this);
18128 				//le.addEventListener("char", (Event ev) {
18129 					//if((ev.character < '0' || ev.character > '9') && ev.character != '-')
18130 						//ev.preventDefault();
18131 				//});
18132 				le.addEventListener(EventType.change, (Event ev) {
18133 					__traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue);
18134 				});
18135 
18136 				updateMemberDelegates[memberName] = () {
18137 					le.content = toInternal!string(__traits(getMember, t, memberName));
18138 				};
18139 			}
18140 		}}
18141 	}
18142 
18143 	void delegate()[string] updateMemberDelegates;
18144 
18145 	override void readUpdatesFromObject() {
18146 		foreach(k, v; updateMemberDelegates)
18147 			v();
18148 	}
18149 }
18150 
18151 /++
18152 	Creates a dialog based on a data structure.
18153 
18154 	---
18155 	dialog(window, (YourStructure value) {
18156 		// the user filled in the struct and clicked OK,
18157 		// you can check the members now
18158 	});
18159 	---
18160 
18161 	Params:
18162 		initialData = the initial value to show in the dialog. It will not modify this unless
18163 		it is a class then it might, no promises.
18164 
18165 	History:
18166 		The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5)
18167 
18168 		The overloads with `parent` were added September 29, 2024. The ones without it are likely to
18169 		be deprecated soon.
18170 +/
18171 /// Group: generating_from_code
18172 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18173 	dialog(null, T.init, onOK, onCancel, title);
18174 }
18175 /// ditto
18176 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18177 	dialog(null, T.init, onOK, onCancel, title);
18178 }
18179 /// ditto
18180 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18181 	dialog(parent, T.init, onOK, onCancel, title);
18182 }
18183 /// ditto
18184 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18185 	dialog(parent, initialData, onOK, onCancel, title);
18186 }
18187 /// ditto
18188 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18189 	auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title);
18190 	dg.show();
18191 }
18192 
18193 private static template I(T...) { alias I = T; }
18194 
18195 
18196 private string beautify(string name, char space = ' ', bool allLowerCase = false) {
18197 	if(name == "id")
18198 		return allLowerCase ? name : "ID";
18199 
18200 	char[160] buffer;
18201 	int bufferIndex = 0;
18202 	bool shouldCap = true;
18203 	bool shouldSpace;
18204 	bool lastWasCap;
18205 	foreach(idx, char ch; name) {
18206 		if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
18207 
18208 		if((ch >= 'A' && ch <= 'Z') || ch == '_') {
18209 			if(lastWasCap) {
18210 				// two caps in a row, don't change. Prolly acronym.
18211 			} else {
18212 				if(idx)
18213 					shouldSpace = true; // new word, add space
18214 			}
18215 
18216 			lastWasCap = true;
18217 		} else {
18218 			lastWasCap = false;
18219 		}
18220 
18221 		if(shouldSpace) {
18222 			buffer[bufferIndex++] = space;
18223 			if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
18224 			shouldSpace = false;
18225 		}
18226 		if(shouldCap) {
18227 			if(ch >= 'a' && ch <= 'z')
18228 				ch -= 32;
18229 			shouldCap = false;
18230 		}
18231 		if(allLowerCase && ch >= 'A' && ch <= 'Z')
18232 			ch += 32;
18233 		buffer[bufferIndex++] = ch;
18234 	}
18235 	return buffer[0 .. bufferIndex].idup;
18236 }
18237 
18238 /++
18239 	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.
18240 +/
18241 class AutomaticDialog(T) : Dialog {
18242 	T t;
18243 
18244 	void delegate(T) onOK;
18245 	void delegate() onCancel;
18246 
18247 	override int paddingTop() { return defaultLineHeight; }
18248 	override int paddingBottom() { return defaultLineHeight; }
18249 	override int paddingRight() { return defaultLineHeight; }
18250 	override int paddingLeft() { return defaultLineHeight; }
18251 
18252 	this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) {
18253 		assert(onOK !is null);
18254 
18255 		t = initialData;
18256 
18257 		static if(is(T == class)) {
18258 			if(t is null)
18259 				t = new T();
18260 		}
18261 		this.onOK = onOK;
18262 		this.onCancel = onCancel;
18263 		super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title);
18264 
18265 		static if(is(T == class))
18266 			this.addDataControllerWidget(t);
18267 		else
18268 			this.addDataControllerWidget(&t);
18269 
18270 		auto hl = new HorizontalLayout(this);
18271 		auto stretch = new HorizontalSpacer(hl); // to right align
18272 		auto ok = new CommandButton("OK", hl);
18273 		auto cancel = new CommandButton("Cancel", hl);
18274 		ok.addEventListener(EventType.triggered, &OK);
18275 		cancel.addEventListener(EventType.triggered, &Cancel);
18276 
18277 		this.addEventListener((KeyDownEvent ev) {
18278 			if(ev.key == Key.Enter || ev.key == Key.PadEnter) {
18279 				ok.focus();
18280 				OK();
18281 				ev.preventDefault();
18282 			}
18283 			if(ev.key == Key.Escape) {
18284 				Cancel();
18285 				ev.preventDefault();
18286 			}
18287 		});
18288 
18289 		this.addEventListener((scope ClosedEvent ce) {
18290 			if(onCancel)
18291 				onCancel();
18292 		});
18293 
18294 		//this.children[0].focus();
18295 	}
18296 
18297 	override void OK() {
18298 		onOK(t);
18299 		close();
18300 	}
18301 
18302 	override void Cancel() {
18303 		if(onCancel)
18304 			onCancel();
18305 		close();
18306 	}
18307 }
18308 
18309 private template baseClassCount(Class) {
18310 	private int helper() {
18311 		int count = 0;
18312 		static if(is(Class bases == super)) {
18313 			foreach(base; bases)
18314 				static if(is(base == class))
18315 					count += 1 + baseClassCount!base;
18316 		}
18317 		return count;
18318 	}
18319 
18320 	enum int baseClassCount = helper();
18321 }
18322 
18323 private long stringToLong(string s) {
18324 	long ret;
18325 	if(s.length == 0)
18326 		return ret;
18327 	bool negative = s[0] == '-';
18328 	if(negative)
18329 		s = s[1 .. $];
18330 	foreach(ch; s) {
18331 		if(ch >= '0' && ch <= '9') {
18332 			ret *= 10;
18333 			ret += ch - '0';
18334 		}
18335 	}
18336 	if(negative)
18337 		ret = -ret;
18338 	return ret;
18339 }
18340 
18341 
18342 interface ReflectableProperties {
18343 	/++
18344 		Iterates the event's properties as strings. Note that keys may be repeated and a get property request may
18345 		call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by
18346 		json in the current implementation.
18347 
18348 		This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have
18349 		properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself
18350 		as of the June 2, 2021 release.
18351 
18352 		History:
18353 			Added June 2, 2021.
18354 
18355 		See_Also: [getPropertyAsString], [setPropertyFromString]
18356 	+/
18357 	void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow;
18358 	/++
18359 		Requests a property to be delivered to you as a string, through your `sink` delegate.
18360 
18361 		If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should
18362 		be interpreted as json, otherwise, it is just a plain string.
18363 
18364 		The sink should always be called exactly once for each call (it is basically a return value, but it might
18365 		use a local buffer it maintains instead of allocating a return value).
18366 
18367 		History:
18368 			Added June 2, 2021.
18369 
18370 		See_Also: [getPropertiesList], [setPropertyFromString]
18371 	+/
18372 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink);
18373 	/++
18374 		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.
18375 
18376 		History:
18377 			Added June 2, 2021.
18378 
18379 		See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult]
18380 	+/
18381 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson);
18382 
18383 	/// [setPropertyFromString] possible return values
18384 	enum SetPropertyResult {
18385 		success = 0, /// the property has been successfully set to the request value
18386 		notPermitted = -1, /// the property exists but it cannot be changed at this time
18387 		notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist)
18388 		noSuchProperty = -3, /// there is no property by that name
18389 		wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value
18390 		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)
18391 	}
18392 
18393 	/++
18394 		You can mix this in to get an implementation in child classes. This does [setPropertyFromString].
18395 
18396 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
18397 
18398 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
18399 		rarely need to use these building blocks directly.
18400 	+/
18401 	mixin template RegisterSetters() {
18402 		override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
18403 			switch(name) {
18404 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
18405 					case memberName:
18406 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
18407 							if(value != "true" && value != "false")
18408 								return SetPropertyResult.wrongFormat;
18409 							__traits(getMember, this, memberName) = value == "true" ? true : false;
18410 							return SetPropertyResult.success;
18411 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
18412 							import core.stdc.stdlib;
18413 							char[128] zero = 0;
18414 							if(buffer.length + 1 >= zero.length)
18415 								return SetPropertyResult.wrongFormat;
18416 							zero[0 .. buffer.length] = buffer[];
18417 							__traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10);
18418 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
18419 							import core.stdc.stdlib;
18420 							char[128] zero = 0;
18421 							if(buffer.length + 1 >= zero.length)
18422 								return SetPropertyResult.wrongFormat;
18423 							zero[0 .. buffer.length] = buffer[];
18424 							__traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10);
18425 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
18426 							__traits(getMember, this, memberName) = value.idup;
18427 						} else {
18428 							return SetPropertyResult.notImplemented;
18429 						}
18430 
18431 				}
18432 				default:
18433 					return super.setPropertyFromString(name, value, valueIsJson);
18434 			}
18435 		}
18436 	}
18437 
18438 	/++
18439 		You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList].
18440 
18441 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
18442 
18443 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
18444 		rarely need to use these building blocks directly.
18445 	+/
18446 	mixin template RegisterGetters() {
18447 		override void getPropertiesList(scope void delegate(string name) sink) const {
18448 			super.getPropertiesList(sink);
18449 
18450 			foreach(memberName; __traits(derivedMembers, typeof(this))) {
18451 				sink(memberName);
18452 			}
18453 		}
18454 		override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
18455 			switch(name) {
18456 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
18457 					case memberName:
18458 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
18459 							sink(name, __traits(getMember, this, memberName) ? "true" : "false", true);
18460 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
18461 							import core.stdc.stdio;
18462 							char[32] buffer;
18463 							auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName));
18464 							sink(name, buffer[0 .. len], true);
18465 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
18466 							import core.stdc.stdio;
18467 							char[32] buffer;
18468 							auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName));
18469 							sink(name, buffer[0 .. len], true);
18470 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
18471 							sink(name, __traits(getMember, this, memberName), false);
18472 							//sinkJsonString(memberName, __traits(getMember, this, memberName), sink);
18473 						} else {
18474 							sink(name, null, true);
18475 						}
18476 
18477 					return;
18478 				}
18479 				default:
18480 					return super.getPropertyAsString(name, sink);
18481 			}
18482 		}
18483 	}
18484 }
18485 
18486 private struct Stack(T) {
18487 	this(int maxSize) {
18488 		internalLength = 0;
18489 		arr = initialBuffer[];
18490 	}
18491 
18492 	///.
18493 	void push(T t) {
18494 		if(internalLength >= arr.length) {
18495 			auto oldarr = arr;
18496 			if(arr.length < 4096)
18497 				arr = new T[arr.length * 2];
18498 			else
18499 				arr = new T[arr.length + 4096];
18500 			arr[0 .. oldarr.length] = oldarr[];
18501 		}
18502 
18503 		arr[internalLength] = t;
18504 		internalLength++;
18505 	}
18506 
18507 	///.
18508 	T pop() {
18509 		assert(internalLength);
18510 		internalLength--;
18511 		return arr[internalLength];
18512 	}
18513 
18514 	///.
18515 	T peek() {
18516 		assert(internalLength);
18517 		return arr[internalLength - 1];
18518 	}
18519 
18520 	///.
18521 	@property bool empty() {
18522 		return internalLength ? false : true;
18523 	}
18524 
18525 	///.
18526 	private T[] arr;
18527 	private size_t internalLength;
18528 	private T[64] initialBuffer;
18529 	// 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),
18530 	// using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push()
18531 	// function thanks to this, and push() was actually one of the slowest individual functions in the code!
18532 }
18533 
18534 /// 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.
18535 private struct WidgetStream {
18536 
18537 	///.
18538 	@property Widget front() {
18539 		return current.widget;
18540 	}
18541 
18542 	/// Use Widget.tree instead.
18543 	this(Widget start) {
18544 		current.widget = start;
18545 		current.childPosition = -1;
18546 		isEmpty = false;
18547 		stack = typeof(stack)(0);
18548 	}
18549 
18550 	/*
18551 		Handle it
18552 		handle its children
18553 
18554 	*/
18555 
18556 	///.
18557 	void popFront() {
18558 	    more:
18559 	    	if(isEmpty) return;
18560 
18561 		// FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times)
18562 
18563 		current.childPosition++;
18564 		if(current.childPosition >= current.widget.children.length) {
18565 			if(stack.empty())
18566 				isEmpty = true;
18567 			else {
18568 				current = stack.pop();
18569 				goto more;
18570 			}
18571 		} else {
18572 			stack.push(current);
18573 			current.widget = current.widget.children[current.childPosition];
18574 			current.childPosition = -1;
18575 		}
18576 	}
18577 
18578 	///.
18579 	@property bool empty() {
18580 		return isEmpty;
18581 	}
18582 
18583 	private:
18584 
18585 	struct Current {
18586 		Widget widget;
18587 		int childPosition;
18588 	}
18589 
18590 	Current current;
18591 
18592 	Stack!(Current) stack;
18593 
18594 	bool isEmpty;
18595 }
18596 
18597 
18598 /+
18599 
18600 	I could fix up the hierarchy kinda like this
18601 
18602 	class Widget {
18603 		Widget[] children() { return null; }
18604 	}
18605 	interface WidgetContainer {
18606 		Widget asWidget();
18607 		void addChild(Widget w);
18608 
18609 		// alias asWidget this; // but meh
18610 	}
18611 
18612 	Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead.
18613 
18614 	class Layout : Widget, WidgetContainer {}
18615 
18616 	class Window : WidgetContainer {}
18617 
18618 
18619 	All constructors that previously took Widgets should now take WidgetContainers instead
18620 
18621 
18622 
18623 	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".
18624 +/
18625 
18626 /+
18627 	LAYOUTS 2.0
18628 
18629 	can just be assigned as a function. assigning a new one will cause it to be immediately called.
18630 
18631 	they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one.
18632 
18633 	recomputeChildLayout only really needs a property accessor proxy... just the layout info too.
18634 
18635 	and even Paint can just use computedStyle...
18636 
18637 		background color
18638 		font
18639 		border color and style
18640 
18641 	And actually the style proxy can offer some helper routines to draw these like the draw 3d box
18642 		please note that many widgets and in some modes will completely ignore properties as they will.
18643 		they are just hints you set, not promises.
18644 
18645 
18646 
18647 
18648 
18649 	So generally the existing virtual functions are just the default for the class. But individual objects
18650 	or stylesheets can override this. The virtual ones count as tag-level specificity in css.
18651 +/
18652 
18653 /++
18654 	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.
18655 
18656 	History:
18657 		Added May 24, 2021.
18658 +/
18659 struct WidgetBackground {
18660 	/++
18661 		A background with the given solid color.
18662 	+/
18663 	this(Color color) {
18664 		this.color = color;
18665 	}
18666 
18667 	this(WidgetBackground bg) {
18668 		this = bg;
18669 	}
18670 
18671 	/++
18672 		Creates a widget from the string.
18673 
18674 		Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css.
18675 	+/
18676 	static WidgetBackground fromString(string s) {
18677 		return WidgetBackground(Color.fromString(s));
18678 	}
18679 
18680 	/++
18681 		The background is not necessarily a solid color, but you can always specify a color as a fallback.
18682 
18683 		History:
18684 			Made `public` on December 18, 2022 (dub v10.10).
18685 	+/
18686 	Color color;
18687 }
18688 
18689 /++
18690 	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!)
18691 
18692 	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.
18693 
18694 	You should not inherit from this directly, but instead use [VisualTheme].
18695 
18696 	History:
18697 		Added May 8, 2021
18698 +/
18699 abstract class BaseVisualTheme {
18700 	/// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override.
18701 	abstract void doPaint(Widget widget, WidgetPainter painter);
18702 
18703 	/+
18704 	/// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override.
18705 	abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg);
18706 	+/
18707 
18708 	/++
18709 		Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css,
18710 		where the interpretation of the string varies for each property and may include things like measurement units.
18711 	+/
18712 	abstract string getPropertyString(Widget widget, string propertyName);
18713 
18714 	/++
18715 		Default background color of the window. Widgets also use this to simulate transparency.
18716 
18717 		Probably some shade of grey.
18718 	+/
18719 	abstract Color windowBackgroundColor();
18720 	abstract Color widgetBackgroundColor();
18721 	abstract Color foregroundColor();
18722 	abstract Color lightAccentColor();
18723 	abstract Color darkAccentColor();
18724 
18725 	/++
18726 		Colors used to indicate active selections in lists and text boxes, etc.
18727 	+/
18728 	abstract Color selectionForegroundColor();
18729 	/// ditto
18730 	abstract Color selectionBackgroundColor();
18731 
18732 	deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); }
18733 
18734 	/++
18735 		If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally.
18736 	+/
18737 	abstract OperatingSystemFont defaultFont(int dpi);
18738 
18739 	private OperatingSystemFont[int] defaultFontCache_;
18740 	private OperatingSystemFont defaultFontCached(int dpi) {
18741 		if(dpi !in defaultFontCache_) {
18742 			// FIXME: set this to false if X disconnect or if visual theme changes
18743 			defaultFontCache_[dpi] = defaultFont(dpi);
18744 		}
18745 		return defaultFontCache_[dpi];
18746 	}
18747 }
18748 
18749 /+
18750 	A widget should have:
18751 		classList
18752 		dataset
18753 		attributes
18754 		computedStyles
18755 		state (persistent)
18756 		dynamic state (focused, hover, etc)
18757 +/
18758 
18759 // visualTheme.computedStyle(this).paddingLeft
18760 
18761 
18762 /++
18763 	This is your entry point to create your own visual theme for custom widgets.
18764 
18765 	You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods.
18766 
18767 	Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating.
18768 +/
18769 abstract class VisualTheme(CRTP) : BaseVisualTheme {
18770 	override string getPropertyString(Widget widget, string propertyName) {
18771 		return null;
18772 	}
18773 
18774 	/+
18775 		mixin StyleOverride!Widget
18776 	final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) {
18777 		w.useStyleProperties(dg);
18778 	}
18779 	+/
18780 
18781 	final override void doPaint(Widget widget, WidgetPainter painter) {
18782 		auto derived = cast(CRTP) cast(void*) this;
18783 
18784 		scope void delegate(Widget, WidgetPainter) bestMatch;
18785 		int bestMatchScore;
18786 
18787 		static if(__traits(hasMember, CRTP, "paint"))
18788 		foreach(overload; __traits(getOverloads, CRTP, "paint")) {
18789 			static if(is(typeof(overload) Params == __parameters)) {
18790 				static assert(Params.length == 2);
18791 				static assert(is(Params[0] : Widget));
18792 				static assert(is(Params[1] == WidgetPainter));
18793 				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);
18794 
18795 				alias type = Params[0];
18796 				if(cast(type) widget) {
18797 					auto score = baseClassCount!type;
18798 
18799 					if(score > bestMatchScore) {
18800 						bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload);
18801 						bestMatchScore = score;
18802 					}
18803 				}
18804 			} else static assert(0, "paint should be a method.");
18805 		}
18806 
18807 		if(bestMatch)
18808 			bestMatch(widget, painter);
18809 		else
18810 			widget.paint(painter);
18811 	}
18812 
18813 	deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; }
18814 
18815 	// 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
18816 	// mixin Beautiful95Theme;
18817 	mixin DefaultLightTheme;
18818 
18819 	private static struct Cached {
18820 		// i prolly want to do this
18821 	}
18822 }
18823 
18824 /// ditto
18825 mixin template Beautiful95Theme() {
18826 	override Color windowBackgroundColor() { return Color(212, 212, 212); }
18827 	override Color widgetBackgroundColor() { return Color.white; }
18828 	override Color foregroundColor() { return Color.black; }
18829 	override Color darkAccentColor() { return Color(172, 172, 172); }
18830 	override Color lightAccentColor() { return Color(223, 223, 223); }
18831 	override Color selectionForegroundColor() { return Color.white; }
18832 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
18833 	override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr
18834 }
18835 
18836 /// ditto
18837 mixin template DefaultLightTheme() {
18838 	override Color windowBackgroundColor() { return Color(232, 232, 232); }
18839 	override Color widgetBackgroundColor() { return Color.white; }
18840 	override Color foregroundColor() { return Color.black; }
18841 	override Color darkAccentColor() { return Color(172, 172, 172); }
18842 	override Color lightAccentColor() { return Color(223, 223, 223); }
18843 	override Color selectionForegroundColor() { return Color.white; }
18844 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
18845 	override OperatingSystemFont defaultFont(int dpi) {
18846 		version(Windows)
18847 			return new OperatingSystemFont("Segoe UI");
18848 		else static if(UsingSimpledisplayCocoa) {
18849 			return (new OperatingSystemFont()).loadDefault;
18850 		} else {
18851 			// FIXME: undo xft's scaling so we don't end up double scaled
18852 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
18853 		}
18854 	}
18855 }
18856 
18857 /// ditto
18858 mixin template DefaultDarkTheme() {
18859 	override Color windowBackgroundColor() { return Color(64, 64, 64); }
18860 	override Color widgetBackgroundColor() { return Color.black; }
18861 	override Color foregroundColor() { return Color.white; }
18862 	override Color darkAccentColor() { return Color(20, 20, 20); }
18863 	override Color lightAccentColor() { return Color(80, 80, 80); }
18864 	override Color selectionForegroundColor() { return Color.white; }
18865 	override Color selectionBackgroundColor() { return Color(128, 0, 128); }
18866 	override OperatingSystemFont defaultFont(int dpi) {
18867 		version(Windows)
18868 			return new OperatingSystemFont("Segoe UI", 12);
18869 		else static if(UsingSimpledisplayCocoa) {
18870 			return (new OperatingSystemFont()).loadDefault;
18871 		} else {
18872 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
18873 		}
18874 	}
18875 }
18876 
18877 /// ditto
18878 alias DefaultTheme = DefaultLightTheme;
18879 
18880 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme {
18881 	/+
18882 	OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); }
18883 	Color windowBackgroundColor() { return Color(242, 242, 242); }
18884 	Color darkAccentColor() { return windowBackgroundColor; }
18885 	Color lightAccentColor() { return windowBackgroundColor; }
18886 	+/
18887 }
18888 
18889 /++
18890 	Event fired when an [Observeable] variable changes. You will want to add an event listener referencing
18891 	the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });`
18892 
18893 	History:
18894 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
18895 
18896 		Made `final` on January 3, 2025
18897 +/
18898 final class StateChanged(alias field) : Event {
18899 	enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change";
18900 	override bool cancelable() const { return false; }
18901 	this(Widget target, typeof(field) newValue) {
18902 		this.newValue = newValue;
18903 		super(EventString, target);
18904 	}
18905 
18906 	typeof(field) newValue;
18907 }
18908 
18909 /++
18910 	Convenience function to add a `triggered` event listener.
18911 
18912 	Its implementation is simply `w.addEventListener("triggered", dg);`
18913 
18914 	History:
18915 		Added November 27, 2021 (dub v10.4)
18916 +/
18917 void addWhenTriggered(Widget w, void delegate() dg) {
18918 	w.addEventListener("triggered", dg);
18919 }
18920 
18921 /++
18922 	Observable variables can be added to widgets and when they are changed, it fires
18923 	off a [StateChanged] event so you can react to it.
18924 
18925 	It is implemented as a getter and setter property, along with another helper you
18926 	can use to subscribe with is `name_changed`. You can also subscribe to the [StateChanged]
18927 	event through the usual means. Just give the name of the variable. See [StateChanged] for an
18928 	example.
18929 
18930 	To get an `ObservableReference` to the observable, use `&yourname_changed`.
18931 
18932 	History:
18933 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
18934 
18935 		As of March 5, 2025, the changed function now returns an [EventListener] handle, which
18936 		you can use to disconnect the observer.
18937 +/
18938 mixin template Observable(T, string name) {
18939 	private T backing;
18940 
18941 	mixin(q{
18942 		EventListener } ~ name ~ q{_changed (void delegate(T) dg) {
18943 			return this.addEventListener((StateChanged!this_thing ev) {
18944 				dg(ev.newValue);
18945 			});
18946 		}
18947 
18948 		@property T } ~ name ~ q{ () {
18949 			return backing;
18950 		}
18951 
18952 		@property void } ~ name ~ q{ (T t) {
18953 			backing = t;
18954 			auto event = new StateChanged!this_thing(this, t);
18955 			event.dispatch();
18956 		}
18957 	});
18958 
18959 	mixin("private alias this_thing = " ~ name ~ ";");
18960 }
18961 
18962 /// ditto
18963 alias ObservableReference(T) = EventListener delegate(void delegate(T));
18964 
18965 private bool startsWith(string test, string thing) {
18966 	if(test.length < thing.length)
18967 		return false;
18968 	return test[0 .. thing.length] == thing;
18969 }
18970 
18971 private bool endsWith(string test, string thing) {
18972 	if(test.length < thing.length)
18973 		return false;
18974 	return test[$ - thing.length .. $] == thing;
18975 }
18976 
18977 /++
18978 	Context menus can have `@hotkey`, `@label`, `@tip`, `@separator`, and `@icon`
18979 
18980 	Note they can NOT have accelerators or toolbars; those annotations will be ignored.
18981 
18982 	Mark the functions callable from it with `@context_menu { ... }` Presence of other `@menu(...)` annotations will exclude it from the context menu at this time.
18983 
18984 	See_Also:
18985 		[Widget.setMenuAndToolbarFromAnnotatedCode]
18986 +/
18987 Menu createContextMenuFromAnnotatedCode(TWidget)(TWidget w) if(is(TWidget : Widget)) {
18988 	return createContextMenuFromAnnotatedCode(w, w);
18989 }
18990 
18991 /// ditto
18992 Menu createContextMenuFromAnnotatedCode(T)(Widget w, ref T t) if(!is(T == class) && !is(T == interface)) {
18993 	return createContextMenuFromAnnotatedCode_internal(w, t);
18994 }
18995 /// ditto
18996 Menu createContextMenuFromAnnotatedCode(T)(Widget w, T t) if(is(T == class) || is(T == interface)) {
18997 	return createContextMenuFromAnnotatedCode_internal(w, t);
18998 }
18999 Menu createContextMenuFromAnnotatedCode_internal(T)(Widget w, ref T t) {
19000 	Menu ret = new Menu("", w);
19001 
19002 	foreach(memberName; __traits(derivedMembers, T)) {
19003 		static if(memberName != "this")
19004 		static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
19005 			.menu menu;
19006 			bool separator;
19007 			.hotkey hotkey;
19008 			.icon icon;
19009 			string label;
19010 			string tip;
19011 			foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
19012 				static if(is(typeof(attr) == .menu))
19013 					menu = attr;
19014 				else static if(is(attr == .separator))
19015 					separator = true;
19016 				else static if(is(typeof(attr) == .hotkey))
19017 					hotkey = attr;
19018 				else static if(is(typeof(attr) == .icon))
19019 					icon = attr;
19020 				else static if(is(typeof(attr) == .label))
19021 					label = attr.label;
19022 				else static if(is(typeof(attr) == .tip))
19023 					tip = attr.tip;
19024 			}
19025 
19026 			if(menu is .menu.init) {
19027 				ushort correctIcon = icon.id; // FIXME
19028 				if(label.length == 0)
19029 					label = memberName.toMenuLabel;
19030 
19031 				auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(w.parentWindow, &__traits(getMember, t, memberName));
19032 
19033 				auto action = new Action(label, correctIcon, handler);
19034 
19035 				if(separator)
19036 					ret.addSeparator();
19037 					ret.addItem(new MenuItem(action));
19038 			}
19039 		}
19040 	}
19041 
19042 	return ret;
19043 }
19044 
19045 // still do layout delegation
19046 // and... split off Window from Widget.
19047 
19048 version(minigui_screenshots)
19049 struct Screenshot {
19050 	string name;
19051 }
19052 
19053 version(minigui_screenshots)
19054 static if(__VERSION__ > 2092)
19055 mixin(q{
19056 shared static this() {
19057 	import core.runtime;
19058 
19059 	static UnitTestResult screenshotMagic() {
19060 		string name;
19061 
19062 		import arsd.png;
19063 
19064 		auto results = new Window();
19065 		auto button = new Button("do it", results);
19066 
19067 		Window.newWindowCreated = delegate(Window w) {
19068 			Timer timer;
19069 			timer = new Timer(250, {
19070 				auto img = w.win.takeScreenshot();
19071 				timer.destroy();
19072 
19073 				version(Windows)
19074 					writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img);
19075 				else
19076 					writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img);
19077 
19078 				w.close();
19079 			});
19080 		};
19081 
19082 		button.addWhenTriggered( {
19083 
19084 		foreach(test; __traits(getUnitTests, mixin("arsd.minigui"))) {
19085 			name = null;
19086 			static foreach(attr; __traits(getAttributes, test)) {
19087 				static if(is(typeof(attr) == Screenshot))
19088 					name = attr.name;
19089 			}
19090 			if(name.length) {
19091 				test();
19092 			}
19093 		}
19094 
19095 		});
19096 
19097 		results.loop();
19098 
19099 		return UnitTestResult(0, 0, false, false);
19100 	}
19101 
19102 
19103 	Runtime.extendedModuleUnitTester = &screenshotMagic;
19104 }
19105 });
19106 version(minigui_screenshots) {
19107 	version(unittest)
19108 		void main() {}
19109 	else static assert(0, "dont forget the -unittest flag to dmd");
19110 }
19111 
19112 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut.
19113 // FIXME: make multiple accelerators disambiguate based ona rgs
19114 // FIXME: MainWindow ctor should have same arg order as Window
19115 // FIXME: mainwindow ctor w/ client area size instead of total size.
19116 // 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.
19117 // FIXME: tri-state checkbox
19118 // FIXME: subordinate controls grouping...