The OpenD Programming Language

1 /+
2 	BreakpointSplitter
3 		- if not all widgets fit, it collapses to tabs
4 		- if they do, you get a splitter
5 		- you set priority to display things first and optional breakpoint (otherwise it uses flex basis and min width)
6 +/
7 
8 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx
9 
10 // if doing nested menus, make sure the straight line from where it pops up to any destination on the new popup is not going to disappear the menu until at least a delay
11 
12 // me@arsd:~/.kde/share/config$ vim kdeglobals
13 
14 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too.
15 
16 // https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/
17 
18 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever.
19 
20 // responsive minigui, menu search, and file open with a preview hook on the side.
21 
22 // FIXME: add menu checkbox and menu icon eventually
23 
24 // FIXME: checkbox menus and submenus and stuff
25 
26 // FOXME: look at Windows rebar control too
27 
28 /*
29 
30 im tempted to add some css kind of thing to minigui. i've not done in the past cuz i have a lot of virtual functins i use but i think i have an evil plan
31 
32 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it...
33 */
34 
35 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places
36 
37 // FIXME: text label must be copyable to the clipboard, at least as a full chunk.
38 
39 // FIXME: opt-in file picker widget with image support
40 
41 // FIXME: number widget
42 
43 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm
44 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles
45 
46 // osx style menu search.
47 
48 // would be cool for a scroll bar to have marking capabilities
49 // kinda like vim's marks just on clicks etc and visual representation
50 // generically. may be cool to add an up arrow to the bottom too
51 //
52 // leave a shadow of where you last were for going back easily
53 
54 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various
55 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for
56 // the window.
57 
58 // so what about context menus?
59 
60 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw
61 
62 // FIXME: make the scroll thing go to bottom when the content changes.
63 
64 // add a knob slider view... you click and go up and down so basically same as a vertical slider, just presented as a round image
65 
66 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood.
67 
68 
69 // FIXME: add a command search thingy built in and implement tip.
70 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?!
71 
72 // On Windows:
73 // FIXME: various labels look broken in high contrast mode
74 // FIXME: changing themes while the program is upen doesn't trigger a redraw
75 
76 // add note about manifest to documentation. also icons.
77 
78 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar
79 // FIXME: clear the corner of scrollbars if they pop up
80 
81 // minigui needs to have a stdout redirection for gui mode on windows writeln
82 
83 // I kinda wanna do state reacting. sort of. idk tho
84 
85 // need a viewer widget that works like a web page - arrows scroll down consistently
86 
87 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside.
88 
89 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two.
90 // and help info about menu items.
91 // and search in menus?
92 
93 // FIXME: a scroll area event signaling when a thing comes into view might be good
94 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must
95 
96 // FIXME: unify Windows style line endings
97 
98 /*
99 	TODO:
100 
101 	pie menu
102 
103 	class Form with submit behavior -- see AutomaticDialog
104 
105 	disabled widgets and menu items
106 
107 	event cleanup
108 	tooltips.
109 	api improvements
110 
111 	margins are kinda broken, they don't collapse like they should. at least.
112 
113 	a table form btw would be a horizontal layout of vertical layouts holding each column
114 	that would give the same width things
115 */
116 
117 /*
118 
119 1(15:19:48) NotSpooky: Menus, text entry, label, notebook, box, frame, file dialogs and layout (this one is very useful because I can draw lines between its child widgets
120 */
121 
122 /++
123 	minigui is a smallish GUI widget library, aiming to be on par with at least
124 	HTML4 forms and a few other expected gui components. It uses native controls
125 	on Windows and does its own thing on Linux (Mac is not currently supported but
126 	I'm slowly working on it).
127 
128 
129 	$(H3 Conceptual Overviews)
130 
131 	A gui application is made out of widgets laid out in windows that display information and respond to events from the user. They also typically have actions available in menus, and you might also want to customize the appearance. How do we do these things with minigui? Let's break it down into several categories.
132 
133 	$(H4 Code structure)
134 
135 	You will typically want to create the ui, prepare event handlers, then run an event loop. The event loop drives the program, calling your methods to respond to user activity.
136 
137 	---
138 	import arsd.minigui;
139 
140 	void main() {
141 		// first, create a window, the (optional) string here is its title
142 		auto window = new MainWindow("Hello, World!");
143 
144 		// lay out some widgets inside the window to create the ui
145 		auto name = new LabeledLineEdit("What is your name?", window);
146 		auto button = new Button("Say Hello", window);
147 
148 		// prepare event handlers
149 		button.addEventListener(EventType.triggered, () {
150 			window.messageBox("Hello, " ~ name.content ~ "!");
151 		});
152 
153 		// show the window and run the event loop until this window is closed
154 		window.loop();
155 	}
156 	---
157 
158 	To compile, run `opend hello.d`, then run the generated `hello` program.
159 
160 	While the specifics will change, nearly all minigui applications will roughly follow this pattern.
161 
162 	$(TIP
163 		There are two other ways to run event loops: `arsd.simpledisplay.EventLoop.get.run();` and `arsd.core.getThisThreadEventLoop().run();`. They all call the same underlying functions, but have different exit conditions - the `EventLoop.get.run()` keeps running until all top-level windows are closed, and `getThisThreadEventLoop().run` keeps running until all "tasks are resolved"; it is more abstract, supporting more than just windows.
164 
165 		You may call this if you don't have a single main window.
166 
167 		Even a basic minigui window can benefit from these if you don't have a single main window:
168 
169 		---
170 		import arsd.minigui;
171 
172 		void main() {
173 			// create a struct to hold gathered info
174 			struct Hello { string name; }
175 			// let minigui create a dialog box to get that
176 			// info from the user. If you have a main window,
177 			// you'd pass that here, but it is not required
178 			dialog((Hello info) {
179 				// inline handler of the "OK" button
180 				messageBox("Hello, " ~ info.name);
181 			});
182 
183 			// since there is no main window to loop on,
184 			// we instead call the event loop singleton ourselves
185 			EventLoop.get.run;
186 		}
187 		---
188 
189 		This is also useful when your programs lives as a notification area (aka systray) icon instead of as a window. But let's not get too far ahead of ourselves!
190 	)
191 
192 	$(H4 How to lay out widgets)
193 
194 	To better understand the details of layout algorithms and see more available included classes, see [Layout].
195 
196 	$(H5 Default layouts)
197 
198 	minigui windows default to a flexible vertical layout, where widgets are added, from top to bottom on the window, in the same order of you creating them, then they are sized according to layout hints on the widget itself to fill the available space. This gives a reasonably usable setup but you'll probably want to customize it.
199 
200 	$(TIP
201 		minigui's default [VerticalLayout] and [HorizontalLayout] are roughly based on css flexbox with wrap turned off.
202 	)
203 
204 	Generally speaking, there are two ways to customize layouts: either subclass the widget and change its hints, or wrap it in another layout widget. You can also create your own layout classes and do it all yourself, but that's fairly complicated. Wrapping existing widgets in other layout widgets is usually the easiest way to make things work.
205 
206 	$(NOTE
207 		minigui widgets are not supposed to overlap, but can contain children, and are always rectangular. Children are laid out as rectangles inside the parent's rectangular area.
208 	)
209 
210 	For example, to display two widgets side-by-side, you can wrap them in a [HorizontalLayout]:
211 
212 	---
213 	import arsd.minigui;
214 	void main() {
215 		auto window = new MainWindow();
216 
217 		// make the layout a child of our window
218 		auto hl = new HorizontalLayout(window);
219 
220 		// then make the widgets children of the layout
221 		auto leftButton = new Button("Left", hl);
222 		auto rightButton = new Button("Right", hl);
223 
224 		window.loop();
225 	}
226 	---
227 
228 	A [HorizontalLayout] works just like the default [VerticalLayout], except in the other direction. These two buttons will take up all the available vertical space, then split available horizontal space equally.
229 
230 	$(H5 Nesting layouts)
231 
232 	Nesting layouts lets you carve up the rectangle in different ways.
233 
234 	$(EMBED_UNITTEST layout-example)
235 
236 	$(H5 Special layouts)
237 
238 	[TabWidget] can show pages of layouts as tabs.
239 
240 	See [ScrollableWidget] but be warned that it is weird. You might want to consider something like [GenericListViewWidget] instead.
241 
242 	$(H5 Other common layout classes)
243 
244 	[HorizontalLayout], [VerticalLayout], [InlineBlockLayout], [GridLayout]
245 
246 	$(H4 How to respond to widget events)
247 
248 	To better understanding the underlying event system, see [Event].
249 
250 	Each widget emits its own events, which propagate up through their parents until they reach their top-level window.
251 
252 	$(H4 How to do overall ui - title, icons, menus, toolbar, hotkeys, statuses, etc.)
253 
254 	We started this series with a [MainWindow], but only added widgets to it. MainWindows also support menus and toolbars with various keyboard shortcuts. You can construct these menus by constructing classes and calling methods, but minigui also lets you just write functions in a command object and it does the rest!
255 
256 	See [MainWindow.setMenuAndToolbarFromAnnotatedCode] for an example.
257 
258 	Note that toggleable menu or toolbar items are not yet implemented, but on the todolist. Submenus and disabled items are also not supported at this time and not currently on the work list (but if you need it, let me know and MAYBE we can work something out. Emphasis on $(I maybe)).
259 
260 	$(TIP
261 		The automatic dialog box logic is also available for you to invoke on demand with [dialog] and the data setting logic can be used with a child widget inside an existing window [addDataControllerWidget], which also has annotation-based layout capabilities.
262 	)
263 
264 	All windows also have titles. You can change this at any time with the `window.title = "string";` property.
265 
266 	Windows also have icons, which can be set with the `window.icon` property. It takes a [arsd.color.MemoryImage] object, which is an in-memory bitmap. [arsd.image] can load common file formats into these objects, or you can make one yourself. The default icon on Windows is the icon of your exe, which you can set through a resource file. (FIXME: explain how to do this easily.)
267 
268 	The `MainWindow` also provides a status bar across the bottom. These aren't so common in new applications, but I love them - on my own computer, I even have a global status bar for my whole desktop! I suggest you use it: a status bar is a consistent place to put information and notifications that will never overlap other content.
269 
270 	A status bar has parts, and the parts have content. The first part's content is assumed to change frequently; the default mouse over event will set it to [Widget.statusTip], a public `string` you can assign to any widget you want at any time.
271 
272 	Other parts can be added by you and are under your control. You add them with:
273 
274 	---
275 	window.statusBar.parts ~= StatusBar.Part(optional_size, optional_units);
276 	---
277 
278 	The size can be in a variety of units and what you get with mixes can get complicated. The rule is: explicit pixel sizes are used first. Then, proportional sizes are applied to the remaining space. Then, finally, if there is any space left, any items without an explicit size split them equally.
279 
280 	You may prefer to set them all at once, with:
281 
282 	---
283 	window.statusBar.parts.setSizes(1, 1, 1);
284 	---
285 
286 	This makes a three-part status bar, each with the same size - they all take the same proportion of the total size. Negative numbers here will use auto-scaled pixels.
287 
288 	You should call this right after creating your `MainWindow` as part of your setup code.
289 
290 	Once you make parts, you can explicitly change their content with `window.statusBar.parts[index].content = "some string";`
291 
292 	$(NOTE
293 		I'm thinking about making the other parts do other things by default too, but if I do change it, I'll try not to break any explicitly set things you do anyway.
294 	)
295 
296 	If you really don't want a status bar on your main window, you can remove it with `window.statusBar = null;` Make sure you don't try to use it again, or your program will likely crash!
297 
298 	Status bars, at this time, cannot hold non-text content, but I do want to change that. They also cannot have event listeners at this time, but again, that is likely to change. I have something in mind where they can hold clickable messages with a history and maybe icons, but haven't implemented any of that yet. Right now, they're just a (still very useful!) display area.
299 
300 	$(H4 How to do custom styles)
301 
302 	Minigui's custom widgets support styling parameters on the level of individual widgets, or application-wide with [VisualTheme]s.
303 
304 	$(WARNING
305 		These don't apply to non-custom widgets! They will use the operating system's native theme unless the documentation for that specific class says otherwise.
306 
307 		At this time, custom widgets gain capability in styling, but lose capability in terms of keeping all the right integrated details of the user experience and availability to accessibility and other automation tools. Evaluate if the benefit is worth the costs before making your decision.
308 
309 		I'd like to erase more and more of these gaps, but no promises as to when - or even if - that will ever actually happen.
310 	)
311 
312 	See [Widget.Style] for more information.
313 
314 	$(H4 Selection of categorized widgets)
315 
316 	$(LIST
317 		* Buttons: [Button]
318 		* Text display widgets: [TextLabel], [TextDisplay]
319 		* Text edit widgets: [LineEdit] (and [LabeledLineEdit]), [PasswordEdit] (and [LabeledPasswordEdit]), [TextEdit]
320 		* Selecting multiple on/off options: [Checkbox]
321 		* Selecting just one from a list of options: [Fieldset], [Radiobox], [DropDownSelection]
322 		* Getting rough numeric input: [HorizontalSlider], [VerticalSlider]
323 		* Displaying data: [ImageBox], [ProgressBar], [TableView]
324 		* Showing a list of editable items: [GenericListViewWidget]
325 		* Helpers for building your own widgets: [OpenGlWidget], [ScrollMessageWidget]
326 	)
327 
328 	And more. See [#members] until I write up more of this later and also be aware of the package [arsd.minigui_addons].
329 
330 	If none of these do what you need, you'll want to write your own. More on that in the following section.
331 
332 	$(H4 custom widgets - how to write your own)
333 
334 	See some example programs: https://github.com/adamdruppe/minigui-samples
335 
336 	When you can't build your application out of existing widgets, you'll want to make your own. The general pattern is to subclass [Widget], write a constructor that takes a `Widget` parent argument you pass to `super`, then set some values, override methods you want to customize, and maybe add child widgets and events as appropriate. You might also be able to subclass an existing other Widget and customize that way.
337 
338 	To get more specific, let's consider a few illustrative examples, then we'll come back to some principles.
339 
340 	$(H5 Custom Widget Examples)
341 
342 	$(H5 More notes)
343 
344 	See [Widget].
345 
346 	If you override [Widget.recomputeChildLayout], don't forget to call `registerMovement()` at the top of it, then call recomputeChildLayout of all its children too!
347 
348 		If you need a nested OS level window, see [NestedChildWindowWidget]. Use [Widget.scaleWithDpi] to convert logical pixels to physical pixels, as required.
349 
350 		See [Widget.OverrideStyle], [Widget.paintContent], [Widget.dynamicState] for some useful starting points.
351 
352 		You may also want to provide layout and style hints by overriding things like [Widget.flexBasisWidth], [Widget.flexBasisHeight], [Widget.minHeight], yada, yada, yada.
353 
354 		You might make a compound widget out of other widgets. [Widget.encapsulatedChildren] can help hide this from the outside world (though is not necessary and might hurt some debugging!)
355 
356 		$(TIP
357 			Compile your application with the `-debug` switch and press F12 in your window to open a web-browser-inspired debug window. It sucks right now and doesn't do a lot, but is sometimes better than nothing.
358 		)
359 
360 	$(H5 Timers and animations)
361 
362 	The [Timer] class is available and you can call `widget.redraw();` to trigger a redraw from a timer handler.
363 
364 	I generally don't like animations in my programs, so it hasn't been a priority for me to do more than this. I also hate uis that move outside of explicit user action, so minigui kinda supports this but I'd rather you didn't. I kinda wanna do something like `requestAnimationFrame` or something but haven't yet so it is just the `Timer` class.
365 
366 	$(H5 Clipboard integrations, drag and drop)
367 
368 	GUI application users tend to expect integration with their system, so clipboard support is basically a must, and drag and drop is nice to offer too. The functions for these are provided in [arsd.simpledisplay], which is public imported from minigui, and thus available to you here too.
369 
370 	I'd like to think of some better abstractions to make this more automagic, but you must do it yourself when implementing your custom widgets right now.
371 
372 	See: [draggable], [DropHandler], [setClipboardText], [setClipboardImage], [getClipboardText], [getClipboardImage], [setPrimarySelection], and others from simpledisplay.
373 
374 	$(H5 Context menus)
375 
376 	Override [Widget.contextMenu] in your subclass.
377 
378 	$(H4 Coming later)
379 
380 	Among the unfinished features: unified selections, translateable strings, external integrations.
381 
382 	$(H2 Running minigui programs)
383 
384 	Note the environment variable ARSD_SCALING_FACTOR on Linux can set multi-monitor scaling factors. I should also read it from a root window property so it easier to do with migrations... maybe a default theme selector from there too.
385 
386 	$(H2 Building minigui programs)
387 
388 	minigui's only required dependencies are [arsd.simpledisplay], [arsd.color], and
389 	[arsd.textlayouter], on which it is built. simpledisplay provides the low-level
390 	interfaces and minigui builds the concept of widgets inside the windows on top of it.
391 
392 	Its #1 goal is to be useful without being large and complicated like GTK and Qt.
393 	It isn't hugely concerned with appearance - on Windows, it just uses the native
394 	controls and native theme, and on Linux, it keeps it simple and I may change that
395 	at any time, though after May 2021, you can customize some things with css-inspired
396 	[Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`,
397 	you can use the custom implementation there too, but... you shouldn't.)
398 
399 	The event model is similar to what you use in the browser with Javascript and the
400 	layout engine tries to automatically fit things in, similar to a css flexbox.
401 
402 	FOR BEST RESULTS: be sure to link with the appropriate subsystem command
403 	`-L/SUBSYSTEM:WINDOWS` and -L/entry:mainCRTStartup`. If using ldc instead
404 	of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w".
405 
406 	Otherwise you'll get a console and possibly other visual bugs. But if you do use
407 	the subsystem:windows, note that Phobos' writeln will crash the program!
408 
409 	HTML_To_Classes:
410 	$(SMALL_TABLE
411 		HTML Code | Minigui Class
412 
413 		`<input type="text">` | [LineEdit]
414 		`<input type="password">` | [PasswordEdit]
415 		`<textarea>` | [TextEdit]
416 		`<select>` | [DropDownSelection]
417 		`<input type="checkbox">` | [Checkbox]
418 		`<input type="radio">` | [Radiobox]
419 		`<button>` | [Button]
420 	)
421 
422 
423 	Stretchiness:
424 		The default is 4. You can use larger numbers for things that should
425 		consume a lot of space, and lower numbers for ones that are better at
426 		smaller sizes.
427 
428 	Overlapped_input:
429 		COMING EVENTUALLY:
430 		minigui will include a little bit of I/O functionality that just works
431 		with the event loop. If you want to get fancy, I suggest spinning up
432 		another thread and posting events back and forth.
433 
434 	$(H2 Add ons)
435 		See the `minigui_addons` directory in the arsd repo for some add on widgets
436 		you can import separately too.
437 
438 	$(H3 XML definitions)
439 		If you use [arsd.minigui_xml], you can create widget trees from XML at runtime.
440 
441 	$(H3 Scriptability)
442 		minigui is compatible with [arsd.script]. If you see `@scriptable` on a method
443 		in this documentation, it means you can call it from the script language.
444 
445 		Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml]
446 		and make [arsd.minigui_xml.makeWidgetFromString] available to your script:
447 
448 		---
449 		import arsd.minigui_xml;
450 		import arsd.script;
451 
452 		var globals = var.emptyObject;
453 		globals.makeWidgetFromString = &makeWidgetFromString;
454 
455 		// this now works
456 		interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals);
457 		---
458 
459 		More to come.
460 
461 	Widget_tree_notes:
462 		minigui doesn't really formalize these distinctions, but in practice, there are multiple types of widgets:
463 
464 		$(LIST
465 			* Containers - a widget that holds other widgets directly, generally [Layout]s. [WidgetContainer] is an attempt to formalize this but is nothing really special.
466 
467 			* Reparenting containers - a widget that holds other widgets inside a different one of their parents. [MainWindow] is an example - any time you try to add a child to the main window, it actually goes to a special container one layer deeper. [ScrollMessageWidget] also works this way.
468 
469 			---
470 			auto child = new Widget(mainWindow);
471 			assert(child.parent is mainWindow); // fails, its actual parent is mainWindow's inner container instead.
472 			---
473 
474 			* Limiting containers - a widget that can only hold children of a particular type. See [TabWidget], which can only hold [TabWidgetPage]s.
475 
476 			* Simple controls - a widget that cannot have children, but instead does a specific job.
477 
478 			* Compound controls - a widget that is comprised of children internally to help it do a specific job, but externally acts like a simple control that does not allow any more children. Ideally, this is encapsulated, but in practice, it leaks right now.
479 		)
480 
481 		In practice, all of these are [Widget]s right now, but this violates the OOP principles of substitutability since some operations are not actually valid on all subclasses.
482 
483 		Future breaking changes might be related to making this more structured but im not sure it is that important to actually break stuff over.
484 
485 	My_UI_Guidelines:
486 		Note that the Linux custom widgets generally aim to be efficient on remote X network connections.
487 
488 		In a perfect world, you'd achieve all the following goals:
489 
490 		$(LIST
491 			* All operations are present in the menu
492 			* The operations the user wants at the moment are right where they want them
493 			* All operations can be scripted
494 			* The UI does not move any elements without explicit user action
495 			* All numbers can be seen and typed in if wanted, even if the ui usually hides them
496 		)
497 
498 	$(H2 Future Directions)
499 
500 	I want to do some newer ideas that might not be easy to keep working fully on Windows, like adding a menu search feature and scrollbar custom marks and typing in numbers. I might make them a default part of the widget with custom, and let you provide them through a menu or something elsewhere.
501 
502 	History:
503 		In January 2025 (dub v12.0), minigui got a few more breaking changes:
504 
505 		$(LIST
506 			* `defaultEventHandler_*` functions take more specific objects. So if you see errors like:
507 
508 			---
509 			Error: function `void arsd.minigui.EditableTextWidget.defaultEventHandler_focusin(Event foe)` does not override any function, did you mean to override `void arsd.minigui.Widget.defaultEventHandler_focusin(arsd.minigui.FocusInEvent event)`?
510 			---
511 
512 			Go to the file+line number from the error message and change `Event` to `FocusInEvent` (or whatever one it tells you in the "did you mean" part of the error) and recompile. No other changes should be necessary to be compatible with this change.
513 
514 			* Most event classes, except those explicitly used as a base class, are now marked `final`. If you depended on this subclassing, let me know and I'll see what I can do, but I expect there's little use of it. I now recommend all event classes the `final` unless you are specifically planning on extending it.
515 		)
516 
517 		Minigui had mostly additive changes or bug fixes since its inception until May 2021.
518 
519 		In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd
520 		tag this as version 2.0.
521 
522 		Among the changes:
523 		$(LIST
524 			* The event model changed to prefer strongly-typed events, though the Javascript string style ones still work, using properties off them is deprecated. It will still compile and function, but you should change the handler to use the classes in its argument list. I adapted my code to use the new model in just a few minutes, so it shouldn't too hard.
525 
526 			See [Event] for details.
527 
528 			* A [DoubleClickEvent] was added. Previously, you'd get two rapidly repeated click events. Now, you get one click event followed by a double click event. If you must recreate the old way exactly, you can listen for a DoubleClickEvent, set a flag upon receiving one, then send yourself a synthetic ClickEvent on the next MouseUpEvent, but your program might be better served just working with [MouseDownEvent]s instead.
529 
530 			See [DoubleClickEvent] for details.
531 
532 			* Styling hints were added, and the few that existed before have been moved to a new helper class. Deprecated forwarders exist for the (few) old properties to help you transition. Note that most of these only affect a `custom_events` build, which is the default on Linux, but opt in only on Windows.
533 
534 			See [Widget.Style] for details.
535 
536 			* Widgets now draw their keyboard focus by default instead of opt in. You may wish to set `tabStop = false;` if it wasn't supposed to receive it.
537 
538 			* Most Widget constructors no longer have a default `parent` argument. You must pass the parent to almost all widgets, or in rare cases, an explict `null`, but more often than not, you need the parent so the default argument was not very useful at best and misleading to a crash at worst.
539 
540 			* [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor.
541 
542 			* Several conversions of public fields to properties, deprecated, or made private. It is unlikely this will affect you, but the compiler will tell you if it does.
543 
544 			* Various non-breaking additions.
545 		)
546 +/
547 module arsd.minigui;
548 			// * A widget must now opt in to receiving keyboard focus, rather than opting out.
549 
550 /++
551 	This hello world sample will have an oversized button, but that's ok, you see your first window!
552 +/
553 version(Demo)
554 unittest {
555 	import arsd.minigui;
556 
557 	void main() {
558 		auto window = new MainWindow();
559 
560 		// note the parent widget is almost always passed as the last argument to a constructor
561 		auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window);
562 		auto button = new Button("Close", window);
563 		button.addWhenTriggered({
564 			window.close();
565 		});
566 
567 		window.loop();
568 	}
569 
570 	main(); // exclude from docs
571 }
572 
573 /++
574 	$(ID layout-example)
575 
576 	This example shows one way you can partition your window into a header
577 	and sidebar. Here, the header and sidebar have a fixed width, while the
578 	rest of the content sizes with the window.
579 
580 	It might be a new way of thinking about window layout to do things this
581 	way - perhaps [GridLayout] more matches your style of thought - but the
582 	concept here is to partition the window into sub-boxes with a particular
583 	size, then partition those boxes into further boxes.
584 
585 	$(IMG //arsdnet.net/minigui-screenshots/windows/layout.png, The example window has a header across the top, then below it a sidebar to the left and a content area to the right.)
586 
587 	So to make the header, start with a child layout that has a max height.
588 	It will use that space from the top, then the remaining children will
589 	split the remaining area, meaning you can think of is as just being another
590 	box you can split again. Keep splitting until you have the look you desire.
591 +/
592 // https://github.com/adamdruppe/arsd/issues/310
593 version(minigui_screenshots)
594 @Screenshot("layout")
595 unittest {
596 	import arsd.minigui;
597 
598 	// This helper class is just to help make the layout boxes visible.
599 	// think of it like a <div style="background-color: whatever;"></div> in HTML.
600 	class ColorWidget : Widget {
601 		this(Color color, Widget parent) {
602 			this.color = color;
603 			super(parent);
604 		}
605 		Color color;
606 		class Style : Widget.Style {
607 			override WidgetBackground background() { return WidgetBackground(color); }
608 		}
609 		mixin OverrideStyle!Style;
610 	}
611 
612 	void main() {
613 		auto window = new Window;
614 
615 		// the key is to give it a max height. This is one way to do it:
616 		auto header = new class HorizontalLayout {
617 			this() { super(window); }
618 			override int maxHeight() { return 50; }
619 		};
620 		// this next line is a shortcut way of doing it too, but it only works
621 		// for HorizontalLayout and VerticalLayout, and is less explicit, so it
622 		// is good to know how to make a new class like above anyway.
623 		// auto header = new HorizontalLayout(50, window);
624 
625 		auto bar = new HorizontalLayout(window);
626 
627 		// or since this is so common, VerticalLayout and HorizontalLayout both
628 		// can just take an argument in their constructor for max width/height respectively
629 
630 		// (could have tone this above too, but I wanted to demo both techniques)
631 		auto left = new VerticalLayout(100, bar);
632 
633 		// and this is the main section's container. A plain Widget instance is good enough here.
634 		auto container = new Widget(bar);
635 
636 		// and these just add color to the containers we made above for the screenshot.
637 		// in a real application, you can just add your actual controls instead of these.
638 		auto headerColorBox = new ColorWidget(Color.teal, header);
639 		auto leftColorBox = new ColorWidget(Color.green, left);
640 		auto rightColorBox = new ColorWidget(Color.purple, container);
641 
642 		window.loop();
643 	}
644 
645 	main(); // exclude from docs
646 }
647 
648 
649 import arsd.core;
650 import arsd.textlayouter;
651 
652 alias Timer = arsd.simpledisplay.Timer;
653 public import arsd.simpledisplay;
654 /++
655 	Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports)
656 
657 	History:
658 		Was private until May 15, 2021.
659 +/
660 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle()
661 
662 version(Windows) {
663 	import core.sys.windows.winnls;
664 	import core.sys.windows.windef;
665 	import core.sys.windows.basetyps;
666 	import core.sys.windows.winbase;
667 	import core.sys.windows.winuser;
668 	import core.sys.windows.wingdi;
669 	static import gdi = core.sys.windows.wingdi;
670 }
671 
672 version(Windows) {
673 	// to swap the default
674 	// version(minigui_manifest) {} else version=minigui_no_manifest;
675 
676 	version(minigui_no_manifest) {} else {
677 		version(D_OpenD) {
678 			// OpenD always supports it
679 			version=UseManifestMinigui;
680 		} else {
681 			version(CRuntime_Microsoft) // FIXME: mingw?
682 				version=UseManifestMinigui;
683 		}
684 
685 	}
686 
687 
688 	version(UseManifestMinigui) {
689 		// assume we want commctrl6 whenever possible since there's really no reason not to
690 		// and this avoids some of the manifest hassle
691 		pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"");
692 	}
693 }
694 
695 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default.
696 private bool lastDefaultPrevented;
697 
698 /// Methods marked with this are available from scripts if added to the [arsd.script] engine.
699 alias scriptable = arsd_jsvar_compatible;
700 
701 version(Windows) {
702 	// use native widgets when available unless specifically asked otherwise
703 	version(custom_widgets) {
704 		enum bool UsingCustomWidgets = true;
705 		enum bool UsingWin32Widgets = false;
706 	} else {
707 		version = win32_widgets;
708 		enum bool UsingCustomWidgets = false;
709 		enum bool UsingWin32Widgets = true;
710 	}
711 	// and native theming when needed
712 	//version = win32_theming;
713 } else {
714 	enum bool UsingCustomWidgets = true;
715 	enum bool UsingWin32Widgets = false;
716 	version=custom_widgets;
717 }
718 
719 
720 
721 /*
722 
723 	The main goals of minigui.d are to:
724 		1) Provide basic widgets that just work in a lightweight lib.
725 		   I basically want things comparable to a plain HTML form,
726 		   plus the easy and obvious things you expect from Windows
727 		   apps like a menu.
728 		2) Use native things when possible for best functionality with
729 		   least library weight.
730 		3) Give building blocks to provide easy extension for your
731 		   custom widgets, or hooking into additional native widgets
732 		   I didn't wrap.
733 		4) Provide interfaces for easy interaction between third
734 		   party minigui extensions. (event model, perhaps
735 		   signals/slots, drop-in ease of use bits.)
736 		5) Zero non-system dependencies, including Phobos as much as
737 		   I reasonably can. It must only import arsd.color and
738 		   my simpledisplay.d. If you need more, it will have to be
739 		   an extension module.
740 		6) An easy layout system that generally works.
741 
742 	A stretch goal is to make it easy to make gui forms with code,
743 	some kind of resource file (xml?) and even a wysiwyg designer.
744 
745 	Another stretch goal is to make it easy to hook data into the gui,
746 	including from reflection. So like auto-generate a form from a
747 	function signature or struct definition, or show a list from an
748 	array that automatically updates as the array is changed. Then,
749 	your program focuses on the data more than the gui interaction.
750 
751 
752 
753 	STILL NEEDED:
754 		* combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect)
755 		* slider
756 		* listbox
757 		* spinner
758 		* label?
759 		* rich text
760 */
761 
762 
763 /+
764 	enum LayoutMethods {
765 		 verticalFlex,
766 		 horizontalFlex,
767 		 inlineBlock, // left to right, no stretch, goes to next line as needed
768 		 static, // just set to x, y
769 		 verticalNoStretch, // browser style default
770 
771 		 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line
772 
773 		 grid, // magic
774 	}
775 +/
776 
777 /++
778 	The `Widget` is the base class for minigui's functionality, ranging from UI components like checkboxes or text displays to abstract groupings of other widgets like a layout container or a html `<div>`. You will likely want to use pre-made widgets as well as creating your own.
779 
780 
781 	To create your own widget, you must inherit from it and create a constructor that passes a parent to `super`. Everything else after that is optional.
782 
783 	---
784 	class MinimalWidget : Widget {
785 		this(Widget parent) {
786 			super(parent);
787 		}
788 	}
789 	---
790 
791 	$(SIDEBAR
792 		I'm not entirely happy with leaf, container, and windows all coming from the same base Widget class, but I so far haven't thought of a better solution that's good enough to justify the breakage of a transition. It hasn't been a major problem in practice anyway.
793 	)
794 
795 	Broadly, there's two kinds of widgets: leaf widgets, which are intended to be the direct user-interactive components, and container widgets, which organize, lay out, and aggregate other widgets in the object tree. A special case of a container widget is [Window], which represents a separate top-level window on the screen. Both leaf and container widgets inherit from `Widget`, so this distinction is more conventional than formal.
796 
797 	Among the things you'll most likely want to change in your custom widget:
798 
799 	$(LIST
800 		* In your constructor, set `tabStop = false;` if the widget is not supposed to receive keyboard focus. (Please note its childen still can, so `tabStop = false;` is appropriate on most container widgets.)
801 
802 		You may explicitly set `tabStop = true;` to ensure you get it, even against future changes to the library, though that's the default right now.
803 
804 		Do this $(I after) calling the `super` constructor.
805 
806 		* Override [paint] if you want full control of the widget's drawing area (except the area obscured by children!), or [paintContent] if you want to participate in the styling engine's system. You'll also possibly want to make a subclass of [Style] and use [OverrideStyle] to change the default hints given to the styling engine for widget.
807 
808 		Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision.
809 
810 		* Override default event handlers with your behavior. For example [defaultEventHandler_click] may be overridden to make clicks do something. Again, this is generally a job for leaf widgets rather than containers; most events are dispatched to the lowest leaf on the widget tree, but they also pass through all their parents. See [Event] for more details about the event model.
811 
812 		* You may also want to override the various layout hints like [minWidth], [maxHeight], etc. In particular [Padding] and [Margin] are often relevant for both container and leaf widgets and the default values of 0 are often not what you want.
813 	)
814 
815 	On Microsoft Windows, many widgets are also based on native controls. You can also do this if `static if(UsingWin32Widgets)` passes. You should use the helper function [createWin32Window] to create the window and let minigui do what it needs to do to create its bridge structures. This will populate [Widget.hwnd] which you can access later for communcating with the native window. You may also consider overriding [Widget.handleWmCommand] and [Widget.handleWmNotify] for the widget to translate those messages into appropriate minigui [Event]s.
816 
817 	It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example.
818 
819 	Your own custom-drawn and native system controls can exist side-by-side.
820 
821 	Later I'll add more complete examples, but for now [TextLabel] and [LabeledPasswordEdit] are both simple widgets you can view implementation to get some ideas.
822 +/
823 class Widget : ReflectableProperties {
824 
825 	private int toolbarIconSize() {
826 		return scaleWithDpi(24);
827 	}
828 
829 
830 	/++
831 		Returns the current size of the widget.
832 
833 		History:
834 			Added January 3, 2025
835 	+/
836 	final Size size() const {
837 		return Size(width, height);
838 	}
839 
840 	private bool willDraw() {
841 		return true;
842 	}
843 
844 	/+
845 	/++
846 		Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need.
847 
848 		History:
849 			Added September 15, 2021
850 			implemented.... ???
851 	+/
852 	void prepareReflection(this This)() {
853 
854 	}
855 	+/
856 
857 	private bool _enabled = true;
858 
859 	/++
860 		Determines whether the control is marked enabled. Disabled controls are generally displayed as greyed out and clicking on them does nothing. It is also possible for a control to be disabled because its parent is disabled, in which case this will still return `true`, but setting `enabled = true` may have no effect. Check [disabledBy] to see which parent caused it to be disabled.
861 
862 		I also recommend you set a [disabledReason] if you chose to set `enabled = false` to tell the user why the control does not work and what they can do to enable it.
863 
864 		History:
865 			Added November 23, 2021 (dub v10.4)
866 
867 			Warning: the specific behavior of disabling with parents may change in the future.
868 		Bugs:
869 			Currently only implemented for widgets backed by native Windows controls.
870 
871 		See_Also: [disabledReason], [disabledBy]
872 	+/
873 	@property bool enabled() {
874 		return disabledBy() is null;
875 	}
876 
877 	/// ditto
878 	@property void enabled(bool yes) {
879 		_enabled = yes;
880 		version(win32_widgets) {
881 			if(hwnd)
882 				EnableWindow(hwnd, yes);
883 		}
884 		setDynamicState(DynamicState.disabled, yes);
885 		redraw();
886 	}
887 
888 	private string disabledReason_;
889 
890 	/++
891 		If the widget is not [enabled] this string may be presented to the user when they try to use it. The exact manner and time it gets displayed is up to the implementation of the control.
892 
893 		Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though.
894 
895 		History:
896 			Added November 23, 2021 (dub v10.4)
897 		See_Also: [enabled], [disabledBy]
898 	+/
899 	@property string disabledReason() {
900 		auto w = disabledBy();
901 		return (w is null) ? null : w.disabledReason_;
902 	}
903 
904 	/// ditto
905 	@property void disabledReason(string reason) {
906 		disabledReason_ = reason;
907 	}
908 
909 	/++
910 		Returns the widget that disabled this. It might be this or one of its parents all the way up the chain, or `null` if the widget is not disabled by anything. You can check [disabledReason] on the return value (after the null check!) to get a hint to display to the user.
911 
912 		History:
913 			Added November 25, 2021 (dub v10.4)
914 		See_Also: [enabled], [disabledReason]
915 	+/
916 	Widget disabledBy() {
917 		Widget p = this;
918 		while(p) {
919 			if(!p._enabled)
920 				return p;
921 			p = p.parent;
922 		}
923 		return null;
924 	}
925 
926 	/// Implementations of [ReflectableProperties] interface. See the interface for details.
927 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
928 		if(valueIsJson)
929 			return SetPropertyResult.wrongFormat;
930 		switch(name) {
931 			case "name":
932 				this.name = value.idup;
933 				return SetPropertyResult.success;
934 			case "statusTip":
935 				this.statusTip = value.idup;
936 				return SetPropertyResult.success;
937 			default:
938 				return SetPropertyResult.noSuchProperty;
939 		}
940 	}
941 	/// ditto
942 	void getPropertiesList(scope void delegate(string name) sink) const {
943 		sink("name");
944 		sink("statusTip");
945 	}
946 	/// ditto
947 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
948 		switch(name) {
949 			case "name":
950 				sink(name, this.name, false);
951 				return;
952 			case "statusTip":
953 				sink(name, this.statusTip, false);
954 				return;
955 			default:
956 				sink(name, null, true);
957 		}
958 	}
959 
960 	/++
961 		Scales the given value to the system-reported DPI for the monitor on which the widget resides.
962 
963 		History:
964 			Added November 25, 2021 (dub v10.5)
965 			`Point` overload added January 12, 2022 (dub v10.6)
966 	+/
967 	int scaleWithDpi(int value, int assumedDpi = 96) {
968 		// avoid potential overflow with common special values
969 		if(value == int.max)
970 			return int.max;
971 		if(value == int.min)
972 			return int.min;
973 		if(value == 0)
974 			return 0;
975 		return value * currentDpi(assumedDpi) / assumedDpi;
976 	}
977 
978 	/// ditto
979 	Point scaleWithDpi(Point value, int assumedDpi = 96) {
980 		return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi));
981 	}
982 
983 	/++
984 		Returns the current scaling factor as a logical dpi value for this widget. Generally speaking, this divided by 96 gives you the user scaling factor.
985 
986 		Not entirely stable.
987 
988 		History:
989 			Added August 25, 2023 (dub v11.1)
990 	+/
991 	final int currentDpi(int assumedDpi = 96) {
992 		// assert(parentWindow !is null);
993 		// assert(parentWindow.win !is null);
994 		auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi;
995 		//divide = 138; // to test 1.5x
996 		// for lower values it is something i don't really want changed anyway since it is an old monitor and you don't want to scale down.
997 		// this also covers the case when actualDpi returns 0.
998 		if(divide < 96)
999 			divide = 96;
1000 		return divide;
1001 	}
1002 
1003 	// avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable
1004 	// I'll think up something better eventually
1005 
1006 	// FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight.
1007 	protected final int defaultLineHeight() {
1008 		auto cs = getComputedStyle();
1009 		if(cs.font && !cs.font.isNull)
1010 			return castFnumToCnum(cs.font.height() * 5 / 4);
1011 		else
1012 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4);
1013 	}
1014 
1015 	/++
1016 
1017 		History:
1018 			Added August 25, 2023 (dub v11.1)
1019 	+/
1020 	protected final int defaultTextHeight(int numberOfLines = 1) {
1021 		auto cs = getComputedStyle();
1022 		if(cs.font && !cs.font.isNull)
1023 			return castFnumToCnum(cs.font.height() * numberOfLines);
1024 		else
1025 			return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines;
1026 	}
1027 
1028 	protected final int defaultTextWidth(const(char)[] text) {
1029 		auto cs = getComputedStyle();
1030 		if(cs.font && !cs.font.isNull)
1031 			return castFnumToCnum(cs.font.stringWidth(text));
1032 		else
1033 			return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2);
1034 	}
1035 
1036 	/++
1037 		If `encapsulatedChildren` returns true, it changes the event handling mechanism to act as if events from the child widgets are actually targeted on this widget.
1038 
1039 		The idea is then you can use child widgets as part of your implementation, but not expose those details through the event system; if someone checks the mouse coordinates and target of the event once it bubbles past you, it will show as it it came from you.
1040 
1041 		History:
1042 			Added May 22, 2021
1043 	+/
1044 	protected bool encapsulatedChildren() {
1045 		return false;
1046 	}
1047 
1048 	private void privateDpiChanged() {
1049 		dpiChanged();
1050 		foreach(child; children)
1051 			child.privateDpiChanged();
1052 	}
1053 
1054 	/++
1055 		Virtual hook to update any caches or fonts you need on the event of a dpi scaling change.
1056 
1057 		History:
1058 			Added January 12, 2022 (dub v10.6)
1059 	+/
1060 	protected void dpiChanged() {
1061 
1062 	}
1063 
1064 	// Default layout properties {
1065 
1066 		int minWidth() { return 0; }
1067 		int minHeight() {
1068 			// default widgets have a vertical layout, therefore the minimum height is the sum of the contents
1069 			int sum = this.paddingTop + this.paddingBottom;
1070 			foreach(child; children) {
1071 				if(child.hidden)
1072 					continue;
1073 				sum += child.minHeight();
1074 				sum += child.marginTop();
1075 				sum += child.marginBottom();
1076 			}
1077 
1078 			return sum;
1079 		}
1080 		int maxWidth() { return int.max; }
1081 		int maxHeight() { return int.max; }
1082 		int widthStretchiness() { return 4; }
1083 		int heightStretchiness() { return 4; }
1084 
1085 		/++
1086 			Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items.
1087 
1088 			History:
1089 				Added June 15, 2021 (dub v10.1)
1090 		+/
1091 		int widthShrinkiness() { return 0; }
1092 		/// ditto
1093 		int heightShrinkiness() { return 0; }
1094 
1095 		/++
1096 			The initial size of the widget for layout calculations. Default is 0.
1097 
1098 			See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis]
1099 
1100 			History:
1101 				Added June 15, 2021 (dub v10.1)
1102 		+/
1103 		int flexBasisWidth() { return 0; }
1104 		/// ditto
1105 		int flexBasisHeight() { return 0; }
1106 
1107 		/++
1108 			Not stable.
1109 
1110 			Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored.
1111 
1112 			So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6.
1113 
1114 			History:
1115 				Added January 5, 2023
1116 		+/
1117 		Rectangle defaultMargin;
1118 		/// ditto
1119 		Rectangle defaultPadding;
1120 
1121 		int marginLeft() { return scaleWithDpi(defaultMargin.left); }
1122 		int marginRight() { return scaleWithDpi(defaultMargin.right); }
1123 		int marginTop() { return scaleWithDpi(defaultMargin.top); }
1124 		int marginBottom() { return scaleWithDpi(defaultMargin.bottom); }
1125 		int paddingLeft() { return scaleWithDpi(defaultPadding.left); }
1126 		int paddingRight() { return scaleWithDpi(defaultPadding.right); }
1127 		int paddingTop() { return scaleWithDpi(defaultPadding.top); }
1128 		int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); }
1129 		//LinePreference linePreference() { return LinePreference.PreferOwnLine; }
1130 
1131 		private bool recomputeChildLayoutRequired = true;
1132 		private static class RecomputeEvent {}
1133 		private __gshared rce = new RecomputeEvent();
1134 		protected final void queueRecomputeChildLayout() {
1135 			recomputeChildLayoutRequired = true;
1136 
1137 			if(this.parentWindow) {
1138 				auto sw = this.parentWindow.win;
1139 				assert(sw !is null);
1140 				if(!sw.eventQueued!RecomputeEvent) {
1141 					sw.postEvent(rce);
1142 					// writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
1143 				}
1144 			}
1145 
1146 		}
1147 
1148 		protected final void recomputeChildLayoutEntry() {
1149 			if(recomputeChildLayoutRequired) {
1150 				recomputeChildLayout();
1151 				recomputeChildLayoutRequired = false;
1152 				redraw();
1153 			} else {
1154 				// I still need to check the tree just in case one of them was queued up
1155 				// and the event came up here instead of there.
1156 				foreach(child; children)
1157 					child.recomputeChildLayoutEntry();
1158 			}
1159 		}
1160 
1161 		// this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon
1162 		void recomputeChildLayout() {
1163 			.recomputeChildLayout!"height"(this);
1164 		}
1165 
1166 	// }
1167 
1168 
1169 	/++
1170 		Returns the style's tag name string this object uses.
1171 
1172 		The default is to use the typeid() name trimmed down to whatever is after the last dot which is typically the identifier of the class.
1173 
1174 		This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS.
1175 
1176 		History:
1177 			Added May 10, 2021
1178 	+/
1179 	string styleTagName() const {
1180 		string n = typeid(this).name;
1181 		foreach_reverse(idx, ch; n)
1182 			if(ch == '.') {
1183 				n = n[idx + 1 .. $];
1184 				break;
1185 			}
1186 		return n;
1187 	}
1188 
1189 	/// API for the [styleClassList]
1190 	static struct ClassList {
1191 		private Widget widget;
1192 
1193 		///
1194 		void add(string s) {
1195 			widget.styleClassList_ ~= s;
1196 		}
1197 
1198 		///
1199 		void remove(string s) {
1200 			foreach(idx, s1; widget.styleClassList_)
1201 				if(s1 == s) {
1202 					widget.styleClassList_[idx] = widget.styleClassList_[$-1];
1203 					widget.styleClassList_ = widget.styleClassList_[0 .. $-1];
1204 					widget.styleClassList_.assumeSafeAppend();
1205 					return;
1206 				}
1207 		}
1208 
1209 		/// Returns true if it was added, false if it was removed.
1210 		bool toggle(string s) {
1211 			if(contains(s)) {
1212 				remove(s);
1213 				return false;
1214 			} else {
1215 				add(s);
1216 				return true;
1217 			}
1218 		}
1219 
1220 		///
1221 		bool contains(string s) const {
1222 			foreach(s1; widget.styleClassList_)
1223 				if(s1 == s)
1224 					return true;
1225 			return false;
1226 
1227 		}
1228 	}
1229 
1230 	private string[] styleClassList_;
1231 
1232 	/++
1233 		Returns a "class list" that can be used by the visual theme's style engine via [VisualTheme.getPropertyString] if it chooses to do something like CSS.
1234 
1235 		It has no inherent meaning, it is really just a place to put some metadata tags on individual objects.
1236 
1237 		History:
1238 			Added May 10, 2021
1239 	+/
1240 	inout(ClassList) styleClassList() inout {
1241 		return cast(inout(ClassList)) ClassList(cast() this);
1242 	}
1243 
1244 	/++
1245 		List of dynamic states made available to the style engine, for cases like CSS pseudo-classes and also used by default paint methods. It is stored in a 64 bit variable attached to the widget that you can update. The style cache is aware of the fact that these can frequently change.
1246 
1247 		The lower 32 bits are defined here or reserved for future use by the library. You should keep these updated if you reasonably can on custom widgets if they apply to you, but don't use them for a purpose they aren't defined for.
1248 
1249 		The upper 32 bits are available for your own extensions.
1250 
1251 		History:
1252 			Added May 10, 2021
1253 
1254 		Examples:
1255 
1256 		---
1257 		addEventListener((MouseUpEvent ev) {
1258 			if(ev.button == MouseButton.left) {
1259 				// the first arg is the state to modify, the second arg is what to set it to
1260 				setDynamicState(DynamicState.depressed, false);
1261 			}
1262 		});
1263 		---
1264 
1265 	+/
1266 	enum DynamicState : ulong {
1267 		focus = (1 << 0), /// the widget currently has the keyboard focus
1268 		hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated)
1269 		valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if no validation has been performed!)
1270 		invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if no validation has been performed!)
1271 		checked = (1 << 4), /// the widget is toggleable and currently toggled on
1272 		selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked.
1273 		disabled = (1 << 6), /// the widget is currently unable to perform its designated task
1274 		indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked
1275 		depressed = (1 << 8), /// the widget is being actively pressed or clicked (compare to css `:active`). Can be combined with hover to visually indicate if a mouse up would result in a click event.
1276 
1277 		USER_BEGIN = (1UL << 32),
1278 	}
1279 
1280 	// I want to add the primary and cancel styles to buttons at least at some point somehow.
1281 
1282 	/// ditto
1283 	@property ulong dynamicState() { return dynamicState_; }
1284 	/// ditto
1285 	@property ulong dynamicState(ulong newValue) {
1286 		if(dynamicState != newValue) {
1287 			auto old = dynamicState_;
1288 			dynamicState_ = newValue;
1289 
1290 			useStyleProperties((scope Widget.Style s) {
1291 				if(s.variesWithState(old ^ newValue))
1292 					redraw();
1293 			});
1294 		}
1295 		return dynamicState_;
1296 	}
1297 
1298 	/// ditto
1299 	void setDynamicState(ulong flags, bool state) {
1300 		auto ds = dynamicState_;
1301 		if(state)
1302 			ds |= flags;
1303 		else
1304 			ds &= ~flags;
1305 
1306 		dynamicState = ds;
1307 	}
1308 
1309 	private ulong dynamicState_;
1310 
1311 	deprecated("Use dynamic styles instead now") {
1312 		Color backgroundColor() { return backgroundColor_; }
1313 		void backgroundColor(Color c){ this.backgroundColor_ = c; }
1314 
1315 		MouseCursor cursor() { return GenericCursor.Default; }
1316 	} private Color backgroundColor_ = Color.transparent;
1317 
1318 
1319 	/++
1320 		Style properties are defined as an accessory class so they can be referenced and overridden independently, but they are nested so you can refer to them easily by name (e.g. generic `Widget.Style` vs `Button.Style` and such).
1321 
1322 		It is here so there can be a specificity switch.
1323 
1324 		See [OverrideStyle] for a helper function to use your own.
1325 
1326 		History:
1327 			Added May 11, 2021
1328 	+/
1329 	static class Style/* : StyleProperties*/ {
1330 		public Widget widget; // public because the mixin template needs access to it
1331 
1332 		/++
1333 			You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style.
1334 
1335 			History:
1336 				Added May 11, 2021, but changed on July 2, 2021 to return false by default. You MUST override this if you want declarative hover effects etc to take effect.
1337 		+/
1338 		bool variesWithState(ulong dynamicStateFlags) {
1339 			version(win32_widgets) {
1340 				if(widget.hwnd)
1341 					return false;
1342 			}
1343 			return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false);
1344 		}
1345 
1346 		///
1347 		Color foregroundColor() {
1348 			return WidgetPainter.visualTheme.foregroundColor;
1349 		}
1350 
1351 		///
1352 		WidgetBackground background() {
1353 			// the default is a "transparent" background, which means
1354 			// it goes as far up as it can to get the color
1355 			if (widget.backgroundColor_ != Color.transparent)
1356 				return WidgetBackground(widget.backgroundColor_);
1357 			if (widget.parent)
1358 				return widget.parent.getComputedStyle.background;
1359 			return WidgetBackground(widget.backgroundColor_);
1360 		}
1361 
1362 		private static OperatingSystemFont fontCached_;
1363 		private OperatingSystemFont fontCached() {
1364 			if(fontCached_ is null)
1365 				fontCached_ = font();
1366 			return fontCached_;
1367 		}
1368 
1369 		/++
1370 			Returns the default font to be used with this widget. The return value will be cached by the library, so you can not expect live updates.
1371 		+/
1372 		OperatingSystemFont font() {
1373 			return null;
1374 		}
1375 
1376 		/++
1377 			Returns the cursor that should be used over this widget. You may change this and updates will be reflected next time the mouse enters the widget.
1378 
1379 			You can return a member of [GenericCursor] or your own [MouseCursor] instance.
1380 
1381 			History:
1382 				Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021
1383 		+/
1384 		MouseCursor cursor() {
1385 			return GenericCursor.Default;
1386 		}
1387 
1388 		FrameStyle borderStyle() {
1389 			return FrameStyle.none;
1390 		}
1391 
1392 		/++
1393 		+/
1394 		Color borderColor() {
1395 			return Color.transparent;
1396 		}
1397 
1398 		FrameStyle outlineStyle() {
1399 			if(widget.dynamicState & DynamicState.focus)
1400 				return FrameStyle.dotted;
1401 			else
1402 				return FrameStyle.none;
1403 		}
1404 
1405 		Color outlineColor() {
1406 			return foregroundColor;
1407 		}
1408 	}
1409 
1410 	/++
1411 		This mixin overrides the [useStyleProperties] method to direct it toward your own style class.
1412 		The basic usage is simple:
1413 
1414 		---
1415 		static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */
1416 			// override style hints as-needed here
1417 		}
1418 		OverrideStyle!Style; // add the method
1419 		---
1420 
1421 		$(TIP
1422 			While the class is not forced to be `static`, for best results, it should be. A non-static class
1423 			can not be inherited by other objects whereas the static one can. A property on the base class,
1424 			called [Widget.Style.widget|widget], is available for you to access its properties.
1425 		)
1426 
1427 		This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must
1428 		repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches
1429 		and that's a little tedious to repeat in your child classes too when you only care about changing the type.
1430 
1431 
1432 		It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget.
1433 		You may also just override `variesWithState` when you use this flag.
1434 
1435 		---
1436 		mixin OverrideStyle!(
1437 			DynamicState.focus, YourFocusedStyle,
1438 			DynamicState.hover, YourHoverStyle,
1439 			YourDefaultStyle
1440 		)
1441 		---
1442 
1443 		It checks if `dynamicState` matches the state and if so, returns the object given.
1444 
1445 		If there is no state mask given, the next one matches everything. The first match given is used.
1446 
1447 		However, since in most cases you'll want check state inside your individual methods, you probably won't
1448 		find much use for this whole-class swap out.
1449 
1450 		History:
1451 			Added May 16, 2021
1452 	+/
1453 	static protected mixin template OverrideStyle(S...) {
1454 		static import amg = arsd.minigui;
1455 		override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) {
1456 			ulong mask = 0;
1457 			foreach(idx, thing; S) {
1458 				static if(is(typeof(thing) : ulong)) {
1459 					mask = thing;
1460 				} else {
1461 					if(!(idx & 1) || (this.dynamicState & mask) == mask) {
1462 						//static assert(!__traits(isNested, thing), thing.stringof ~ " is a nested class. For best results, mark it `static`. You can still access the widget through a `widget` variable inside the Style class.");
1463 						scope amg.Widget.Style s = new thing();
1464 						s.widget = this;
1465 						dg(s);
1466 						return;
1467 					}
1468 				}
1469 			}
1470 		}
1471 	}
1472 	/++
1473 		You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose.
1474 	+/
1475 	void useStyleProperties(scope void delegate(scope Style props) dg) {
1476 		scope Style s = new Style();
1477 		s.widget = this;
1478 		dg(s);
1479 	}
1480 
1481 
1482 	protected void sendResizeEvent() {
1483 		this.emit!ResizeEvent();
1484 	}
1485 
1486 	/++
1487 		Override this to provide a custom context menu for your widget. (x, y) is where the menu was requested. If x == -1 && y == -1, the menu was triggered by the keyboard instead of the mouse and it should use the current cursor, selection, or whatever would make sense for where a keyboard user's attention would currently be.
1488 
1489 		It should return an instance of the [Menu] object. You may choose to cache this object. To construct one, either make `new Menu("", this);` (the empty string there is the menu's label, but for a context menu, that is not important), then call the `menu.addItem(new Action("Label Text", 0 /* icon id */, () { on clicked handler }), menu);` and `menu.addSeparator() methods, or use `return createContextMenuFromAnnotatedCode(this, some_command_struct);`
1490 
1491 		Context menus are automatically triggered by default by the keyboard menu key, mouse right click, and possibly other conventions per platform. You can also invoke one by calling the [showContextMenu] method.
1492 
1493 		See_Also:
1494 			[createContextMenuFromAnnotatedCode]
1495 	+/
1496 	Menu contextMenu(int x, int y) { return null; }
1497 
1498 	/++
1499 		Shows the widget's context menu, as if the user right clicked at the x, y position. You should rarely, if ever, have to call this, since default event handlers will do it for you automatically. To control what menu shows up, you can pass one as `menuToShow`, but if you don't, it will call [contextMenu], which you can override on a per-widget basis.
1500 
1501 		History:
1502 			The `menuToShow` parameter was added on March 19, 2025.
1503 	+/
1504 	final bool showContextMenu(int x, int y, Menu menuToShow = null) {
1505 		return showContextMenu(x, y, -2, -2, menuToShow);
1506 	}
1507 
1508 	private final bool showContextMenu(int x, int y, int screenX, int screenY, Menu menu = null) {
1509 		if(parentWindow is null || parentWindow.win is null) return false;
1510 
1511 		if(menu is null)
1512 			menu = this.contextMenu(x, y);
1513 
1514 		if(menu is null)
1515 			return false;
1516 
1517 		version(win32_widgets) {
1518 			// FIXME: if it is -1, -1, do it at the current selection location instead
1519 			// tho the corner of the window, which it does now, isn't the literal worst.
1520 
1521 			// i see notepad just seems to put it in the center of the window so idk
1522 
1523 			if(screenX < 0 && screenY < 0) {
1524 				auto p = this.globalCoordinates();
1525 				if(screenX == -2)
1526 					p.x += x;
1527 				if(screenY == -2)
1528 					p.y += y;
1529 
1530 				screenX = p.x;
1531 				screenY = p.y;
1532 			}
1533 
1534 			if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null))
1535 				throw new Exception("TrackContextMenuEx");
1536 		} else version(custom_widgets) {
1537 			menu.popup(this, x, y);
1538 		}
1539 
1540 		return true;
1541 	}
1542 
1543 	/++
1544 		Removes this widget from its parent.
1545 
1546 		History:
1547 			`removeWidget` was made `final` on May 11, 2021.
1548 	+/
1549 	@scriptable
1550 	final void removeWidget() {
1551 		auto p = this.parent;
1552 		if(p) {
1553 			int item;
1554 			for(item = 0; item < p._children.length; item++)
1555 				if(p._children[item] is this)
1556 					break;
1557 			auto idx = item;
1558 			for(; item < p._children.length - 1; item++)
1559 				p._children[item] = p._children[item + 1];
1560 			p._children = p._children[0 .. $-1];
1561 
1562 			this.parent.widgetRemoved(idx, this);
1563 			//this.parent = null;
1564 
1565 			p.queueRecomputeChildLayout();
1566 		}
1567 		version(win32_widgets) {
1568 			removeAllChildren();
1569 			if(hwnd) {
1570 				DestroyWindow(hwnd);
1571 				hwnd = null;
1572 			}
1573 		}
1574 	}
1575 
1576 	/++
1577 		Notifies the subclass that a widget was removed. If you keep auxillary data about your children, you can override this to help keep that data in sync.
1578 
1579 		History:
1580 			Added September 19, 2021
1581 	+/
1582 	protected void widgetRemoved(size_t oldIndex, Widget oldReference) { }
1583 
1584 	/++
1585 		Removes all child widgets from `this`. You should not use the removed widgets again.
1586 
1587 		Note that on Windows, it also destroys the native handles for the removed children recursively.
1588 
1589 		History:
1590 			Added July 1, 2021 (dub v10.2)
1591 	+/
1592 	void removeAllChildren() {
1593 		version(win32_widgets)
1594 		foreach(child; _children) {
1595 			child.removeAllChildren();
1596 			if(child.hwnd) {
1597 				DestroyWindow(child.hwnd);
1598 				child.hwnd = null;
1599 			}
1600 		}
1601 		auto orig = this._children;
1602 		this._children = null;
1603 		foreach(idx, w; orig)
1604 			this.widgetRemoved(idx, w);
1605 
1606 		queueRecomputeChildLayout();
1607 	}
1608 
1609 	/++
1610 		Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible.
1611 	+/
1612 	@scriptable
1613 	Widget getChildByName(string name) {
1614 		return getByName(name);
1615 	}
1616 	/++
1617 		Finds the nearest descendant with the requested type and [name]. May return `this`.
1618 	+/
1619 	final WidgetClass getByName(WidgetClass = Widget)(string name) {
1620 		if(this.name == name)
1621 			if(auto c = cast(WidgetClass) this)
1622 				return c;
1623 		foreach(child; children) {
1624 			auto w = child.getByName(name);
1625 			if(auto c = cast(WidgetClass) w)
1626 				return c;
1627 		}
1628 		return null;
1629 	}
1630 
1631 	/++
1632 		The name is a string tag that is used to reference the widget from scripts, gui loaders, declarative ui templates, etc. Similar to a HTML id attribute.
1633 		Names should be unique in a window.
1634 
1635 		See_Also: [getByName], [getChildByName]
1636 	+/
1637 	@scriptable string name;
1638 
1639 	private EventHandler[][string] bubblingEventHandlers;
1640 	private EventHandler[][string] capturingEventHandlers;
1641 
1642 	/++
1643 		Default event handlers. These are called on the appropriate
1644 		event unless [Event.preventDefault] is called on the event at
1645 		some point through the bubbling process.
1646 
1647 
1648 		If you are implementing your own widget and want to add custom
1649 		events, you should follow the same pattern here: create a virtual
1650 		function named `defaultEventHandler_eventname` with the implementation,
1651 		then, override [setupDefaultEventHandlers] and add a wrapped caller to
1652 		`defaultEventHandlers["eventname"]`. It should be wrapped like so:
1653 		`defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`.
1654 		This ensures virtual dispatch based on the correct subclass.
1655 
1656 		Also, don't forget to call `super.setupDefaultEventHandlers();` too in your
1657 		overridden version.
1658 
1659 		You only need to do that on parent classes adding NEW event types. If you
1660 		just want to change the default behavior of an existing event type in a subclass,
1661 		you override the function (and optionally call `super.method_name`) like normal.
1662 
1663 		History:
1664 			Some of the events changed to take specific subclasses instead of generic `Event`
1665 			on January 3, 2025.
1666 
1667 	+/
1668 	protected EventHandler[string] defaultEventHandlers;
1669 
1670 	/// ditto
1671 	void setupDefaultEventHandlers() {
1672 		defaultEventHandlers["click"] = (Widget t, Event event)      { if(auto e = cast(ClickEvent) event) t.defaultEventHandler_click(e); };
1673 		defaultEventHandlers["dblclick"] = (Widget t, Event event)   { if(auto e = cast(DoubleClickEvent) event) t.defaultEventHandler_dblclick(e); };
1674 		defaultEventHandlers["keydown"] = (Widget t, Event event)    { if(auto e = cast(KeyDownEvent) event) t.defaultEventHandler_keydown(e); };
1675 		defaultEventHandlers["keyup"] = (Widget t, Event event)      { if(auto e = cast(KeyUpEvent) event) t.defaultEventHandler_keyup(e); };
1676 		defaultEventHandlers["mouseover"] = (Widget t, Event event)  { if(auto e = cast(MouseOverEvent) event) t.defaultEventHandler_mouseover(e); };
1677 		defaultEventHandlers["mouseout"] = (Widget t, Event event)   { if(auto e = cast(MouseOutEvent) event) t.defaultEventHandler_mouseout(e); };
1678 		defaultEventHandlers["mousedown"] = (Widget t, Event event)  { if(auto e = cast(MouseDownEvent) event) t.defaultEventHandler_mousedown(e); };
1679 		defaultEventHandlers["mouseup"] = (Widget t, Event event)    { if(auto e = cast(MouseUpEvent) event) t.defaultEventHandler_mouseup(e); };
1680 		defaultEventHandlers["mouseenter"] = (Widget t, Event event) { if(auto e = cast(MouseEnterEvent) event) t.defaultEventHandler_mouseenter(e); };
1681 		defaultEventHandlers["mouseleave"] = (Widget t, Event event) { if(auto e = cast(MouseLeaveEvent) event) t.defaultEventHandler_mouseleave(e); };
1682 		defaultEventHandlers["mousemove"] = (Widget t, Event event)  { if(auto e = cast(MouseMoveEvent) event) t.defaultEventHandler_mousemove(e); };
1683 		defaultEventHandlers["char"] = (Widget t, Event event)       { if(auto e = cast(CharEvent) event) t.defaultEventHandler_char(e); };
1684 		defaultEventHandlers["triggered"] = (Widget t, Event event)  { if(auto e = cast(Event) event) t.defaultEventHandler_triggered(e); };
1685 		defaultEventHandlers["change"] = (Widget t, Event event)     { if(auto e = cast(ChangeEventBase) event) t.defaultEventHandler_change(e); };
1686 		defaultEventHandlers["focus"] = (Widget t, Event event)      { if(auto e = cast(FocusEvent) event) t.defaultEventHandler_focus(e); };
1687 		defaultEventHandlers["blur"] = (Widget t, Event event)       { if(auto e = cast(BlurEvent) event) t.defaultEventHandler_blur(e); };
1688 		defaultEventHandlers["focusin"] = (Widget t, Event event)    { if(auto e = cast(FocusInEvent) event) t.defaultEventHandler_focusin(e); };
1689 		defaultEventHandlers["focusout"] = (Widget t, Event event)   { if(auto e = cast(FocusOutEvent) event) t.defaultEventHandler_focusout(e); };
1690 	}
1691 
1692 	/// ditto
1693 	void defaultEventHandler_click(ClickEvent event) {}
1694 	/// ditto
1695 	void defaultEventHandler_dblclick(DoubleClickEvent event) {}
1696 	/// ditto
1697 	void defaultEventHandler_keydown(KeyDownEvent event) {}
1698 	/// ditto
1699 	void defaultEventHandler_keyup(KeyUpEvent event) {}
1700 	/// ditto
1701 	void defaultEventHandler_mousedown(MouseDownEvent event) {
1702 		if(event.button == MouseButton.left) {
1703 			if(this.tabStop) {
1704 				this.focus();
1705 			}
1706 		} else if(event.button == MouseButton.right) {
1707 			showContextMenu(event.clientX, event.clientY);
1708 		}
1709 	}
1710 	/// ditto
1711 	void defaultEventHandler_mouseover(MouseOverEvent event) {}
1712 	/// ditto
1713 	void defaultEventHandler_mouseout(MouseOutEvent event) {}
1714 	/// ditto
1715 	void defaultEventHandler_mouseup(MouseUpEvent event) {}
1716 	/// ditto
1717 	void defaultEventHandler_mousemove(MouseMoveEvent event) {}
1718 	/// ditto
1719 	void defaultEventHandler_mouseenter(MouseEnterEvent event) {}
1720 	/// ditto
1721 	void defaultEventHandler_mouseleave(MouseLeaveEvent event) {}
1722 	/// ditto
1723 	void defaultEventHandler_char(CharEvent event) {}
1724 	/// ditto
1725 	void defaultEventHandler_triggered(Event event) {}
1726 	/// ditto
1727 	void defaultEventHandler_change(ChangeEventBase event) {}
1728 	/// ditto
1729 	void defaultEventHandler_focus(FocusEvent event) {}
1730 	/// ditto
1731 	void defaultEventHandler_blur(BlurEvent event) {}
1732 	/// ditto
1733 	void defaultEventHandler_focusin(FocusInEvent event) {}
1734 	/// ditto
1735 	void defaultEventHandler_focusout(FocusOutEvent event) {}
1736 
1737 	/++
1738 		[Event]s use a Javascript-esque model. See more details on the [Event] page.
1739 
1740 		[addEventListener] returns an opaque handle that you can later pass to [removeEventListener].
1741 
1742 		addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out
1743 		of participating in handler delegation.
1744 
1745 		$(TIP
1746 			Use `scope` on your handlers when you can. While it currently does nothing, this will future-proof your code against future optimizations I want to do. Instead of copying whole event objects out if you do need to store them, just copy the properties you need.
1747 		)
1748 	+/
1749 	EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) {
1750 		return addEventListener(event, (Widget, scope Event e) {
1751 			if(e.srcElement is this)
1752 				handler();
1753 		}, useCapture);
1754 	}
1755 
1756 	/// ditto
1757 	EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1758 		return addEventListener(event, (Widget, Event e) {
1759 			if(e.srcElement is this)
1760 				handler(e);
1761 		}, useCapture);
1762 	}
1763 
1764 	/// ditto
1765 	EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) {
1766 		static if(is(Handler Fn == delegate)) {
1767 		static if(is(Fn Params == __parameters)) {
1768 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1769 				if(e.srcElement !is this)
1770 					return;
1771 				auto ty = cast(Params[0]) e;
1772 				if(ty !is null)
1773 					handler(ty);
1774 			}, useCapture);
1775 		} else static assert(0);
1776 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1777 	}
1778 
1779 	/// ditto
1780 	@scriptable
1781 	EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) {
1782 		return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture);
1783 	}
1784 
1785 	/// ditto
1786 	EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) {
1787 		static if(is(Handler Fn == delegate)) {
1788 		static if(is(Fn Params == __parameters)) {
1789 			return addEventListener(EventString!(Params[0]), (Widget, Event e) {
1790 				auto ty = cast(Params[0]) e;
1791 				if(ty !is null)
1792 					handler(ty);
1793 			}, useCapture);
1794 		} else static assert(0);
1795 		} else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site.");
1796 	}
1797 
1798 	/// ditto
1799 	EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) {
1800 		return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture);
1801 	}
1802 
1803 	/// ditto
1804 	EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) {
1805 		if(event.length > 2 && event[0..2] == "on")
1806 			event = event[2 .. $];
1807 
1808 		if(useCapture)
1809 			capturingEventHandlers[event] ~= handler;
1810 		else
1811 			bubblingEventHandlers[event] ~= handler;
1812 
1813 		return EventListener(this, event, handler, useCapture);
1814 	}
1815 
1816 	/// ditto
1817 	void removeEventListener(string event, EventHandler handler, bool useCapture = false) {
1818 		if(event.length > 2 && event[0..2] == "on")
1819 			event = event[2 .. $];
1820 
1821 		if(useCapture) {
1822 			if(event in capturingEventHandlers)
1823 			foreach(ref evt; capturingEventHandlers[event])
1824 				if(evt is handler) evt = null;
1825 		} else {
1826 			if(event in bubblingEventHandlers)
1827 			foreach(ref evt; bubblingEventHandlers[event])
1828 				if(evt is handler) evt = null;
1829 		}
1830 	}
1831 
1832 	/// ditto
1833 	void removeEventListener(EventListener listener) {
1834 		removeEventListener(listener.event, listener.handler, listener.useCapture);
1835 	}
1836 
1837 	static if(UsingSimpledisplayX11) {
1838 		void discardXConnectionState() {
1839 			foreach(child; children)
1840 				child.discardXConnectionState();
1841 		}
1842 
1843 		void recreateXConnectionState() {
1844 			foreach(child; children)
1845 				child.recreateXConnectionState();
1846 			redraw();
1847 		}
1848 	}
1849 
1850 	/++
1851 		Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen.
1852 
1853 		History:
1854 			`globalCoordinates` was made `final` on May 11, 2021.
1855 	+/
1856 	Point globalCoordinates() {
1857 		int x = this.x;
1858 		int y = this.y;
1859 		auto p = this.parent;
1860 		while(p) {
1861 			x += p.x;
1862 			y += p.y;
1863 			p = p.parent;
1864 		}
1865 
1866 		static if(UsingSimpledisplayX11) {
1867 			auto dpy = XDisplayConnection.get;
1868 			arsd.simpledisplay.Window dummyw;
1869 			XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw);
1870 		} else version(Windows) {
1871 			POINT pt;
1872 			pt.x = x;
1873 			pt.y = y;
1874 			MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1);
1875 			x = pt.x;
1876 			y = pt.y;
1877 		} else {
1878 			auto rect = this.parentWindow.win.impl.window.frame;
1879 			// FIXME: confirm?
1880 			x += cast(int) rect.origin.x;
1881 			y += cast(int) rect.origin.y;
1882 		}
1883 
1884 		return Point(x, y);
1885 	}
1886 
1887 	version(win32_widgets)
1888 	int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; }
1889 
1890 	version(win32_widgets)
1891 	/// Called when a WM_COMMAND is sent to the associated hwnd.
1892 	void handleWmCommand(ushort cmd, ushort id) {}
1893 
1894 	version(win32_widgets)
1895 	/++
1896 		Called when a WM_NOTIFY is sent to the associated hwnd.
1897 
1898 		History:
1899 	+/
1900 	int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; }
1901 
1902 	version(win32_widgets)
1903 	deprecated("This overload is problematic since it is liable to discard return values. Add the `out int mustReturn` to your override as the last parameter and set it to 1 when you must forward the return value to Windows. Otherwise, you can just add the parameter then ignore it and use the default value of 0 to maintain the status quo.") int handleWmNotify(NMHDR* hdr, int code) { int ignored; return handleWmNotify(hdr, code, ignored); }
1904 
1905 	/++
1906 		This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget.
1907 
1908 		Updates to this variable will only be made visible on the next mouse enter event.
1909 	+/
1910 	@scriptable string statusTip;
1911 	// string toolTip;
1912 	// string helpText;
1913 
1914 	/++
1915 		If true, this widget can be focused via keyboard control with the tab key.
1916 
1917 		If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to).
1918 	+/
1919 	bool tabStop = true;
1920 	/++
1921 		The tab key cycles through widgets by the order of a.tabOrder < b.tabOrder. If they are equal, it does them in child order (which is typically the order they were added to the widget.)
1922 	+/
1923 	int tabOrder;
1924 
1925 	version(win32_widgets) {
1926 		static Widget[HWND] nativeMapping;
1927 		/// The native handle, if there is one.
1928 		HWND hwnd;
1929 		WNDPROC originalWindowProcedure;
1930 
1931 		SimpleWindow simpleWindowWrappingHwnd;
1932 
1933 		// please note it IGNORES your return value and does NOT forward it to Windows!
1934 		int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) {
1935 			return 0;
1936 		}
1937 	}
1938 	private bool implicitlyCreated;
1939 
1940 	/// Child's position relative to the parent's origin. only the layout manager should be modifying this and even reading it is of limited utility. It may be made `private` at some point in the future without advance notice. Do NOT depend on it being available unless you are writing a layout manager.
1941 	int x;
1942 	/// ditto
1943 	int y;
1944 	private int _width;
1945 	private int _height;
1946 	private Widget[] _children;
1947 	private Widget _parent;
1948 	private Window _parentWindow;
1949 
1950 	/++
1951 		Returns the window to which this widget is attached.
1952 
1953 		History:
1954 			Prior to May 11, 2021, the `Window parentWindow` variable was directly available. Now, only this property getter is available and the actual store is private.
1955 	+/
1956 	final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; }
1957 	private @property void parentWindow(Window parent) {
1958 		auto old = _parentWindow;
1959 		_parentWindow = parent;
1960 		newParentWindow(old, _parentWindow);
1961 		foreach(child; children)
1962 			child.parentWindow = parent; // please note that this is recursive
1963 	}
1964 
1965 	/++
1966 		Called when the widget has been added to or remove from a parent window.
1967 
1968 		Note that either oldParent and/or newParent may be null any time this is called.
1969 
1970 		History:
1971 			Added September 13, 2024
1972 	+/
1973 	protected void newParentWindow(Window oldParent, Window newParent) {}
1974 
1975 	/++
1976 		Returns the list of the widget's children.
1977 
1978 		History:
1979 			Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private.
1980 
1981 			Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list.
1982 	+/
1983 	final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; }
1984 
1985 	/++
1986 		Returns the widget's parent.
1987 
1988 		History:
1989 			Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted.
1990 
1991 			The parent should only be managed by the [addChild] and [removeWidget] method.
1992 	+/
1993 	final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; }
1994 
1995 	/// The widget's current size.
1996 	final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; }
1997 	/// ditto
1998 	final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; }
1999 
2000 	/// Only the layout manager should be calling these.
2001 	final protected @property int width(int a) @safe { return _width = a; }
2002 	/// ditto
2003 	final protected @property int height(int a) @safe { return _height = a; }
2004 
2005 	/++
2006 		This function is called by the layout engine after it has updated the position (in variables `x` and `y`) and the size (in properties `width` and `height`) to give you a chance to update the actual position of the native child window (if there is one) or whatever.
2007 
2008 		It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size.
2009 	+/
2010 	protected void registerMovement() {
2011 		version(win32_widgets) {
2012 			if(hwnd) {
2013 				auto pos = getChildPositionRelativeToParentHwnd(this);
2014 				MoveWindow(hwnd, pos[0], pos[1], width, height, true); // setting this to false can sometimes speed things up but only if it is actually drawn later and that's kinda iffy to do right here so being slower but safer rn
2015 				this.redraw();
2016 			}
2017 		}
2018 		sendResizeEvent();
2019 	}
2020 
2021 	/// Creates the widget and adds it to the parent.
2022 	this(Widget parent) {
2023 		if(parent !is null)
2024 			parent.addChild(this);
2025 		setupDefaultEventHandlers();
2026 	}
2027 
2028 	/// Returns true if this is the current focused widget inside the parent window. Please note it may return `true` when the window itself is unfocused. In that case, it indicates this widget will receive focuse again when the window does.
2029 	@scriptable
2030 	bool isFocused() {
2031 		return parentWindow && parentWindow.focusedWidget is this;
2032 	}
2033 
2034 	private bool showing_ = true;
2035 	///
2036 	bool showing() const { return showing_; }
2037 	///
2038 	bool hidden() const { return !showing_; }
2039 	/++
2040 		Shows or hides the window. Meant to be assigned as a property. If `recalculate` is true (the default), it recalculates the layout of the parent widget to use the space this widget being hidden frees up or make space for this widget to appear again.
2041 
2042 		Note that a widget only ever shows if all its parents are showing too.
2043 	+/
2044 	void showing(bool s, bool recalculate = true) {
2045 		if(s != showing_) {
2046 			showing_ = s;
2047 			// writeln(typeid(this).toString, " ", this.parent ? typeid(this.parent).toString : "null", " ", s);
2048 
2049 			showNativeWindowChildren(s);
2050 
2051 			if(parent && recalculate) {
2052 				parent.queueRecomputeChildLayout();
2053 				parent.redraw();
2054 			}
2055 
2056 			if(s) {
2057 				queueRecomputeChildLayout();
2058 				redraw();
2059 			}
2060 		}
2061 	}
2062 	/// Convenience method for `showing = true`
2063 	@scriptable
2064 	void show() {
2065 		showing = true;
2066 	}
2067 	/// Convenience method for `showing = false`
2068 	@scriptable
2069 	void hide() {
2070 		showing = false;
2071 	}
2072 
2073 	/++
2074 		If you are a native window, show/hide it based on shouldShow and return `true`.
2075 
2076 		Otherwise, do nothing and return false.
2077 	+/
2078 	protected bool showOrHideIfNativeWindow(bool shouldShow) {
2079 		version(win32_widgets) {
2080 			if(hwnd) {
2081 				ShowWindow(hwnd, shouldShow ? SW_SHOW : SW_HIDE);
2082 				return true;
2083 			} else {
2084 				return false;
2085 			}
2086 		} else {
2087 			return false;
2088 		}
2089 	}
2090 
2091 	private void showNativeWindowChildren(bool s) {
2092 		if(!showOrHideIfNativeWindow(s && showing))
2093 			foreach(child; children)
2094 				child.showNativeWindowChildren(s);
2095 	}
2096 
2097 	///
2098 	@scriptable
2099 	void focus() {
2100 		assert(parentWindow !is null);
2101 		if(isFocused())
2102 			return;
2103 
2104 		if(parentWindow.focusedWidget) {
2105 			// FIXME: more details here? like from and to
2106 			auto from = parentWindow.focusedWidget;
2107 			parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false);
2108 			parentWindow.focusedWidget = null;
2109 			from.emit!BlurEvent();
2110 			from.emit!FocusOutEvent();
2111 		}
2112 
2113 
2114 		version(win32_widgets) {
2115 			if(this.hwnd !is null)
2116 				SetFocus(this.hwnd);
2117 		}
2118 		//else static if(UsingSimpledisplayX11)
2119 			//this.parentWindow.win.focus();
2120 
2121 		parentWindow.focusedWidget = this;
2122 		parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true);
2123 		this.emit!FocusEvent();
2124 		this.emit!FocusInEvent();
2125 	}
2126 
2127 	/+
2128 	/++
2129 		Unfocuses the widget. This may reset
2130 	+/
2131 	@scriptable
2132 	void blur() {
2133 
2134 	}
2135 	+/
2136 
2137 
2138 	/++
2139 		This is called when the widget is added to a window. It gives you a chance to set up event hooks.
2140 
2141 		Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods.
2142 	+/
2143 	void attachedToWindow(Window w) {}
2144 	/++
2145 		Callback when the widget is added to another widget.
2146 
2147 		Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful.
2148 	+/
2149 	void addedTo(Widget w) {}
2150 
2151 	/++
2152 		Adds a child to the given position. This is `protected` because you generally shouldn't be calling this directly. Instead, construct widgets with the parent directly.
2153 
2154 		This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget.
2155 	+/
2156 	protected void addChild(Widget w, int position = int.max) {
2157 		assert(w._parent !is this, "Child cannot be added twice to the same parent");
2158 		assert(w !is this, "Child cannot be its own parent!");
2159 		w._parent = this;
2160 		if(position == int.max || position == children.length) {
2161 			_children ~= w;
2162 		} else {
2163 			assert(position < _children.length);
2164 			_children.length = _children.length + 1;
2165 			for(int i = cast(int) _children.length - 1; i > position; i--)
2166 				_children[i] = _children[i - 1];
2167 			_children[position] = w;
2168 		}
2169 
2170 		this.parentWindow = this._parentWindow;
2171 
2172 		w.addedTo(this);
2173 
2174 		bool parentIsNative;
2175 		version(win32_widgets) {
2176 			parentIsNative = hwnd !is null;
2177 		}
2178 		if(!parentIsNative && !showing)
2179 			w.showOrHideIfNativeWindow(false);
2180 
2181 		if(parentWindow !is null) {
2182 			w.attachedToWindow(parentWindow);
2183 			parentWindow.queueRecomputeChildLayout();
2184 			parentWindow.redraw();
2185 		}
2186 	}
2187 
2188 	/++
2189 		Finds the child at the top of the z-order at the given coordinates (relative to the `this` widget's origin), or null if none are found.
2190 	+/
2191 	Widget getChildAtPosition(int x, int y) {
2192 		// it goes backward so the last one to show gets picked first
2193 		// might use z-index later
2194 		foreach_reverse(child; children) {
2195 			if(child.hidden)
2196 				continue;
2197 			if(child.x <= x && child.y <= y
2198 				&& ((x - child.x) < child.width)
2199 				&& ((y - child.y) < child.height))
2200 			{
2201 				return child;
2202 			}
2203 		}
2204 
2205 		return null;
2206 	}
2207 
2208 	/++
2209 		If the widget is a scrollable container, this should add the current scroll position to the given coordinates so the mouse events can be dispatched correctly.
2210 
2211 		History:
2212 			Added July 2, 2021 (v10.2)
2213 	+/
2214 	protected void addScrollPosition(ref int x, ref int y) {}
2215 
2216 	/++
2217 		Responsible for actually painting the widget to the screen. The clip rectangle and coordinate translation in the [WidgetPainter] are pre-configured so you can draw independently.
2218 
2219 		This function paints the entire widget, including styled borders, backgrounds, etc. You are also responsible for displaying any important active state to the user, including if you hold the active keyboard focus. If you only want to be responsible for the content while letting the style engine draw the rest, override [paintContent] instead.
2220 
2221 		[paint] is not called for system widgets as the OS library draws them instead.
2222 
2223 
2224 		The default implementation forwards to [WidgetPainter.drawThemed], passing [paintContent] as the delegate. If you override this, you might use those same functions or you can do your own thing.
2225 
2226 		You should also look at [WidgetPainter.visualTheme] to be theme aware.
2227 
2228 		History:
2229 			Prior to May 15, 2021, the default implementation was empty. Now, it is `painter.drawThemed(&paintContent);`. You may wish to override [paintContent] instead of [paint] to take advantage of the new styling engine.
2230 	+/
2231 	void paint(WidgetPainter painter) {
2232 		version(win32_widgets)
2233 			if(hwnd) {
2234 				return;
2235 			}
2236 		painter.drawThemed(&paintContent); // note this refers to the following overload
2237 	}
2238 
2239 	/++
2240 		Responsible for drawing the content as the theme engine is responsible for other elements.
2241 
2242 		$(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.)
2243 
2244 		Params:
2245 			painter = your painter (forwarded from [paint]) for drawing on the widget. The clip rectangle and coordinate translation are prepared for you ahead of time so you can use widget coordinates. It also has the theme foreground preloaded into the painter outline color, the theme font preloaded as the painter's active font, and the theme background preloaded as the painter's fill color.
2246 
2247 			bounds = the bounds, inside the widget, where your content should be drawn. This is the rectangle inside the border and padding (if any). The stuff outside is not clipped - it is still part of your widget - but you should respect these bounds for visual consistency and respecting the theme's area.
2248 
2249 			If you do want to clip it, you can of course call `auto oldClip = painter.setClipRectangle(bounds); scope(exit) painter.setClipRectangle(oldClip);` to modify it and return to the previous setting when you return.
2250 
2251 		Returns:
2252 			The rectangle representing your actual content. Typically, this is simply `return bounds;`. The theme engine uses this return value to determine where the outline and overlay should be.
2253 
2254 		History:
2255 			Added May 15, 2021
2256 	+/
2257 	Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
2258 		return bounds;
2259 	}
2260 
2261 	deprecated("Change ScreenPainter to WidgetPainter")
2262 	final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
2263 
2264 	/// I don't actually like the name of this
2265 	/// this draws a background on it
2266 	void erase(WidgetPainter painter) {
2267 		version(win32_widgets)
2268 			if(hwnd) return; // Windows will do it. I think.
2269 
2270 		auto c = getComputedStyle().background.color;
2271 		painter.fillColor = c;
2272 		painter.outlineColor = c;
2273 
2274 		version(win32_widgets) {
2275 			HANDLE b, p;
2276 			if(c.a == 0 && parent is parentWindow) {
2277 				// I don't remember why I had this really...
2278 				b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
2279 				p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
2280 			}
2281 		}
2282 		painter.drawRectangle(Point(0, 0), width, height);
2283 		version(win32_widgets) {
2284 			if(c.a == 0 && parent is parentWindow) {
2285 				SelectObject(painter.impl.hdc, p);
2286 				SelectObject(painter.impl.hdc, b);
2287 			}
2288 		}
2289 	}
2290 
2291 	///
2292 	WidgetPainter draw() {
2293 		int x = this.x, y = this.y;
2294 		auto parent = this.parent;
2295 		while(parent) {
2296 			x += parent.x;
2297 			y += parent.y;
2298 			parent = parent.parent;
2299 		}
2300 
2301 		auto painter = parentWindow.win.draw(true);
2302 		painter.originX = x;
2303 		painter.originY = y;
2304 		painter.setClipRectangle(Point(0, 0), width, height);
2305 		return WidgetPainter(painter, this);
2306 	}
2307 
2308 	/// This can be overridden by scroll things. It is responsible for actually calling [paint]. Do not override unless you've studied minigui.d's source code. There are no stability guarantees if you do override this; it can (and likely will) break without notice.
2309 	protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
2310 		if(hidden)
2311 			return;
2312 
2313 		int paintX = x;
2314 		int paintY = y;
2315 		if(this.useNativeDrawing()) {
2316 			paintX = 0;
2317 			paintY = 0;
2318 			lox = 0;
2319 			loy = 0;
2320 			containment = Rectangle(0, 0, int.max, int.max);
2321 		}
2322 
2323 		painter.originX = lox + paintX;
2324 		painter.originY = loy + paintY;
2325 
2326 		bool actuallyPainted = false;
2327 
2328 		const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height)));
2329 		if(clip == Rectangle.init) {
2330 			// writeln(this, " clipped out");
2331 			return;
2332 		}
2333 
2334 		bool invalidateChildren = invalidate;
2335 
2336 		if(redrawRequested || force) {
2337 			painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
2338 
2339 			painter.drawingUpon = this;
2340 
2341 			erase(painter);
2342 			if(painter.visualTheme)
2343 				painter.visualTheme.doPaint(this, painter);
2344 			else
2345 				paint(painter);
2346 
2347 			if(invalidate) {
2348 				// sdpyPrintDebugString("invalidate " ~ typeid(this).name);
2349 				auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height));
2350 				painter.invalidateRect(region);
2351 				// children are contained inside this, so no need to do extra work
2352 				invalidateChildren = false;
2353 			}
2354 
2355 			redrawRequested = false;
2356 			actuallyPainted = true;
2357 		}
2358 
2359 		foreach(child; children) {
2360 			version(win32_widgets)
2361 				if(child.useNativeDrawing()) continue;
2362 			child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren);
2363 		}
2364 
2365 		version(win32_widgets)
2366 		foreach(child; children) {
2367 			if(child.useNativeDrawing) {
2368 				painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child);
2369 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, true); // have to reset the invalidate flag since these are not necessarily affected the same way, being native children with a clip
2370 			}
2371 		}
2372 	}
2373 
2374 	protected bool useNativeDrawing() nothrow {
2375 		version(win32_widgets)
2376 			return hwnd !is null;
2377 		else
2378 			return false;
2379 	}
2380 
2381 	private static class RedrawEvent {}
2382 	private __gshared re = new RedrawEvent();
2383 
2384 	private bool redrawRequested;
2385 	///
2386 	final void redraw(string file = __FILE__, size_t line = __LINE__) {
2387 		redrawRequested = true;
2388 
2389 		if(this.parentWindow) {
2390 			auto sw = this.parentWindow.win;
2391 			assert(sw !is null);
2392 			if(!sw.eventQueued!RedrawEvent) {
2393 				sw.postEvent(re);
2394 				//  writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window);
2395 			}
2396 		}
2397 	}
2398 
2399 	private SimpleWindow drawableWindow;
2400 
2401 	/++
2402 		Allows a class to easily dispatch its own statically-declared event (see [Emits]). The main benefit of using this over constructing an event yourself is simply that you ensure you haven't sent something you haven't documented you can send.
2403 
2404 		Returns:
2405 			`true` if you should do your default behavior.
2406 
2407 		History:
2408 			Added May 5, 2021
2409 
2410 		Bugs:
2411 			It does not do the static checks on gdc right now.
2412 	+/
2413 	final protected bool emit(EventType, this This, Args...)(Args args) {
2414 		version(GNU) {} else
2415 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2416 		auto e = new EventType(this, args);
2417 		e.dispatch();
2418 		return !e.defaultPrevented;
2419 	}
2420 	/// ditto
2421 	final protected bool emit(string eventString, this This)() {
2422 		auto e = new Event(eventString, this);
2423 		e.dispatch();
2424 		return !e.defaultPrevented;
2425 	}
2426 
2427 	/++
2428 		Does the same as [addEventListener]'s delegate overload, but adds an additional check to ensure the event you are subscribing to is actually emitted by the static type you are using. Since it works on static types, if you have a generic [Widget], this can only subscribe to events declared as [Emits] inside [Widget] itself, not any child classes nor any child elements. If this is too restrictive, simply use [addEventListener] instead.
2429 
2430 		History:
2431 			Added May 5, 2021
2432 	+/
2433 	final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) {
2434 		static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof);
2435 		return addEventListener(handler);
2436 	}
2437 
2438 	/++
2439 		Gets the computed style properties from the visual theme.
2440 
2441 		You should use this in your paint and layout functions instead of the direct properties on the widget if you want to be style aware. (But when setting defaults in your classes, overriding is the right thing to do. Override to set defaults, but then read out of [getComputedStyle].)
2442 
2443 		History:
2444 			Added May 8, 2021
2445 	+/
2446 	final StyleInformation getComputedStyle() {
2447 		return StyleInformation(this);
2448 	}
2449 
2450 	int focusableWidgets(scope int delegate(Widget) dg) {
2451 		foreach(widget; WidgetStream(this)) {
2452 			if(widget.tabStop && !widget.hidden) {
2453 				int result = dg(widget);
2454 				if (result)
2455 					return result;
2456 			}
2457 		}
2458 		return 0;
2459 	}
2460 
2461 	/++
2462 		Calculates the border box (that is, the full width/height of the widget, from border edge to border edge)
2463 		for the given content box (the area between the padding)
2464 
2465 		History:
2466 			Added January 4, 2023 (dub v11.0)
2467 	+/
2468 	Rectangle borderBoxForContentBox(Rectangle contentBox) {
2469 		auto cs = getComputedStyle();
2470 
2471 		auto borderWidth = getBorderWidth(cs.borderStyle);
2472 
2473 		auto rect = contentBox;
2474 
2475 		rect.left -= borderWidth;
2476 		rect.right += borderWidth;
2477 		rect.top -= borderWidth;
2478 		rect.bottom += borderWidth;
2479 
2480 		auto insideBorderRect = rect;
2481 
2482 		rect.left -= cs.paddingLeft;
2483 		rect.right += cs.paddingRight;
2484 		rect.top -= cs.paddingTop;
2485 		rect.bottom += cs.paddingBottom;
2486 
2487 		return rect;
2488 	}
2489 
2490 
2491 	// FIXME: I kinda want to hide events from implementation widgets
2492 	// so it just catches them all and stops propagation...
2493 	// i guess i can do it with a event listener on star.
2494 
2495 	mixin Emits!KeyDownEvent; ///
2496 	mixin Emits!KeyUpEvent; ///
2497 	mixin Emits!CharEvent; ///
2498 
2499 	mixin Emits!MouseDownEvent; ///
2500 	mixin Emits!MouseUpEvent; ///
2501 	mixin Emits!ClickEvent; ///
2502 	mixin Emits!DoubleClickEvent; ///
2503 	mixin Emits!MouseMoveEvent; ///
2504 	mixin Emits!MouseOverEvent; ///
2505 	mixin Emits!MouseOutEvent; ///
2506 	mixin Emits!MouseEnterEvent; ///
2507 	mixin Emits!MouseLeaveEvent; ///
2508 
2509 	mixin Emits!ResizeEvent; ///
2510 
2511 	mixin Emits!BlurEvent; ///
2512 	mixin Emits!FocusEvent; ///
2513 
2514 	mixin Emits!FocusInEvent; ///
2515 	mixin Emits!FocusOutEvent; ///
2516 }
2517 
2518 /+
2519 /++
2520 	Interface to indicate that the widget has a simple value property.
2521 
2522 	History:
2523 		Added August 26, 2021
2524 +/
2525 interface HasValue!T {
2526 	/// Getter
2527 	@property T value();
2528 	/// Setter
2529 	@property void value(T);
2530 }
2531 
2532 /++
2533 	Interface to indicate that the widget has a range of possible values for its simple value property.
2534 	This would be present on something like a slider or possibly a number picker.
2535 
2536 	History:
2537 		Added September 11, 2021
2538 +/
2539 interface HasRangeOfValues!T : HasValue!T {
2540 	/// The minimum and maximum values in the range, inclusive.
2541 	@property T minValue();
2542 	@property void minValue(T); /// ditto
2543 	@property T maxValue(); /// ditto
2544 	@property void maxValue(T); /// ditto
2545 
2546 	/// The smallest step the user interface allows. User may still type in values without this limitation.
2547 	@property void step(T);
2548 	@property T step(); /// ditto
2549 }
2550 
2551 /++
2552 	Interface to indicate that the widget has a list of possible values the user can choose from.
2553 	This would be present on something like a drop-down selector.
2554 
2555 	The value is NOT necessarily one of the items on the list. Consider the case of a free-entry
2556 	combobox.
2557 
2558 	History:
2559 		Added September 11, 2021
2560 +/
2561 interface HasListOfValues!T : HasValue!T {
2562 	@property T[] values;
2563 	@property void values(T[]);
2564 
2565 	@property int selectedIndex(); // note it may return -1!
2566 	@property void selectedIndex(int);
2567 }
2568 +/
2569 
2570 /++
2571 	History:
2572 		Added September 2021 (dub v10.4)
2573 +/
2574 class GridLayout : Layout {
2575 
2576 	// FIXME: grid padding around edges and also cell spacing between units. even though you could do that by just specifying some gutter yourself in the layout.
2577 
2578 	/++
2579 		If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to.
2580 	+/
2581 	enum Gravity {
2582 		Center    = 0,
2583 		NorthWest = North | West,
2584 		North     = 0b10_00,
2585 		NorthEast = North | East,
2586 		West      = 0b00_10,
2587 		East      = 0b00_01,
2588 		SouthWest = South | West,
2589 		South     = 0b01_00,
2590 		SouthEast = South | East,
2591 	}
2592 
2593 	/++
2594 		The width and height are in some proportional units and can often just be 12.
2595 	+/
2596 	this(int width, int height, Widget parent) {
2597 		this.gridWidth = width;
2598 		this.gridHeight = height;
2599 		super(parent);
2600 	}
2601 
2602 	/++
2603 		Sets the position of the given child.
2604 
2605 		The units of these arguments are in the proportional grid units you set in the constructor.
2606 	+/
2607 	Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) {
2608 		// ensure it is in bounds
2609 		// then ensure no overlaps
2610 
2611 		ChildPosition p = ChildPosition(child, x, y, width, height, gravity);
2612 
2613 		foreach(ref position; positions) {
2614 			if(position.widget is child) {
2615 				position = p;
2616 				goto set;
2617 			}
2618 		}
2619 
2620 		positions ~= p;
2621 
2622 		set:
2623 
2624 		// FIXME: should this batch?
2625 		queueRecomputeChildLayout();
2626 
2627 		return child;
2628 	}
2629 
2630 	override void addChild(Widget w, int position = int.max) {
2631 		super.addChild(w, position);
2632 		//positions ~= ChildPosition(w);
2633 		if(position != int.max) {
2634 			// FIXME: align it so they actually match.
2635 		}
2636 	}
2637 
2638 	override void widgetRemoved(size_t idx, Widget w) {
2639 		// FIXME: keep the positions array aligned
2640 		// positions[idx].widget = null;
2641 	}
2642 
2643 	override void recomputeChildLayout() {
2644 		registerMovement();
2645 		int onGrid = cast(int) positions.length;
2646 		c: foreach(child; children) {
2647 			// just snap it to the grid
2648 			if(onGrid)
2649 			foreach(position; positions)
2650 				if(position.widget is child) {
2651 					child.x = this.width * position.x / this.gridWidth;
2652 					child.y = this.height * position.y / this.gridHeight;
2653 					child.width = this.width * position.width / this.gridWidth;
2654 					child.height = this.height * position.height / this.gridHeight;
2655 
2656 					auto diff = child.width - child.maxWidth();
2657 					// FIXME: gravity?
2658 					if(diff > 0) {
2659 						child.width = child.width - diff;
2660 
2661 						if(position.gravity & Gravity.West) {
2662 							// nothing needed, already aligned
2663 						} else if(position.gravity & Gravity.East) {
2664 							child.x += diff;
2665 						} else {
2666 							child.x += diff / 2;
2667 						}
2668 					}
2669 
2670 					diff = child.height - child.maxHeight();
2671 					// FIXME: gravity?
2672 					if(diff > 0) {
2673 						child.height = child.height - diff;
2674 
2675 						if(position.gravity & Gravity.North) {
2676 							// nothing needed, already aligned
2677 						} else if(position.gravity & Gravity.South) {
2678 							child.y += diff;
2679 						} else {
2680 							child.y += diff / 2;
2681 						}
2682 					}
2683 					child.recomputeChildLayout();
2684 					onGrid--;
2685 					continue c;
2686 				}
2687 			// the position isn't given on the grid array, we'll just fill in from where the explicit ones left off.
2688 		}
2689 	}
2690 
2691 	private struct ChildPosition {
2692 		Widget widget;
2693 		int x;
2694 		int y;
2695 		int width;
2696 		int height;
2697 		Gravity gravity;
2698 	}
2699 	private ChildPosition[] positions;
2700 
2701 	int gridWidth = 12;
2702 	int gridHeight = 12;
2703 }
2704 
2705 ///
2706 abstract class ComboboxBase : Widget {
2707 	// if the user can enter arbitrary data, we want to use  2 == CBS_DROPDOWN
2708 	// or to always show the list, we want CBS_SIMPLE == 1
2709 	version(win32_widgets)
2710 		this(uint style, Widget parent) {
2711 			super(parent);
2712 			createWin32Window(this, "ComboBox"w, null, style);
2713 		}
2714 	else version(custom_widgets)
2715 		this(Widget parent) {
2716 			super(parent);
2717 
2718 			addEventListener((KeyDownEvent event) {
2719 				if(event.key == Key.Up) {
2720 					setSelection(selection_-1);
2721 					event.preventDefault();
2722 				}
2723 				if(event.key == Key.Down) {
2724 					setSelection(selection_+1);
2725 					event.preventDefault();
2726 				}
2727 
2728 			});
2729 
2730 		}
2731 	else static assert(false);
2732 
2733 	protected void scrollSelectionIntoView() {}
2734 
2735 	/++
2736 		Returns the current list of options in the selection.
2737 
2738 		History:
2739 			Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private.
2740 	+/
2741 	final @property string[] options() const {
2742 		return cast(string[]) options_;
2743 	}
2744 
2745 	/++
2746 		Replaces the list of options in the box. Note that calling this will also reset the selection.
2747 
2748 		History:
2749 			Added December, 29 2024
2750 	+/
2751 	final @property void options(string[] options) {
2752 		version(win32_widgets)
2753 			SendMessageW(hwnd, 331 /*CB_RESETCONTENT*/, 0, 0);
2754 		selection_ = -1;
2755 		options_ = null;
2756 		foreach(opt; options)
2757 			addOption(opt);
2758 
2759 		version(custom_widgets)
2760 			redraw();
2761 	}
2762 
2763 	private string[] options_;
2764 	private int selection_ = -1;
2765 
2766 	/++
2767 		Adds an option to the end of options array.
2768 	+/
2769 	void addOption(string s) {
2770 		options_ ~= s;
2771 		version(win32_widgets)
2772 		SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s));
2773 	}
2774 
2775 	/++
2776 		Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected.
2777 	+/
2778 	int getSelection() {
2779 		return selection_;
2780 	}
2781 
2782 	/++
2783 		Returns the current selection as a string.
2784 
2785 		History:
2786 			Added November 17, 2021
2787 	+/
2788 	string getSelectionString() {
2789 		return selection_ == -1 ? null : options[selection_];
2790 	}
2791 
2792 	/++
2793 		Sets the current selection to an index in the options array, or to the given option if present.
2794 		Please note that the string version may do a linear lookup.
2795 
2796 		Returns:
2797 			the index you passed in
2798 
2799 		History:
2800 			The `string` based overload was added on March 1, 2022 (dub v10.7).
2801 
2802 			The return value was `void` prior to March 1, 2022.
2803 	+/
2804 	int setSelection(int idx) {
2805 		if(idx < -1)
2806 			idx = -1;
2807 		if(idx + 1 > options.length)
2808 			idx = cast(int) options.length - 1;
2809 
2810 		selection_ = idx;
2811 
2812 		version(win32_widgets)
2813 		SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0);
2814 
2815 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2816 		t.dispatch();
2817 
2818 		scrollSelectionIntoView();
2819 
2820 		return idx;
2821 	}
2822 
2823 	/// ditto
2824 	int setSelection(string s) {
2825 		if(s !is null)
2826 		foreach(idx, item; options)
2827 			if(item == s) {
2828 				return setSelection(cast(int) idx);
2829 			}
2830 		return setSelection(-1);
2831 	}
2832 
2833 	/++
2834 		This event is fired when the selection changes. Both [Event.stringValue] and
2835 		[Event.intValue] are filled in - `stringValue` is the text in the selection
2836 		and `intValue` is the index of the selection. If the combo box allows multiple
2837 		selection, these values will include only one of the selected items - for those,
2838 		you should loop through the values and check their selected flag instead.
2839 
2840 		(I know that sucks, but it is how it is right now.)
2841 
2842 		History:
2843 			It originally inherited from `ChangeEvent!String`, but now does from [ChangeEventBase] as of January 3, 2025.
2844 			This shouldn't break anything if you used it through either its own name `SelectionChangedEvent` or through the
2845 			base `Event`, only if you specifically used `ChangeEvent!string` - those handlers may now get `null` or fail to
2846 			be called. If you did do this, just change it to generic `Event`, as `stringValue` and `intValue` are already there.
2847 	+/
2848 	static final class SelectionChangedEvent : ChangeEventBase {
2849 		this(Widget target, int iv, string sv) {
2850 			super(target);
2851 			this.iv = iv;
2852 			this.sv = sv;
2853 		}
2854 		immutable int iv;
2855 		immutable string sv;
2856 
2857 		deprecated("Use stringValue or intValue instead") @property string value() {
2858 			return sv;
2859 		}
2860 
2861 		override @property string stringValue() { return sv; }
2862 		override @property int intValue() { return iv; }
2863 	}
2864 
2865 	version(win32_widgets)
2866 	override void handleWmCommand(ushort cmd, ushort id) {
2867 		if(cmd == CBN_SELCHANGE) {
2868 			selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0);
2869 			fireChangeEvent();
2870 		}
2871 	}
2872 
2873 	private void fireChangeEvent() {
2874 		if(selection_ >= options.length)
2875 			selection_ = -1;
2876 
2877 		auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]);
2878 		t.dispatch();
2879 	}
2880 
2881 	override int minWidth() { return scaleWithDpi(32); }
2882 
2883 	version(win32_widgets) {
2884 		override int minHeight() { return defaultLineHeight + 6; }
2885 		override int maxHeight() { return defaultLineHeight + 6; }
2886 	} else {
2887 		override int minHeight() { return defaultLineHeight + 4; }
2888 		override int maxHeight() { return defaultLineHeight + 4; }
2889 	}
2890 
2891 	version(custom_widgets)
2892 	void popup() {
2893 		CustomComboBoxPopup popup = new CustomComboBoxPopup(this);
2894 	}
2895 
2896 }
2897 
2898 private class CustomComboBoxPopup : Window {
2899 	private ComboboxBase associatedWidget;
2900 	private ListWidget lw;
2901 	private bool cancelled;
2902 
2903 	this(ComboboxBase associatedWidget) {
2904 		this.associatedWidget = associatedWidget;
2905 
2906 		// FIXME: this should scroll if there's too many elements to reasonably fit on screen
2907 
2908 		auto w = associatedWidget.width;
2909 		// FIXME: suggestedDropdownHeight see below
2910 		auto h = cast(int) associatedWidget.options.length * associatedWidget.defaultLineHeight + associatedWidget.scaleWithDpi(8);
2911 
2912 		// FIXME: this sux
2913 		if(h > associatedWidget.parentWindow.height)
2914 			h = associatedWidget.parentWindow.height;
2915 
2916 		auto mh = associatedWidget.scaleWithDpi(16 + 16 + 32); // to make the scrollbar look ok
2917 		if(h < mh)
2918 			h = mh;
2919 
2920 		auto coord = associatedWidget.globalCoordinates();
2921 		auto dropDown = new SimpleWindow(
2922 			w, h,
2923 			null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, associatedWidget.parentWindow ? associatedWidget.parentWindow.win : null);
2924 
2925 		super(dropDown);
2926 
2927 		dropDown.move(coord.x, coord.y + associatedWidget.height);
2928 
2929 		this.lw = new ListWidget(this);
2930 		version(custom_widgets)
2931 			lw.multiSelect = false;
2932 		foreach(option; associatedWidget.options)
2933 			lw.addOption(option);
2934 
2935 		auto originalSelection = associatedWidget.getSelection;
2936 		lw.setSelection(originalSelection);
2937 		lw.scrollSelectionIntoView();
2938 
2939 		/+
2940 		{
2941 			auto cs = getComputedStyle();
2942 			auto painter = dropDown.draw();
2943 			draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color);
2944 			auto p = Point(4, 4);
2945 			painter.outlineColor = cs.foregroundColor;
2946 			foreach(option; associatedWidget.options) {
2947 				painter.drawText(p, option);
2948 				p.y += defaultLineHeight;
2949 			}
2950 		}
2951 
2952 		dropDown.setEventHandlers(
2953 			(MouseEvent event) {
2954 				if(event.type == MouseEventType.buttonReleased) {
2955 					dropDown.close();
2956 					auto element = (event.y - 4) / defaultLineHeight;
2957 					if(element >= 0 && element <= associatedWidget.options.length) {
2958 						associatedWidget.selection_ = element;
2959 
2960 						associatedWidget.fireChangeEvent();
2961 					}
2962 				}
2963 			}
2964 		);
2965 		+/
2966 
2967 		Widget previouslyFocusedWidget;
2968 
2969 		dropDown.visibilityChanged = (bool visible) {
2970 			if(visible) {
2971 				this.redraw();
2972 				captureMouse(this);
2973 
2974 				if(previouslyFocusedWidget is null)
2975 					previouslyFocusedWidget = associatedWidget.parentWindow.focusedWidget;
2976 				associatedWidget.parentWindow.focusedWidget = lw;
2977 			} else {
2978 				//dropDown.releaseInputGrab();
2979 				releaseMouseCapture();
2980 
2981 				if(!cancelled)
2982 					associatedWidget.setSelection(lw.getSelection);
2983 
2984 				associatedWidget.parentWindow.focusedWidget = previouslyFocusedWidget;
2985 			}
2986 		};
2987 
2988 		dropDown.show();
2989 	}
2990 
2991 	private bool shouldCloseIfClicked(Widget w) {
2992 		if(w is this)
2993 			return true;
2994 		version(custom_widgets)
2995 		if(cast(TextListViewWidget.TextListViewItem) w)
2996 			return true;
2997 		return false;
2998 	}
2999 
3000 	override void defaultEventHandler_click(ClickEvent ce) {
3001 		if(ce.button == MouseButton.left && shouldCloseIfClicked(ce.target)) {
3002 			this.win.close();
3003 		}
3004 	}
3005 
3006 	override void defaultEventHandler_char(CharEvent ce) {
3007 		if(ce.character == '\n')
3008 			this.win.close();
3009 	}
3010 
3011 	override void defaultEventHandler_keydown(KeyDownEvent kde) {
3012 		if(kde.key == Key.Escape) {
3013 			cancelled = true;
3014 			this.win.close();
3015 		}/+ else if(kde.key == Key.Up || kde.key == Key.Down)
3016 			{} // intentionally blank, the list view handles these
3017 			// separately from the scroll message widget default handler
3018 		else if(lw && lw.glvw && lw.glvw.smw)
3019 			lw.glvw.smw.defaultKeyboardListener(kde);+/
3020 	}
3021 }
3022 
3023 /++
3024 	A drop-down list where the user must select one of the
3025 	given options. Like `<select>` in HTML.
3026 
3027 	The current selection is given as a string or an index.
3028 	It emits a SelectionChangedEvent when it changes.
3029 +/
3030 class DropDownSelection : ComboboxBase {
3031 	/++
3032 		Creates a drop down selection, optionally passing its initial list of options.
3033 
3034 		History:
3035 			The overload with the `options` parameter was added December 29, 2024.
3036 	+/
3037 	this(Widget parent) {
3038 		version(win32_widgets)
3039 			super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent);
3040 		else version(custom_widgets) {
3041 			super(parent);
3042 
3043 			addEventListener("focus", () { this.redraw; });
3044 			addEventListener("blur", () { this.redraw; });
3045 			addEventListener(EventType.change, () { this.redraw; });
3046 			addEventListener("mousedown", () { this.focus(); this.popup(); });
3047 			addEventListener((KeyDownEvent event) {
3048 				if(event.key == Key.Space)
3049 					popup();
3050 			});
3051 		} else static assert(false);
3052 	}
3053 
3054 	/// ditto
3055 	this(string[] options, Widget parent) {
3056 		this(parent);
3057 		this.options = options;
3058 	}
3059 
3060 	mixin Padding!q{2};
3061 	static class Style : Widget.Style {
3062 		override FrameStyle borderStyle() { return FrameStyle.risen; }
3063 	}
3064 	mixin OverrideStyle!Style;
3065 
3066 	version(custom_widgets)
3067 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
3068 		auto cs = getComputedStyle();
3069 
3070 		painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]);
3071 
3072 		painter.outlineColor = cs.foregroundColor;
3073 		painter.fillColor = cs.foregroundColor;
3074 
3075 		/+
3076 		Point[4] triangle;
3077 		enum padding = 6;
3078 		enum paddingV = 7;
3079 		enum triangleWidth = 10;
3080 		triangle[0] = Point(width - padding - triangleWidth, paddingV);
3081 		triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV);
3082 		triangle[2] = Point(width - padding - 0, paddingV);
3083 		triangle[3] = triangle[0];
3084 		painter.drawPolygon(triangle[]);
3085 		+/
3086 
3087 		auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2);
3088 
3089 		painter.drawPolygon(
3090 			scaleWithDpi(Point(2, 6) + offset),
3091 			scaleWithDpi(Point(7, 11) + offset),
3092 			scaleWithDpi(Point(12, 6) + offset),
3093 			scaleWithDpi(Point(2, 6) + offset)
3094 		);
3095 
3096 
3097 		return bounds;
3098 	}
3099 
3100 	version(win32_widgets)
3101 	override void registerMovement() {
3102 		version(win32_widgets) {
3103 			if(hwnd) {
3104 				auto pos = getChildPositionRelativeToParentHwnd(this);
3105 				// the height given to this from Windows' perspective is supposed
3106 				// to include the drop down's height. so I add to it to give some
3107 				// room for that.
3108 				// FIXME: maybe make the subclass provide a suggestedDropdownHeight thing
3109 				MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true);
3110 			}
3111 		}
3112 		sendResizeEvent();
3113 	}
3114 }
3115 
3116 /++
3117 	A text box with a drop down arrow listing selections.
3118 	The user can choose from the list, or type their own.
3119 +/
3120 class FreeEntrySelection : ComboboxBase {
3121 	this(Widget parent) {
3122 		this(null, parent);
3123 	}
3124 
3125 	this(string[] options, Widget parent) {
3126 		version(win32_widgets)
3127 			super(2 /* CBS_DROPDOWN */, parent);
3128 		else version(custom_widgets) {
3129 			super(parent);
3130 			auto hl = new HorizontalLayout(this);
3131 			lineEdit = new LineEdit(hl);
3132 
3133 			tabStop = false;
3134 
3135 			// lineEdit.addEventListener((FocusEvent fe) {  lineEdit.selectAll(); } );
3136 
3137 			auto btn = new class ArrowButton {
3138 				this() {
3139 					super(ArrowDirection.down, hl);
3140 				}
3141 				override int heightStretchiness() {
3142 					return 1;
3143 				}
3144 				override int heightShrinkiness() {
3145 					return 1;
3146 				}
3147 				override int maxHeight() {
3148 					return lineEdit.maxHeight;
3149 				}
3150 			};
3151 			//btn.addDirectEventListener("focus", &lineEdit.focus);
3152 			btn.addEventListener("triggered", &this.popup);
3153 			addEventListener(EventType.change, (Event event) {
3154 				lineEdit.content = event.stringValue;
3155 				lineEdit.focus();
3156 				redraw();
3157 			});
3158 		}
3159 		else static assert(false);
3160 
3161 		this.options = options;
3162 	}
3163 
3164 	string content() {
3165 		version(win32_widgets)
3166 			assert(0, "not implemented");
3167 		else version(custom_widgets)
3168 			return lineEdit.content;
3169 		else static assert(0);
3170 	}
3171 
3172 	void content(string s) {
3173 		version(win32_widgets)
3174 			assert(0, "not implemented");
3175 		else version(custom_widgets)
3176 			lineEdit.content = s;
3177 		else static assert(0);
3178 	}
3179 
3180 	version(custom_widgets) {
3181 		LineEdit lineEdit;
3182 
3183 		override int widthStretchiness() {
3184 			return lineEdit ? lineEdit.widthStretchiness : super.widthStretchiness;
3185 		}
3186 		override int flexBasisWidth() {
3187 			return lineEdit ? lineEdit.flexBasisWidth : super.flexBasisWidth;
3188 		}
3189 	}
3190 }
3191 
3192 /++
3193 	A combination of free entry with a list below it.
3194 +/
3195 class ComboBox : ComboboxBase {
3196 	this(Widget parent) {
3197 		version(win32_widgets)
3198 			super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent);
3199 		else version(custom_widgets) {
3200 			super(parent);
3201 			lineEdit = new LineEdit(this);
3202 			listWidget = new ListWidget(this);
3203 			listWidget.multiSelect = false;
3204 			listWidget.addEventListener(EventType.change, delegate(Widget, Event) {
3205 				string c = null;
3206 				foreach(option; listWidget.options)
3207 					if(option.selected) {
3208 						c = option.label;
3209 						break;
3210 					}
3211 				lineEdit.content = c;
3212 			});
3213 
3214 			listWidget.tabStop = false;
3215 			this.tabStop = false;
3216 			listWidget.addEventListener("focusin", &lineEdit.focus);
3217 			this.addEventListener("focusin", &lineEdit.focus);
3218 
3219 			addDirectEventListener(EventType.change, {
3220 				listWidget.setSelection(selection_);
3221 				if(selection_ != -1)
3222 					lineEdit.content = options[selection_];
3223 				lineEdit.focus();
3224 				redraw();
3225 			});
3226 
3227 			lineEdit.addEventListener("focusin", &lineEdit.selectAll);
3228 
3229 			listWidget.addDirectEventListener(EventType.change, {
3230 				int set = -1;
3231 				foreach(idx, opt; listWidget.options)
3232 					if(opt.selected) {
3233 						set = cast(int) idx;
3234 						break;
3235 					}
3236 				if(set != selection_)
3237 					this.setSelection(set);
3238 			});
3239 		} else static assert(false);
3240 	}
3241 
3242 	override int minHeight() { return defaultLineHeight * 3; }
3243 	override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; }
3244 	override int heightStretchiness() { return 5; }
3245 
3246 	version(custom_widgets) {
3247 		LineEdit lineEdit;
3248 		ListWidget listWidget;
3249 
3250 		override void addOption(string s) {
3251 			listWidget.addOption(s);
3252 			ComboboxBase.addOption(s);
3253 		}
3254 
3255 		override void scrollSelectionIntoView() {
3256 			listWidget.scrollSelectionIntoView();
3257 		}
3258 	}
3259 }
3260 
3261 /+
3262 class Spinner : Widget {
3263 	version(win32_widgets)
3264 	this(Widget parent) {
3265 		super(parent);
3266 		parentWindow = parent.parentWindow;
3267 		auto hlayout = new HorizontalLayout(this);
3268 		lineEdit = new LineEdit(hlayout);
3269 		upDownControl = new UpDownControl(hlayout);
3270 	}
3271 
3272 	LineEdit lineEdit;
3273 	UpDownControl upDownControl;
3274 }
3275 
3276 class UpDownControl : Widget {
3277 	version(win32_widgets)
3278 	this(Widget parent) {
3279 		super(parent);
3280 		parentWindow = parent.parentWindow;
3281 		createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */);
3282 	}
3283 
3284 	override int minHeight() { return defaultLineHeight; }
3285 	override int maxHeight() { return defaultLineHeight * 3/2; }
3286 
3287 	override int minWidth() { return defaultLineHeight * 3/2; }
3288 	override int maxWidth() { return defaultLineHeight * 3/2; }
3289 }
3290 +/
3291 
3292 /+
3293 class DataView : Widget {
3294 	// this is the omnibus data viewer
3295 	// the internal data layout is something like:
3296 	// string[string][] but also each node can have parents
3297 }
3298 +/
3299 
3300 
3301 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS
3302 
3303 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d
3304 
3305 // FIXME: menus should prolly capture the mouse. ugh i kno.
3306 /*
3307 	TextEdit needs:
3308 
3309 	* caret manipulation
3310 	* selection control
3311 	* convenience functions for appendText, insertText, insertTextAtCaret, etc.
3312 
3313 	For example:
3314 
3315 	connect(paste, &textEdit.insertTextAtCaret);
3316 
3317 	would be nice.
3318 
3319 
3320 
3321 	I kinda want an omnibus dataview that combines list, tree,
3322 	and table - it can be switched dynamically between them.
3323 
3324 	Flattening policy: only show top level, show recursive, show grouped
3325 	List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer)
3326 
3327 	Single select, multi select, organization, drag+drop
3328 */
3329 
3330 //static if(UsingSimpledisplayX11)
3331 version(win32_widgets) {}
3332 else version(custom_widgets) {
3333 	enum scrollClickRepeatInterval = 50;
3334 
3335 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") {
3336 	enum windowBackgroundColor = Color(212, 212, 212); // used to be 192
3337 	enum activeTabColor = lightAccentColor;
3338 	enum hoveringColor = Color(228, 228, 228);
3339 	enum buttonColor = windowBackgroundColor;
3340 	enum depressedButtonColor = darkAccentColor;
3341 	enum activeListXorColor = Color(255, 255, 127);
3342 	enum progressBarColor = Color(0, 0, 128);
3343 	enum activeMenuItemColor = Color(0, 0, 128);
3344 
3345 }}
3346 else static assert(false);
3347 deprecated("Get these properties off the `visualTheme` instead.") {
3348 	// these are used by horizontal rule so not just custom_widgets. for now at least.
3349 	enum darkAccentColor = Color(172, 172, 172);
3350 	enum lightAccentColor = Color(223, 223, 223); // used to be 223
3351 }
3352 
3353 private const(wchar)* toWstringzInternal(in char[] s) {
3354 	wchar[] str;
3355 	str.reserve(s.length + 1);
3356 	foreach(dchar ch; s)
3357 		str ~= ch;
3358 	str ~= '\0';
3359 	return str.ptr;
3360 }
3361 
3362 static if(SimpledisplayTimerAvailable)
3363 void setClickRepeat(Widget w, int interval, int delay = 250) {
3364 	Timer timer;
3365 	int delayRemaining = delay / interval;
3366 	if(delayRemaining <= 1)
3367 		delayRemaining = 2;
3368 
3369 	immutable originalDelayRemaining = delayRemaining;
3370 
3371 	w.addDirectEventListener((scope MouseDownEvent ev) {
3372 		if(ev.srcElement !is w)
3373 			return;
3374 		if(timer !is null) {
3375 			timer.destroy();
3376 			timer = null;
3377 		}
3378 		delayRemaining = originalDelayRemaining;
3379 		timer = new Timer(interval, () {
3380 			if(delayRemaining > 0)
3381 				delayRemaining--;
3382 			else {
3383 				auto ev = new Event("triggered", w);
3384 				ev.sendDirectly();
3385 			}
3386 		});
3387 	});
3388 
3389 	w.addDirectEventListener((scope MouseUpEvent ev) {
3390 		if(ev.srcElement !is w)
3391 			return;
3392 		if(timer !is null) {
3393 			timer.destroy();
3394 			timer = null;
3395 		}
3396 	});
3397 
3398 	w.addDirectEventListener((scope MouseLeaveEvent ev) {
3399 		if(ev.srcElement !is w)
3400 			return;
3401 		if(timer !is null) {
3402 			timer.destroy();
3403 			timer = null;
3404 		}
3405 	});
3406 
3407 }
3408 else
3409 void setClickRepeat(Widget w, int interval, int delay = 250) {}
3410 
3411 enum FrameStyle {
3412 	none, ///
3413 	risen, /// a 3d pop-out effect (think Windows 95 button)
3414 	sunk, /// a 3d sunken effect (think Windows 95 button as you click on it)
3415 	solid, ///
3416 	dotted, ///
3417 	fantasy, /// a style based on a popular fantasy video game
3418 	rounded, /// a rounded rectangle
3419 }
3420 
3421 version(custom_widgets)
3422 deprecated
3423 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) {
3424 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
3425 }
3426 
3427 version(custom_widgets)
3428 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) {
3429 	draw3dFrame(0, 0, widget.width, widget.height, painter, style, background);
3430 }
3431 
3432 version(custom_widgets)
3433 deprecated
3434 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) {
3435 	draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor);
3436 }
3437 
3438 int getBorderWidth(FrameStyle style) {
3439 	final switch(style) {
3440 		case FrameStyle.sunk, FrameStyle.risen:
3441 			return 2;
3442 		case FrameStyle.none:
3443 			return 0;
3444 		case FrameStyle.solid:
3445 			return 1;
3446 		case FrameStyle.dotted:
3447 			return 1;
3448 		case FrameStyle.fantasy:
3449 			return 3;
3450 		case FrameStyle.rounded:
3451 			return 2;
3452 	}
3453 }
3454 
3455 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) {
3456 	int borderWidth = getBorderWidth(style);
3457 	final switch(style) {
3458 		case FrameStyle.sunk, FrameStyle.risen:
3459 			// outer layer
3460 			painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black;
3461 		break;
3462 		case FrameStyle.none:
3463 			painter.outlineColor = background;
3464 		break;
3465 		case FrameStyle.solid:
3466 		case FrameStyle.rounded:
3467 			painter.pen = Pen(border, 1);
3468 		break;
3469 		case FrameStyle.dotted:
3470 			painter.pen = Pen(border, 1, Pen.Style.Dotted);
3471 		break;
3472 		case FrameStyle.fantasy:
3473 			painter.pen = Pen(border, 3);
3474 		break;
3475 	}
3476 
3477 	painter.fillColor = background;
3478 
3479 	if(style == FrameStyle.rounded) {
3480 		painter.drawRectangleRounded(Point(x, y), Size(width, height), 6);
3481 	} else {
3482 		painter.drawRectangle(Point(x + 0, y + 0), width, height);
3483 
3484 		if(style == FrameStyle.sunk || style == FrameStyle.risen) {
3485 			// 3d effect
3486 			auto vt = WidgetPainter.visualTheme;
3487 
3488 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor;
3489 			painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0));
3490 			painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1));
3491 
3492 			// inner layer
3493 			//right, bottom
3494 			painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor;
3495 			painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2));
3496 			painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2));
3497 			// left, top
3498 			painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white;
3499 			painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1));
3500 			painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2));
3501 		} else if(style == FrameStyle.fantasy) {
3502 			painter.pen = Pen(Color.white, 1, Pen.Style.Solid);
3503 			painter.fillColor = Color.transparent;
3504 			painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1));
3505 		}
3506 	}
3507 
3508 	return borderWidth;
3509 }
3510 
3511 /++
3512 	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.
3513 
3514 	See_Also:
3515 		[MenuItem]
3516 		[ToolButton]
3517 		[Menu.addItem]
3518 +/
3519 class Action {
3520 	version(win32_widgets) {
3521 		private int id;
3522 		private static int lastId = 9000;
3523 		private static Action[int] mapping;
3524 	}
3525 
3526 	KeyEvent accelerator;
3527 
3528 	// FIXME: disable message
3529 	// and toggle thing?
3530 	// ??? and trigger arguments too ???
3531 
3532 	/++
3533 		Params:
3534 			label = the textual label
3535 			icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons.
3536 			triggered = initial handler, more can be added via the [triggered] member.
3537 	+/
3538 	this(string label, ushort icon = 0, void delegate() triggered = null) {
3539 		this.label = label;
3540 		this.iconId = icon;
3541 		if(triggered !is null)
3542 			this.triggered ~= triggered;
3543 		version(win32_widgets) {
3544 			id = ++lastId;
3545 			mapping[id] = this;
3546 		}
3547 	}
3548 
3549 	private string label;
3550 	private ushort iconId;
3551 	// icon
3552 
3553 	// when it is triggered, the triggered event is fired on the window
3554 	/// The list of handlers when it is triggered.
3555 	void delegate()[] triggered;
3556 }
3557 
3558 /*
3559 	plan:
3560 		keyboard accelerators
3561 
3562 		* menus (and popups and tooltips)
3563 		* status bar
3564 		* toolbars and buttons
3565 
3566 		sortable table view
3567 
3568 		maybe notification area icons
3569 		basic clipboard
3570 
3571 		* radio box
3572 		splitter
3573 		toggle buttons (optionally mutually exclusive, like in Paint)
3574 		label, rich text display, multi line plain text (selectable)
3575 		* fieldset
3576 		* nestable grid layout
3577 		single line text input
3578 		* multi line text input
3579 		slider
3580 		spinner
3581 		list box
3582 		drop down
3583 		combo box
3584 		auto complete box
3585 		* progress bar
3586 
3587 		terminal window/widget (on unix it might even be a pty but really idk)
3588 
3589 		ok button
3590 		cancel button
3591 
3592 		keyboard hotkeys
3593 
3594 		scroll widget
3595 
3596 		event redirections and network transparency
3597 		script integration
3598 */
3599 
3600 
3601 /*
3602 	MENUS
3603 
3604 	auto bar = new MenuBar(window);
3605 	window.menuBar = bar;
3606 
3607 	auto fileMenu = bar.addItem(new Menu("&File"));
3608 	fileMenu.addItem(new MenuItem("&Exit"));
3609 
3610 
3611 	EVENTS
3612 
3613 	For controls, you should usually use "triggered" rather than "click", etc., because
3614 	triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation.
3615 	This is the case on menus and pushbuttons.
3616 
3617 	"click", on the other hand, currently only fires when it is literally clicked by the mouse.
3618 */
3619 
3620 
3621 /*
3622 enum LinePreference {
3623 	AlwaysOnOwnLine, // always on its own line
3624 	PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way
3625 	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
3626 }
3627 */
3628 
3629 /++
3630 	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.
3631 
3632 	---
3633 	class MyWidget : Widget {
3634 		this(Widget parent) { super(parent); }
3635 
3636 		// set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go:
3637 		mixin Padding!q{4};
3638 
3639 		// set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go:
3640 		mixin Margin!q{8};
3641 
3642 		// but if I specify one outside, it overrides the override, so now marginLeft is 2,
3643 		// while Top/Bottom/Right remain 8 from the mixin above.
3644 		override int marginLeft() { return 2; }
3645 	}
3646 	---
3647 
3648 
3649 	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]).
3650 
3651 	Padding is the area inside a widget where its background is drawn, but the content avoids.
3652 
3653 	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!).
3654 
3655 	* 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.
3656 +/
3657 mixin template Padding(string code) {
3658 	override int paddingLeft() { return mixin(code);}
3659 	override int paddingRight() { return mixin(code);}
3660 	override int paddingTop() { return mixin(code);}
3661 	override int paddingBottom() { return mixin(code);}
3662 }
3663 
3664 /// ditto
3665 mixin template Margin(string code) {
3666 	override int marginLeft() { return mixin(code);}
3667 	override int marginRight() { return mixin(code);}
3668 	override int marginTop() { return mixin(code);}
3669 	override int marginBottom() { return mixin(code);}
3670 }
3671 
3672 private
3673 void recomputeChildLayout(string relevantMeasure)(Widget parent) {
3674 	enum calcingV = relevantMeasure == "height";
3675 
3676 	parent.registerMovement();
3677 
3678 	if(parent.children.length == 0)
3679 		return;
3680 
3681 	auto parentStyle = parent.getComputedStyle();
3682 
3683 	enum firstThingy = relevantMeasure == "height" ? "Top" : "Left";
3684 	enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right";
3685 
3686 	enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top";
3687 	enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom";
3688 
3689 	// my own width and height should already be set by the caller of this function...
3690 	int spaceRemaining = mixin("parent." ~ relevantMeasure) -
3691 		mixin("parentStyle.padding"~firstThingy~"()") -
3692 		mixin("parentStyle.padding"~secondThingy~"()");
3693 
3694 	int stretchinessSum;
3695 	int stretchyChildSum;
3696 	int lastMargin = 0;
3697 
3698 	int shrinkinessSum;
3699 	int shrinkyChildSum;
3700 
3701 	// set initial size
3702 	foreach(child; parent.children) {
3703 
3704 		auto childStyle = child.getComputedStyle();
3705 
3706 		if(cast(StaticPosition) child)
3707 			continue;
3708 		if(child.hidden)
3709 			continue;
3710 
3711 		const iw = child.flexBasisWidth();
3712 		const ih = child.flexBasisHeight();
3713 
3714 		static if(calcingV) {
3715 			child.width = parent.width -
3716 				mixin("childStyle.margin"~otherFirstThingy~"()") -
3717 				mixin("childStyle.margin"~otherSecondThingy~"()") -
3718 				mixin("parentStyle.padding"~otherFirstThingy~"()") -
3719 				mixin("parentStyle.padding"~otherSecondThingy~"()");
3720 
3721 			if(child.width < 0)
3722 				child.width = 0;
3723 			if(child.width > childStyle.maxWidth())
3724 				child.width = childStyle.maxWidth();
3725 
3726 			if(iw > 0) {
3727 				auto totalPossible = child.width;
3728 				if(child.width > iw && child.widthStretchiness() == 0)
3729 					child.width = iw;
3730 			}
3731 
3732 			child.height = mymax(childStyle.minHeight(), ih);
3733 		} else {
3734 			// set to take all the space
3735 			child.height = parent.height -
3736 				mixin("childStyle.margin"~firstThingy~"()") -
3737 				mixin("childStyle.margin"~secondThingy~"()") -
3738 				mixin("parentStyle.padding"~firstThingy~"()") -
3739 				mixin("parentStyle.padding"~secondThingy~"()");
3740 
3741 			// then clamp it
3742 			if(child.height < 0)
3743 				child.height = 0;
3744 			if(child.height > childStyle.maxHeight())
3745 				child.height = childStyle.maxHeight();
3746 
3747 			// and if possible, respect the ideal target
3748 			if(ih > 0) {
3749 				auto totalPossible = child.height;
3750 				if(child.height > ih && child.heightStretchiness() == 0)
3751 					child.height = ih;
3752 			}
3753 
3754 			// if we have an ideal, try to respect it, otehrwise, just use the minimum
3755 			child.width = mymax(childStyle.minWidth(), iw);
3756 		}
3757 
3758 		spaceRemaining -= mixin("child." ~ relevantMeasure);
3759 
3760 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3761 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3762 		lastMargin = margin;
3763 		spaceRemaining -= thisMargin + margin;
3764 
3765 		auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3766 		stretchinessSum += s;
3767 		if(s > 0)
3768 			stretchyChildSum++;
3769 
3770 		auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()");
3771 		shrinkinessSum += s2;
3772 		if(s2 > 0)
3773 			shrinkyChildSum++;
3774 	}
3775 
3776 	if(spaceRemaining < 0 && shrinkyChildSum) {
3777 		// shrink to get into the space if it is possible
3778 		auto toRemove = -spaceRemaining;
3779 		auto removalPerItem = toRemove / shrinkinessSum;
3780 		auto remainder = toRemove % shrinkinessSum;
3781 
3782 		// FIXME: wtf why am i shrinking things with no shrinkiness?
3783 
3784 		foreach(child; parent.children) {
3785 			auto childStyle = child.getComputedStyle();
3786 			if(cast(StaticPosition) child)
3787 				continue;
3788 			if(child.hidden)
3789 				continue;
3790 			static if(calcingV) {
3791 				auto minimum = childStyle.minHeight();
3792 				auto stretch = childStyle.heightShrinkiness();
3793 			} else {
3794 				auto minimum = childStyle.minWidth();
3795 				auto stretch = childStyle.widthShrinkiness();
3796 			}
3797 
3798 			if(mixin("child._" ~ relevantMeasure) <= minimum)
3799 				continue;
3800 			// import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch);
3801 
3802 			mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh.
3803 
3804 			spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum;
3805 		}
3806 	}
3807 
3808 	// stretch to fill space
3809 	while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) {
3810 		auto spacePerChild = spaceRemaining / stretchinessSum;
3811 		bool spreadEvenly;
3812 		bool giveToBiggest;
3813 		if(spacePerChild <= 0) {
3814 			spacePerChild = spaceRemaining / stretchyChildSum;
3815 			spreadEvenly = true;
3816 		}
3817 		if(spacePerChild <= 0) {
3818 			giveToBiggest = true;
3819 		}
3820 		int previousSpaceRemaining = spaceRemaining;
3821 		stretchinessSum = 0;
3822 		Widget mostStretchy;
3823 		int mostStretchyS;
3824 		foreach(child; parent.children) {
3825 			auto childStyle = child.getComputedStyle();
3826 			if(cast(StaticPosition) child)
3827 				continue;
3828 			if(child.hidden)
3829 				continue;
3830 			static if(calcingV) {
3831 				auto maximum = childStyle.maxHeight();
3832 			} else {
3833 				auto maximum = childStyle.maxWidth();
3834 			}
3835 
3836 			if(mixin("child." ~ relevantMeasure) >= maximum) {
3837 				auto adj = mixin("child." ~ relevantMeasure) - maximum;
3838 				mixin("child._" ~ relevantMeasure) -= adj;
3839 				spaceRemaining += adj;
3840 				continue;
3841 			}
3842 			auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3843 			if(s <= 0)
3844 				continue;
3845 			auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s);
3846 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3847 			spaceRemaining -= spaceAdjustment;
3848 			if(mixin("child." ~ relevantMeasure) > maximum) {
3849 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3850 				mixin("child._" ~ relevantMeasure) -= diff;
3851 				spaceRemaining += diff;
3852 			} else if(mixin("child._" ~ relevantMeasure) < maximum) {
3853 				stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()");
3854 				if(mostStretchy is null || s >= mostStretchyS) {
3855 					mostStretchy = child;
3856 					mostStretchyS = s;
3857 				}
3858 			}
3859 		}
3860 
3861 		if(giveToBiggest && mostStretchy !is null) {
3862 			auto child = mostStretchy;
3863 			auto childStyle = child.getComputedStyle();
3864 			int spaceAdjustment = spaceRemaining;
3865 
3866 			static if(calcingV)
3867 				auto maximum = childStyle.maxHeight();
3868 			else
3869 				auto maximum = childStyle.maxWidth();
3870 
3871 			mixin("child._" ~ relevantMeasure) += spaceAdjustment;
3872 			spaceRemaining -= spaceAdjustment;
3873 			if(mixin("child._" ~ relevantMeasure) > maximum) {
3874 				auto diff = mixin("child." ~ relevantMeasure) - maximum;
3875 				mixin("child._" ~ relevantMeasure) -= diff;
3876 				spaceRemaining += diff;
3877 			}
3878 		}
3879 
3880 		if(spaceRemaining == previousSpaceRemaining) {
3881 			if(mostStretchy !is null) {
3882 				static if(calcingV)
3883 					auto maximum = mostStretchy.maxHeight();
3884 				else
3885 					auto maximum = mostStretchy.maxWidth();
3886 
3887 				mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining;
3888 				if(mixin("mostStretchy._" ~ relevantMeasure) > maximum)
3889 					mixin("mostStretchy._" ~ relevantMeasure) = maximum;
3890 			}
3891 			break; // apparently nothing more we can do
3892 		}
3893 	}
3894 
3895 	foreach(child; parent.children) {
3896 		auto childStyle = child.getComputedStyle();
3897 		if(cast(StaticPosition) child)
3898 			continue;
3899 		if(child.hidden)
3900 			continue;
3901 
3902 		static if(calcingV)
3903 			auto maximum = childStyle.maxHeight();
3904 		else
3905 			auto maximum = childStyle.maxWidth();
3906 		if(mixin("child._" ~ relevantMeasure) > maximum)
3907 			mixin("child._" ~ relevantMeasure) = maximum;
3908 	}
3909 
3910 	// position
3911 	lastMargin = 0;
3912 	int currentPos = mixin("parent.padding"~firstThingy~"()");
3913 	foreach(child; parent.children) {
3914 		auto childStyle = child.getComputedStyle();
3915 		if(cast(StaticPosition) child) {
3916 			child.recomputeChildLayout();
3917 			continue;
3918 		}
3919 		if(child.hidden)
3920 			continue;
3921 		auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()");
3922 		int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()"));
3923 		currentPos += thisMargin;
3924 		static if(calcingV) {
3925 			child.x = parentStyle.paddingLeft() + childStyle.marginLeft();
3926 			child.y = currentPos;
3927 		} else {
3928 			child.x = currentPos;
3929 			child.y = parentStyle.paddingTop() + childStyle.marginTop();
3930 
3931 		}
3932 		currentPos += mixin("child." ~ relevantMeasure);
3933 		currentPos += margin;
3934 		lastMargin = margin;
3935 
3936 		child.recomputeChildLayout();
3937 	}
3938 }
3939 
3940 int mymax(int a, int b) { return a > b ? a : b; }
3941 int mymax(int a, int b, int c) {
3942 	auto d = mymax(a, b);
3943 	return c > d ? c : d;
3944 }
3945 
3946 // OK so we need to make getting at the native window stuff possible in simpledisplay.d
3947 // and here, it must be integrable with the layout, the event system, and not be painted over.
3948 version(win32_widgets) {
3949 
3950 	// this function just does stuff that a parent window needs for redirection
3951 	int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
3952 		this_.hookedWndProc(msg, wParam, lParam);
3953 
3954 		switch(msg) {
3955 
3956 			case WM_VSCROLL, WM_HSCROLL:
3957 				auto pos = HIWORD(wParam);
3958 				auto m = LOWORD(wParam);
3959 
3960 				auto scrollbarHwnd = cast(HWND) lParam;
3961 
3962 				if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) {
3963 
3964 					//auto smw = cast(ScrollMessageWidget) widgetp.parent;
3965 
3966 					switch(m) {
3967 						/+
3968 						// I don't think those messages are ever actually sent normally by the widget itself,
3969 						// they are more used for the keyboard interface. methinks.
3970 						case SB_BOTTOM:
3971 							// writeln("end");
3972 							auto event = new Event("scrolltoend", *widgetp);
3973 							event.dispatch();
3974 							//if(!event.defaultPrevented)
3975 						break;
3976 						case SB_TOP:
3977 							// writeln("top");
3978 							auto event = new Event("scrolltobeginning", *widgetp);
3979 							event.dispatch();
3980 						break;
3981 						case SB_ENDSCROLL:
3982 							// idk
3983 						break;
3984 						+/
3985 						case SB_LINEDOWN:
3986 							(*widgetp).emitCommand!"scrolltonextline"();
3987 						return 0;
3988 						case SB_LINEUP:
3989 							(*widgetp).emitCommand!"scrolltopreviousline"();
3990 						return 0;
3991 						case SB_PAGEDOWN:
3992 							(*widgetp).emitCommand!"scrolltonextpage"();
3993 						return 0;
3994 						case SB_PAGEUP:
3995 							(*widgetp).emitCommand!"scrolltopreviouspage"();
3996 						return 0;
3997 						case SB_THUMBPOSITION:
3998 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
3999 							ev.dispatch();
4000 						return 0;
4001 						case SB_THUMBTRACK:
4002 							// eh kinda lying but i like the real time update display
4003 							auto ev = new ScrollToPositionEvent(*widgetp, pos);
4004 							ev.dispatch();
4005 
4006 							// the event loop doesn't seem to carry on with a requested redraw..
4007 							// so we request it to get our dirty bit set...
4008 							// then we need to immediately actually redraw it too for instant feedback to user
4009 							SimpleWindow.processAllCustomEvents();
4010 							SimpleWindow.processAllCustomEvents();
4011 							//if(this_.parentWindow)
4012 								//this_.parentWindow.actualRedraw();
4013 
4014 							// and this ensures the WM_PAINT message is sent fairly quickly
4015 							// still seems to lag a little in large windows but meh it basically works.
4016 							if(this_.parentWindow) {
4017 								// FIXME: if painting is slow, this does still lag
4018 								// we probably will want to expose some user hook to ScrollWindowEx
4019 								// or something.
4020 								UpdateWindow(this_.parentWindow.hwnd);
4021 							}
4022 						return 0;
4023 						default:
4024 					}
4025 				}
4026 			break;
4027 
4028 			case WM_CONTEXTMENU:
4029 				auto hwndFrom = cast(HWND) wParam;
4030 
4031 				auto xPos = cast(short) LOWORD(lParam);
4032 				auto yPos = cast(short) HIWORD(lParam);
4033 
4034 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
4035 					POINT p;
4036 					p.x = xPos;
4037 					p.y = yPos;
4038 					ScreenToClient(hwnd, &p);
4039 					auto clientX = cast(ushort) p.x;
4040 					auto clientY = cast(ushort) p.y;
4041 
4042 					auto wap = widgetAtPoint(*widgetp, clientX, clientY);
4043 
4044 					if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) {
4045 						return 0;
4046 					}
4047 				}
4048 			break;
4049 
4050 			case WM_DRAWITEM:
4051 				auto dis = cast(DRAWITEMSTRUCT*) lParam;
4052 				if(auto widgetp = dis.hwndItem in Widget.nativeMapping) {
4053 					return (*widgetp).handleWmDrawItem(dis);
4054 				}
4055 			break;
4056 
4057 			case WM_NOTIFY:
4058 				auto hdr = cast(NMHDR*) lParam;
4059 				auto hwndFrom = hdr.hwndFrom;
4060 				auto code = hdr.code;
4061 
4062 				if(auto widgetp = hwndFrom in Widget.nativeMapping) {
4063 					return (*widgetp).handleWmNotify(hdr, code, mustReturn);
4064 				}
4065 			break;
4066 			case WM_COMMAND:
4067 				auto handle = cast(HWND) lParam;
4068 				auto cmd = HIWORD(wParam);
4069 				return processWmCommand(hwnd, handle, cmd, LOWORD(wParam));
4070 
4071 			default:
4072 				// pass it on
4073 		}
4074 		return 0;
4075 	}
4076 
4077 
4078 
4079 	extern(Windows)
4080 	private
4081 	// this is called by native child windows, whereas the other hook is done by simpledisplay windows
4082 	// but can i merge them?!
4083 	LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
4084 		// try { writeln(iMessage); } catch(Exception e) {};
4085 
4086 		if(auto te = hWnd in Widget.nativeMapping) {
4087 			try {
4088 
4089 				te.hookedWndProc(iMessage, wParam, lParam);
4090 
4091 				int mustReturn;
4092 				auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn);
4093 				if(mustReturn)
4094 					return ret;
4095 
4096 				if(iMessage == WM_SETFOCUS) {
4097 					auto lol = *te;
4098 					while(lol !is null && lol.implicitlyCreated)
4099 						lol = lol.parent;
4100 					lol.focus();
4101 					//(*te).parentWindow.focusedWidget = lol;
4102 				}
4103 
4104 
4105 				if(iMessage == WM_CTLCOLOREDIT) {
4106 
4107 				}
4108 				if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) {
4109 					SetBkMode(cast(HDC) wParam, TRANSPARENT);
4110 					return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color...
4111 						//GetStockObject(NULL_BRUSH);
4112 				}
4113 
4114 				auto pos = getChildPositionRelativeToParentOrigin(*te);
4115 				lastDefaultPrevented = false;
4116 				// try { writeln(typeid(*te)); } catch(Exception e) {}
4117 				if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented)
4118 					return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam);
4119 				else {
4120 					// it was something we recognized, should only call the window procedure if the default was not prevented
4121 				}
4122 			} catch(Exception e) {
4123 				assert(0, e.toString());
4124 			}
4125 			return 0;
4126 		}
4127 		assert(0, "shouldn't be receiving messages for this window....");
4128 		//assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen
4129 	}
4130 
4131 	extern(Windows)
4132 	private
4133 	// see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/
4134 	LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
4135 		if(iMessage == WM_ERASEBKGND) {
4136 			auto dc = GetDC(hWnd);
4137 			auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE));
4138 			auto p = SelectObject(dc, GetStockObject(NULL_PEN));
4139 			RECT r;
4140 			GetWindowRect(hWnd, &r);
4141 			// since the pen is null, to fill the whole space, we need the +1 on both.
4142 			gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1);
4143 			SelectObject(dc, p);
4144 			SelectObject(dc, b);
4145 			ReleaseDC(hWnd, dc);
4146 			InvalidateRect(hWnd, null, false); // redraw the border
4147 			return 1;
4148 		}
4149 		return HookedWndProc(hWnd, iMessage, wParam, lParam);
4150 	}
4151 
4152 	/++
4153 		Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create
4154 		needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest
4155 		of minigui's expectations.
4156 
4157 		This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window
4158 		member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor.
4159 
4160 		It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`.
4161 
4162 		To check if you can use this, use `static if(UsingWin32Widgets)`.
4163 	+/
4164 	void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) {
4165 		assert(p.parentWindow !is null);
4166 		assert(p.parentWindow.win.impl.hwnd !is null);
4167 
4168 		auto bsgroupbox = style == BS_GROUPBOX;
4169 
4170 		HWND phwnd;
4171 
4172 		auto wtf = p.parent;
4173 		while(wtf) {
4174 			if(wtf.hwnd !is null) {
4175 				phwnd = wtf.hwnd;
4176 				break;
4177 			}
4178 			wtf = wtf.parent;
4179 		}
4180 
4181 		if(phwnd is null)
4182 			phwnd = p.parentWindow.win.impl.hwnd;
4183 
4184 		assert(phwnd !is null);
4185 
4186 		WCharzBuffer wt = WCharzBuffer(windowText);
4187 
4188 		style |= WS_VISIBLE | WS_CHILD;
4189 		//if(className != WC_TABCONTROL)
4190 			style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
4191 		p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style,
4192 				CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
4193 				phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null);
4194 
4195 		assert(p.hwnd !is null);
4196 
4197 
4198 		static HFONT font;
4199 		if(font is null) {
4200 			NONCLIENTMETRICS params;
4201 			params.cbSize = params.sizeof;
4202 			if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, &params, 0)) {
4203 				font = CreateFontIndirect(&params.lfMessageFont);
4204 			}
4205 		}
4206 
4207 		if(font)
4208 			SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true);
4209 
4210 		p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd);
4211 		p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
4212 		Widget.nativeMapping[p.hwnd] = p;
4213 
4214 		if(bsgroupbox)
4215 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK);
4216 		else
4217 		p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4218 
4219 		EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
4220 
4221 		p.registerMovement();
4222 	}
4223 }
4224 
4225 version(win32_widgets)
4226 private
4227 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) {
4228 	if(hwnd is null || hwnd in Widget.nativeMapping)
4229 		return true;
4230 	auto parent = cast(Widget) cast(void*) lparam;
4231 	Widget p = new Widget(null);
4232 	p._parent = parent;
4233 	p.parentWindow = parent.parentWindow;
4234 	p.hwnd = hwnd;
4235 	p.implicitlyCreated = true;
4236 	Widget.nativeMapping[p.hwnd] = p;
4237 	p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
4238 	return true;
4239 }
4240 
4241 /++
4242 	Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness.
4243 +/
4244 struct WidgetPainter {
4245 	this(ScreenPainter screenPainter, Widget drawingUpon) {
4246 		this.drawingUpon = drawingUpon;
4247 		this.screenPainter = screenPainter;
4248 
4249 		this.widgetClipRectangle = screenPainter.currentClipRectangle;
4250 
4251 		// this.screenPainter.impl.enableXftDraw();
4252 		if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi))
4253 			this.screenPainter.setFont(font);
4254 	}
4255 
4256 	/++
4257 		EXPERIMENTAL. subject to change.
4258 
4259 		When you draw a cursor, you can draw this to notify your window of where it is,
4260 		for IME systems to use.
4261 	+/
4262 	void notifyCursorPosition(int x, int y, int width, int height) {
4263 		if(auto a = drawingUpon.parentWindow)
4264 		if(auto w = a.inputProxy) {
4265 			w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height);
4266 		}
4267 	}
4268 
4269 	private Rectangle widgetClipRectangle;
4270 
4271 	private Rectangle setClipRectangleForWidget(Point upperLeft, int width, int height) {
4272 		widgetClipRectangle = Rectangle(upperLeft, Size(width, height));
4273 
4274 		return screenPainter.setClipRectangle(widgetClipRectangle);
4275 	}
4276 
4277 	/++
4278 		Sets the clip rectangle to the given settings. It will automatically calculate the intersection
4279 		of your widget's content boundaries and your requested clip rectangle.
4280 
4281 		History:
4282 			Before February 26, 2025, you could sometimes exceed widget boundaries, as this forwarded
4283 			directly to the underlying `ScreenPainter`. It now wraps it to calculate the intersection.
4284 	+/
4285 	Rectangle setClipRectangle(Rectangle rectangle) {
4286 		return screenPainter.setClipRectangle(rectangle.intersectionOf(widgetClipRectangle));
4287 	}
4288 	/// ditto
4289 	Rectangle setClipRectangle(Point upperLeft, int width, int height) {
4290 		return setClipRectangle(Rectangle(upperLeft, Size(width, height)));
4291 	}
4292 	/// ditto
4293 	Rectangle setClipRectangle(Point upperLeft, Size size) {
4294 		return setClipRectangle(Rectangle(upperLeft, size));
4295 	}
4296 
4297 	///
4298 	ScreenPainter screenPainter;
4299 	/// Forward to the screen painter for all other methods, see [arsd.simpledisplay.ScreenPainter] for more information
4300 	alias screenPainter this;
4301 
4302 	private Widget drawingUpon;
4303 
4304 	/++
4305 		This is the list of rectangles that actually need to be redrawn.
4306 
4307 		Not actually implemented yet.
4308 	+/
4309 	Rectangle[] invalidatedRectangles;
4310 
4311 	private static BaseVisualTheme _visualTheme;
4312 
4313 	/++
4314 		Functions to access the visual theme and helpers to easily use it.
4315 
4316 		These are aware of the current widget's computed style out of the theme.
4317 	+/
4318 	static @property BaseVisualTheme visualTheme() {
4319 		if(_visualTheme is null)
4320 			_visualTheme = new DefaultVisualTheme();
4321 		return _visualTheme;
4322 	}
4323 
4324 	/// ditto
4325 	static @property void visualTheme(BaseVisualTheme theme) {
4326 		_visualTheme = theme;
4327 
4328 		// FIXME: notify all windows about the new theme, they should recompute layout and redraw.
4329 	}
4330 
4331 	/// ditto
4332 	Color themeForeground() {
4333 		return drawingUpon.getComputedStyle().foregroundColor();
4334 	}
4335 
4336 	/// ditto
4337 	Color themeBackground() {
4338 		return drawingUpon.getComputedStyle().background.color;
4339 	}
4340 
4341 	int isDarkTheme() {
4342 		return 0; // unspecified, yes, no as enum. FIXME
4343 	}
4344 
4345 	/++
4346 		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.
4347 
4348 		It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding.
4349 
4350 		If you change teh clip rectangle, you should change it back before you return.
4351 
4352 
4353 		The sequence it uses is:
4354 			background
4355 			content (delegated to you)
4356 			border
4357 			focused outline
4358 			selected overlay
4359 
4360 		Example code:
4361 
4362 		---
4363 		void paint(WidgetPainter painter) {
4364 			painter.drawThemed((bounds) {
4365 				return bounds; // if the selection overlay should be contained, you can return it here.
4366 			});
4367 		}
4368 		---
4369 	+/
4370 	void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) {
4371 		drawThemed((WidgetPainter painter, const Rectangle bounds) {
4372 			return drawBody(bounds);
4373 		});
4374 	}
4375 	// this overload is actually mroe for setting the delegate to a virtual function
4376 	void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) {
4377 		Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height);
4378 
4379 		auto cs = drawingUpon.getComputedStyle();
4380 
4381 		auto bg = cs.background.color;
4382 
4383 		auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor);
4384 
4385 		rect.left += borderWidth;
4386 		rect.right -= borderWidth;
4387 		rect.top += borderWidth;
4388 		rect.bottom -= borderWidth;
4389 
4390 		auto insideBorderRect = rect;
4391 
4392 		rect.left += cs.paddingLeft;
4393 		rect.right -= cs.paddingRight;
4394 		rect.top += cs.paddingTop;
4395 		rect.bottom -= cs.paddingBottom;
4396 
4397 		this.outlineColor = this.themeForeground;
4398 		this.fillColor = bg;
4399 
4400 		auto widgetFont = cs.fontCached;
4401 		if(widgetFont !is null)
4402 			this.setFont(widgetFont);
4403 
4404 		rect = drawBody(this, rect);
4405 
4406 		if(widgetFont !is null) {
4407 			if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi))
4408 				this.setFont(vtFont);
4409 			else
4410 				this.setFont(null);
4411 		}
4412 
4413 		if(auto os = cs.outlineStyle()) {
4414 			this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid);
4415 			this.fillColor = Color.transparent;
4416 			this.drawRectangle(insideBorderRect);
4417 		}
4418 	}
4419 
4420 	/++
4421 		First, draw the background.
4422 		Then draw your content.
4423 		Next, draw the border.
4424 		And the focused indicator.
4425 		And the is-selected box.
4426 
4427 		If it is focused i can draw the outline too...
4428 
4429 		If selected i can even do the xor action but that's at the end.
4430 	+/
4431 	void drawThemeBackground() {
4432 
4433 	}
4434 
4435 	void drawThemeBorder() {
4436 
4437 	}
4438 
4439 	// all this stuff is a dangerous experiment....
4440 	static class ScriptableVersion {
4441 		ScreenPainterImplementation* p;
4442 		int originX, originY;
4443 
4444 		@scriptable:
4445 		void drawRectangle(int x, int y, int width, int height) {
4446 			p.drawRectangle(x + originX, y + originY, width, height);
4447 		}
4448 		void drawLine(int x1, int y1, int x2, int y2) {
4449 			p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY);
4450 		}
4451 		void drawText(int x, int y, string text) {
4452 			p.drawText(x + originX, y + originY, 100000, 100000, text, 0);
4453 		}
4454 		void setOutlineColor(int r, int g, int b) {
4455 			p.pen = Pen(Color(r,g,b), 1);
4456 		}
4457 		void setFillColor(int r, int g, int b) {
4458 			p.fillColor = Color(r,g,b);
4459 		}
4460 	}
4461 
4462 	ScriptableVersion toArsdJsvar() {
4463 		auto sv = new ScriptableVersion;
4464 		sv.p = this.screenPainter.impl;
4465 		sv.originX = this.screenPainter.originX;
4466 		sv.originY = this.screenPainter.originY;
4467 		return sv;
4468 	}
4469 
4470 	static WidgetPainter fromJsVar(T)(T t) {
4471 		return WidgetPainter.init;
4472 	}
4473 	// done..........
4474 }
4475 
4476 
4477 struct Style {
4478 	static struct helper(string m, T) {
4479 		enum method = m;
4480 		T v;
4481 
4482 		mixin template MethodOverride(typeof(this) v) {
4483 			mixin("override typeof(v.v) "~v.method~"() { return v.v; }");
4484 		}
4485 	}
4486 
4487 	static auto opDispatch(string method, T)(T value) {
4488 		return helper!(method, T)(value);
4489 	}
4490 }
4491 
4492 /++
4493 	Implementation detail of the [ControlledBy] UDA.
4494 
4495 	History:
4496 		Added Oct 28, 2020
4497 +/
4498 struct ControlledBy_(T, Args...) {
4499 	Args args;
4500 
4501 	static if(Args.length)
4502 	this(Args args) {
4503 		this.args = args;
4504 	}
4505 
4506 	private T construct(Widget parent) {
4507 		return new T(args, parent);
4508 	}
4509 }
4510 
4511 /++
4512 	User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them.
4513 
4514 	History:
4515 		Added Oct 28, 2020
4516 +/
4517 auto ControlledBy(T, Args...)(Args args) {
4518 	return ControlledBy_!(T, Args)(args);
4519 }
4520 
4521 struct ContainerMeta {
4522 	string name;
4523 	ContainerMeta[] children;
4524 	Widget function(Widget parent) factory;
4525 
4526 	Widget instantiate(Widget parent) {
4527 		auto n = factory(parent);
4528 		n.name = name;
4529 		foreach(child; children)
4530 			child.instantiate(n);
4531 		return n;
4532 	}
4533 }
4534 
4535 /++
4536 	This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See
4537 	http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information.
4538 
4539 	Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level
4540 	structures. It works fine on structs declared inside functions though.
4541 
4542 	See: https://issues.dlang.org/show_bug.cgi?id=21984
4543 +/
4544 template Container(CArgs...) {
4545 	static if(CArgs.length && is(CArgs[0] : Widget)) {
4546 		private alias Super = CArgs[0];
4547 		private alias CArgs2 = CArgs[1 .. $];
4548 	} else {
4549 		private alias Super = Layout;
4550 		private alias CArgs2 = CArgs;
4551 	}
4552 
4553 	class Container : Super {
4554 		this(Widget parent) { super(parent); }
4555 
4556 		// just to partially support old gdc versions
4557 		version(GNU) {
4558 			static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); }
4559 			static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); }
4560 			static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); }
4561 			static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time");
4562 		} else mixin(q{
4563 			static foreach(Arg; CArgs2) {
4564 				mixin Arg.MethodOverride!(Arg);
4565 			}
4566 		});
4567 
4568 		static ContainerMeta opCall(string name, ContainerMeta[] children...) {
4569 			return ContainerMeta(
4570 				name,
4571 				children.dup,
4572 				function (Widget parent) { return new typeof(this)(parent); }
4573 			);
4574 		}
4575 
4576 		static ContainerMeta opCall(ContainerMeta[] children...) {
4577 			return opCall(null, children);
4578 		}
4579 	}
4580 }
4581 
4582 /++
4583 	The data controller widget is created by reflecting over the given
4584 	data type. You can use [ControlledBy] as a UDA on a struct or
4585 	just let it create things automatically.
4586 
4587 	Unlike [dialog], this uses real-time updating of the data and
4588 	you add it to another window yourself.
4589 
4590 	---
4591 		struct Test {
4592 			int x;
4593 			int y;
4594 		}
4595 
4596 		auto window = new Window();
4597 		auto dcw = new DataControllerWidget!Test(new Test, window);
4598 	---
4599 
4600 	The way it works is any public members are given a widget based
4601 	on their data type, and public methods trigger an action button
4602 	if no relevant parameters or a dialog action if it does have
4603 	parameters, similar to the [menu] facility.
4604 
4605 	If you change data programmatically, without going through the
4606 	DataControllerWidget methods, you will have to tell it something
4607 	has changed and it needs to redraw. This is done with the `invalidate`
4608 	method.
4609 
4610 	History:
4611 		Added Oct 28, 2020
4612 +/
4613 /// Group: generating_from_code
4614 class DataControllerWidget(T) : WidgetContainer {
4615 	static if(is(T == class) || is(T == interface) || is(T : const E[], E))
4616 		private alias Tref = T;
4617 	else
4618 		private alias Tref = T*;
4619 
4620 	Tref datum;
4621 
4622 	/++
4623 		See_also: [addDataControllerWidget]
4624 	+/
4625 	this(Tref datum, Widget parent) {
4626 		this.datum = datum;
4627 
4628 		Widget cp = this;
4629 
4630 		super(parent);
4631 
4632 		foreach(attr; __traits(getAttributes, T))
4633 			static if(is(typeof(attr) == ContainerMeta)) {
4634 				cp = attr.instantiate(this);
4635 			}
4636 
4637 		auto def = this.getByName("default");
4638 		if(def !is null)
4639 			cp = def;
4640 
4641 		Widget helper(string name) {
4642 			auto maybe = this.getByName(name);
4643 			if(maybe is null)
4644 				return cp;
4645 			return maybe;
4646 
4647 		}
4648 
4649 		foreach(member; __traits(allMembers, T))
4650 		static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011
4651 		static if(is(typeof(__traits(getMember, this.datum, member))))
4652 		static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") {
4653 			void delegate() update;
4654 
4655 			auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update);
4656 
4657 			if(update)
4658 				updaters ~= update;
4659 
4660 			static if(is(typeof(__traits(getMember, this.datum, member)) == function)) {
4661 				w.addEventListener("triggered", delegate() {
4662 					makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))();
4663 					notifyDataUpdated();
4664 				});
4665 			} else static if(is(typeof(w.isChecked) == bool)) {
4666 				w.addEventListener(EventType.change, (Event ev) {
4667 					__traits(getMember, this.datum, member) = w.isChecked;
4668 				});
4669 			} else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) {
4670 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } );
4671 			} else static if(is(typeof(w.value) == int)) {
4672 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4673 			} else static if(is(typeof(w) == DropDownSelection)) {
4674 				// special case for this to kinda support enums and such. coudl be better though
4675 				w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } );
4676 			} else {
4677 				//static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof);
4678 			}
4679 		}
4680 	}
4681 
4682 	/++
4683 		If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages.
4684 
4685 		History:
4686 			Added May 28, 2021
4687 	+/
4688 	void notifyDataUpdated() {
4689 		foreach(updater; updaters)
4690 			updater();
4691 
4692 		this.emit!(ChangeEvent!void)(delegate{});
4693 	}
4694 
4695 	private Widget[string] memberWidgets;
4696 	private void delegate()[] updaters;
4697 
4698 	mixin Emits!(ChangeEvent!void);
4699 }
4700 
4701 private int saturatedSum(int[] values...) {
4702 	int sum;
4703 	foreach(value; values) {
4704 		if(value == int.max)
4705 			return int.max;
4706 		sum += value;
4707 	}
4708 	return sum;
4709 }
4710 
4711 void genericSetValue(T, W)(T* where, W what) {
4712 	import std.conv;
4713 	*where = to!T(what);
4714 	//*where = cast(T) stringToLong(what);
4715 }
4716 
4717 /++
4718 	Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent.
4719 
4720 	The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget.
4721 
4722 	Note that this creates the widget but does not attach any event handlers to it.
4723 +/
4724 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) {
4725 
4726 	string displayName = __traits(identifier, tt).beautify;
4727 
4728 	static if(controlledByCount!tt == 1) {
4729 		foreach(i, attr; __traits(getAttributes, tt)) {
4730 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) {
4731 				auto w = attr.construct(parent);
4732 				static if(__traits(compiles, w.setPosition(*valptr)))
4733 					update = () { w.setPosition(*valptr); };
4734 				else static if(__traits(compiles, w.setValue(*valptr)))
4735 					update = () { w.setValue(*valptr); };
4736 
4737 				if(update)
4738 					update();
4739 				return w;
4740 			}
4741 		}
4742 	} else static if(controlledByCount!tt == 0) {
4743 		static if(is(typeof(tt) == enum)) {
4744 			// FIXME: update
4745 			auto dds = new DropDownSelection(parent);
4746 			foreach(idx, option; __traits(allMembers, typeof(tt))) {
4747 				dds.addOption(option);
4748 				if(__traits(getMember, typeof(tt), option) == *valptr)
4749 					dds.setSelection(cast(int) idx);
4750 			}
4751 			return dds;
4752 		} else static if(is(typeof(tt) == bool)) {
4753 			auto box = new Checkbox(displayName, parent);
4754 			update = () { box.isChecked = *valptr; };
4755 			update();
4756 			return box;
4757 		} else static if(is(typeof(tt) : const long)) {
4758 			auto le = new LabeledLineEdit(displayName, parent);
4759 			update = () { le.content = toInternal!string(*valptr); };
4760 			update();
4761 			return le;
4762 		} else static if(is(typeof(tt) : const double)) {
4763 			auto le = new LabeledLineEdit(displayName, parent);
4764 			import std.conv;
4765 			update = () { le.content = to!string(*valptr); };
4766 			update();
4767 			return le;
4768 		} else static if(is(typeof(tt) : const string)) {
4769 			auto le = new LabeledLineEdit(displayName, parent);
4770 			update = () { le.content = *valptr; };
4771 			update();
4772 			return le;
4773 		} else static if(is(typeof(tt) == E[], E)) {
4774 			auto w = new ArrayEditingWidget!E(parent);
4775 			// FIXME update
4776 			return w;
4777 		} else static if(is(typeof(tt) == function)) {
4778 			auto w = new Button(displayName, parent);
4779 			return w;
4780 		} else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) {
4781 			return parent.addDataControllerWidget(tt);
4782 		} else static assert(0, typeof(tt).stringof);
4783 	} else static assert(0, "multiple controllers not yet supported");
4784 }
4785 
4786 class ArrayEditingWidget(T) : ArrayEditingWidgetBase {
4787 	this(Widget parent) {
4788 		super(parent);
4789 	}
4790 }
4791 
4792 class ArrayEditingWidgetBase : Widget {
4793 	this(Widget parent) {
4794 		super(parent);
4795 
4796 		// FIXME: a trash can to move items into to delete them?
4797 		static class MyListViewItem : GenericListViewItem {
4798 			this(Widget parent) {
4799 				super(parent);
4800 
4801 				/+
4802 					drag handle
4803 						left click lets you move the whole selection. if the current element is not selected, it changes the selection to it.
4804 						right click here gives you the movement controls too
4805 					index/key view zone
4806 						left click here selects/unselects
4807 					element view/edit zone
4808 					delete button
4809 				+/
4810 
4811 				// FIXME: make sure the index is viewable
4812 
4813 				auto hl = new HorizontalLayout(this);
4814 
4815 				button = new CommandButton("d", hl);
4816 
4817 				label = new TextLabel("unloaded", TextAlignment.Left, hl);
4818 				// if member editable, have edit view... get from the subclass.
4819 
4820 				// or a "..." menu?
4821 				button = new CommandButton("Up", hl); // shift+click is move to top
4822 				button = new CommandButton("Down", hl); // shift+click is move to bottom
4823 				button = new CommandButton("Move to", hl); // move before, after, or swap
4824 				button = new CommandButton("Delete", hl);
4825 
4826 				button.addEventListener("triggered", delegate(){
4827 					//messageBox(text("clicked ", currentIndexLoaded()));
4828 				});
4829 			}
4830 			override void showItem(int idx) {
4831 				label.label = "Item ";// ~ to!string(idx);
4832 			}
4833 
4834 			TextLabel label;
4835 			Button button;
4836 		}
4837 
4838 		auto outer_this = this;
4839 
4840 		// FIXME: make sure item count is easy to see
4841 
4842 		glvw = new class GenericListViewWidget {
4843 			this() {
4844 				super(outer_this);
4845 			}
4846 			override GenericListViewItem itemFactory(Widget parent) {
4847 				return new MyListViewItem(parent);
4848 			}
4849 			override Size itemSize() {
4850 				return Size(0, scaleWithDpi(80));
4851 			}
4852 
4853 			override Menu contextMenu(int x, int y) {
4854 				return createContextMenuFromAnnotatedCode(this);
4855 			}
4856 
4857 			@context_menu {
4858 				void Select_All() {
4859 
4860 				}
4861 
4862 				void Undo() {
4863 
4864 				}
4865 
4866 				void Redo() {
4867 
4868 				}
4869 
4870 				void Cut() {
4871 
4872 				}
4873 
4874 				void Copy() {
4875 
4876 				}
4877 
4878 				void Paste() {
4879 
4880 				}
4881 
4882 				void Delete() {
4883 
4884 				}
4885 
4886 				void Find() {
4887 
4888 				}
4889 			}
4890 		};
4891 
4892 		glvw.setItemCount(400);
4893 
4894 		auto hl = new HorizontalLayout(this);
4895 		add = new FreeEntrySelection(hl);
4896 		addButton = new Button("Add", hl);
4897 	}
4898 
4899 	GenericListViewWidget glvw;
4900 	ComboboxBase add;
4901 	Button addButton;
4902 	/+
4903 		Controls:
4904 			clear (select all / delete)
4905 			reset (confirmation blocked button, maybe only on the whole form? or hit undo so many times to get back there)
4906 			add item
4907 				palette of options to add to the array (add prolly a combo box)
4908 			rearrange - move up/down, drag and drop a selection? right click can always do, left click only drags when on a selection handle.
4909 			edit/input/view items (GLVW? or it could be a table view in a way.)
4910 			undo/redo
4911 			select whole elements (even if a struct)
4912 			cut/copy/paste elements
4913 
4914 			could have an element picker, a details pane, and an add bare?
4915 
4916 
4917 			put a handle on the elements for left click dragging. allow right click drag anywhere but pretty big wiggle until it enables.
4918 			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.
4919 			the handle should let dragging w/o changing the selection, or if part of the selection, drag the whole selection i think.
4920 			make it textured and use the grabby hand mouse cursor.
4921 	+/
4922 }
4923 
4924 /++
4925 	A button that pops up a menu on click for working on a particular item or selection.
4926 
4927 	History:
4928 		Added March 23, 2025
4929 +/
4930 class MenuPopupButton : Button {
4931 	/++
4932 		You might consider using [createContextMenuFromAnnotatedCode] to populate the `menu` argument.
4933 
4934 		You also may want to set the [prepare] delegate after construction.
4935 	+/
4936 	this(Menu menu, Widget parent) {
4937 		assert(menu !is null);
4938 
4939 		this.menu = menu;
4940 		super("...", parent);
4941 	}
4942 
4943 	private Menu menu;
4944 	/++
4945 		If set, this delegate is called before popping up the window. This gives you a chance
4946 		to prepare your dynamic data structures for the element(s) selected.
4947 
4948 		For example, if your `MenuPopupButton` is attached to a [GenericListViewItem], you can call
4949 		[GenericListViewItem.currentIndexLoaded] in here and set it to a variable in the object you
4950 		called [createContextMenuFromAnnotatedCode] to apply the operation to the right object.
4951 
4952 		(The api could probably be simpler...)
4953 	+/
4954 	void delegate() prepare;
4955 
4956 	override void defaultEventHandler_triggered(scope Event e) {
4957 		if(prepare)
4958 			prepare();
4959 		showContextMenu(this.x, this.y + this.height, -2, -2, menu);
4960 	}
4961 
4962 	override int maxHeight() {
4963 		return defaultLineHeight;
4964 	}
4965 
4966 	override int maxWidth() {
4967 		return defaultLineHeight;
4968 	}
4969 }
4970 
4971 /++
4972 	A button that pops up an information box, similar to a tooltip, but explicitly triggered.
4973 
4974 	FIXME: i want to be able to easily embed these in other things too.
4975 +/
4976 class TipPopupButton : Button {
4977 	/++
4978 	+/
4979 	this(Widget delegate(Widget p) factory, Widget parent) {
4980 		this.factory = factory;
4981 		super("?", parent);
4982 	}
4983 	/// ditto
4984 	this(string tip, Widget parent) {
4985 		this((parent) {
4986 			auto td = new TextDisplayTooltip(tip, parent);
4987 			return td;
4988 		}, parent);
4989 	}
4990 
4991 	private Widget delegate(Widget p) factory;
4992 
4993 	override void defaultEventHandler_triggered(scope Event e) {
4994 		auto window = new TooltipWindow(factory, this);
4995 		window.popup(this);
4996 	}
4997 
4998 	private static class TextDisplayTooltip : TextDisplay {
4999 		this(string txt, Widget parent) {
5000 			super(txt, parent);
5001 		}
5002 
5003 		// override int minHeight() { return defaultLineHeight; }
5004 		// override int flexBasisHeight() { return defaultLineHeight; }
5005 
5006 		static class Style : TextDisplay.Style {
5007 			override WidgetBackground background() {
5008 				return WidgetBackground(Color.yellow);
5009 			}
5010 
5011 			override FrameStyle borderStyle() {
5012 				return FrameStyle.solid;
5013 			}
5014 
5015 			override Color borderColor() {
5016 				return Color.black;
5017 			}
5018 		}
5019 
5020 		mixin OverrideStyle!Style;
5021 	}
5022 }
5023 
5024 /++
5025 	History:
5026 		Added March 23, 2025
5027 +/
5028 class TooltipWindow : Window {
5029 
5030 	private Widget previouslyFocusedWidget;
5031 	private Widget* previouslyFocusedWidgetBelongsIn;
5032 
5033 	void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
5034 		if(offsetY == int.min)
5035 			offsetY = 0;
5036 
5037 		int w = child.flexBasisWidth();
5038 		int h = child.flexBasisHeight() + this.paddingTop + this.paddingBottom + /* horiz scroll bar - FIXME */ 16 + 2 /* for border */;
5039 
5040 		auto coord = parent.globalCoordinates();
5041 		dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
5042 
5043 		this.width = w;
5044 		this.height = h;
5045 
5046 		this.recomputeChildLayout();
5047 
5048 		static if(UsingSimpledisplayX11)
5049 			XSync(XDisplayConnection.get, 0);
5050 
5051 		dropDown.visibilityChanged = (bool visible) {
5052 			if(visible) {
5053 				this.redraw();
5054 				//dropDown.grabInput();
5055 				captureMouse(this);
5056 
5057 				if(previouslyFocusedWidget is null)
5058 					previouslyFocusedWidget = parent.parentWindow.focusedWidget;
5059 				parent.parentWindow.focusedWidget = this;
5060 			} else {
5061 				releaseMouseCapture();
5062 				//dropDown.releaseInputGrab();
5063 
5064 				parent.parentWindow.focusedWidget = previouslyFocusedWidget;
5065 
5066 				static if(UsingSimpledisplayX11)
5067 					flushGui();
5068 			}
5069 		};
5070 
5071 		dropDown.show();
5072 
5073 		clickListener = this.addEventListener((scope ClickEvent ev) {
5074 			if(ev.target is this) {
5075 				unpopup();
5076 			}
5077 		}, true /* again for asap action */);
5078 	}
5079 
5080 	private EventListener clickListener;
5081 
5082 	void unpopup() {
5083 		mouseLastOver = mouseLastDownOn = null;
5084 		dropDown.hide();
5085 		clickListener.disconnect();
5086 	}
5087 
5088 	override void defaultEventHandler_char(CharEvent ce) {
5089 		if(ce.character == '\033')
5090 			unpopup();
5091 	}
5092 
5093 	private SimpleWindow dropDown;
5094 	private Widget child;
5095 
5096 	///
5097 	this(Widget delegate(Widget p) factory, Widget parent) {
5098 		assert(parent);
5099 		assert(parent.parentWindow);
5100 		assert(parent.parentWindow.win);
5101 		dropDown = new SimpleWindow(
5102 			250, 40,
5103 			null, OpenGlOptions.no, Resizability.fixedSize,
5104 			WindowTypes.tooltip,
5105 			WindowFlags.dontAutoShow,
5106 			parent ? parent.parentWindow.win : null
5107 		);
5108 
5109 		super(dropDown);
5110 
5111 		child = factory(this);
5112 	}
5113 }
5114 
5115 private template controlledByCount(alias tt) {
5116 	static int helper() {
5117 		int count;
5118 		foreach(i, attr; __traits(getAttributes, tt))
5119 			static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...))
5120 				count++;
5121 		return count;
5122 	}
5123 
5124 	enum controlledByCount = helper;
5125 }
5126 
5127 /++
5128 	Intended for UFCS action like `window.addDataControllerWidget(new MyObject());`
5129 
5130 	If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method.
5131 
5132 	History:
5133 		The `redrawOnChange` parameter was added on May 28, 2021.
5134 +/
5135 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) {
5136 	auto dcw = new DataControllerWidget!T(t, parent);
5137 	initializeDataControllerWidget(dcw, redrawOnChange);
5138 	return dcw;
5139 }
5140 
5141 /// ditto
5142 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) {
5143 	auto dcw = new DataControllerWidget!T(t, parent);
5144 	initializeDataControllerWidget(dcw, redrawOnChange);
5145 	return dcw;
5146 }
5147 
5148 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) {
5149 	if(redrawOnChange !is null)
5150 		w.addEventListener("change", delegate() { redrawOnChange.redraw(); });
5151 }
5152 
5153 /++
5154 	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.
5155 
5156 	History:
5157 		Finalized on June 3, 2021 for the dub v10.0 release
5158 +/
5159 struct StyleInformation {
5160 	private Widget w;
5161 	private BaseVisualTheme visualTheme;
5162 
5163 	private this(Widget w) {
5164 		this.w = w;
5165 		this.visualTheme = WidgetPainter.visualTheme;
5166 	}
5167 
5168 	/++
5169 		Forwards to [Widget.Style]
5170 
5171 		Bugs:
5172 			It is supposed to fall back to the [VisualTheme] if
5173 			the style doesn't override the default, but that is
5174 			not generally implemented. Many of them may end up
5175 			being explicit overloads instead of the generic
5176 			opDispatch fallback, like [font] is now.
5177 	+/
5178 	public @property opDispatch(string name)() {
5179 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
5180 		w.useStyleProperties((scope Widget.Style props) {
5181 		//visualTheme.useStyleProperties(w, (props) {
5182 			prop = __traits(getMember, props, name);
5183 		});
5184 		return prop;
5185 	}
5186 
5187 	/++
5188 		Returns the cached font object associated with the widget,
5189 		if overridden by the [Widget.Style|Style], or the [VisualTheme] if not.
5190 
5191 		History:
5192 			Prior to March 21, 2022 (dub v10.7), `font` went through
5193 			[opDispatch], which did not use the cache. You can now call it
5194 			repeatedly without guilt.
5195 	+/
5196 	public @property OperatingSystemFont font() {
5197 		OperatingSystemFont prop;
5198 		w.useStyleProperties((scope Widget.Style props) {
5199 			prop = props.fontCached;
5200 		});
5201 		if(prop is null) {
5202 			prop = visualTheme.defaultFontCached(w.currentDpi);
5203 		}
5204 		return prop;
5205 	}
5206 
5207 	@property {
5208 		// Layout helpers. Currently just forwarding since I haven't made up my mind on a better way.
5209 		/** */ int paddingLeft() { return w.paddingLeft(); }
5210 		/** */ int paddingRight() { return w.paddingRight(); }
5211 		/** */ int paddingTop() { return w.paddingTop(); }
5212 		/** */ int paddingBottom() { return w.paddingBottom(); }
5213 
5214 		/** */ int marginLeft() { return w.marginLeft(); }
5215 		/** */ int marginRight() { return w.marginRight(); }
5216 		/** */ int marginTop() { return w.marginTop(); }
5217 		/** */ int marginBottom() { return w.marginBottom(); }
5218 
5219 		/** */ int maxHeight() { return w.maxHeight(); }
5220 		/** */ int minHeight() { return w.minHeight(); }
5221 
5222 		/** */ int maxWidth() { return w.maxWidth(); }
5223 		/** */ int minWidth() { return w.minWidth(); }
5224 
5225 		/** */ int flexBasisWidth() { return w.flexBasisWidth(); }
5226 		/** */ int flexBasisHeight() { return w.flexBasisHeight(); }
5227 
5228 		/** */ int heightStretchiness() { return w.heightStretchiness(); }
5229 		/** */ int widthStretchiness() { return w.widthStretchiness(); }
5230 
5231 		/** */ int heightShrinkiness() { return w.heightShrinkiness(); }
5232 		/** */ int widthShrinkiness() { return w.widthShrinkiness(); }
5233 
5234 		// Global helpers some of these are unstable.
5235 		static:
5236 		/** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
5237 		/** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
5238 		/** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
5239 		/** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
5240 		/** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); }
5241 		/** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
5242 
5243 		/** */ Color activeTabColor() { return lightAccentColor; }
5244 		/** */ Color buttonColor() { return windowBackgroundColor; }
5245 		/** */ Color depressedButtonColor() { return darkAccentColor; }
5246 		/** the background color of the widget when mouse hovering over it, if it responds to mouse hovers */ Color hoveringColor() { return lightAccentColor; }
5247 		deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() {
5248 			auto c = WidgetPainter.visualTheme.selectionColor();
5249 			return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
5250 		}
5251 		/** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
5252 		/** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); }
5253 	}
5254 
5255 
5256 
5257 	/+
5258 
5259 	private static auto extractStyleProperty(string name)(Widget w) {
5260 		typeof(__traits(getMember, Widget.Style.init, name)()) prop;
5261 		w.useStyleProperties((props) {
5262 			prop = __traits(getMember, props, name);
5263 		});
5264 		return prop;
5265 	}
5266 
5267 	// FIXME: clear this upon a X server disconnect
5268 	private static OperatingSystemFont[string] fontCache;
5269 
5270 	T getProperty(T)(string name, lazy T default_) {
5271 		if(visualTheme !is null) {
5272 			auto str = visualTheme.getPropertyString(w, name);
5273 			if(str is null)
5274 				return default_;
5275 			static if(is(T == Color))
5276 				return Color.fromString(str);
5277 			else static if(is(T == Measurement))
5278 				return Measurement(cast(int) toInternal!int(str));
5279 			else static if(is(T == WidgetBackground))
5280 				return WidgetBackground.fromString(str);
5281 			else static if(is(T == OperatingSystemFont)) {
5282 				if(auto f = str in fontCache)
5283 					return *f;
5284 				else
5285 					return fontCache[str] = new OperatingSystemFont(str);
5286 			} else static if(is(T == FrameStyle)) {
5287 				switch(str) {
5288 					default:
5289 						return FrameStyle.none;
5290 					foreach(style; __traits(allMembers, FrameStyle))
5291 					case style:
5292 						return __traits(getMember, FrameStyle, style);
5293 				}
5294 			} else static assert(0);
5295 		} else
5296 			return default_;
5297 	}
5298 
5299 	static struct Measurement {
5300 		int value;
5301 		alias value this;
5302 	}
5303 
5304 	@property:
5305 
5306 	int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); }
5307 	int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); }
5308 	int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); }
5309 	int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); }
5310 
5311 	int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); }
5312 	int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); }
5313 	int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); }
5314 	int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); }
5315 
5316 	int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); }
5317 	int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); }
5318 
5319 	int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); }
5320 	int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); }
5321 
5322 
5323 	WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); }
5324 	Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); }
5325 
5326 	OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); }
5327 
5328 	FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); }
5329 	Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); }
5330 
5331 	FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); }
5332 	Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); }
5333 
5334 
5335 	Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); }
5336 	Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); }
5337 	Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); }
5338 	Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); }
5339 
5340 	Color activeTabColor() { return lightAccentColor; }
5341 	Color buttonColor() { return windowBackgroundColor; }
5342 	Color depressedButtonColor() { return darkAccentColor; }
5343 	Color hoveringColor() { return Color(228, 228, 228); }
5344 	Color activeListXorColor() {
5345 		auto c = WidgetPainter.visualTheme.selectionColor();
5346 		return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a);
5347 	}
5348 	Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); }
5349 	Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); }
5350 	+/
5351 }
5352 
5353 
5354 
5355 // pragma(msg, __traits(classInstanceSize, Widget));
5356 
5357 /*private*/ template EventString(E) {
5358 	static if(is(typeof(E.EventString)))
5359 		enum EventString = E.EventString;
5360 	else
5361 		enum EventString = E.mangleof; // FIXME fqn? or something more user friendly
5362 }
5363 
5364 /*private*/ template EventStringIdentifier(E) {
5365 	string helper() {
5366 		auto es = EventString!E;
5367 		char[] id = new char[](es.length * 2);
5368 		size_t idx;
5369 		foreach(char ch; es) {
5370 			id[idx++] = cast(char)('a' + (ch >> 4));
5371 			id[idx++] = cast(char)('a' + (ch & 0x0f));
5372 		}
5373 		return cast(string) id;
5374 	}
5375 
5376 	enum EventStringIdentifier = helper();
5377 }
5378 
5379 
5380 template classStaticallyEmits(This, EventType) {
5381 	static if(is(This Base == super))
5382 		static if(is(Base : Widget))
5383 			enum baseEmits = classStaticallyEmits!(Base, EventType);
5384 		else
5385 			enum baseEmits = false;
5386 	else
5387 		enum baseEmits = false;
5388 
5389 	enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]);
5390 
5391 	enum classStaticallyEmits = thisEmits || baseEmits;
5392 }
5393 
5394 /++
5395 	A helper to make widgets out of other native windows.
5396 
5397 	History:
5398 		Factored out of OpenGlWidget on November 5, 2021
5399 +/
5400 class NestedChildWindowWidget : Widget {
5401 	SimpleWindow win;
5402 
5403 	/++
5404 		Used on X to send focus to the appropriate child window when requested by the window manager.
5405 
5406 		Normally returns its own nested window. Can also return another child or null to revert to the parent
5407 		if you override it in a child class.
5408 
5409 		History:
5410 			Added April 2, 2022 (dub v10.8)
5411 	+/
5412 	SimpleWindow focusableWindow() {
5413 		return win;
5414 	}
5415 
5416 	///
5417 	// win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
5418 	this(SimpleWindow win, Widget parent) {
5419 		this.parentWindow = parent.parentWindow;
5420 		this.win = win;
5421 
5422 		super(parent);
5423 		windowsetup(win);
5424 	}
5425 
5426 	static protected SimpleWindow getParentWindow(Widget parent) {
5427 		assert(parent !is null);
5428 		SimpleWindow pwin = parent.parentWindow.win;
5429 
5430 		version(win32_widgets) {
5431 			HWND phwnd;
5432 			auto wtf = parent;
5433 			while(wtf) {
5434 				if(wtf.hwnd) {
5435 					phwnd = wtf.hwnd;
5436 					break;
5437 				}
5438 				wtf = wtf.parent;
5439 			}
5440 			// kinda a hack here just because the ctor below just needs a SimpleWindow wrapper....
5441 			if(phwnd)
5442 				pwin = new SimpleWindow(phwnd);
5443 		}
5444 
5445 		return pwin;
5446 	}
5447 
5448 	/++
5449 		Called upon the nested window being destroyed.
5450 		Remember the window has already been destroyed at
5451 		this point, so don't use the native handle for anything.
5452 
5453 		History:
5454 			Added April 3, 2022 (dub v10.8)
5455 	+/
5456 	protected void dispose() {
5457 
5458 	}
5459 
5460 	protected void windowsetup(SimpleWindow w) {
5461 		/*
5462 		win.onFocusChange = (bool getting) {
5463 			if(getting)
5464 				this.focus();
5465 		};
5466 		*/
5467 
5468 		/+
5469 		win.onFocusChange = (bool getting) {
5470 			if(getting) {
5471 				this.parentWindow.focusedWidget = this;
5472 				this.emit!FocusEvent();
5473 				this.emit!FocusInEvent();
5474 			} else {
5475 				this.emit!BlurEvent();
5476 				this.emit!FocusOutEvent();
5477 			}
5478 		};
5479 		+/
5480 
5481 		win.onDestroyed = () {
5482 			this.dispose();
5483 		};
5484 
5485 		version(win32_widgets) {
5486 			Widget.nativeMapping[win.hwnd] = this;
5487 			this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
5488 		} else {
5489 			win.setEventHandlers(
5490 				(MouseEvent e) {
5491 					Widget p = this;
5492 					while(p ! is parentWindow) {
5493 						e.x += p.x;
5494 						e.y += p.y;
5495 						p = p.parent;
5496 					}
5497 					parentWindow.dispatchMouseEvent(e);
5498 				},
5499 				(KeyEvent e) {
5500 					//writefln("%s %x   %s", cast(void*) win, cast(uint) e.key, e.key);
5501 					parentWindow.dispatchKeyEvent(e);
5502 				},
5503 				(dchar e) {
5504 					parentWindow.dispatchCharEvent(e);
5505 				},
5506 			);
5507 		}
5508 
5509 	}
5510 
5511 	override bool showOrHideIfNativeWindow(bool shouldShow) {
5512 		auto cur = hidden;
5513 		win.hidden = !shouldShow;
5514 		if(cur != shouldShow && shouldShow)
5515 			redraw();
5516 		return true;
5517 	}
5518 
5519 	/// OpenGL widgets cannot have child widgets. Do not call this.
5520 	/* @disable */ final override void addChild(Widget, int) {
5521 		throw new Error("cannot add children to OpenGL widgets");
5522 	}
5523 
5524 	/// When an opengl widget is laid out, it will adjust the glViewport for you automatically.
5525 	/// Keep in mind that events like mouse coordinates are still relative to your size.
5526 	override void registerMovement() {
5527 		// writefln("%d %d %d %d", x,y,width,height);
5528 		version(win32_widgets)
5529 			auto pos = getChildPositionRelativeToParentHwnd(this);
5530 		else
5531 			auto pos = getChildPositionRelativeToParentOrigin(this);
5532 		win.moveResize(pos[0], pos[1], width, height);
5533 
5534 		registerMovementAdditionalWork();
5535 		sendResizeEvent();
5536 	}
5537 
5538 	abstract void registerMovementAdditionalWork();
5539 }
5540 
5541 /++
5542 	Nests an opengl capable window inside this window as a widget.
5543 
5544 	You may also just want to create an additional [SimpleWindow] with
5545 	[OpenGlOptions.yes] yourself.
5546 
5547 	An OpenGL widget cannot have child widgets. It will throw if you try.
5548 +/
5549 static if(OpenGlEnabled)
5550 class OpenGlWidget : NestedChildWindowWidget {
5551 
5552 	override void registerMovementAdditionalWork() {
5553 		win.setAsCurrentOpenGlContext();
5554 	}
5555 
5556 	///
5557 	this(Widget parent) {
5558 		auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
5559 		super(win, parent);
5560 	}
5561 
5562 	override void paint(WidgetPainter painter) {
5563 		win.setAsCurrentOpenGlContext();
5564 		glViewport(0, 0, this.width, this.height);
5565 		win.redrawOpenGlSceneNow();
5566 	}
5567 
5568 	void redrawOpenGlScene(void delegate() dg) {
5569 		win.redrawOpenGlScene = dg;
5570 	}
5571 }
5572 
5573 /++
5574 	This demo shows how to draw text in an opengl scene.
5575 +/
5576 unittest {
5577 	import arsd.minigui;
5578 	import arsd.ttf;
5579 
5580 	void main() {
5581 		auto window = new Window();
5582 
5583 		auto widget = new OpenGlWidget(window);
5584 
5585 		// old means non-shader code so compatible with glBegin etc.
5586 		// tbh I haven't implemented new one in font yet...
5587 		// anyway, declaring here, will construct soon.
5588 		OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont;
5589 
5590 		// this is a little bit awkward, calling some methods through
5591 		// the underlying SimpleWindow `win` method, and you can't do this
5592 		// on a nanovega widget due to conflicts so I should probably fix
5593 		// the api to be a bit easier. But here it will work.
5594 		//
5595 		// Alternatively, you could load the font on the first draw, inside
5596 		// the redrawOpenGlScene, and keep a flag so you don't do it every
5597 		// time. That'd be a bit easier since the lib sets up the context
5598 		// by then guaranteed.
5599 		//
5600 		// But still, I wanna show this.
5601 		widget.win.visibleForTheFirstTime = delegate {
5602 			// must set the opengl context
5603 			widget.win.setAsCurrentOpenGlContext();
5604 
5605 			// if you were doing a OpenGL 3+ shader, this
5606 			// gets especially important to do in order. With
5607 			// old-style opengl, I think you can even do it
5608 			// in main(), but meh, let's show it more correctly.
5609 
5610 			// Anyway, now it is time to load the font from the
5611 			// OS (you can alternatively load one from a .ttf file
5612 			// you bundle with the application), then load the
5613 			// font into texture for drawing.
5614 
5615 			auto osfont = new OperatingSystemFont("DejaVu Sans", 18);
5616 
5617 			assert(!osfont.isNull()); // make sure it actually loaded
5618 
5619 			// using typeof to avoid repeating the long name lol
5620 			glfont = new typeof(glfont)(
5621 				// get the raw data from the font for loading in here
5622 				// since it doesn't use the OS function to draw the
5623 				// text, we gotta treat it more as a file than as
5624 				// a drawing api.
5625 				osfont.getTtfBytes(),
5626 				18, // need to respecify size since opengl world is different coordinate system
5627 
5628 				// these last two numbers are why it is called
5629 				// "Limited" font. It only loads the characters
5630 				// in the given range, since the texture atlas
5631 				// it references is all a big image generated ahead
5632 				// of time. You could maybe do the whole thing but
5633 				// idk how much memory that is.
5634 				//
5635 				// But here, 0-128 represents the ASCII range, so
5636 				// good enough for most English things, numeric labels,
5637 				// etc.
5638 				0,
5639 				128
5640 			);
5641 		};
5642 
5643 		widget.redrawOpenGlScene = () {
5644 			// now we can use the glfont's drawString function
5645 
5646 			// first some opengl setup. You can do this in one place
5647 			// on window first visible too in many cases, just showing
5648 			// here cuz it is easier for me.
5649 
5650 			// gonna need some alpha blending or it just looks awful
5651 			glEnable(GL_BLEND);
5652 			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
5653 			glClearColor(0,0,0,0);
5654 			glDepthFunc(GL_LEQUAL);
5655 
5656 			// Also need to enable 2d textures, since it draws the
5657 			// font characters as images baked in
5658 			glMatrixMode(GL_MODELVIEW);
5659 			glLoadIdentity();
5660 			glDisable(GL_DEPTH_TEST);
5661 			glEnable(GL_TEXTURE_2D);
5662 
5663 			// the orthographic matrix is best for 2d things like text
5664 			// so let's set that up. This matrix makes the coordinates
5665 			// in the opengl scene be one-to-one with the actual pixels
5666 			// on screen. (Not necessarily best, you may wish to scale
5667 			// things, but it does help keep fonts looking normal.)
5668 			glMatrixMode(GL_PROJECTION);
5669 			glLoadIdentity();
5670 			glOrtho(0, widget.width, widget.height, 0, 0, 1);
5671 
5672 			// you can do other glScale, glRotate, glTranslate, etc
5673 			// to the matrix here of course if you want.
5674 
5675 			// note the x,y coordinates here are for the text baseline
5676 			// NOT the upper-left corner. The baseline is like the line
5677 			// in the notebook you write on. Most the letters are actually
5678 			// above it, but some, like p and q, dip a bit below it.
5679 			//
5680 			// So if you're used to the upper left coordinate like the
5681 			// rest of simpledisplay/minigui usually do, do the
5682 			// y + glfont.ascent to bring it down a little. So this
5683 			// example puts the string in the upper left of the window.
5684 			glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green);
5685 
5686 			// re color btw: the function sets a solid color internally,
5687 			// but you actually COULD do your own thing for rainbow effects
5688 			// and the sort if you wanted too, by pulling its guts out.
5689 			// Just view its source for an idea of how it actually draws:
5690 			// http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332
5691 
5692 			// it gets a bit complicated with the character positioning,
5693 			// but the opengl parts are fairly simple: bind a texture,
5694 			// set the color, draw a quad for each letter.
5695 
5696 
5697 			// the last optional argument there btw is a bounding box
5698 			// it will/ use to word wrap and return an object you can
5699 			// use to implement scrolling or pagination; it tells how
5700 			// much of the string didn't fit in the box. But for simple
5701 			// labels we can just ignore that.
5702 
5703 
5704 			// I'd suggest drawing text as the last step, after you
5705 			// do your other drawing. You might use the push/pop matrix
5706 			// stuff to keep your place. You, in theory, should be able
5707 			// to do text in a 3d space but I've never actually tried
5708 			// that....
5709 		};
5710 
5711 		window.loop();
5712 	}
5713 }
5714 
5715 version(custom_widgets)
5716 private class TextListViewWidget : GenericListViewWidget {
5717 	static class TextListViewItem : GenericListViewItem {
5718 		ListWidget controller;
5719 		this(ListWidget controller, Widget parent) {
5720 			this.controller = controller;
5721 			this.tabStop = false;
5722 			super(parent);
5723 		}
5724 
5725 		ListWidget.Option* showing;
5726 
5727 		override void showItem(int idx) {
5728 			showing = idx < controller.options.length ? &controller.options[idx] : null;
5729 			redraw(); // is this necessary? the generic thing might call it...
5730 		}
5731 
5732 		override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
5733 			if(showing is null)
5734 				return bounds;
5735 			painter.drawText(bounds.upperLeft, showing.label);
5736 			return bounds;
5737 		}
5738 
5739 		static class Style : Widget.Style {
5740 			override WidgetBackground background() {
5741 				// FIXME: change it if it is focused or not
5742 				// 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
5743 				auto tlvi = cast(TextListViewItem) widget;
5744 				if(tlvi && tlvi.showing && tlvi && tlvi.showing.selected)
5745 					return WidgetBackground(true /*widget.parent.isFocused*/ ? WidgetPainter.visualTheme.selectionBackgroundColor : Color(128, 128, 128)); // FIXME: don't hardcode
5746 				return super.background();
5747 			}
5748 
5749 			override Color foregroundColor() {
5750 				auto tlvi = cast(TextListViewItem) widget;
5751 				return tlvi && tlvi.showing && tlvi && tlvi.showing.selected ? WidgetPainter.visualTheme.selectionForegroundColor : super.foregroundColor();
5752 			}
5753 
5754 			override FrameStyle outlineStyle() {
5755 				// FIXME: change it if it is focused or not
5756 				auto tlvi = cast(TextListViewItem) widget;
5757 				return (tlvi && tlvi.currentIndexLoaded() == tlvi.controller.focusOn) ? FrameStyle.dotted : super.outlineStyle();
5758 			}
5759 		}
5760 		mixin OverrideStyle!Style;
5761 
5762 		mixin Padding!q{2};
5763 
5764 		override void defaultEventHandler_click(ClickEvent event) {
5765 			if(event.button == MouseButton.left) {
5766 				controller.setSelection(currentIndexLoaded());
5767 				controller.focusOn = currentIndexLoaded();
5768 			}
5769 		}
5770 
5771 	}
5772 
5773 	ListWidget controller;
5774 
5775 	this(ListWidget parent) {
5776 		this.controller = parent;
5777 		this.tabStop = false; // this is only used as a child of the ListWidget
5778 		super(parent);
5779 
5780 		smw.movementPerButtonClick(1, itemSize().height);
5781 	}
5782 
5783 	override Size itemSize() {
5784 		return Size(0, defaultLineHeight + scaleWithDpi(4 /* the top and bottom padding */));
5785 	}
5786 
5787 	override GenericListViewItem itemFactory(Widget parent) {
5788 		return new TextListViewItem(controller, parent);
5789 	}
5790 
5791 	static class Style : Widget.Style {
5792 		override FrameStyle borderStyle() {
5793 			return FrameStyle.sunk;
5794 		}
5795 
5796 		override WidgetBackground background() {
5797 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
5798 		}
5799 	}
5800 	mixin OverrideStyle!Style;
5801 }
5802 
5803 /++
5804 	A list widget contains a list of strings that the user can examine and select.
5805 
5806 
5807 	In the future, items in the list may be possible to be more than just strings.
5808 
5809 	See_Also:
5810 		[TableView]
5811 +/
5812 class ListWidget : Widget {
5813 	/// 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.
5814 	mixin Emits!(ChangeEvent!void);
5815 
5816 	version(custom_widgets)
5817 		TextListViewWidget glvw;
5818 
5819 	static struct Option {
5820 		string label;
5821 		bool selected;
5822 		void* tag;
5823 	}
5824 	private Option[] options;
5825 
5826 	/++
5827 		Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete.
5828 	+/
5829 	void setSelection(int y) {
5830 		if(!multiSelect)
5831 			foreach(ref opt; options)
5832 				opt.selected = false;
5833 		if(y >= 0 && y < options.length)
5834 			options[y].selected = !options[y].selected;
5835 
5836 		version(custom_widgets)
5837 			focusOn = y;
5838 
5839 		this.emit!(ChangeEvent!void)(delegate {});
5840 
5841 		version(custom_widgets)
5842 			redraw();
5843 	}
5844 
5845 	/++
5846 		Gets the index of the selected item. In case of multi select, the index of the first selected item is returned.
5847 		Returns -1 if nothing is selected.
5848 	+/
5849 	int getSelection()
5850 	{
5851 		foreach(i, opt; options) {
5852 			if (opt.selected)
5853 				return cast(int) i;
5854 		}
5855 		return -1;
5856 	}
5857 
5858 	version(custom_widgets)
5859 	private int focusOn;
5860 
5861 	this(Widget parent) {
5862 		super(parent);
5863 
5864 		version(custom_widgets)
5865 			glvw = new TextListViewWidget(this);
5866 
5867 		version(win32_widgets)
5868 			createWin32Window(this, WC_LISTBOX, "",
5869 				0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0);
5870 	}
5871 
5872 	version(win32_widgets)
5873 	override void handleWmCommand(ushort code, ushort id) {
5874 		switch(code) {
5875 			case LBN_SELCHANGE:
5876 				auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0);
5877 				setSelection(cast(int) sel);
5878 			break;
5879 			default:
5880 		}
5881 	}
5882 
5883 
5884 	void addOption(string text, void* tag = null) {
5885 		options ~= Option(text, false, tag);
5886 		version(win32_widgets) {
5887 			WCharzBuffer buffer = WCharzBuffer(text);
5888 			SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr);
5889 		}
5890 		version(custom_widgets) {
5891 			glvw.setItemCount(cast(int) options.length);
5892 			//setContentSize(width, cast(int) (options.length * defaultLineHeight));
5893 			redraw();
5894 		}
5895 	}
5896 
5897 	void clear() {
5898 		options = null;
5899 		version(win32_widgets) {
5900 			while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0)
5901 				{}
5902 
5903 		} else version(custom_widgets) {
5904 			focusOn = -1;
5905 			glvw.setItemCount(0);
5906 			redraw();
5907 		}
5908 	}
5909 
5910 	version(custom_widgets)
5911 	override void defaultEventHandler_keydown(KeyDownEvent kde) {
5912 		void changedFocusOn() {
5913 			scrollFocusIntoView();
5914 			if(multiSelect)
5915 				redraw();
5916 			else
5917 				setSelection(focusOn);
5918 		}
5919 		switch(kde.key) {
5920 			case Key.Up:
5921 				if(focusOn) {
5922 					focusOn--;
5923 					changedFocusOn();
5924 				}
5925 			break;
5926 			case Key.Down:
5927 				if(focusOn + 1 < options.length) {
5928 					focusOn++;
5929 					changedFocusOn();
5930 				}
5931 			break;
5932 			case Key.Home:
5933 				if(focusOn) {
5934 					focusOn = 0;
5935 					changedFocusOn();
5936 				}
5937 			break;
5938 			case Key.End:
5939 				if(options.length && focusOn + 1 != options.length) {
5940 					focusOn = cast(int) options.length - 1;
5941 					changedFocusOn();
5942 				}
5943 			break;
5944 			case Key.PageUp:
5945 				auto n = glvw.numberOfCurrentlyFullyVisibleItems;
5946 				focusOn -= n;
5947 				if(focusOn < 0)
5948 					focusOn = 0;
5949 				changedFocusOn();
5950 			break;
5951 			case Key.PageDown:
5952 				if(options.length == 0)
5953 					break;
5954 				auto n = glvw.numberOfCurrentlyFullyVisibleItems;
5955 				focusOn += n;
5956 				if(focusOn >= options.length)
5957 					focusOn = cast(int) options.length - 1;
5958 				changedFocusOn();
5959 			break;
5960 
5961 			default:
5962 		}
5963 	}
5964 
5965 	version(custom_widgets)
5966 	override void defaultEventHandler_char(CharEvent ce) {
5967 		if(ce.character == '\n' || ce.character == ' ') {
5968 			setSelection(focusOn);
5969 		} else {
5970 			// search for the item that best matches and jump to it
5971 			// FIXME this sucks in tons of ways. the normal thing toolkits
5972 			// do here is to search for a substring on a timer, but i'd kinda
5973 			// rather make an actual little dialog with some options. still meh for now.
5974 			dchar search = ce.character;
5975 			if(search >= 'A' && search <= 'Z')
5976 				search += 32;
5977 			foreach(idx, option; options) {
5978 				auto ch = option.label.length ? option.label[0] : 0;
5979 				if(ch >= 'A' && ch <= 'Z')
5980 					ch += 32;
5981 				if(ch == search) {
5982 					setSelection(cast(int) idx);
5983 					scrollSelectionIntoView();
5984 					break;
5985 				}
5986 			}
5987 
5988 		}
5989 	}
5990 
5991 	version(win32_widgets)
5992 		enum multiSelect = false; /// not implemented yet
5993 	else
5994 		bool multiSelect;
5995 
5996 	override int heightStretchiness() { return 6; }
5997 
5998 	version(custom_widgets)
5999 	void scrollFocusIntoView() {
6000 		glvw.ensureItemVisibleInScroll(focusOn);
6001 	}
6002 
6003 	void scrollSelectionIntoView() {
6004 		// FIXME: implement on Windows
6005 
6006 		version(custom_widgets)
6007 			glvw.ensureItemVisibleInScroll(getSelection());
6008 	}
6009 
6010 	/*
6011 	version(custom_widgets)
6012 	override void defaultEventHandler_focusout(Event foe) {
6013 		glvw.redraw();
6014 	}
6015 
6016 	version(custom_widgets)
6017 	override void defaultEventHandler_focusin(Event foe) {
6018 		glvw.redraw();
6019 	}
6020 	*/
6021 
6022 }
6023 
6024 
6025 
6026 /// For [ScrollableWidget], determines when to show the scroll bar to the user.
6027 /// NEVER USED
6028 enum ScrollBarShowPolicy {
6029 	automatic, /// automatically show the scroll bar if it is necessary
6030 	never, /// never show the scroll bar (scrolling must be done programmatically)
6031 	always /// always show the scroll bar, even if it is disabled
6032 }
6033 
6034 /++
6035 	A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner.
6036 
6037 	It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code.
6038 +/
6039 // FIXME ScrollBarShowPolicy
6040 // FIXME: use the ScrollMessageWidget in here now that it exists
6041 deprecated("Use ScrollMessageWidget or ScrollableContainerWidget instead") // ugh compiler won't let me do it
6042 class ScrollableWidget : Widget {
6043 	// FIXME: make line size configurable
6044 	// FIXME: add keyboard controls
6045 	version(win32_widgets) {
6046 		override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) {
6047 			if(msg == WM_VSCROLL || msg == WM_HSCROLL) {
6048 				auto pos = HIWORD(wParam);
6049 				auto m = LOWORD(wParam);
6050 
6051 				// FIXME: I can reintroduce the
6052 				// scroll bars now by using this
6053 				// in the top-level window handler
6054 				// to forward comamnds
6055 				auto scrollbarHwnd = lParam;
6056 				switch(m) {
6057 					case SB_BOTTOM:
6058 						if(msg == WM_HSCROLL)
6059 							horizontalScrollTo(contentWidth_);
6060 						else
6061 							verticalScrollTo(contentHeight_);
6062 					break;
6063 					case SB_TOP:
6064 						if(msg == WM_HSCROLL)
6065 							horizontalScrollTo(0);
6066 						else
6067 							verticalScrollTo(0);
6068 					break;
6069 					case SB_ENDSCROLL:
6070 						// idk
6071 					break;
6072 					case SB_LINEDOWN:
6073 						if(msg == WM_HSCROLL)
6074 							horizontalScroll(scaleWithDpi(16));
6075 						else
6076 							verticalScroll(scaleWithDpi(16));
6077 					break;
6078 					case SB_LINEUP:
6079 						if(msg == WM_HSCROLL)
6080 							horizontalScroll(scaleWithDpi(-16));
6081 						else
6082 							verticalScroll(scaleWithDpi(-16));
6083 					break;
6084 					case SB_PAGEDOWN:
6085 						if(msg == WM_HSCROLL)
6086 							horizontalScroll(scaleWithDpi(100));
6087 						else
6088 							verticalScroll(scaleWithDpi(100));
6089 					break;
6090 					case SB_PAGEUP:
6091 						if(msg == WM_HSCROLL)
6092 							horizontalScroll(scaleWithDpi(-100));
6093 						else
6094 							verticalScroll(scaleWithDpi(-100));
6095 					break;
6096 					case SB_THUMBPOSITION:
6097 					case SB_THUMBTRACK:
6098 						if(msg == WM_HSCROLL)
6099 							horizontalScrollTo(pos);
6100 						else
6101 							verticalScrollTo(pos);
6102 
6103 						if(m == SB_THUMBTRACK) {
6104 							// the event loop doesn't seem to carry on with a requested redraw..
6105 							// so we request it to get our dirty bit set...
6106 							redraw();
6107 
6108 							// then we need to immediately actually redraw it too for instant feedback to user
6109 
6110 							SimpleWindow.processAllCustomEvents();
6111 							//if(parentWindow)
6112 								//parentWindow.actualRedraw();
6113 						}
6114 					break;
6115 					default:
6116 				}
6117 			}
6118 			return super.hookedWndProc(msg, wParam, lParam);
6119 		}
6120 	}
6121 	///
6122 	this(Widget parent) {
6123 		this.parentWindow = parent.parentWindow;
6124 
6125 		version(win32_widgets) {
6126 			createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "",
6127 				0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0);
6128 			super(parent);
6129 		} else version(custom_widgets) {
6130 			outerContainer = new InternalScrollableContainerWidget(this, parent);
6131 			super(outerContainer);
6132 		} else static assert(0);
6133 	}
6134 
6135 	version(custom_widgets)
6136 		InternalScrollableContainerWidget outerContainer;
6137 
6138 	override void defaultEventHandler_click(ClickEvent event) {
6139 		if(event.button == MouseButton.wheelUp)
6140 			verticalScroll(scaleWithDpi(-16));
6141 		if(event.button == MouseButton.wheelDown)
6142 			verticalScroll(scaleWithDpi(16));
6143 		super.defaultEventHandler_click(event);
6144 	}
6145 
6146 	override void defaultEventHandler_keydown(KeyDownEvent event) {
6147 		switch(event.key) {
6148 			case Key.Left:
6149 				horizontalScroll(scaleWithDpi(-16));
6150 			break;
6151 			case Key.Right:
6152 				horizontalScroll(scaleWithDpi(16));
6153 			break;
6154 			case Key.Up:
6155 				verticalScroll(scaleWithDpi(-16));
6156 			break;
6157 			case Key.Down:
6158 				verticalScroll(scaleWithDpi(16));
6159 			break;
6160 			case Key.Home:
6161 				verticalScrollTo(0);
6162 			break;
6163 			case Key.End:
6164 				verticalScrollTo(contentHeight);
6165 			break;
6166 			case Key.PageUp:
6167 				verticalScroll(scaleWithDpi(-160));
6168 			break;
6169 			case Key.PageDown:
6170 				verticalScroll(scaleWithDpi(160));
6171 			break;
6172 			default:
6173 		}
6174 		super.defaultEventHandler_keydown(event);
6175 	}
6176 
6177 
6178 	version(win32_widgets)
6179 	override void recomputeChildLayout() {
6180 		super.recomputeChildLayout();
6181 		SCROLLINFO info;
6182 		info.cbSize = info.sizeof;
6183 		info.nPage = viewportHeight;
6184 		info.fMask = SIF_PAGE | SIF_RANGE;
6185 		info.nMin = 0;
6186 		info.nMax = contentHeight_;
6187 		SetScrollInfo(hwnd, SB_VERT, &info, true);
6188 
6189 		info.cbSize = info.sizeof;
6190 		info.nPage = viewportWidth;
6191 		info.fMask = SIF_PAGE | SIF_RANGE;
6192 		info.nMin = 0;
6193 		info.nMax = contentWidth_;
6194 		SetScrollInfo(hwnd, SB_HORZ, &info, true);
6195 	}
6196 
6197 	/*
6198 		Scrolling
6199 		------------
6200 
6201 		You are assigned a width and a height by the layout engine, which
6202 		is your viewport box. However, you may draw more than that by setting
6203 		a contentWidth and contentHeight.
6204 
6205 		If these can be contained by the viewport, no scrollbar is displayed.
6206 		If they cannot fit though, it will automatically show scroll as necessary.
6207 
6208 		If contentWidth == 0, no horizontal scrolling is performed. If contentHeight
6209 		is zero, no vertical scrolling is performed.
6210 
6211 		If scrolling is necessary, the lib will automatically work with the bars.
6212 		When you redraw, the origin and clipping info in the painter is set so if
6213 		you just draw everything, it will work, but you can be more efficient by checking
6214 		the viewportWidth, viewportHeight, and scrollOrigin members.
6215 	*/
6216 
6217 	///
6218 	final @property int viewportWidth() {
6219 		return width - (showingVerticalScroll ? scaleWithDpi(16) : 0);
6220 	}
6221 	///
6222 	final @property int viewportHeight() {
6223 		return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0);
6224 	}
6225 
6226 	// FIXME property
6227 	Point scrollOrigin_;
6228 
6229 	///
6230 	final const(Point) scrollOrigin() {
6231 		return scrollOrigin_;
6232 	}
6233 
6234 	// the user sets these two
6235 	private int contentWidth_ = 0;
6236 	private int contentHeight_ = 0;
6237 
6238 	///
6239 	int contentWidth() { return contentWidth_; }
6240 	///
6241 	int contentHeight() { return contentHeight_; }
6242 
6243 	///
6244 	void setContentSize(int width, int height) {
6245 		contentWidth_ = width;
6246 		contentHeight_ = height;
6247 
6248 		version(custom_widgets) {
6249 			if(showingVerticalScroll || showingHorizontalScroll) {
6250 				outerContainer.queueRecomputeChildLayout();
6251 			}
6252 
6253 			if(showingVerticalScroll())
6254 				outerContainer.verticalScrollBar.redraw();
6255 			if(showingHorizontalScroll())
6256 				outerContainer.horizontalScrollBar.redraw();
6257 		} else version(win32_widgets) {
6258 			queueRecomputeChildLayout();
6259 		} else static assert(0);
6260 	}
6261 
6262 	///
6263 	void verticalScroll(int delta) {
6264 		verticalScrollTo(scrollOrigin.y + delta);
6265 	}
6266 	///
6267 	void verticalScrollTo(int pos) {
6268 		scrollOrigin_.y = pos;
6269 		if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight))
6270 			scrollOrigin_.y = contentHeight - viewportHeight;
6271 
6272 		if(scrollOrigin_.y < 0)
6273 			scrollOrigin_.y = 0;
6274 
6275 		version(win32_widgets) {
6276 			SCROLLINFO info;
6277 			info.cbSize = info.sizeof;
6278 			info.fMask = SIF_POS;
6279 			info.nPos = scrollOrigin_.y;
6280 			SetScrollInfo(hwnd, SB_VERT, &info, true);
6281 		} else version(custom_widgets) {
6282 			outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y);
6283 		} else static assert(0);
6284 
6285 		redraw();
6286 	}
6287 
6288 	///
6289 	void horizontalScroll(int delta) {
6290 		horizontalScrollTo(scrollOrigin.x + delta);
6291 	}
6292 	///
6293 	void horizontalScrollTo(int pos) {
6294 		scrollOrigin_.x = pos;
6295 		if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth))
6296 			scrollOrigin_.x = contentWidth - viewportWidth;
6297 
6298 		if(scrollOrigin_.x < 0)
6299 			scrollOrigin_.x = 0;
6300 
6301 		version(win32_widgets) {
6302 			SCROLLINFO info;
6303 			info.cbSize = info.sizeof;
6304 			info.fMask = SIF_POS;
6305 			info.nPos = scrollOrigin_.x;
6306 			SetScrollInfo(hwnd, SB_HORZ, &info, true);
6307 		} else version(custom_widgets) {
6308 			outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x);
6309 		} else static assert(0);
6310 
6311 		redraw();
6312 	}
6313 	///
6314 	void scrollTo(Point p) {
6315 		verticalScrollTo(p.y);
6316 		horizontalScrollTo(p.x);
6317 	}
6318 
6319 	///
6320 	void ensureVisibleInScroll(Point p) {
6321 		auto rect = viewportRectangle();
6322 		if(rect.contains(p))
6323 			return;
6324 		if(p.x < rect.left)
6325 			horizontalScroll(p.x - rect.left);
6326 		else if(p.x > rect.right)
6327 			horizontalScroll(p.x - rect.right);
6328 
6329 		if(p.y < rect.top)
6330 			verticalScroll(p.y - rect.top);
6331 		else if(p.y > rect.bottom)
6332 			verticalScroll(p.y - rect.bottom);
6333 	}
6334 
6335 	///
6336 	void ensureVisibleInScroll(Rectangle rect) {
6337 		ensureVisibleInScroll(rect.upperLeft);
6338 		ensureVisibleInScroll(rect.lowerRight);
6339 	}
6340 
6341 	///
6342 	Rectangle viewportRectangle() {
6343 		return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight));
6344 	}
6345 
6346 	///
6347 	bool showingHorizontalScroll() {
6348 		return contentWidth > width;
6349 	}
6350 	///
6351 	bool showingVerticalScroll() {
6352 		return contentHeight > height;
6353 	}
6354 
6355 	/// This is called before the ordinary paint delegate,
6356 	/// giving you a chance to draw the window frame, etc,
6357 	/// before the scroll clip takes effect
6358 	void paintFrameAndBackground(WidgetPainter painter) {
6359 		version(win32_widgets) {
6360 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
6361 			auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
6362 			// since the pen is null, to fill the whole space, we need the +1 on both.
6363 			gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
6364 			SelectObject(painter.impl.hdc, p);
6365 			SelectObject(painter.impl.hdc, b);
6366 		}
6367 
6368 	}
6369 
6370 	// make space for the scroll bar, and that's it.
6371 	final override int paddingRight() { return scaleWithDpi(16); }
6372 	final override int paddingBottom() { return scaleWithDpi(16); }
6373 
6374 	/*
6375 		END SCROLLING
6376 	*/
6377 
6378 	override WidgetPainter draw() {
6379 		int x = this.x, y = this.y;
6380 		auto parent = this.parent;
6381 		while(parent) {
6382 			x += parent.x;
6383 			y += parent.y;
6384 			parent = parent.parent;
6385 		}
6386 
6387 		//version(win32_widgets) {
6388 			//auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
6389 		//} else {
6390 			auto painter = parentWindow.win.draw(true);
6391 		//}
6392 		painter.originX = x;
6393 		painter.originY = y;
6394 
6395 		painter.originX = painter.originX - scrollOrigin.x;
6396 		painter.originY = painter.originY - scrollOrigin.y;
6397 		painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight());
6398 
6399 		return WidgetPainter(painter, this);
6400 	}
6401 
6402 	override void addScrollPosition(ref int x, ref int y) {
6403 		x += scrollOrigin.x;
6404 		y += scrollOrigin.y;
6405 	}
6406 
6407 	mixin ScrollableChildren;
6408 }
6409 
6410 // you need to have a Point scrollOrigin in the class somewhere
6411 // and a paintFrameAndBackground
6412 private mixin template ScrollableChildren() {
6413 	static assert(!__traits(isSame, this.addScrollPosition, Widget.addScrollPosition), "Your widget should provide `Point scrollOrigin()` and `override void addScrollPosition`");
6414 
6415 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
6416 		if(hidden)
6417 			return;
6418 
6419 		//version(win32_widgets)
6420 			//painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true);
6421 
6422 		painter.originX = lox + x;
6423 		painter.originY = loy + y;
6424 
6425 		bool actuallyPainted = false;
6426 
6427 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height)));
6428 		if(clip == Rectangle.init)
6429 			return;
6430 
6431 		if(force || redrawRequested) {
6432 			//painter.setClipRectangle(scrollOrigin, width, height);
6433 			painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
6434 			paintFrameAndBackground(painter);
6435 		}
6436 
6437 		/+
6438 		version(win32_widgets) {
6439 			if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW);
6440 		}
6441 		+/
6442 
6443 		painter.originX = painter.originX - scrollOrigin.x;
6444 		painter.originY = painter.originY - scrollOrigin.y;
6445 		if(force || redrawRequested) {
6446 			painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4);
6447 			//painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4);
6448 
6449 			//erase(painter); // we paintFrameAndBackground above so no need
6450 			if(painter.visualTheme)
6451 				painter.visualTheme.doPaint(this, painter);
6452 			else
6453 				paint(painter);
6454 
6455 			if(invalidate) {
6456 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
6457 				// children are contained inside this, so no need to do extra work
6458 				invalidate = false;
6459 			}
6460 
6461 
6462 			actuallyPainted = true;
6463 			redrawRequested = false;
6464 		}
6465 
6466 		foreach(child; children) {
6467 			if(cast(FixedPosition) child)
6468 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
6469 			else
6470 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
6471 		}
6472 	}
6473 }
6474 
6475 private class InternalScrollableContainerInsideWidget : ContainerWidget {
6476 	ScrollableContainerWidget scw;
6477 
6478 	this(ScrollableContainerWidget parent) {
6479 		scw = parent;
6480 		super(parent);
6481 	}
6482 
6483 	version(custom_widgets)
6484 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
6485 		if(hidden)
6486 			return;
6487 
6488 		bool actuallyPainted = false;
6489 
6490 		auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_);
6491 
6492 		const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_)));
6493 		if(clip == Rectangle.init)
6494 			return;
6495 
6496 		painter.originX = lox + x - scrollOrigin.x;
6497 		painter.originY = loy + y - scrollOrigin.y;
6498 		if(force || redrawRequested) {
6499 			painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height);
6500 
6501 			erase(painter);
6502 			if(painter.visualTheme)
6503 				painter.visualTheme.doPaint(this, painter);
6504 			else
6505 				paint(painter);
6506 
6507 			if(invalidate) {
6508 				painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)));
6509 				// children are contained inside this, so no need to do extra work
6510 				invalidate = false;
6511 			}
6512 
6513 			actuallyPainted = true;
6514 			redrawRequested = false;
6515 		}
6516 		foreach(child; children) {
6517 			if(cast(FixedPosition) child)
6518 				child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate);
6519 			else
6520 				child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate);
6521 		}
6522 	}
6523 
6524 	version(custom_widgets)
6525 	override protected void addScrollPosition(ref int x, ref int y) {
6526 		x += scw.scrollX_;
6527 		y += scw.scrollY_;
6528 	}
6529 }
6530 
6531 /++
6532 	A widget meant to contain other widgets that may need to scroll.
6533 
6534 	Currently buggy.
6535 
6536 	History:
6537 		Added July 1, 2021 (dub v10.2)
6538 
6539 		On January 3, 2022, I tried to use it in a few other cases
6540 		and found it only worked well in the original test case. Since
6541 		it still sucks, I think I'm going to rewrite it again.
6542 +/
6543 class ScrollableContainerWidget : ContainerWidget {
6544 	///
6545 	this(Widget parent) {
6546 		super(parent);
6547 
6548 		container = new InternalScrollableContainerInsideWidget(this);
6549 		hsb = new HorizontalScrollbar(this);
6550 		vsb = new VerticalScrollbar(this);
6551 
6552 		tabStop = false;
6553 		container.tabStop = false;
6554 		magic = true;
6555 
6556 
6557 		vsb.addEventListener("scrolltonextline", () {
6558 			scrollBy(0, scaleWithDpi(16));
6559 		});
6560 		vsb.addEventListener("scrolltopreviousline", () {
6561 			scrollBy(0,scaleWithDpi( -16));
6562 		});
6563 		vsb.addEventListener("scrolltonextpage", () {
6564 			scrollBy(0, container.height);
6565 		});
6566 		vsb.addEventListener("scrolltopreviouspage", () {
6567 			scrollBy(0, -container.height);
6568 		});
6569 		vsb.addEventListener((scope ScrollToPositionEvent spe) {
6570 			scrollTo(scrollX_, spe.value);
6571 		});
6572 
6573 		this.addEventListener(delegate (scope ClickEvent e) {
6574 			if(e.button == MouseButton.wheelUp) {
6575 				if(!e.defaultPrevented)
6576 					scrollBy(0, scaleWithDpi(-16));
6577 				e.stopPropagation();
6578 			} else if(e.button == MouseButton.wheelDown) {
6579 				if(!e.defaultPrevented)
6580 					scrollBy(0, scaleWithDpi(16));
6581 				e.stopPropagation();
6582 			}
6583 		});
6584 	}
6585 
6586 	/+
6587 	override void defaultEventHandler_click(ClickEvent e) {
6588 	}
6589 	+/
6590 
6591 	override void removeAllChildren() {
6592 		container.removeAllChildren();
6593 	}
6594 
6595 	void scrollTo(int x, int y) {
6596 		scrollBy(x - scrollX_, y - scrollY_);
6597 	}
6598 
6599 	void scrollBy(int x, int y) {
6600 		auto ox = scrollX_;
6601 		auto oy = scrollY_;
6602 
6603 		auto nx = ox + x;
6604 		auto ny = oy + y;
6605 
6606 		if(nx < 0)
6607 			nx = 0;
6608 		if(ny < 0)
6609 			ny = 0;
6610 
6611 		auto maxX = hsb.max - container.width;
6612 		if(maxX < 0) maxX = 0;
6613 		auto maxY = vsb.max - container.height;
6614 		if(maxY < 0) maxY = 0;
6615 
6616 		if(nx > maxX)
6617 			nx = maxX;
6618 		if(ny > maxY)
6619 			ny = maxY;
6620 
6621 		auto dx = nx - ox;
6622 		auto dy = ny - oy;
6623 
6624 		if(dx || dy) {
6625 			version(win32_widgets)
6626 				ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
6627 			else {
6628 				redraw();
6629 			}
6630 
6631 			hsb.setPosition = nx;
6632 			vsb.setPosition = ny;
6633 
6634 			scrollX_ = nx;
6635 			scrollY_ = ny;
6636 		}
6637 	}
6638 
6639 	private int scrollX_;
6640 	private int scrollY_;
6641 
6642 	void setTotalArea(int width, int height) {
6643 		hsb.setMax(width);
6644 		vsb.setMax(height);
6645 	}
6646 
6647 	///
6648 	void setViewableArea(int width, int height) {
6649 		hsb.setViewableArea(width);
6650 		vsb.setViewableArea(height);
6651 	}
6652 
6653 	private bool magic;
6654 	override void addChild(Widget w, int position = int.max) {
6655 		if(magic)
6656 			container.addChild(w, position);
6657 		else
6658 			super.addChild(w, position);
6659 	}
6660 
6661 	override void recomputeChildLayout() {
6662 		if(hsb is null || vsb is null || container is null) return;
6663 
6664 		/+
6665 		writeln(x, " ", y , " ", width, " ", height);
6666 		writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight());
6667 		+/
6668 
6669 		registerMovement();
6670 
6671 		hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane?
6672 		hsb.x = 0;
6673 		hsb.y = this.height - hsb.height;
6674 		hsb.width = this.width - scaleWithDpi(16);
6675 		hsb.recomputeChildLayout();
6676 
6677 		vsb.width = scaleWithDpi(16); // FIXME?
6678 		vsb.x = this.width - vsb.width;
6679 		vsb.y = 0;
6680 		vsb.height = this.height - scaleWithDpi(16);
6681 		vsb.recomputeChildLayout();
6682 
6683 		container.x = 0;
6684 		container.y = 0;
6685 		container.width = this.width - vsb.width;
6686 		container.height = this.height - hsb.height;
6687 		container.recomputeChildLayout();
6688 
6689 		scrollX_ = 0;
6690 		scrollY_ = 0;
6691 
6692 		hsb.setPosition(0);
6693 		vsb.setPosition(0);
6694 
6695 		int mw, mh;
6696 		Widget c = container;
6697 		// FIXME: hack here to handle a layout inside...
6698 		if(c.children.length == 1 && cast(Layout) c.children[0])
6699 			c = c.children[0];
6700 		foreach(child; c.children) {
6701 			auto w = child.x + child.width;
6702 			auto h = child.y + child.height;
6703 
6704 			if(w > mw) mw = w;
6705 			if(h > mh) mh = h;
6706 		}
6707 
6708 		setTotalArea(mw, mh);
6709 		setViewableArea(width, height);
6710 	}
6711 
6712 	override int minHeight() { return scaleWithDpi(64); }
6713 
6714 	HorizontalScrollbar hsb;
6715 	VerticalScrollbar vsb;
6716 	ContainerWidget container;
6717 }
6718 
6719 
6720 version(custom_widgets)
6721 deprecated
6722 private class InternalScrollableContainerWidget : Widget {
6723 
6724 	ScrollableWidget sw;
6725 
6726 	VerticalScrollbar verticalScrollBar;
6727 	HorizontalScrollbar horizontalScrollBar;
6728 
6729 	this(ScrollableWidget sw, Widget parent) {
6730 		this.sw = sw;
6731 
6732 		this.tabStop = false;
6733 
6734 		super(parent);
6735 
6736 		horizontalScrollBar = new HorizontalScrollbar(this);
6737 		verticalScrollBar = new VerticalScrollbar(this);
6738 
6739 		horizontalScrollBar.showing_ = false;
6740 		verticalScrollBar.showing_ = false;
6741 
6742 		horizontalScrollBar.addEventListener("scrolltonextline", {
6743 			horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
6744 			sw.horizontalScrollTo(horizontalScrollBar.position);
6745 		});
6746 		horizontalScrollBar.addEventListener("scrolltopreviousline", {
6747 			horizontalScrollBar.setPosition(horizontalScrollBar.position - 1);
6748 			sw.horizontalScrollTo(horizontalScrollBar.position);
6749 		});
6750 		verticalScrollBar.addEventListener("scrolltonextline", {
6751 			verticalScrollBar.setPosition(verticalScrollBar.position + 1);
6752 			sw.verticalScrollTo(verticalScrollBar.position);
6753 		});
6754 		verticalScrollBar.addEventListener("scrolltopreviousline", {
6755 			verticalScrollBar.setPosition(verticalScrollBar.position - 1);
6756 			sw.verticalScrollTo(verticalScrollBar.position);
6757 		});
6758 		horizontalScrollBar.addEventListener("scrolltonextpage", {
6759 			horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_);
6760 			sw.horizontalScrollTo(horizontalScrollBar.position);
6761 		});
6762 		horizontalScrollBar.addEventListener("scrolltopreviouspage", {
6763 			horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_);
6764 			sw.horizontalScrollTo(horizontalScrollBar.position);
6765 		});
6766 		verticalScrollBar.addEventListener("scrolltonextpage", {
6767 			verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_);
6768 			sw.verticalScrollTo(verticalScrollBar.position);
6769 		});
6770 		verticalScrollBar.addEventListener("scrolltopreviouspage", {
6771 			verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_);
6772 			sw.verticalScrollTo(verticalScrollBar.position);
6773 		});
6774 		horizontalScrollBar.addEventListener("scrolltoposition", (Event event) {
6775 			horizontalScrollBar.setPosition(event.intValue);
6776 			sw.horizontalScrollTo(horizontalScrollBar.position);
6777 		});
6778 		verticalScrollBar.addEventListener("scrolltoposition", (Event event) {
6779 			verticalScrollBar.setPosition(event.intValue);
6780 			sw.verticalScrollTo(verticalScrollBar.position);
6781 		});
6782 		horizontalScrollBar.addEventListener("scrolltrack", (Event event) {
6783 			horizontalScrollBar.setPosition(event.intValue);
6784 			sw.horizontalScrollTo(horizontalScrollBar.position);
6785 		});
6786 		verticalScrollBar.addEventListener("scrolltrack", (Event event) {
6787 			verticalScrollBar.setPosition(event.intValue);
6788 		});
6789 	}
6790 
6791 	// this is supposed to be basically invisible...
6792 	override int minWidth() { return sw.minWidth; }
6793 	override int minHeight() { return sw.minHeight; }
6794 	override int maxWidth() { return sw.maxWidth; }
6795 	override int maxHeight() { return sw.maxHeight; }
6796 	override int widthStretchiness() { return sw.widthStretchiness; }
6797 	override int heightStretchiness() { return sw.heightStretchiness; }
6798 	override int marginLeft() { return sw.marginLeft; }
6799 	override int marginRight() { return sw.marginRight; }
6800 	override int marginTop() { return sw.marginTop; }
6801 	override int marginBottom() { return sw.marginBottom; }
6802 	override int paddingLeft() { return sw.paddingLeft; }
6803 	override int paddingRight() { return sw.paddingRight; }
6804 	override int paddingTop() { return sw.paddingTop; }
6805 	override int paddingBottom() { return sw.paddingBottom; }
6806 	override void focus() { sw.focus(); }
6807 
6808 
6809 	override void recomputeChildLayout() {
6810 		// The stupid thing needs to calculate if a scroll bar is needed...
6811 		recomputeChildLayoutHelper();
6812 		// then running it again will position things correctly if the bar is NOT needed
6813 		recomputeChildLayoutHelper();
6814 
6815 		// this sucks but meh it barely works
6816 	}
6817 
6818 	private void recomputeChildLayoutHelper() {
6819 		if(sw is null) return;
6820 
6821 		bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll;
6822 		if(horizontalScrollBar && verticalScrollBar) {
6823 			horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0);
6824 			horizontalScrollBar.height = horizontalScrollBar.minHeight();
6825 			horizontalScrollBar.x = 0;
6826 			horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight();
6827 
6828 			verticalScrollBar.width = verticalScrollBar.minWidth();
6829 			verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2;
6830 			verticalScrollBar.x = this.width - verticalScrollBar.minWidth();
6831 			verticalScrollBar.y = 0 + 2;
6832 
6833 			sw.x = 0;
6834 			sw.y = 0;
6835 			sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0);
6836 			sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0);
6837 
6838 			if(sw.contentWidth_ <= this.width)
6839 				sw.scrollOrigin_.x = 0;
6840 			if(sw.contentHeight_ <= this.height)
6841 				sw.scrollOrigin_.y = 0;
6842 
6843 			horizontalScrollBar.recomputeChildLayout();
6844 			verticalScrollBar.recomputeChildLayout();
6845 			sw.recomputeChildLayout();
6846 		}
6847 
6848 		if(sw.contentWidth_ <= this.width)
6849 			sw.scrollOrigin_.x = 0;
6850 		if(sw.contentHeight_ <= this.height)
6851 			sw.scrollOrigin_.y = 0;
6852 
6853 		if(sw.showingHorizontalScroll())
6854 			horizontalScrollBar.showing(true, false);
6855 		else
6856 			horizontalScrollBar.showing(false, false);
6857 		if(sw.showingVerticalScroll())
6858 			verticalScrollBar.showing(true, false);
6859 		else
6860 			verticalScrollBar.showing(false, false);
6861 
6862 		verticalScrollBar.setViewableArea(sw.viewportHeight());
6863 		verticalScrollBar.setMax(sw.contentHeight);
6864 		verticalScrollBar.setPosition(sw.scrollOrigin.y);
6865 
6866 		horizontalScrollBar.setViewableArea(sw.viewportWidth());
6867 		horizontalScrollBar.setMax(sw.contentWidth);
6868 		horizontalScrollBar.setPosition(sw.scrollOrigin.x);
6869 	}
6870 }
6871 
6872 /*
6873 class ScrollableClientWidget : Widget {
6874 	this(Widget parent) {
6875 		super(parent);
6876 	}
6877 	override void paint(WidgetPainter p) {
6878 		parent.paint(p);
6879 	}
6880 }
6881 */
6882 
6883 /++
6884 	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.
6885 +/
6886 abstract class Slider : Widget {
6887 	this(int min, int max, int step, Widget parent) {
6888 		min_ = min;
6889 		max_ = max;
6890 		step_ = step;
6891 		page_ = step;
6892 		super(parent);
6893 	}
6894 
6895 	private int min_;
6896 	private int max_;
6897 	private int step_;
6898 	private int position_;
6899 	private int page_;
6900 
6901 	// selection start and selection end
6902 	// tics
6903 	// tooltip?
6904 	// some way to see and just type the value
6905 	// win32 buddy controls are labels
6906 
6907 	///
6908 	void setMin(int a) {
6909 		min_ = a;
6910 		version(custom_widgets)
6911 			redraw();
6912 		version(win32_widgets)
6913 			SendMessage(hwnd, TBM_SETRANGEMIN, true, a);
6914 	}
6915 	///
6916 	int min() {
6917 		return min_;
6918 	}
6919 	///
6920 	void setMax(int a) {
6921 		max_ = a;
6922 		version(custom_widgets)
6923 			redraw();
6924 		version(win32_widgets)
6925 			SendMessage(hwnd, TBM_SETRANGEMAX, true, a);
6926 	}
6927 	///
6928 	int max() {
6929 		return max_;
6930 	}
6931 	///
6932 	void setPosition(int a) {
6933 		if(a > max)
6934 			a = max;
6935 		if(a < min)
6936 			a = min;
6937 		position_ = a;
6938 		version(custom_widgets)
6939 			setPositionCustom(a);
6940 
6941 		version(win32_widgets)
6942 			setPositionWindows(a);
6943 	}
6944 	version(win32_widgets) {
6945 		protected abstract void setPositionWindows(int a);
6946 	}
6947 
6948 	protected abstract int win32direction();
6949 
6950 	/++
6951 		Alias for [position] for better compatibility with generic code.
6952 
6953 		History:
6954 			Added October 5, 2021
6955 	+/
6956 	@property int value() {
6957 		return position;
6958 	}
6959 
6960 	///
6961 	int position() {
6962 		return position_;
6963 	}
6964 	///
6965 	void setStep(int a) {
6966 		step_ = a;
6967 		version(win32_widgets)
6968 			SendMessage(hwnd, TBM_SETLINESIZE, 0, a);
6969 	}
6970 	///
6971 	int step() {
6972 		return step_;
6973 	}
6974 	///
6975 	void setPageSize(int a) {
6976 		page_ = a;
6977 		version(win32_widgets)
6978 			SendMessage(hwnd, TBM_SETPAGESIZE, 0, a);
6979 	}
6980 	///
6981 	int pageSize() {
6982 		return page_;
6983 	}
6984 
6985 	private void notify() {
6986 		auto event = new ChangeEvent!int(this, &this.position);
6987 		event.dispatch();
6988 	}
6989 
6990 	version(win32_widgets)
6991 	void win32Setup(int style) {
6992 		createWin32Window(this, TRACKBAR_CLASS, "",
6993 			0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0);
6994 
6995 		// the trackbar sends the same messages as scroll, which
6996 		// our other layer sends as these... just gonna translate
6997 		// here
6998 		this.addDirectEventListener("scrolltoposition", (Event event) {
6999 			event.stopPropagation();
7000 			this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue);
7001 			notify();
7002 		});
7003 		this.addDirectEventListener("scrolltonextline", (Event event) {
7004 			event.stopPropagation();
7005 			this.setPosition(this.position + this.step_ * this.win32direction);
7006 			notify();
7007 		});
7008 		this.addDirectEventListener("scrolltopreviousline", (Event event) {
7009 			event.stopPropagation();
7010 			this.setPosition(this.position - this.step_ * this.win32direction);
7011 			notify();
7012 		});
7013 		this.addDirectEventListener("scrolltonextpage", (Event event) {
7014 			event.stopPropagation();
7015 			this.setPosition(this.position + this.page_ * this.win32direction);
7016 			notify();
7017 		});
7018 		this.addDirectEventListener("scrolltopreviouspage", (Event event) {
7019 			event.stopPropagation();
7020 			this.setPosition(this.position - this.page_ * this.win32direction);
7021 			notify();
7022 		});
7023 
7024 		setMin(min_);
7025 		setMax(max_);
7026 		setStep(step_);
7027 		setPageSize(page_);
7028 	}
7029 
7030 	version(custom_widgets) {
7031 		protected MouseTrackingWidget thumb;
7032 
7033 		protected abstract void setPositionCustom(int a);
7034 
7035 		override void defaultEventHandler_keydown(KeyDownEvent event) {
7036 			switch(event.key) {
7037 				case Key.Up:
7038 				case Key.Right:
7039 					setPosition(position() - step() * win32direction);
7040 					changed();
7041 				break;
7042 				case Key.Down:
7043 				case Key.Left:
7044 					setPosition(position() + step() * win32direction);
7045 					changed();
7046 				break;
7047 				case Key.Home:
7048 					setPosition(win32direction > 0 ? min() : max());
7049 					changed();
7050 				break;
7051 				case Key.End:
7052 					setPosition(win32direction > 0 ? max() : min());
7053 					changed();
7054 				break;
7055 				case Key.PageUp:
7056 					setPosition(position() - pageSize() * win32direction);
7057 					changed();
7058 				break;
7059 				case Key.PageDown:
7060 					setPosition(position() + pageSize() * win32direction);
7061 					changed();
7062 				break;
7063 				default:
7064 			}
7065 			super.defaultEventHandler_keydown(event);
7066 		}
7067 
7068 		protected void changed() {
7069 			auto ev = new ChangeEvent!int(this, &position);
7070 			ev.dispatch();
7071 		}
7072 	}
7073 }
7074 
7075 /++
7076 
7077 +/
7078 class VerticalSlider : Slider {
7079 	this(int min, int max, int step, Widget parent) {
7080 		version(custom_widgets)
7081 			initialize();
7082 
7083 		super(min, max, step, parent);
7084 
7085 		version(win32_widgets)
7086 			win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */);
7087 	}
7088 
7089 	protected override int win32direction() {
7090 		return -1;
7091 	}
7092 
7093 	version(win32_widgets)
7094 	protected override void setPositionWindows(int a) {
7095 		// the windows thing makes the top 0 and i don't like that.
7096 		SendMessage(hwnd, TBM_SETPOS, true, max - a);
7097 	}
7098 
7099 	version(custom_widgets)
7100 	private void initialize() {
7101 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this);
7102 
7103 		thumb.tabStop = false;
7104 
7105 		thumb.thumbWidth = width;
7106 		thumb.thumbHeight = scaleWithDpi(16);
7107 
7108 		thumb.addEventListener(EventType.change, () {
7109 			auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16));
7110 			sx = max - sx;
7111 			//informProgramThatUserChangedPosition(sx);
7112 
7113 			position_ = sx;
7114 
7115 			changed();
7116 		});
7117 	}
7118 
7119 	version(custom_widgets)
7120 	override void recomputeChildLayout() {
7121 		thumb.thumbWidth = this.width;
7122 		super.recomputeChildLayout();
7123 		setPositionCustom(position_);
7124 	}
7125 
7126 	version(custom_widgets)
7127 	protected override void setPositionCustom(int a) {
7128 		if(max())
7129 			thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max();
7130 		redraw();
7131 	}
7132 }
7133 
7134 /++
7135 
7136 +/
7137 class HorizontalSlider : Slider {
7138 	this(int min, int max, int step, Widget parent) {
7139 		version(custom_widgets)
7140 			initialize();
7141 
7142 		super(min, max, step, parent);
7143 
7144 		version(win32_widgets)
7145 			win32Setup(TBS_HORZ);
7146 	}
7147 
7148 	version(win32_widgets)
7149 	protected override void setPositionWindows(int a) {
7150 		SendMessage(hwnd, TBM_SETPOS, true, a);
7151 	}
7152 
7153 	protected override int win32direction() {
7154 		return 1;
7155 	}
7156 
7157 	version(custom_widgets)
7158 	private void initialize() {
7159 		thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this);
7160 
7161 		thumb.tabStop = false;
7162 
7163 		thumb.thumbWidth = scaleWithDpi(16);
7164 		thumb.thumbHeight = height;
7165 
7166 		thumb.addEventListener(EventType.change, () {
7167 			auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16));
7168 			//informProgramThatUserChangedPosition(sx);
7169 
7170 			position_ = sx;
7171 
7172 			changed();
7173 		});
7174 	}
7175 
7176 	version(custom_widgets)
7177 	override void recomputeChildLayout() {
7178 		thumb.thumbHeight = this.height;
7179 		super.recomputeChildLayout();
7180 		setPositionCustom(position_);
7181 	}
7182 
7183 	version(custom_widgets)
7184 	protected override void setPositionCustom(int a) {
7185 		if(max())
7186 			thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max();
7187 		redraw();
7188 	}
7189 }
7190 
7191 
7192 ///
7193 abstract class ScrollbarBase : Widget {
7194 	///
7195 	this(Widget parent) {
7196 		super(parent);
7197 		tabStop = false;
7198 		step_ = scaleWithDpi(16);
7199 	}
7200 
7201 	private int viewableArea_;
7202 	private int max_;
7203 	private int step_;// = 16;
7204 	private int position_;
7205 
7206 	///
7207 	bool atEnd() {
7208 		return position_ + viewableArea_ >= max_;
7209 	}
7210 
7211 	///
7212 	bool atStart() {
7213 		return position_ == 0;
7214 	}
7215 
7216 	///
7217 	void setViewableArea(int a) {
7218 		viewableArea_ = a;
7219 		version(custom_widgets)
7220 			redraw();
7221 	}
7222 	///
7223 	void setMax(int a) {
7224 		max_ = a;
7225 		version(custom_widgets)
7226 			redraw();
7227 	}
7228 	///
7229 	int max() {
7230 		return max_;
7231 	}
7232 	///
7233 	void setPosition(int a) {
7234 		auto logicalMax = max_ - viewableArea_;
7235 		if(a == int.max)
7236 			a = logicalMax;
7237 
7238 		if(a > logicalMax)
7239 			a = logicalMax;
7240 		if(a < 0)
7241 			a = 0;
7242 
7243 		position_ = a;
7244 
7245 		version(custom_widgets)
7246 			redraw();
7247 	}
7248 	///
7249 	int position() {
7250 		return position_;
7251 	}
7252 	///
7253 	void setStep(int a) {
7254 		step_ = a;
7255 	}
7256 	///
7257 	int step() {
7258 		return step_;
7259 	}
7260 
7261 	// FIXME: remove this.... maybe
7262 	/+
7263 	protected void informProgramThatUserChangedPosition(int n) {
7264 		position_ = n;
7265 		auto evt = new Event(EventType.change, this);
7266 		evt.intValue = n;
7267 		evt.dispatch();
7268 	}
7269 	+/
7270 
7271 	version(custom_widgets) {
7272 		enum MIN_THUMB_SIZE = 8;
7273 
7274 		abstract protected int getBarDim();
7275 		int thumbSize() {
7276 			if(viewableArea_ >= max_ || max_ == 0)
7277 				return getBarDim();
7278 
7279 			int res = viewableArea_ * getBarDim() / max_;
7280 
7281 			if(res < scaleWithDpi(MIN_THUMB_SIZE))
7282 				res = scaleWithDpi(MIN_THUMB_SIZE);
7283 
7284 			return res;
7285 		}
7286 
7287 		int thumbPosition() {
7288 			/*
7289 				viewableArea_ is the viewport height/width
7290 				position_ is where we are
7291 			*/
7292 			//if(position_ + viewableArea_ >= max_)
7293 				//return getBarDim - thumbSize;
7294 
7295 			auto maximumPossibleValue = getBarDim() - thumbSize;
7296 			auto maximiumLogicalValue = max_ - viewableArea_;
7297 
7298 			auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0;
7299 
7300 			return p;
7301 		}
7302 	}
7303 }
7304 
7305 //public import mgt;
7306 
7307 /++
7308 	A mouse tracking widget is one that follows the mouse when dragged inside it.
7309 
7310 	Concrete subclasses may include a scrollbar thumb and a volume control.
7311 +/
7312 //version(custom_widgets)
7313 class MouseTrackingWidget : Widget {
7314 
7315 	///
7316 	int positionX() { return positionX_; }
7317 	///
7318 	int positionY() { return positionY_; }
7319 
7320 	///
7321 	void positionX(int p) { positionX_ = p; }
7322 	///
7323 	void positionY(int p) { positionY_ = p; }
7324 
7325 	private int positionX_;
7326 	private int positionY_;
7327 
7328 	///
7329 	enum Orientation {
7330 		horizontal, ///
7331 		vertical, ///
7332 		twoDimensional, ///
7333 	}
7334 
7335 	private int thumbWidth_;
7336 	private int thumbHeight_;
7337 
7338 	///
7339 	int thumbWidth() { return thumbWidth_; }
7340 	///
7341 	int thumbHeight() { return thumbHeight_; }
7342 	///
7343 	int thumbWidth(int a) { return thumbWidth_ = a; }
7344 	///
7345 	int thumbHeight(int a) { return thumbHeight_ = a; }
7346 
7347 	private bool dragging;
7348 	private bool hovering;
7349 	private int startMouseX, startMouseY;
7350 
7351 	///
7352 	this(Orientation orientation, Widget parent) {
7353 		super(parent);
7354 
7355 		//assert(parentWindow !is null);
7356 
7357 		addEventListener((MouseDownEvent event) {
7358 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
7359 				dragging = true;
7360 				startMouseX = event.clientX - positionX;
7361 				startMouseY = event.clientY - positionY;
7362 				parentWindow.captureMouse(this);
7363 			} else {
7364 				if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
7365 					positionX = event.clientX - thumbWidth / 2;
7366 				if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
7367 					positionY = event.clientY - thumbHeight / 2;
7368 
7369 				if(positionX + thumbWidth > this.width)
7370 					positionX = this.width - thumbWidth;
7371 				if(positionY + thumbHeight > this.height)
7372 					positionY = this.height - thumbHeight;
7373 
7374 				if(positionX < 0)
7375 					positionX = 0;
7376 				if(positionY < 0)
7377 					positionY = 0;
7378 
7379 
7380 				// this.emit!(ChangeEvent!void)();
7381 				auto evt = new Event(EventType.change, this);
7382 				evt.sendDirectly();
7383 
7384 				redraw();
7385 
7386 			}
7387 		});
7388 
7389 		addEventListener(EventType.mouseup, (Event event) {
7390 			dragging = false;
7391 			parentWindow.releaseMouseCapture();
7392 		});
7393 
7394 		addEventListener(EventType.mouseout, (Event event) {
7395 			if(!hovering)
7396 				return;
7397 			hovering = false;
7398 			redraw();
7399 		});
7400 
7401 		int lpx, lpy;
7402 
7403 		addEventListener((MouseMoveEvent event) {
7404 			auto oh = hovering;
7405 			if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
7406 				hovering = true;
7407 			} else {
7408 				hovering = false;
7409 			}
7410 			if(!dragging) {
7411 				if(hovering != oh)
7412 					redraw();
7413 				return;
7414 			}
7415 
7416 			if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional)
7417 				positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it
7418 			if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional)
7419 				positionY = event.clientY - startMouseY;
7420 
7421 			if(positionX + thumbWidth > this.width)
7422 				positionX = this.width - thumbWidth;
7423 			if(positionY + thumbHeight > this.height)
7424 				positionY = this.height - thumbHeight;
7425 
7426 			if(positionX < 0)
7427 				positionX = 0;
7428 			if(positionY < 0)
7429 				positionY = 0;
7430 
7431 			if(positionX != lpx || positionY != lpy) {
7432 				lpx = positionX;
7433 				lpy = positionY;
7434 
7435 				auto evt = new Event(EventType.change, this);
7436 				evt.sendDirectly();
7437 			}
7438 
7439 			redraw();
7440 		});
7441 	}
7442 
7443 	version(custom_widgets)
7444 	override void paint(WidgetPainter painter) {
7445 		auto cs = getComputedStyle();
7446 		auto c = darken(cs.windowBackgroundColor, 0.2);
7447 		painter.outlineColor = c;
7448 		painter.fillColor = c;
7449 		painter.drawRectangle(Point(0, 0), this.width, this.height);
7450 
7451 		auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor;
7452 		draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color);
7453 	}
7454 }
7455 
7456 //version(custom_widgets)
7457 //private
7458 class HorizontalScrollbar : ScrollbarBase {
7459 
7460 	version(custom_widgets) {
7461 		private MouseTrackingWidget thumb;
7462 
7463 		override int getBarDim() {
7464 			return thumb.width;
7465 		}
7466 	}
7467 
7468 	override void setViewableArea(int a) {
7469 		super.setViewableArea(a);
7470 
7471 		version(win32_widgets) {
7472 			SCROLLINFO info;
7473 			info.cbSize = info.sizeof;
7474 			info.nPage = a + 1;
7475 			info.fMask = SIF_PAGE;
7476 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7477 		} else version(custom_widgets) {
7478 			thumb.positionX = thumbPosition;
7479 			thumb.thumbWidth = thumbSize;
7480 			thumb.redraw();
7481 		} else static assert(0);
7482 
7483 	}
7484 
7485 	override void setMax(int a) {
7486 		super.setMax(a);
7487 		version(win32_widgets) {
7488 			SCROLLINFO info;
7489 			info.cbSize = info.sizeof;
7490 			info.nMin = 0;
7491 			info.nMax = max;
7492 			info.fMask = SIF_RANGE;
7493 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7494 		} else version(custom_widgets) {
7495 			thumb.positionX = thumbPosition;
7496 			thumb.thumbWidth = thumbSize;
7497 			thumb.redraw();
7498 		}
7499 	}
7500 
7501 	override void setPosition(int a) {
7502 		super.setPosition(a);
7503 		version(win32_widgets) {
7504 			SCROLLINFO info;
7505 			info.cbSize = info.sizeof;
7506 			info.fMask = SIF_POS;
7507 			info.nPos = position;
7508 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7509 		} else version(custom_widgets) {
7510 			thumb.positionX = thumbPosition();
7511 			thumb.thumbWidth = thumbSize;
7512 			thumb.redraw();
7513 		} else static assert(0);
7514 	}
7515 
7516 	this(Widget parent) {
7517 		super(parent);
7518 
7519 		version(win32_widgets) {
7520 			createWin32Window(this, "Scrollbar"w, "",
7521 				0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0);
7522 		} else version(custom_widgets) {
7523 			auto vl = new HorizontalLayout(this);
7524 			auto leftButton = new ArrowButton(ArrowDirection.left, vl);
7525 			leftButton.setClickRepeat(scrollClickRepeatInterval);
7526 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl);
7527 			auto rightButton = new ArrowButton(ArrowDirection.right, vl);
7528 			rightButton.setClickRepeat(scrollClickRepeatInterval);
7529 
7530 			leftButton.tabStop = false;
7531 			rightButton.tabStop = false;
7532 			thumb.tabStop = false;
7533 
7534 			leftButton.addEventListener(EventType.triggered, () {
7535 				this.emitCommand!"scrolltopreviousline"();
7536 				//informProgramThatUserChangedPosition(position - step());
7537 			});
7538 			rightButton.addEventListener(EventType.triggered, () {
7539 				this.emitCommand!"scrolltonextline"();
7540 				//informProgramThatUserChangedPosition(position + step());
7541 			});
7542 
7543 			thumb.thumbWidth = this.minWidth;
7544 			thumb.thumbHeight = scaleWithDpi(16);
7545 
7546 			thumb.addEventListener(EventType.change, () {
7547 				auto maximumPossibleValue = thumb.width - thumb.thumbWidth;
7548 				auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0;
7549 
7550 				//informProgramThatUserChangedPosition(sx);
7551 
7552 				auto ev = new ScrollToPositionEvent(this, sx);
7553 				ev.dispatch();
7554 			});
7555 		}
7556 	}
7557 
7558 	override int minHeight() { return scaleWithDpi(16); }
7559 	override int maxHeight() { return scaleWithDpi(16); }
7560 	override int minWidth() { return scaleWithDpi(48); }
7561 }
7562 
7563 final class ScrollToPositionEvent : Event {
7564 	enum EventString = "scrolltoposition";
7565 
7566 	this(Widget target, int value) {
7567 		this.value = value;
7568 		super(EventString, target);
7569 	}
7570 
7571 	immutable int value;
7572 
7573 	override @property int intValue() {
7574 		return value;
7575 	}
7576 }
7577 
7578 //version(custom_widgets)
7579 //private
7580 class VerticalScrollbar : ScrollbarBase {
7581 
7582 	version(custom_widgets) {
7583 		override int getBarDim() {
7584 			return thumb.height;
7585 		}
7586 
7587 		private MouseTrackingWidget thumb;
7588 	}
7589 
7590 	override void setViewableArea(int a) {
7591 		super.setViewableArea(a);
7592 
7593 		version(win32_widgets) {
7594 			SCROLLINFO info;
7595 			info.cbSize = info.sizeof;
7596 			info.nPage = a + 1;
7597 			info.fMask = SIF_PAGE;
7598 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7599 		} else version(custom_widgets) {
7600 			thumb.positionY = thumbPosition;
7601 			thumb.thumbHeight = thumbSize;
7602 			thumb.redraw();
7603 		} else static assert(0);
7604 
7605 	}
7606 
7607 	override void setMax(int a) {
7608 		super.setMax(a);
7609 		version(win32_widgets) {
7610 			SCROLLINFO info;
7611 			info.cbSize = info.sizeof;
7612 			info.nMin = 0;
7613 			info.nMax = max;
7614 			info.fMask = SIF_RANGE;
7615 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7616 		} else version(custom_widgets) {
7617 			thumb.positionY = thumbPosition;
7618 			thumb.thumbHeight = thumbSize;
7619 			thumb.redraw();
7620 		}
7621 	}
7622 
7623 	override void setPosition(int a) {
7624 		super.setPosition(a);
7625 		version(win32_widgets) {
7626 			SCROLLINFO info;
7627 			info.cbSize = info.sizeof;
7628 			info.fMask = SIF_POS;
7629 			info.nPos = position;
7630 			SetScrollInfo(hwnd, SB_CTL, &info, true);
7631 		} else version(custom_widgets) {
7632 			thumb.positionY = thumbPosition;
7633 			thumb.thumbHeight = thumbSize;
7634 			thumb.redraw();
7635 		} else static assert(0);
7636 	}
7637 
7638 	this(Widget parent) {
7639 		super(parent);
7640 
7641 		version(win32_widgets) {
7642 			createWin32Window(this, "Scrollbar"w, "",
7643 				0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0);
7644 		} else version(custom_widgets) {
7645 			auto vl = new VerticalLayout(this);
7646 			auto upButton = new ArrowButton(ArrowDirection.up, vl);
7647 			upButton.setClickRepeat(scrollClickRepeatInterval);
7648 			thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl);
7649 			auto downButton = new ArrowButton(ArrowDirection.down, vl);
7650 			downButton.setClickRepeat(scrollClickRepeatInterval);
7651 
7652 			upButton.addEventListener(EventType.triggered, () {
7653 				this.emitCommand!"scrolltopreviousline"();
7654 				//informProgramThatUserChangedPosition(position - step());
7655 			});
7656 			downButton.addEventListener(EventType.triggered, () {
7657 				this.emitCommand!"scrolltonextline"();
7658 				//informProgramThatUserChangedPosition(position + step());
7659 			});
7660 
7661 			thumb.thumbWidth = this.minWidth;
7662 			thumb.thumbHeight = scaleWithDpi(16);
7663 
7664 			thumb.addEventListener(EventType.change, () {
7665 				auto maximumPossibleValue = thumb.height - thumb.thumbHeight;
7666 				auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0;
7667 
7668 				auto ev = new ScrollToPositionEvent(this, sy);
7669 				ev.dispatch();
7670 
7671 				//informProgramThatUserChangedPosition(sy);
7672 			});
7673 
7674 			upButton.tabStop = false;
7675 			downButton.tabStop = false;
7676 			thumb.tabStop = false;
7677 		}
7678 	}
7679 
7680 	override int minWidth() { return scaleWithDpi(16); }
7681 	override int maxWidth() { return scaleWithDpi(16); }
7682 	override int minHeight() { return scaleWithDpi(48); }
7683 }
7684 
7685 
7686 /++
7687 	EXPERIMENTAL
7688 
7689 	A widget specialized for being a container for other widgets.
7690 
7691 	History:
7692 		Added May 29, 2021. Not stabilized at this time.
7693 +/
7694 class WidgetContainer : Widget {
7695 	this(Widget parent) {
7696 		tabStop = false;
7697 		super(parent);
7698 	}
7699 
7700 	override int maxHeight() {
7701 		if(this.children.length == 1) {
7702 			return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom);
7703 		} else {
7704 			return int.max;
7705 		}
7706 	}
7707 
7708 	override int maxWidth() {
7709 		if(this.children.length == 1) {
7710 			return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight);
7711 		} else {
7712 			return int.max;
7713 		}
7714 	}
7715 
7716 	/+
7717 
7718 	override int minHeight() {
7719 		int largest = 0;
7720 		int margins = 0;
7721 		int lastMargin = 0;
7722 		foreach(child; children) {
7723 			auto mh = child.minHeight();
7724 			if(mh > largest)
7725 				largest = mh;
7726 			margins += mymax(lastMargin, child.marginTop());
7727 			lastMargin = child.marginBottom();
7728 		}
7729 		return largest + margins;
7730 	}
7731 
7732 	override int maxHeight() {
7733 		int largest = 0;
7734 		int margins = 0;
7735 		int lastMargin = 0;
7736 		foreach(child; children) {
7737 			auto mh = child.maxHeight();
7738 			if(mh == int.max)
7739 				return int.max;
7740 			if(mh > largest)
7741 				largest = mh;
7742 			margins += mymax(lastMargin, child.marginTop());
7743 			lastMargin = child.marginBottom();
7744 		}
7745 		return largest + margins;
7746 	}
7747 
7748 	override int minWidth() {
7749 		int min;
7750 		foreach(child; children) {
7751 			auto cm = child.minWidth;
7752 			if(cm > min)
7753 				min = cm;
7754 		}
7755 		return min + paddingLeft + paddingRight;
7756 	}
7757 
7758 	override int minHeight() {
7759 		int min;
7760 		foreach(child; children) {
7761 			auto cm = child.minHeight;
7762 			if(cm > min)
7763 				min = cm;
7764 		}
7765 		return min + paddingTop + paddingBottom;
7766 	}
7767 
7768 	override int maxHeight() {
7769 		int largest = 0;
7770 		int margins = 0;
7771 		int lastMargin = 0;
7772 		foreach(child; children) {
7773 			auto mh = child.maxHeight();
7774 			if(mh == int.max)
7775 				return int.max;
7776 			if(mh > largest)
7777 				largest = mh;
7778 			margins += mymax(lastMargin, child.marginTop());
7779 			lastMargin = child.marginBottom();
7780 		}
7781 		return largest + margins;
7782 	}
7783 
7784 	override int heightStretchiness() {
7785 		int max;
7786 		foreach(child; children) {
7787 			auto c = child.heightStretchiness;
7788 			if(c > max)
7789 				max = c;
7790 		}
7791 		return max;
7792 	}
7793 
7794 	override int marginTop() {
7795 		if(this.children.length)
7796 			return this.children[0].marginTop;
7797 		return 0;
7798 	}
7799 	+/
7800 }
7801 
7802 ///
7803 abstract class Layout : Widget {
7804 	this(Widget parent) {
7805 		tabStop = false;
7806 		super(parent);
7807 	}
7808 }
7809 
7810 /++
7811 	Makes all children minimum width and height, placing them down
7812 	left to right, top to bottom.
7813 
7814 	Useful if you want to make a list of buttons that automatically
7815 	wrap to a new line when necessary.
7816 +/
7817 class InlineBlockLayout : Layout {
7818 	///
7819 	this(Widget parent) { super(parent); }
7820 
7821 	override void recomputeChildLayout() {
7822 		registerMovement();
7823 
7824 		int x = this.paddingLeft, y = this.paddingTop;
7825 
7826 		int lineHeight;
7827 		int previousMargin = 0;
7828 		int previousMarginBottom = 0;
7829 
7830 		foreach(child; children) {
7831 			if(child.hidden)
7832 				continue;
7833 			if(cast(FixedPosition) child) {
7834 				child.recomputeChildLayout();
7835 				continue;
7836 			}
7837 			child.width = child.flexBasisWidth();
7838 			if(child.width == 0)
7839 				child.width = child.minWidth();
7840 			if(child.width == 0)
7841 				child.width = 32;
7842 
7843 			child.height = child.flexBasisHeight();
7844 			if(child.height == 0)
7845 				child.height = child.minHeight();
7846 			if(child.height == 0)
7847 				child.height = 32;
7848 
7849 			if(x + child.width + paddingRight > this.width) {
7850 				x = this.paddingLeft;
7851 				y += lineHeight;
7852 				lineHeight = 0;
7853 				previousMargin = 0;
7854 				previousMarginBottom = 0;
7855 			}
7856 
7857 			auto margin = child.marginLeft;
7858 			if(previousMargin > margin)
7859 				margin = previousMargin;
7860 
7861 			x += margin;
7862 
7863 			child.x = x;
7864 			child.y = y;
7865 
7866 			int marginTopApplied;
7867 			if(child.marginTop > previousMarginBottom) {
7868 				child.y += child.marginTop;
7869 				marginTopApplied = child.marginTop;
7870 			}
7871 
7872 			x += child.width;
7873 			previousMargin = child.marginRight;
7874 
7875 			if(child.marginBottom > previousMarginBottom)
7876 				previousMarginBottom = child.marginBottom;
7877 
7878 			auto h = child.height + previousMarginBottom + marginTopApplied;
7879 			if(h > lineHeight)
7880 				lineHeight = h;
7881 
7882 			child.recomputeChildLayout();
7883 		}
7884 
7885 	}
7886 
7887 	override int minWidth() {
7888 		int min;
7889 		foreach(child; children) {
7890 			auto cm = child.minWidth;
7891 			if(cm > min)
7892 				min = cm;
7893 		}
7894 		return min + paddingLeft + paddingRight;
7895 	}
7896 
7897 	override int minHeight() {
7898 		int min;
7899 		foreach(child; children) {
7900 			auto cm = child.minHeight;
7901 			if(cm > min)
7902 				min = cm;
7903 		}
7904 		return min + paddingTop + paddingBottom;
7905 	}
7906 }
7907 
7908 /++
7909 	A TabMessageWidget is a clickable row of tabs followed by a content area, very similar
7910 	to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas
7911 	the [TabWidget] will automatically change pages of child widgets.
7912 
7913 	This allows you to react to it however you see fit rather than having to
7914 	be tied to just the new sets of child widgets.
7915 
7916 	It sends the message in the form of `this.emitCommand!"changetab"();`.
7917 
7918 	History:
7919 		Added December 24, 2021 (dub v10.5)
7920 +/
7921 class TabMessageWidget : Widget {
7922 
7923 	protected void tabIndexClicked(int item) {
7924 		this.emitCommand!"changetab"();
7925 	}
7926 
7927 	/++
7928 		Adds the a new tab to the control with the given title.
7929 
7930 		Returns:
7931 			The index of the newly added tab. You will need to know
7932 			this index to refer to it later and to know which tab to
7933 			change to when you get a changetab message.
7934 	+/
7935 	int addTab(string title, int pos = int.max) {
7936 		version(win32_widgets) {
7937 			TCITEM item;
7938 			item.mask = TCIF_TEXT;
7939 			WCharzBuffer buf = WCharzBuffer(title);
7940 			item.pszText = buf.ptr;
7941 			return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item);
7942 		} else version(custom_widgets) {
7943 			if(pos >= tabs.length) {
7944 				tabs ~= title;
7945 				redraw();
7946 				return cast(int) tabs.length - 1;
7947 			} else if(pos <= 0) {
7948 				tabs = title ~ tabs;
7949 				redraw();
7950 				return 0;
7951 			} else {
7952 				tabs = tabs[0 .. pos] ~ title ~ title[pos .. $];
7953 				redraw();
7954 				return pos;
7955 			}
7956 		}
7957 	}
7958 
7959 	override void addChild(Widget child, int pos = int.max) {
7960 		if(container)
7961 			container.addChild(child, pos);
7962 		else
7963 			super.addChild(child, pos);
7964 	}
7965 
7966 	protected Widget makeContainer() {
7967 		return new Widget(this);
7968 	}
7969 
7970 	private Widget container;
7971 
7972 	override void recomputeChildLayout() {
7973 		version(win32_widgets) {
7974 			this.registerMovement();
7975 
7976 			RECT rect;
7977 			GetWindowRect(hwnd, &rect);
7978 
7979 			auto left = rect.left;
7980 			auto top = rect.top;
7981 
7982 			TabCtrl_AdjustRect(hwnd, false, &rect);
7983 			foreach(child; children) {
7984 				if(!child.showing) continue;
7985 				child.x = rect.left - left;
7986 				child.y = rect.top - top;
7987 				child.width = rect.right - rect.left;
7988 				child.height = rect.bottom - rect.top;
7989 				child.recomputeChildLayout();
7990 			}
7991 		} else version(custom_widgets) {
7992 			this.registerMovement();
7993 			foreach(child; children) {
7994 				if(!child.showing) continue;
7995 				child.x = 2;
7996 				child.y = tabBarHeight + 2; // for the border
7997 				child.width = width - 4; // for the border
7998 				child.height = height - tabBarHeight - 2 - 2; // for the border
7999 				child.recomputeChildLayout();
8000 			}
8001 		} else static assert(0);
8002 	}
8003 
8004 	this(Widget parent) {
8005 		super(parent);
8006 
8007 		tabStop = false;
8008 
8009 		version(win32_widgets) {
8010 			createWin32Window(this, WC_TABCONTROL, "", 0);
8011 		} else version(custom_widgets) {
8012 			addEventListener((ClickEvent event) {
8013 				if(event.target !is this)
8014 					return;
8015 				if(event.clientY >= 0 && event.clientY < tabBarHeight) {
8016 					auto t = (event.clientX / tabWidth);
8017 					if(t >= 0 && t < tabs.length) {
8018 						currentTab_ = t;
8019 						tabIndexClicked(t);
8020 						redraw();
8021 					}
8022 				}
8023 			});
8024 		} else static assert(0);
8025 
8026 		this.container = makeContainer();
8027 	}
8028 
8029 	override int marginTop() { return 4; }
8030 	override int paddingBottom() { return 4; }
8031 
8032 	override int minHeight() {
8033 		int max = 0;
8034 		foreach(child; children)
8035 			max = mymax(child.minHeight, max);
8036 
8037 
8038 		version(win32_widgets) {
8039 			RECT rect;
8040 			rect.right = this.width;
8041 			rect.bottom = max;
8042 			TabCtrl_AdjustRect(hwnd, true, &rect);
8043 
8044 			max = rect.bottom;
8045 		} else {
8046 			max += defaultLineHeight + 4;
8047 		}
8048 
8049 
8050 		return max;
8051 	}
8052 
8053 	version(win32_widgets)
8054 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
8055 		switch(code) {
8056 			case TCN_SELCHANGE:
8057 				auto sel = TabCtrl_GetCurSel(hwnd);
8058 				tabIndexClicked(sel);
8059 			break;
8060 			default:
8061 		}
8062 		return 0;
8063 	}
8064 
8065 	version(custom_widgets) {
8066 		private int currentTab_;
8067 		private int tabBarHeight() { return defaultLineHeight; }
8068 		int tabWidth() { return scaleWithDpi(80); }
8069 
8070 		string[] tabs;
8071 	}
8072 
8073 	version(win32_widgets)
8074 	override void paint(WidgetPainter painter) {}
8075 
8076 	version(custom_widgets)
8077 	override void paint(WidgetPainter painter) {
8078 		auto cs = getComputedStyle();
8079 
8080 		draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color);
8081 
8082 		int posX = 0;
8083 		foreach(idx, title; tabs) {
8084 			auto isCurrent = idx == getCurrentTab();
8085 
8086 			painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight);
8087 
8088 			draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1));
8089 			painter.outlineColor = cs.foregroundColor;
8090 			painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter);
8091 
8092 			if(isCurrent) {
8093 				painter.outlineColor = cs.windowBackgroundColor;
8094 				painter.fillColor = Color.transparent;
8095 				painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1));
8096 				painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2));
8097 
8098 				painter.outlineColor = Color.white;
8099 				painter.drawPixel(Point(posX + 1, tabBarHeight - 1));
8100 				painter.drawPixel(Point(posX + 1, tabBarHeight - 2));
8101 				painter.outlineColor = cs.activeTabColor;
8102 				painter.drawPixel(Point(posX, tabBarHeight - 1));
8103 			}
8104 
8105 			posX += tabWidth - 2;
8106 		}
8107 	}
8108 
8109 	///
8110 	@scriptable
8111 	void setCurrentTab(int item) {
8112 		version(win32_widgets)
8113 			TabCtrl_SetCurSel(hwnd, item);
8114 		else version(custom_widgets)
8115 			currentTab_ = item;
8116 		else static assert(0);
8117 
8118 		tabIndexClicked(item);
8119 	}
8120 
8121 	///
8122 	@scriptable
8123 	int getCurrentTab() {
8124 		version(win32_widgets)
8125 			return TabCtrl_GetCurSel(hwnd);
8126 		else version(custom_widgets)
8127 			return currentTab_; // FIXME
8128 		else static assert(0);
8129 	}
8130 
8131 	///
8132 	@scriptable
8133 	void removeTab(int item) {
8134 		if(item && item == getCurrentTab())
8135 			setCurrentTab(item - 1);
8136 
8137 		version(win32_widgets) {
8138 			TabCtrl_DeleteItem(hwnd, item);
8139 		}
8140 
8141 		for(int a = item; a < children.length - 1; a++)
8142 			this._children[a] = this._children[a + 1];
8143 		this._children = this._children[0 .. $-1];
8144 	}
8145 
8146 }
8147 
8148 
8149 /++
8150 	A tab widget is a set of clickable tab buttons followed by a content area.
8151 
8152 
8153 	Tabs can change existing content or can be new pages.
8154 
8155 	When the user picks a different tab, a `change` message is generated.
8156 +/
8157 class TabWidget : TabMessageWidget {
8158 	this(Widget parent) {
8159 		super(parent);
8160 	}
8161 
8162 	override protected Widget makeContainer() {
8163 		return null;
8164 	}
8165 
8166 	override void addChild(Widget child, int pos = int.max) {
8167 		if(auto twp = cast(TabWidgetPage) child) {
8168 			Widget.addChild(child, pos);
8169 			if(pos == int.max)
8170 				pos = cast(int) this.children.length - 1;
8171 
8172 			super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop...
8173 
8174 			if(pos != getCurrentTab) {
8175 				child.showing = false;
8176 			}
8177 		} else {
8178 			assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)");
8179 		}
8180 	}
8181 
8182 	// FIXME: add tab icons at some point, Windows supports them
8183 	/++
8184 		Adds a page and its associated tab with the given label to the widget.
8185 
8186 		Returns:
8187 			The added page object, to which you can add other widgets.
8188 	+/
8189 	@scriptable
8190 	TabWidgetPage addPage(string title) {
8191 		return new TabWidgetPage(title, this);
8192 	}
8193 
8194 	/++
8195 		Gets the page at the given tab index, or `null` if the index is bad.
8196 
8197 		History:
8198 			Added December 24, 2021.
8199 	+/
8200 	TabWidgetPage getPage(int index) {
8201 		if(index < this.children.length)
8202 			return null;
8203 		return cast(TabWidgetPage) this.children[index];
8204 	}
8205 
8206 	/++
8207 		While you can still use the addTab from the parent class,
8208 		*strongly* recommend you use [addPage] insteaad.
8209 
8210 		History:
8211 			Added December 24, 2021 to fulful the interface
8212 			requirement that came from adding [TabMessageWidget].
8213 
8214 			You should not use it though since the [addPage] function
8215 			is much easier to use here.
8216 	+/
8217 	override int addTab(string title, int pos = int.max) {
8218 		auto p = addPage(title);
8219 		foreach(idx, child; this.children)
8220 			if(child is p)
8221 				return cast(int) idx;
8222 		return -1;
8223 	}
8224 
8225 	protected override void tabIndexClicked(int item) {
8226 		foreach(idx, child; children) {
8227 			child.showing(false, false); // batch the recalculates for the end
8228 		}
8229 
8230 		foreach(idx, child; children) {
8231 			if(idx == item) {
8232 				child.showing(true, false);
8233 				if(parentWindow) {
8234 					auto f = parentWindow.getFirstFocusable(child);
8235 					if(f)
8236 						f.focus();
8237 				}
8238 				recomputeChildLayout();
8239 			}
8240 		}
8241 
8242 		version(win32_widgets) {
8243 			InvalidateRect(hwnd, null, true);
8244 		} else version(custom_widgets) {
8245 			this.redraw();
8246 		}
8247 	}
8248 
8249 }
8250 
8251 /++
8252 	A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget".
8253 
8254 	You add [TabWidgetPage]s to it.
8255 +/
8256 class PageWidget : Widget {
8257 	this(Widget parent) {
8258 		super(parent);
8259 	}
8260 
8261 	override int minHeight() {
8262 		int max = 0;
8263 		foreach(child; children)
8264 			max = mymax(child.minHeight, max);
8265 
8266 		return max;
8267 	}
8268 
8269 
8270 	override void addChild(Widget child, int pos = int.max) {
8271 		if(auto twp = cast(TabWidgetPage) child) {
8272 			super.addChild(child, pos);
8273 			if(pos == int.max)
8274 				pos = cast(int) this.children.length - 1;
8275 
8276 			if(pos != getCurrentTab) {
8277 				child.showing = false;
8278 			}
8279 		} else {
8280 			assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)");
8281 		}
8282 	}
8283 
8284 	override void recomputeChildLayout() {
8285 		this.registerMovement();
8286 		foreach(child; children) {
8287 			child.x = 0;
8288 			child.y = 0;
8289 			child.width = width;
8290 			child.height = height;
8291 			child.recomputeChildLayout();
8292 		}
8293 	}
8294 
8295 	private int currentTab_;
8296 
8297 	///
8298 	@scriptable
8299 	void setCurrentTab(int item) {
8300 		currentTab_ = item;
8301 
8302 		showOnly(item);
8303 	}
8304 
8305 	///
8306 	@scriptable
8307 	int getCurrentTab() {
8308 		return currentTab_;
8309 	}
8310 
8311 	///
8312 	@scriptable
8313 	void removeTab(int item) {
8314 		if(item && item == getCurrentTab())
8315 			setCurrentTab(item - 1);
8316 
8317 		for(int a = item; a < children.length - 1; a++)
8318 			this._children[a] = this._children[a + 1];
8319 		this._children = this._children[0 .. $-1];
8320 	}
8321 
8322 	///
8323 	@scriptable
8324 	TabWidgetPage addPage(string title) {
8325 		return new TabWidgetPage(title, this);
8326 	}
8327 
8328 	private void showOnly(int item) {
8329 		foreach(idx, child; children)
8330 			if(idx == item) {
8331 				child.show();
8332 				child.queueRecomputeChildLayout();
8333 			} else {
8334 				child.hide();
8335 			}
8336 	}
8337 }
8338 
8339 /++
8340 
8341 +/
8342 class TabWidgetPage : Widget {
8343 	this(string title, Widget parent) {
8344 		this.title_ = title;
8345 		this.tabStop = false;
8346 		super(parent);
8347 
8348 		///*
8349 		version(win32_widgets) {
8350 			createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0);
8351 		}
8352 		//*/
8353 	}
8354 
8355 	private string title_;
8356 
8357 	/++
8358 		History:
8359 			Prior to April 6, 2025, it was a public field. It was changed to properties so it can queue redraws;
8360 	+/
8361 	string title() {
8362 		return title_;
8363 	}
8364 
8365 	/// ditto
8366 	void title(string t) {
8367 		title_ = t;
8368 		version(custom_widgets) {
8369 			if(auto tw = cast(TabWidget) parent) {
8370 				foreach(idx, child; tw.children)
8371 					if(child is this)
8372 						tw.tabs[idx] = t;
8373 				tw.redraw();
8374 			}
8375 		}
8376 	}
8377 
8378 	override int minHeight() {
8379 		int sum = 0;
8380 		foreach(child; children)
8381 			sum += child.minHeight();
8382 		return sum;
8383 	}
8384 }
8385 
8386 version(none)
8387 /++
8388 	A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button.
8389 
8390 	I think I need to modify the layout algorithms to support this.
8391 +/
8392 class CollapsableSidebar : Widget {
8393 
8394 }
8395 
8396 /// Stacks the widgets vertically, taking all the available width for each child.
8397 class VerticalLayout : Layout {
8398 	// most of this is intentionally blank - widget's default is vertical layout right now
8399 	///
8400 	this(Widget parent) { super(parent); }
8401 
8402 	/++
8403 		Sets a max width for the layout so you don't have to subclass. The max width
8404 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
8405 
8406 		History:
8407 			Added November 29, 2021 (dub v10.5)
8408 	+/
8409 	this(int maxWidth, Widget parent) {
8410 		this.mw = maxWidth;
8411 		super(parent);
8412 	}
8413 
8414 	private int mw = int.max;
8415 
8416 	override int maxWidth() { return scaleWithDpi(mw); }
8417 }
8418 
8419 /// Stacks the widgets horizontally, taking all the available height for each child.
8420 class HorizontalLayout : Layout {
8421 	///
8422 	this(Widget parent) { super(parent); }
8423 
8424 	/++
8425 		Sets a max height for the layout so you don't have to subclass. The max height
8426 		is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
8427 
8428 		History:
8429 			Added November 29, 2021 (dub v10.5)
8430 	+/
8431 	this(int maxHeight, Widget parent) {
8432 		this.mh = maxHeight;
8433 		super(parent);
8434 	}
8435 
8436 	private int mh = 0;
8437 
8438 
8439 
8440 	override void recomputeChildLayout() {
8441 		.recomputeChildLayout!"width"(this);
8442 	}
8443 
8444 	override int minHeight() {
8445 		int largest = 0;
8446 		int margins = 0;
8447 		int lastMargin = 0;
8448 		foreach(child; children) {
8449 			auto mh = child.minHeight();
8450 			if(mh > largest)
8451 				largest = mh;
8452 			margins += mymax(lastMargin, child.marginTop());
8453 			lastMargin = child.marginBottom();
8454 		}
8455 		return largest + margins;
8456 	}
8457 
8458 	override int maxHeight() {
8459 		if(mh != 0)
8460 			return mymax(minHeight, scaleWithDpi(mh));
8461 
8462 		int largest = 0;
8463 		int margins = 0;
8464 		int lastMargin = 0;
8465 		foreach(child; children) {
8466 			auto mh = child.maxHeight();
8467 			if(mh == int.max)
8468 				return int.max;
8469 			if(mh > largest)
8470 				largest = mh;
8471 			margins += mymax(lastMargin, child.marginTop());
8472 			lastMargin = child.marginBottom();
8473 		}
8474 		return largest + margins;
8475 	}
8476 
8477 	override int heightStretchiness() {
8478 		int max;
8479 		foreach(child; children) {
8480 			auto c = child.heightStretchiness;
8481 			if(c > max)
8482 				max = c;
8483 		}
8484 		return max;
8485 	}
8486 }
8487 
8488 version(win32_widgets)
8489 private
8490 extern(Windows)
8491 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow {
8492 	Widget* pwin = hwnd in Widget.nativeMapping;
8493 	if(pwin is null)
8494 		return DefWindowProc(hwnd, message, wparam, lparam);
8495 	SimpleWindow win = pwin.simpleWindowWrappingHwnd;
8496 	if(win is null)
8497 		return DefWindowProc(hwnd, message, wparam, lparam);
8498 
8499 	switch(message) {
8500 		case WM_SIZE:
8501 			auto width = LOWORD(lparam);
8502 			auto height = HIWORD(lparam);
8503 
8504 			auto hdc = GetDC(hwnd);
8505 			auto hdcBmp = CreateCompatibleDC(hdc);
8506 
8507 			// FIXME: could this be more efficient? it never relinquishes a large bitmap
8508 			if(width > win.bmpWidth || height > win.bmpHeight) {
8509 				auto oldBuffer = win.buffer;
8510 				win.buffer = CreateCompatibleBitmap(hdc, width, height);
8511 
8512 				if(oldBuffer)
8513 					DeleteObject(oldBuffer);
8514 
8515 				win.bmpWidth = width;
8516 				win.bmpHeight = height;
8517 			}
8518 
8519 			// just always erase it upon resizing so minigui can draw over with a clean slate
8520 			auto oldBmp = SelectObject(hdcBmp, win.buffer);
8521 
8522 			auto brush = GetSysColorBrush(COLOR_3DFACE);
8523 			RECT r;
8524 			r.left = 0;
8525 			r.top = 0;
8526 			r.right = width;
8527 			r.bottom = height;
8528 			FillRect(hdcBmp, &r, brush);
8529 
8530 			SelectObject(hdcBmp, oldBmp);
8531 			DeleteDC(hdcBmp);
8532 			ReleaseDC(hwnd, hdc);
8533 		break;
8534 		case WM_PAINT:
8535 			if(win.buffer is null)
8536 				goto default;
8537 
8538 			BITMAP bm;
8539 			PAINTSTRUCT ps;
8540 
8541 			HDC hdc = BeginPaint(hwnd, &ps);
8542 
8543 			HDC hdcMem = CreateCompatibleDC(hdc);
8544 			HBITMAP hbmOld = SelectObject(hdcMem, win.buffer);
8545 
8546 			GetObject(win.buffer, bm.sizeof, &bm);
8547 
8548 			BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
8549 
8550 			SelectObject(hdcMem, hbmOld);
8551 			DeleteDC(hdcMem);
8552 			EndPaint(hwnd, &ps);
8553 		break;
8554 		default:
8555 			return DefWindowProc(hwnd, message, wparam, lparam);
8556 	}
8557 
8558 	return 0;
8559 }
8560 
8561 private wstring Win32Class(wstring name)() {
8562 	static bool classRegistered;
8563 	if(!classRegistered) {
8564 		HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null);
8565 		WNDCLASSEX wc;
8566 		wc.cbSize = wc.sizeof;
8567 		wc.hInstance = hInstance;
8568 		wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH);
8569 		wc.lpfnWndProc = &DoubleBufferWndProc;
8570 		wc.lpszClassName = name.ptr;
8571 		if(!RegisterClassExW(&wc))
8572 			throw new Exception("RegisterClass ");// ~ to!string(GetLastError()));
8573 		classRegistered = true;
8574 	}
8575 
8576 		return name;
8577 }
8578 
8579 /+
8580 version(win32_widgets)
8581 extern(Windows)
8582 private
8583 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
8584 	switch(iMessage) {
8585 		case WM_PAINT:
8586 			if(auto te = hWnd in Widget.nativeMapping) {
8587 				try {
8588 					//te.redraw();
8589 					writeln(te, " drawing");
8590 				} catch(Exception) {}
8591 			}
8592 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
8593 		default:
8594 			return DefWindowProc(hWnd, iMessage, wParam, lParam);
8595 	}
8596 }
8597 +/
8598 
8599 
8600 /++
8601 	A widget specifically designed to hold other widgets.
8602 
8603 	History:
8604 		Added July 1, 2021
8605 +/
8606 class ContainerWidget : Widget {
8607 	this(Widget parent) {
8608 		super(parent);
8609 		this.tabStop = false;
8610 
8611 		version(win32_widgets) {
8612 			createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0);
8613 		}
8614 	}
8615 }
8616 
8617 /++
8618 	A widget that takes your widget, puts scroll bars around it, and sends
8619 	messages to it when the user scrolls. Unlike [ScrollableWidget], it makes
8620 	no effort to automatically scroll or clip its child widgets - it just sends
8621 	the messages.
8622 
8623 
8624 	A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed.
8625 	The scroll coordinates are all given in a unit you interpret as you wish. One
8626 	of these units is moved on each press of the arrow buttons and represents the
8627 	smallest amount the user can scroll. The intention is for this to be one line,
8628 	one item in a list, one row in a table, etc. Whatever makes sense for your widget
8629 	in each direction that the user might be interested in.
8630 
8631 	You can set a "page size" with the [step] property. (Yes, I regret the name...)
8632 	This is the amount it jumps when the user pressed page up and page down, or clicks
8633 	in the exposed part of the scroll bar.
8634 
8635 	You should add child content to the ScrollMessageWidget. However, it is important to
8636 	note that the coordinates are always independent of the scroll position! It is YOUR
8637 	responsibility to do any necessary transforms, clipping, etc., while drawing the
8638 	content and interpreting mouse events if they are supposed to change with the scroll.
8639 	This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries
8640 	to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives
8641 	you more control (which can be considerably more efficient and adapted to your actual data)
8642 	at the expense of you also needing to be aware of its reality.
8643 
8644 	Please note that it does NOT react to mouse wheel events or various keyboard events as of
8645 	version 10.3. Maybe this will change in the future.... but for now you must call
8646 	[addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself.
8647 +/
8648 class ScrollMessageWidget : Widget {
8649 	this(Widget parent) {
8650 		super(parent);
8651 
8652 		container = new Widget(this);
8653 		hsb = new HorizontalScrollbar(this);
8654 		vsb = new VerticalScrollbar(this);
8655 
8656 		hsb.addEventListener("scrolltonextline", {
8657 			hsb.setPosition(hsb.position + movementPerButtonClickH_);
8658 			notify();
8659 		});
8660 		hsb.addEventListener("scrolltopreviousline", {
8661 			hsb.setPosition(hsb.position - movementPerButtonClickH_);
8662 			notify();
8663 		});
8664 		vsb.addEventListener("scrolltonextline", {
8665 			vsb.setPosition(vsb.position + movementPerButtonClickV_);
8666 			notify();
8667 		});
8668 		vsb.addEventListener("scrolltopreviousline", {
8669 			vsb.setPosition(vsb.position - movementPerButtonClickV_);
8670 			notify();
8671 		});
8672 		hsb.addEventListener("scrolltonextpage", {
8673 			hsb.setPosition(hsb.position + hsb.step_);
8674 			notify();
8675 		});
8676 		hsb.addEventListener("scrolltopreviouspage", {
8677 			hsb.setPosition(hsb.position - hsb.step_);
8678 			notify();
8679 		});
8680 		vsb.addEventListener("scrolltonextpage", {
8681 			vsb.setPosition(vsb.position + vsb.step_);
8682 			notify();
8683 		});
8684 		vsb.addEventListener("scrolltopreviouspage", {
8685 			vsb.setPosition(vsb.position - vsb.step_);
8686 			notify();
8687 		});
8688 		hsb.addEventListener("scrolltoposition", (Event event) {
8689 			hsb.setPosition(event.intValue);
8690 			notify();
8691 		});
8692 		vsb.addEventListener("scrolltoposition", (Event event) {
8693 			vsb.setPosition(event.intValue);
8694 			notify();
8695 		});
8696 
8697 
8698 		tabStop = false;
8699 		container.tabStop = false;
8700 		magic = true;
8701 	}
8702 
8703 	private int movementPerButtonClickH_ = 1;
8704 	private int movementPerButtonClickV_ = 1;
8705 	public void movementPerButtonClick(int h, int v) {
8706 		movementPerButtonClickH_ = h;
8707 		movementPerButtonClickV_ = v;
8708 	}
8709 
8710 	/++
8711 		Add default event listeners for keyboard and mouse wheel scrolling shortcuts.
8712 
8713 
8714 		The defaults for [addDefaultWheelListeners] are:
8715 
8716 			$(LIST
8717 				* Mouse wheel scrolls vertically
8718 				* Alt key + mouse wheel scrolls horiontally
8719 				* Shift + mouse wheel scrolls faster.
8720 				* Any mouse click or wheel event will focus the inner widget if it has `tabStop = true`
8721 			)
8722 
8723 		The defaults for [addDefaultKeyboardListeners] are:
8724 
8725 			$(LIST
8726 				* Arrow keys scroll by the given amounts
8727 				* Shift+arrow keys scroll by the given amounts times the given shiftMultiplier
8728 				* Page up and down scroll by the vertical viewable area
8729 				* Home and end scroll to the start and end of the verticle viewable area.
8730 				* Alt + page up / page down / home / end will horizonally scroll instead of vertical.
8731 			)
8732 
8733 		My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line.
8734 
8735 		Params:
8736 			horizontalArrowScrollAmount =
8737 			verticalArrowScrollAmount =
8738 			verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel
8739 			horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel
8740 			shiftMultiplier = multiplies the scroll amount by this when shift is held
8741 	+/
8742 	void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) {
8743 		defaultKeyboardListener_verticalArrowScrollAmount = verticalArrowScrollAmount;
8744 		defaultKeyboardListener_horizontalArrowScrollAmount = horizontalArrowScrollAmount;
8745 		defaultKeyboardListener_shiftMultiplier = shiftMultiplier;
8746 
8747 		container.addEventListener(&defaultKeyboardListener);
8748 	}
8749 
8750 	/// ditto
8751 	void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) {
8752 		auto _this = this;
8753 		container.addEventListener((scope ClickEvent ce) {
8754 
8755 			//if(ce.target && ce.target.tabStop)
8756 				//ce.target.focus();
8757 
8758 			// ctrl is reserved for the application
8759 			if(ce.ctrlKey)
8760 				return;
8761 
8762 			if(horizontalWheelScrollAmount == 0 && ce.altKey)
8763 				return;
8764 
8765 			if(shiftMultiplier == 0 && ce.shiftKey)
8766 				return;
8767 
8768 			if(ce.button == MouseButton.wheelDown) {
8769 				if(ce.altKey)
8770 					_this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8771 				else
8772 					_this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8773 			} else if(ce.button == MouseButton.wheelUp) {
8774 				if(ce.altKey)
8775 					_this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8776 				else
8777 					_this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1));
8778 			}
8779 		});
8780 	}
8781 
8782 	int defaultKeyboardListener_verticalArrowScrollAmount = 1;
8783 	int defaultKeyboardListener_horizontalArrowScrollAmount = 1;
8784 	int defaultKeyboardListener_shiftMultiplier = 3;
8785 
8786 	void defaultKeyboardListener(scope KeyDownEvent ke) {
8787 		switch(ke.key) {
8788 			case Key.Left:
8789 				this.scrollLeft(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8790 			break;
8791 			case Key.Right:
8792 				this.scrollRight(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8793 			break;
8794 			case Key.Up:
8795 				this.scrollUp(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8796 			break;
8797 			case Key.Down:
8798 				this.scrollDown(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8799 			break;
8800 			case Key.PageUp:
8801 				if(ke.altKey)
8802 					this.scrollLeft(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8803 				else
8804 					this.scrollUp(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8805 			break;
8806 			case Key.PageDown:
8807 				if(ke.altKey)
8808 					this.scrollRight(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8809 				else
8810 					this.scrollDown(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1));
8811 			break;
8812 			case Key.Home:
8813 				if(ke.altKey)
8814 					this.scrollLeft(short.max * 16);
8815 				else
8816 					this.scrollUp(short.max * 16);
8817 			break;
8818 			case Key.End:
8819 				if(ke.altKey)
8820 					this.scrollRight(short.max * 16);
8821 				else
8822 					this.scrollDown(short.max * 16);
8823 			break;
8824 
8825 			default:
8826 				// ignore, not for us.
8827 		}
8828 	}
8829 
8830 	/++
8831 		Scrolls the given amount.
8832 
8833 		History:
8834 			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.
8835 	+/
8836 	void scrollUp(int amount = 1) {
8837 		vsb.setPosition(vsb.position.NonOverflowingInt - amount);
8838 		notify();
8839 	}
8840 	/// ditto
8841 	void scrollDown(int amount = 1) {
8842 		vsb.setPosition(vsb.position.NonOverflowingInt + amount);
8843 		notify();
8844 	}
8845 	/// ditto
8846 	void scrollLeft(int amount = 1) {
8847 		hsb.setPosition(hsb.position.NonOverflowingInt - amount);
8848 		notify();
8849 	}
8850 	/// ditto
8851 	void scrollRight(int amount = 1) {
8852 		hsb.setPosition(hsb.position.NonOverflowingInt + amount);
8853 		notify();
8854 	}
8855 
8856 	///
8857 	VerticalScrollbar verticalScrollBar() { return vsb; }
8858 	///
8859 	HorizontalScrollbar horizontalScrollBar() { return hsb; }
8860 
8861 	void notify() {
8862 		static bool insideNotify;
8863 
8864 		if(insideNotify)
8865 			return; // avoid the recursive call, even if it isn't strictly correct
8866 
8867 		insideNotify = true;
8868 		scope(exit) insideNotify = false;
8869 
8870 		this.emit!ScrollEvent();
8871 	}
8872 
8873 	mixin Emits!ScrollEvent;
8874 
8875 	///
8876 	Point position() {
8877 		return Point(hsb.position, vsb.position);
8878 	}
8879 
8880 	///
8881 	void setPosition(int x, int y) {
8882 		hsb.setPosition(x);
8883 		vsb.setPosition(y);
8884 	}
8885 
8886 	///
8887 	void setPageSize(int unitsX, int unitsY) {
8888 		hsb.setStep(unitsX);
8889 		vsb.setStep(unitsY);
8890 	}
8891 
8892 	/// Always call this BEFORE setViewableArea
8893 	void setTotalArea(int width, int height) {
8894 		hsb.setMax(width);
8895 		vsb.setMax(height);
8896 	}
8897 
8898 	/++
8899 		Always set the viewable area AFTER setitng the total area if you are going to change both.
8900 		NEVER call this from inside a scroll event. This includes through recomputeChildLayout.
8901 		If you need to do that, use [queueRecomputeChildLayout].
8902 	+/
8903 	void setViewableArea(int width, int height) {
8904 
8905 		// actually there IS A need to dothis cuz the max might have changed since then
8906 		//if(width == hsb.viewableArea_ && height == vsb.viewableArea_)
8907 			//return; // no need to do what is already done
8908 		hsb.setViewableArea(width);
8909 		vsb.setViewableArea(height);
8910 
8911 		bool needsNotify = false;
8912 
8913 		// FIXME: if at any point the rhs is outside the scrollbar, we need
8914 		// to reset to 0. but it should remember the old position in case the
8915 		// window resizes again, so it can kinda return ot where it was.
8916 		//
8917 		// so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0)
8918 		if(width >= hsb.max) {
8919 			// there's plenty of room to display it all so we need to reset to zero
8920 			// FIXME: adjust so it matches the note above
8921 			hsb.setPosition(0);
8922 			needsNotify = true;
8923 		}
8924 		if(height >= vsb.max) {
8925 			// there's plenty of room to display it all so we need to reset to zero
8926 			// FIXME: adjust so it matches the note above
8927 			vsb.setPosition(0);
8928 			needsNotify = true;
8929 		}
8930 		if(needsNotify)
8931 			notify();
8932 	}
8933 
8934 	private bool magic;
8935 	override void addChild(Widget w, int position = int.max) {
8936 		if(magic)
8937 			container.addChild(w, position);
8938 		else
8939 			super.addChild(w, position);
8940 	}
8941 
8942 	override void recomputeChildLayout() {
8943 		if(hsb is null || vsb is null || container is null) return;
8944 
8945 		registerMovement();
8946 
8947 		enum BUTTON_SIZE = 16;
8948 
8949 		hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane?
8950 		hsb.x = 0;
8951 		hsb.y = this.height - hsb.height;
8952 
8953 		vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME?
8954 		vsb.x = this.width - vsb.width;
8955 		vsb.y = 0;
8956 
8957 		auto vsb_width = vsb.showing ? vsb.width : 0;
8958 		auto hsb_height = hsb.showing ? hsb.height : 0;
8959 
8960 		hsb.width = this.width - vsb_width;
8961 		vsb.height = this.height - hsb_height;
8962 
8963 		hsb.recomputeChildLayout();
8964 		vsb.recomputeChildLayout();
8965 
8966 		if(this.header is null) {
8967 			container.x = 0;
8968 			container.y = 0;
8969 			container.width = this.width - vsb_width;
8970 			container.height = this.height - hsb_height;
8971 			container.recomputeChildLayout();
8972 		} else {
8973 			header.x = 0;
8974 			header.y = 0;
8975 			header.width = this.width - vsb_width;
8976 			header.height = scaleWithDpi(BUTTON_SIZE); // size of the button
8977 			header.recomputeChildLayout();
8978 
8979 			container.x = 0;
8980 			container.y = scaleWithDpi(BUTTON_SIZE);
8981 			container.width = this.width - vsb_width;
8982 			container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE);
8983 			container.recomputeChildLayout();
8984 		}
8985 	}
8986 
8987 	private HorizontalScrollbar hsb;
8988 	private VerticalScrollbar vsb;
8989 	Widget container;
8990 	private Widget header;
8991 
8992 	/++
8993 		Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button.
8994 
8995 		History:
8996 			Added September 27, 2021 (dub v10.3)
8997 	+/
8998 	Widget getHeader() {
8999 		if(this.header is null) {
9000 			magic = false;
9001 			scope(exit) magic = true;
9002 			this.header = new Widget(this);
9003 			queueRecomputeChildLayout();
9004 		}
9005 		return this.header;
9006 	}
9007 
9008 	/++
9009 		Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary.
9010 
9011 		History:
9012 			Added January 3, 2023 (dub v11.0)
9013 	+/
9014 	void scrollIntoView(Rectangle rect) {
9015 		Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_));
9016 
9017 		// import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1)));
9018 
9019 		// the lower right is exclusive normally
9020 		auto test = rect.lowerRight;
9021 		if(test.x > 0) test.x--;
9022 		if(test.y > 0) test.y--;
9023 
9024 		if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) {
9025 			// try to scroll only one dimension at a time if we can
9026 			if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y)))
9027 				setPosition(rect.upperLeft.x, position.y);
9028 			if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y)))
9029 				setPosition(position.x, rect.upperLeft.y);
9030 		}
9031 
9032 	}
9033 
9034 	override int minHeight() {
9035 		int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0));
9036 		if(header !is null)
9037 			min += header.minHeight;
9038 		if(horizontalScrollBar.showing)
9039 			min += horizontalScrollBar.minHeight;
9040 		return min;
9041 	}
9042 
9043 	override int maxHeight() {
9044 		int max = container ? container.maxHeight : int.max;
9045 		if(max == int.max)
9046 			return max;
9047 		if(horizontalScrollBar.showing)
9048 			max += horizontalScrollBar.minHeight;
9049 		return max;
9050 	}
9051 
9052 	static class Style : Widget.Style {
9053 		override WidgetBackground background() {
9054 			return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
9055 		}
9056 	}
9057 	mixin OverrideStyle!Style;
9058 }
9059 
9060 /++
9061 	$(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")
9062 	$(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.)
9063 +/
9064 version(minigui_screenshots)
9065 @Screenshot("ScrollMessageWidget")
9066 unittest {
9067 	auto window = new Window("ScrollMessageWidget");
9068 
9069 	auto smw = new ScrollMessageWidget(window);
9070 	smw.addDefaultKeyboardListeners();
9071 	smw.addDefaultWheelListeners();
9072 
9073 	window.loop();
9074 }
9075 
9076 /++
9077 	Bypasses automatic layout for its children, using manual positioning and sizing only.
9078 	While you need to manually position them, you must ensure they are inside the StaticLayout's
9079 	bounding box to avoid undefined behavior.
9080 
9081 	You should almost never use this.
9082 +/
9083 class StaticLayout : Layout {
9084 	///
9085 	this(Widget parent) { super(parent); }
9086 	override void recomputeChildLayout() {
9087 		registerMovement();
9088 		foreach(child; children)
9089 			child.recomputeChildLayout();
9090 	}
9091 }
9092 
9093 /++
9094 	Bypasses automatic positioning when being laid out. It is your responsibility to make
9095 	room for this widget in the parent layout.
9096 
9097 	Its children are laid out normally, unless there is exactly one, in which case it takes
9098 	on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you
9099 	can do that with `padding`).
9100 +/
9101 class StaticPosition : Layout {
9102 	///
9103 	this(Widget parent) { super(parent); }
9104 
9105 	override void recomputeChildLayout() {
9106 		registerMovement();
9107 		if(this.children.length == 1) {
9108 			auto child = children[0];
9109 			child.x = 0;
9110 			child.y = 0;
9111 			child.width = this.width;
9112 			child.height = this.height;
9113 			child.recomputeChildLayout();
9114 		} else
9115 		foreach(child; children)
9116 			child.recomputeChildLayout();
9117 	}
9118 
9119 	alias width = typeof(super).width;
9120 	alias height = typeof(super).height;
9121 
9122 	@property int width(int w) @nogc pure @safe nothrow {
9123 		return this._width = w;
9124 	}
9125 
9126 	@property int height(int w) @nogc pure @safe nothrow {
9127 		return this._height = w;
9128 	}
9129 
9130 }
9131 
9132 /++
9133 	FixedPosition is like [StaticPosition], but its coordinates
9134 	are always relative to the viewport, meaning they do not scroll with
9135 	the parent content.
9136 +/
9137 class FixedPosition : StaticPosition {
9138 	///
9139 	this(Widget parent) { super(parent); }
9140 }
9141 
9142 version(win32_widgets)
9143 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) {
9144 	if(true) {
9145 		// cmd == 0 = menu, cmd == 1 = accelerator
9146 		if(auto item = idm in Action.mapping) {
9147 			foreach(handler; (*item).triggered)
9148 				handler();
9149 		/*
9150 			auto event = new Event("triggered", *item);
9151 			event.button = idm;
9152 			event.dispatch();
9153 		*/
9154 			return 0;
9155 		}
9156 	}
9157 	if(handle)
9158 	if(auto widgetp = handle in Widget.nativeMapping) {
9159 		(*widgetp).handleWmCommand(cmd, idm);
9160 		return 0;
9161 	}
9162 	return 1;
9163 }
9164 
9165 
9166 ///
9167 class Window : Widget {
9168 	Widget[] mouseCapturedBy;
9169 	void captureMouse(Widget byWhom) {
9170 		assert(byWhom !is null);
9171 		if(mouseCapturedBy.length > 0) {
9172 			auto cc = mouseCapturedBy[$-1];
9173 			if(cc is byWhom)
9174 				return; // or should it throw?
9175 			auto par = byWhom;
9176 			while(par) {
9177 				if(cc is par)
9178 					goto allowed;
9179 				par = par.parent;
9180 			}
9181 
9182 			throw new Exception("mouse is already captured by other widget");
9183 		}
9184 		allowed:
9185 		mouseCapturedBy ~= byWhom;
9186 		if(mouseCapturedBy.length == 1)
9187 			win.grabInput(false, true, false);
9188 		//void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) {
9189 	}
9190 	void releaseMouseCapture() {
9191 		if(mouseCapturedBy.length == 0)
9192 			return; // or should it throw?
9193 		mouseCapturedBy = mouseCapturedBy[0 .. $-1];
9194 		mouseCapturedBy.assumeSafeAppend();
9195 		if(mouseCapturedBy.length == 0)
9196 			win.releaseInputGrab();
9197 	}
9198 
9199 
9200 	/++
9201 
9202 	+/
9203 	MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
9204 		return .messageBox(this, title, message, style, icon);
9205 	}
9206 
9207 	/// ditto
9208 	int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
9209 		return messageBox(null, message, style, icon);
9210 	}
9211 
9212 
9213 	/++
9214 		Sets the window icon which is often seen in title bars and taskbars.
9215 
9216 		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.
9217 
9218 		History:
9219 			Added April 5, 2022 (dub v10.8)
9220 	+/
9221 	@property void icon(MemoryImage icon) {
9222 		if(win && icon)
9223 			win.icon = icon;
9224 	}
9225 
9226 	// 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
9227 	// this does NOT change the icon on the window! That's what the other overload is for
9228 	static @property .icon icon(GenericIcons i) {
9229 		return .icon(i);
9230 	}
9231 
9232 	///
9233 	@scriptable
9234 	@property bool focused() {
9235 		return win.focused;
9236 	}
9237 
9238 	static class Style : Widget.Style {
9239 		override WidgetBackground background() {
9240 			version(custom_widgets)
9241 				return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor);
9242 			else version(win32_widgets)
9243 				return WidgetBackground(Color.transparent);
9244 			else static assert(0);
9245 		}
9246 	}
9247 	mixin OverrideStyle!Style;
9248 
9249 	/++
9250 		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.
9251 	+/
9252 	deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() {
9253 		return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback();
9254 	}
9255 
9256 	private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() {
9257 		OperatingSystemFont font;
9258 		if(auto vt = WidgetPainter.visualTheme) {
9259 			font = vt.defaultFontCached(96); // FIXME
9260 		}
9261 
9262 		if(font is null) {
9263 			static int defaultHeightCache;
9264 			if(defaultHeightCache == 0) {
9265 				font = new OperatingSystemFont;
9266 				font.loadDefault;
9267 				defaultHeightCache = castFnumToCnum(font.height());// * 5 / 4;
9268 			}
9269 			return defaultHeightCache;
9270 		}
9271 
9272 		return castFnumToCnum(font.height());// * 5 / 4;
9273 	}
9274 
9275 	Widget focusedWidget;
9276 
9277 	private SimpleWindow win_;
9278 
9279 	@property {
9280 		/++
9281 			Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers.
9282 
9283 			History:
9284 				Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property.
9285 		+/
9286 		public SimpleWindow win() {
9287 			return win_;
9288 		}
9289 		///
9290 		protected void win(SimpleWindow w) {
9291 			win_ = w;
9292 		}
9293 	}
9294 
9295 	/// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing.
9296 	this(Widget p) {
9297 		tabStop = false;
9298 		super(p);
9299 	}
9300 
9301 	private void actualRedraw() {
9302 		if(recomputeChildLayoutRequired)
9303 			recomputeChildLayoutEntry();
9304 		if(!showing) return;
9305 
9306 		assert(parentWindow !is null);
9307 
9308 		auto w = drawableWindow;
9309 		if(w is null)
9310 			w = parentWindow.win;
9311 
9312 		if(w.closed())
9313 			return;
9314 
9315 		auto ugh = this.parent;
9316 		int lox, loy;
9317 		while(ugh) {
9318 			lox += ugh.x;
9319 			loy += ugh.y;
9320 			ugh = ugh.parent;
9321 		}
9322 		auto painter = w.draw(true);
9323 		privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw());
9324 	}
9325 
9326 
9327 	private bool skipNextChar = false;
9328 
9329 	/++
9330 		Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers.
9331 
9332 		This constructor is intended primarily for internal use and may be changed to `protected` later.
9333 	+/
9334 	this(SimpleWindow win) {
9335 
9336 		static if(UsingSimpledisplayX11) {
9337 			win.discardAdditionalConnectionState = &discardXConnectionState;
9338 			win.recreateAdditionalConnectionState = &recreateXConnectionState;
9339 		}
9340 
9341 		tabStop = false;
9342 		super(null);
9343 		this.win = win;
9344 
9345 		win.addEventListener((Widget.RedrawEvent) {
9346 			if(win.eventQueued!RecomputeEvent) {
9347 				// writeln("skipping");
9348 				return; // let the recompute event do the actual redraw
9349 			}
9350 			this.actualRedraw();
9351 		});
9352 
9353 		win.addEventListener((Widget.RecomputeEvent) {
9354 			recomputeChildLayoutEntry();
9355 			if(win.eventQueued!RedrawEvent)
9356 				return; // let the queued one do it
9357 			else {
9358 				// writeln("drawing");
9359 				this.actualRedraw(); // if not queued, it needs to be done now anyway
9360 			}
9361 		});
9362 
9363 		this.width = win.width;
9364 		this.height = win.height;
9365 		this.parentWindow = this;
9366 
9367 		win.closeQuery = () {
9368 			if(this.emit!ClosingEvent())
9369 				win.close();
9370 		};
9371 		win.onClosing = () {
9372 			this.emit!ClosedEvent();
9373 		};
9374 
9375 		win.windowResized = (int w, int h) {
9376 			this.width = w;
9377 			this.height = h;
9378 			queueRecomputeChildLayout();
9379 			// this causes a HUGE performance problem for no apparent benefit, hence the commenting
9380 			//version(win32_widgets)
9381 				//InvalidateRect(hwnd, null, true);
9382 			redraw();
9383 		};
9384 
9385 		win.onFocusChange = (bool getting) {
9386 			// sdpyPrintDebugString("onFocusChange ", getting, " ", this.toString);
9387 			if(this.focusedWidget) {
9388 				if(getting) {
9389 					this.focusedWidget.emit!FocusEvent();
9390 					this.focusedWidget.emit!FocusInEvent();
9391 				} else {
9392 					this.focusedWidget.emit!BlurEvent();
9393 					this.focusedWidget.emit!FocusOutEvent();
9394 				}
9395 			}
9396 
9397 			if(getting) {
9398 				this.emit!FocusEvent();
9399 				this.emit!FocusInEvent();
9400 			} else {
9401 				this.emit!BlurEvent();
9402 				this.emit!FocusOutEvent();
9403 			}
9404 		};
9405 
9406 		win.onDpiChanged = {
9407 			this.queueRecomputeChildLayout();
9408 			auto event = new DpiChangedEvent(this);
9409 			event.sendDirectly();
9410 
9411 			privateDpiChanged();
9412 		};
9413 
9414 		win.setEventHandlers(
9415 			(MouseEvent e) {
9416 				dispatchMouseEvent(e);
9417 			},
9418 			(KeyEvent e) {
9419 				//writefln("%x   %s", cast(uint) e.key, e.key);
9420 				dispatchKeyEvent(e);
9421 			},
9422 			(dchar e) {
9423 				if(e == 13) e = 10; // hack?
9424 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
9425 				dispatchCharEvent(e);
9426 			},
9427 		);
9428 
9429 		addEventListener("char", (Widget, Event ev) {
9430 			if(skipNextChar) {
9431 				ev.preventDefault();
9432 				skipNextChar = false;
9433 			}
9434 		});
9435 
9436 		version(win32_widgets)
9437 		win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) {
9438 			if(hwnd !is this.win.impl.hwnd)
9439 				return 1; // we don't care... pass it on
9440 			auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn);
9441 			if(mustReturn)
9442 				return ret;
9443 			return 1; // pass it on
9444 		};
9445 
9446 		if(Window.newWindowCreated)
9447 			Window.newWindowCreated(this);
9448 	}
9449 
9450 	version(custom_widgets)
9451 	override void defaultEventHandler_click(ClickEvent event) {
9452 		if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) {
9453 			if(event.target && event.target.tabStop)
9454 				event.target.focus();
9455 		}
9456 	}
9457 
9458 	private static void delegate(Window) newWindowCreated;
9459 
9460 	version(win32_widgets)
9461 	override void paint(WidgetPainter painter) {
9462 		/*
9463 		RECT rect;
9464 		rect.right = this.width;
9465 		rect.bottom = this.height;
9466 		DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null);
9467 		*/
9468 		// 3dface is used as window backgrounds by Windows too, so that's why I'm using it here
9469 		auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
9470 		auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN));
9471 		// since the pen is null, to fill the whole space, we need the +1 on both.
9472 		gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1);
9473 		SelectObject(painter.impl.hdc, p);
9474 		SelectObject(painter.impl.hdc, b);
9475 	}
9476 	version(custom_widgets)
9477 	override void paint(WidgetPainter painter) {
9478 		auto cs = getComputedStyle();
9479 		painter.fillColor = cs.windowBackgroundColor;
9480 		painter.outlineColor = cs.windowBackgroundColor;
9481 		painter.drawRectangle(Point(0, 0), this.width, this.height);
9482 	}
9483 
9484 
9485 	override void defaultEventHandler_keydown(KeyDownEvent event) {
9486 		Widget _this = event.target;
9487 
9488 		if(event.key == Key.Tab) {
9489 			/* Window tab ordering is a recursive thingy with each group */
9490 
9491 			// FIXME inefficient
9492 			Widget[] helper(Widget p) {
9493 				if(p.hidden)
9494 					return null;
9495 				Widget[] childOrdering;
9496 
9497 				auto children = p.children.dup;
9498 
9499 				while(true) {
9500 					// UIs should be generally small, so gonna brute force it a little
9501 					// note that it must be a stable sort here; if all are index 0, it should be in order of declaration
9502 
9503 					Widget smallestTab;
9504 					foreach(ref c; children) {
9505 						if(c is null) continue;
9506 						if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) {
9507 							smallestTab = c;
9508 							c = null;
9509 						}
9510 					}
9511 					if(smallestTab !is null) {
9512 						if(smallestTab.tabStop && !smallestTab.hidden)
9513 							childOrdering ~= smallestTab;
9514 						if(!smallestTab.hidden)
9515 							childOrdering ~= helper(smallestTab);
9516 					} else
9517 						break;
9518 
9519 				}
9520 
9521 				return childOrdering;
9522 			}
9523 
9524 			Widget[] tabOrdering = helper(this);
9525 
9526 			Widget recipient;
9527 
9528 			if(tabOrdering.length) {
9529 				bool seenThis = false;
9530 				Widget previous;
9531 				foreach(idx, child; tabOrdering) {
9532 					if(child is focusedWidget) {
9533 
9534 						if(event.shiftKey) {
9535 							if(idx == 0)
9536 								recipient = tabOrdering[$-1];
9537 							else
9538 								recipient = tabOrdering[idx - 1];
9539 							break;
9540 						}
9541 
9542 						seenThis = true;
9543 						if(idx + 1 == tabOrdering.length) {
9544 							// we're at the end, either move to the next group
9545 							// or start back over
9546 							recipient = tabOrdering[0];
9547 						}
9548 						continue;
9549 					}
9550 					if(seenThis) {
9551 						recipient = child;
9552 						break;
9553 					}
9554 					previous = child;
9555 				}
9556 			}
9557 
9558 			if(recipient !is null) {
9559 				//  writeln(typeid(recipient));
9560 				recipient.focus();
9561 
9562 				skipNextChar = true;
9563 			}
9564 		}
9565 
9566 		debug if(event.key == Key.F12) {
9567 			if(devTools) {
9568 				devTools.close();
9569 				devTools = null;
9570 			} else {
9571 				devTools = new DevToolWindow(this);
9572 				devTools.show();
9573 			}
9574 		}
9575 	}
9576 
9577 	debug DevToolWindow devTools;
9578 
9579 
9580 	/++
9581 		Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display.
9582 
9583 		History:
9584 			Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead.
9585 
9586 			The width and height arguments were added to the overload that takes `string` first on June 21, 2021.
9587 	+/
9588 	this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
9589 		if(title is null) {
9590 			import core.runtime;
9591 			if(Runtime.args.length)
9592 				title = Runtime.args[0];
9593 		}
9594 		win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent);
9595 
9596 		static if(UsingSimpledisplayX11)
9597 		if(windowFlags & WindowFlags.managesChildWindowFocus) {
9598 		///+
9599 		// for input proxy
9600 		auto display = XDisplayConnection.get;
9601 		auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0);
9602 		XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask);
9603 		XMapWindow(display, inputProxy);
9604 		// writefln("input proxy: 0x%0x", inputProxy);
9605 		this.inputProxy = new SimpleWindow(inputProxy);
9606 
9607 		/+
9608 		this.inputProxy.onFocusChange = (bool getting) {
9609 			sdpyPrintDebugString("input proxy focus change ", getting);
9610 		};
9611 		+/
9612 
9613 		XEvent lastEvent;
9614 		this.inputProxy.handleNativeEvent = (XEvent ev) {
9615 			lastEvent = ev;
9616 			return 1;
9617 		};
9618 		this.inputProxy.setEventHandlers(
9619 			(MouseEvent e) {
9620 				dispatchMouseEvent(e);
9621 			},
9622 			(KeyEvent e) {
9623 				//writefln("%x   %s", cast(uint) e.key, e.key);
9624 				if(dispatchKeyEvent(e)) {
9625 					// FIXME: i should trap error
9626 					if(auto nw = cast(NestedChildWindowWidget) focusedWidget) {
9627 						auto thing = nw.focusableWindow();
9628 						if(thing && thing.window) {
9629 							lastEvent.xkey.window = thing.window;
9630 							// writeln("sending event ", lastEvent.xkey);
9631 							trapXErrors( {
9632 								XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent);
9633 							});
9634 						}
9635 					}
9636 				}
9637 			},
9638 			(dchar e) {
9639 				if(e == 13) e = 10; // hack?
9640 				if(e == 127) return; // linux sends this, windows doesn't. we don't want it.
9641 				dispatchCharEvent(e);
9642 			},
9643 		);
9644 
9645 		this.inputProxy.populateXic();
9646 		// done
9647 		//+/
9648 		}
9649 
9650 
9651 
9652 		win.setRequestedInputFocus = &this.setRequestedInputFocus;
9653 
9654 		this(win);
9655 	}
9656 
9657 	SimpleWindow inputProxy;
9658 
9659 	private SimpleWindow setRequestedInputFocus() {
9660 		return inputProxy;
9661 	}
9662 
9663 	/// ditto
9664 	this(string title, int width = 500, int height = 500) {
9665 		this(width, height, title);
9666 	}
9667 
9668 	///
9669 	@property string title() { return parentWindow.win.title; }
9670 	///
9671 	@property void title(string title) { parentWindow.win.title = title; }
9672 
9673 	///
9674 	@scriptable
9675 	void close() {
9676 		win.close();
9677 		// I synchronize here upon window closing to ensure all child windows
9678 		// get updated too before the event loop. This avoids some random X errors.
9679 		static if(UsingSimpledisplayX11) {
9680 			runInGuiThread( {
9681 				XSync(XDisplayConnection.get, false);
9682 			});
9683 		}
9684 	}
9685 
9686 	bool dispatchKeyEvent(KeyEvent ev) {
9687 		auto wid = focusedWidget;
9688 		if(wid is null)
9689 			wid = this;
9690 		KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid);
9691 		event.originalKeyEvent = ev;
9692 		event.key = ev.key;
9693 		event.state = ev.modifierState;
9694 		event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
9695 		event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
9696 		event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
9697 		event.dispatch();
9698 
9699 		return !event.propagationStopped;
9700 	}
9701 
9702 	// returns true if propagation should continue into nested things.... prolly not a great thing to do.
9703 	bool dispatchCharEvent(dchar ch) {
9704 		if(focusedWidget) {
9705 			auto event = new CharEvent(focusedWidget, ch);
9706 			event.dispatch();
9707 			return !event.propagationStopped;
9708 		}
9709 		return true;
9710 	}
9711 
9712 	Widget mouseLastOver;
9713 	Widget mouseLastDownOn;
9714 	bool lastWasDoubleClick;
9715 	bool dispatchMouseEvent(MouseEvent ev) {
9716 		auto eleR = widgetAtPoint(this, ev.x, ev.y);
9717 		auto ele = eleR.widget;
9718 
9719 		auto captureEle = ele;
9720 
9721 		auto mouseCapturedBy = this.mouseCapturedBy.length ? this.mouseCapturedBy[$-1] : null;
9722 		if(mouseCapturedBy !is null) {
9723 			if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele))
9724 				captureEle = mouseCapturedBy;
9725 		}
9726 
9727 		// a hack to get it relative to the widget.
9728 		eleR.x = ev.x;
9729 		eleR.y = ev.y;
9730 		auto pain = captureEle;
9731 
9732 		auto vpx = eleR.x;
9733 		auto vpy = eleR.y;
9734 
9735 		while(pain) {
9736 			eleR.x -= pain.x;
9737 			eleR.y -= pain.y;
9738 			pain.addScrollPosition(eleR.x, eleR.y);
9739 
9740 			vpx -= pain.x;
9741 			vpy -= pain.y;
9742 
9743 			pain = pain.parent;
9744 		}
9745 
9746 		void populateMouseEventBase(MouseEventBase event) {
9747 			event.button = ev.button;
9748 			event.buttonLinear = ev.buttonLinear;
9749 			event.state = ev.modifierState;
9750 			event.clientX = eleR.x;
9751 			event.clientY = eleR.y;
9752 
9753 			event.viewportX = vpx;
9754 			event.viewportY = vpy;
9755 
9756 			event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
9757 			event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
9758 			event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
9759 		}
9760 
9761 		if(ev.type == MouseEventType.buttonPressed) {
9762 			{
9763 				auto event = new MouseDownEvent(captureEle);
9764 				populateMouseEventBase(event);
9765 				event.dispatch();
9766 			}
9767 
9768 			if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) {
9769 				auto event = new DoubleClickEvent(captureEle);
9770 				populateMouseEventBase(event);
9771 				event.dispatch();
9772 				lastWasDoubleClick = ev.doubleClick;
9773 			} else {
9774 				lastWasDoubleClick = false;
9775 			}
9776 
9777 			mouseLastDownOn = ele;
9778 		} else if(ev.type == MouseEventType.buttonReleased) {
9779 			{
9780 				auto event = new MouseUpEvent(captureEle);
9781 				populateMouseEventBase(event);
9782 				event.dispatch();
9783 			}
9784 			if(!lastWasDoubleClick && mouseLastDownOn is ele) {
9785 				auto event = new ClickEvent(captureEle);
9786 				populateMouseEventBase(event);
9787 				event.dispatch();
9788 			}
9789 		} else if(ev.type == MouseEventType.motion) {
9790 			// motion
9791 			{
9792 				auto event = new MouseMoveEvent(captureEle);
9793 				populateMouseEventBase(event); // fills in button which is meaningless but meh
9794 				event.dispatch();
9795 			}
9796 
9797 			if(mouseLastOver !is ele) {
9798 				if(ele !is null) {
9799 					if(!isAParentOf(ele, mouseLastOver)) {
9800 						ele.setDynamicState(DynamicState.hover, true);
9801 						auto event = new MouseEnterEvent(ele);
9802 						event.relatedTarget = mouseLastOver;
9803 						event.sendDirectly();
9804 
9805 						ele.useStyleProperties((scope Widget.Style s) {
9806 							ele.parentWindow.win.cursor = s.cursor;
9807 						});
9808 					}
9809 				}
9810 
9811 				if(mouseLastOver !is null) {
9812 					if(!isAParentOf(mouseLastOver, ele)) {
9813 						mouseLastOver.setDynamicState(DynamicState.hover, false);
9814 						auto event = new MouseLeaveEvent(mouseLastOver);
9815 						event.relatedTarget = ele;
9816 						event.sendDirectly();
9817 					}
9818 				}
9819 
9820 				if(ele !is null) {
9821 					auto event = new MouseOverEvent(ele);
9822 					event.relatedTarget = mouseLastOver;
9823 					event.dispatch();
9824 				}
9825 
9826 				if(mouseLastOver !is null) {
9827 					auto event = new MouseOutEvent(mouseLastOver);
9828 					event.relatedTarget = ele;
9829 					event.dispatch();
9830 				}
9831 
9832 				mouseLastOver = ele;
9833 			}
9834 		}
9835 
9836 		return true; // FIXME: the event default prevented?
9837 	}
9838 
9839 	/++
9840 		Shows the window and runs the application event loop.
9841 
9842 		Blocks until this window is closed.
9843 
9844 		Bugs:
9845 
9846 		$(PITFALL
9847 			You should always have one event loop live for your application.
9848 			If you make two windows in sequence, the second call to loop (or
9849 			simpledisplay's [SimpleWindow.eventLoop], upon which this is built)
9850 			might fail:
9851 
9852 			---
9853 			// don't do this!
9854 			auto window = new Window();
9855 			window.loop();
9856 
9857 			// or new Window or new MainWindow, all the same
9858 			auto window2 = new SimpleWindow();
9859 			window2.eventLoop(0); // problematic! might crash
9860 			---
9861 
9862 			simpledisplay's current implementation assumes that final cleanup is
9863 			done when the event loop refcount reaches zero. So after the first
9864 			eventLoop returns, when there isn't already another one active, it assumes
9865 			the program will exit soon and cleans up.
9866 
9867 			This is arguably a bug that it doesn't reinitialize, and I'll probably change
9868 			it eventually, but in the mean time, there's an easy solution:
9869 
9870 			---
9871 			// do this
9872 			EventLoop mainEventLoop = EventLoop.get; // just add this line
9873 
9874 			auto window = new Window();
9875 			window.loop();
9876 
9877 			// or any other type of Window etc.
9878 			auto window2 = new Window();
9879 			window2.loop(); // perfectly fine since mainEventLoop still alive
9880 			---
9881 
9882 			By adding a top-level reference to the event loop, it ensures the final cleanup
9883 			is not performed until it goes out of scope too, letting the individual window loops
9884 			work without trouble despite the bug.
9885 		)
9886 
9887 		History:
9888 			The [BlockingMode] parameter was added on December 8, 2021.
9889 			The default behavior is to block until the application quits
9890 			(so all windows have been closed), unless another minigui or
9891 			simpledisplay event loop is already running, in which case it
9892 			will block until this window closes specifically.
9893 	+/
9894 	@scriptable
9895 	void loop(BlockingMode bm = BlockingMode.automatic) {
9896 		if(win.closed)
9897 			return; // otherwise show will throw
9898 		show();
9899 		win.eventLoopWithBlockingMode(bm, 0);
9900 	}
9901 
9902 	private bool firstShow = true;
9903 
9904 	@scriptable
9905 	override void show() {
9906 		bool rd = false;
9907 		if(firstShow) {
9908 			firstShow = false;
9909 			queueRecomputeChildLayout();
9910 			// unless the programmer already called focus on something, pick something ourselves
9911 			auto f = focusedWidget is null ? getFirstFocusable(this) : focusedWidget; // FIXME: autofocus?
9912 			if(f)
9913 				f.focus();
9914 			redraw();
9915 		}
9916 		win.show();
9917 		super.show();
9918 	}
9919 	@scriptable
9920 	override void hide() {
9921 		win.hide();
9922 		super.hide();
9923 	}
9924 
9925 	static Widget getFirstFocusable(Widget start) {
9926 		if(start is null)
9927 			return null;
9928 
9929 		foreach(widget; &start.focusableWidgets) {
9930 			return widget;
9931 		}
9932 
9933 		return null;
9934 	}
9935 
9936 	static Widget getLastFocusable(Widget start) {
9937 		if(start is null)
9938 			return null;
9939 
9940 		Widget last;
9941 		foreach(widget; &start.focusableWidgets) {
9942 			last = widget;
9943 		}
9944 
9945 		return last;
9946 	}
9947 
9948 
9949 	mixin Emits!ClosingEvent;
9950 	mixin Emits!ClosedEvent;
9951 }
9952 
9953 /++
9954 	History:
9955 		Added January 12, 2022
9956 
9957 		Made `final` on January 3, 2025
9958 +/
9959 final class DpiChangedEvent : Event {
9960 	enum EventString = "dpichanged";
9961 
9962 	this(Widget target) {
9963 		super(EventString, target);
9964 	}
9965 }
9966 
9967 debug private class DevToolWindow : Window {
9968 	Window p;
9969 
9970 	TextEdit parentList;
9971 	TextEdit logWindow;
9972 	TextLabel clickX, clickY;
9973 
9974 	this(Window p) {
9975 		this.p = p;
9976 		super(400, 300, "Developer Toolbox");
9977 
9978 		logWindow = new TextEdit(this);
9979 		parentList = new TextEdit(this);
9980 
9981 		auto hl = new HorizontalLayout(this);
9982 		clickX = new TextLabel("", TextAlignment.Right, hl);
9983 		clickY = new TextLabel("", TextAlignment.Right, hl);
9984 
9985 		parentListeners ~= p.addEventListener("*", (Event ev) {
9986 			log(typeid(ev.source).name, " emitted ", typeid(ev).name);
9987 		});
9988 
9989 		parentListeners ~= p.addEventListener((ClickEvent ev) {
9990 			auto s = ev.srcElement;
9991 
9992 			string list;
9993 
9994 			void addInfo(Widget s) {
9995 				list ~= s.toString();
9996 				list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight);
9997 				list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight);
9998 				list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness);
9999 				list ~= "\n\theight: " ~ toInternal!string(s.height);
10000 				list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth);
10001 				list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth);
10002 				list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness);
10003 				list ~= "\n\twidth: " ~ toInternal!string(s.width);
10004 				list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop);
10005 				list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom);
10006 			}
10007 
10008 			addInfo(s);
10009 
10010 			s = s.parent;
10011 			while(s) {
10012 				list ~= "\n";
10013 				addInfo(s);
10014 				s = s.parent;
10015 			}
10016 			parentList.content = list;
10017 
10018 			clickX.label = toInternal!string(ev.clientX);
10019 			clickY.label = toInternal!string(ev.clientY);
10020 		});
10021 	}
10022 
10023 	EventListener[] parentListeners;
10024 
10025 	override void close() {
10026 		assert(p !is null);
10027 		foreach(p; parentListeners)
10028 			p.disconnect();
10029 		parentListeners = null;
10030 		p.devTools = null;
10031 		p = null;
10032 		super.close();
10033 	}
10034 
10035 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
10036 		if(ev.key == Key.F12) {
10037 			this.close();
10038 			if(p)
10039 				p.devTools = null;
10040 		} else {
10041 			super.defaultEventHandler_keydown(ev);
10042 		}
10043 	}
10044 
10045 	void log(T...)(T t) {
10046 		string str;
10047 		import std.conv;
10048 		foreach(i; t)
10049 			str ~= to!string(i);
10050 		str ~= "\n";
10051 		logWindow.addText(str);
10052 		logWindow.scrollToBottom();
10053 
10054 		//version(custom_widgets)
10055 		//logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox());
10056 	}
10057 }
10058 
10059 /++
10060 	A dialog is a transient window that intends to get information from
10061 	the user before being dismissed.
10062 +/
10063 class Dialog : Window {
10064 	///
10065 	this(Window parent, int width, int height, string title = null) {
10066 		super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win);
10067 
10068 		// this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) {
10069 	}
10070 
10071 	///
10072 	this(Window parent, string title, int width, int height) {
10073 		this(parent, width, height, title);
10074 	}
10075 
10076 	deprecated("Pass an explicit parent window, even if it is `null`")
10077 	this(int width, int height, string title = null) {
10078 		this(null, width, height, title);
10079 	}
10080 
10081 	///
10082 	void OK() {
10083 
10084 	}
10085 
10086 	///
10087 	void Cancel() {
10088 		this.close();
10089 	}
10090 }
10091 
10092 /++
10093 	A custom widget similar to the HTML5 <details> tag.
10094 +/
10095 version(none)
10096 class DetailsView : Widget {
10097 
10098 }
10099 
10100 // FIXME: maybe i should expose the other list views Windows offers too
10101 
10102 /++
10103 	A TableView is a widget made to display a table of data strings.
10104 
10105 
10106 	Future_Directions:
10107 		Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers.
10108 
10109 		I will add a selection changed event at some point, as well as item clicked events.
10110 	History:
10111 		Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet.
10112 	See_Also:
10113 		[ListWidget] which displays a list of strings without additional columns.
10114 +/
10115 class TableView : Widget {
10116 	/++
10117 
10118 	+/
10119 	this(Widget parent) {
10120 		super(parent);
10121 
10122 		version(win32_widgets) {
10123 			// LVS_EX_LABELTIP might be worth too
10124 			// LVS_OWNERDRAWFIXED
10125 			createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//, LVS_EX_TRACKSELECT); // ex style for for LVN_HOTTRACK
10126 		} else version(custom_widgets) {
10127 			auto smw = new ScrollMessageWidget(this);
10128 			smw.addDefaultKeyboardListeners();
10129 			smw.addDefaultWheelListeners(1, scaleWithDpi(16));
10130 			tvwi = new TableViewWidgetInner(this, smw);
10131 		}
10132 	}
10133 
10134 	// FIXME: auto-size columns on double click of header thing like in Windows
10135 	// it need only make the currently displayed things fit well.
10136 
10137 
10138 	private ColumnInfo[] columns;
10139 	private int itemCount;
10140 
10141 	version(custom_widgets) private {
10142 		TableViewWidgetInner tvwi;
10143 	}
10144 
10145 	/// Passed to [setColumnInfo]
10146 	static struct ColumnInfo {
10147 		const(char)[] name; /// the name displayed in the header
10148 		/++
10149 			The default width, in pixels. As a special case, you can set this to -1
10150 			if you want the system to try to automatically size the width to fit visible
10151 			content. If it can't, it will try to pick a sensible default size.
10152 
10153 			Any other negative value is not allowed and may lead to unpredictable results.
10154 
10155 			History:
10156 				The -1 behavior was specified on December 3, 2021. It actually worked before
10157 				anyway on Win32 but now it is a formal feature with partial Linux support.
10158 
10159 			Bugs:
10160 				It doesn't actually attempt to calculate a best-fit width on Linux as of
10161 				December 3, 2021. I do plan to fix this in the future, but Windows is the
10162 				priority right now. At least it doesn't break things when you use it now.
10163 		+/
10164 		int width;
10165 
10166 		/++
10167 			Alignment of the text in the cell. Applies to the header as well as all data in this
10168 			column.
10169 
10170 			Bugs:
10171 				On Windows, the first column ignores this member and is always left aligned.
10172 				You can work around this by inserting a dummy first column with width = 0
10173 				then putting your actual data in the second column, which does respect the
10174 				alignment.
10175 
10176 				This is a quirk of the operating system's implementation going back a very
10177 				long time and is unlikely to ever be fixed.
10178 		+/
10179 		TextAlignment alignment;
10180 
10181 		/++
10182 			After all the pixel widths have been assigned, any left over
10183 			space is divided up among all columns and distributed to according
10184 			to the widthPercent field.
10185 
10186 
10187 			For example, if you have two fields, both with width 50 and one with
10188 			widthPercent of 25 and the other with widthPercent of 75, and the
10189 			container is 200 pixels wide, first both get their width of 50.
10190 			then the 100 remaining pixels are split up, so the one gets a total
10191 			of 75 pixels and the other gets a total of 125.
10192 
10193 			This is automatically applied as the window is resized.
10194 
10195 			If there is not enough space - that is, when a horizontal scrollbar
10196 			needs to appear - there are 0 pixels divided up, and thus everyone
10197 			gets 0. This can cause a column to shrink out of proportion when
10198 			passing the scroll threshold.
10199 
10200 			It is important to still set a fixed width (that is, to populate the
10201 			`width` field) even if you use the percents because that will be the
10202 			default minimum in the event of a scroll bar appearing.
10203 
10204 			The percents total in the column can never exceed 100 or be less than 0.
10205 			Doing this will trigger an assert error.
10206 
10207 			Implementation note:
10208 
10209 			Please note that percentages are only recalculated 1) upon original
10210 			construction and 2) upon resizing the control. If the user adjusts the
10211 			width of a column, the percentage items will not be updated.
10212 
10213 			On the other hand, if the user adjusts the width of a percentage column
10214 			then resizes the window, it is recalculated, meaning their hand adjustment
10215 			is discarded. This specific behavior may change in the future as it is
10216 			arguably a bug, but I'm not certain yet.
10217 
10218 			History:
10219 				Added November 10, 2021 (dub v10.4)
10220 		+/
10221 		int widthPercent;
10222 
10223 
10224 		private int calculatedWidth;
10225 	}
10226 	/++
10227 		Sets the number of columns along with information about the headers.
10228 
10229 		Please note: on Windows, the first column ignores your alignment preference
10230 		and is always left aligned.
10231 	+/
10232 	void setColumnInfo(ColumnInfo[] columns...) {
10233 
10234 		foreach(ref c; columns) {
10235 			c.name = c.name.idup;
10236 		}
10237 		this.columns = columns.dup;
10238 
10239 		updateCalculatedWidth(false);
10240 
10241 		version(custom_widgets) {
10242 			tvwi.header.updateHeaders();
10243 			tvwi.updateScrolls();
10244 		} else version(win32_widgets)
10245 		foreach(i, column; this.columns) {
10246 			LVCOLUMN lvColumn;
10247 			lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
10248 			lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth;
10249 
10250 			auto bfr = WCharzBuffer(column.name);
10251 			lvColumn.pszText = bfr.ptr;
10252 
10253 			if(column.alignment & TextAlignment.Center)
10254 				lvColumn.fmt = LVCFMT_CENTER;
10255 			else if(column.alignment & TextAlignment.Right)
10256 				lvColumn.fmt = LVCFMT_RIGHT;
10257 			else
10258 				lvColumn.fmt = LVCFMT_LEFT;
10259 
10260 			if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1)
10261 				throw new WindowsApiException("Insert Column Fail", GetLastError());
10262 		}
10263 	}
10264 
10265 	version(custom_widgets)
10266 	private int getColumnSizeForContent(size_t columnIndex) {
10267 		// FIXME: idk where the problem is but with a 2x scale the horizontal scroll is insuffiicent. i think the SMW is doing it wrong.
10268 		// might also want a user-defined max size too
10269 		int padding = scaleWithDpi(6);
10270 		int m = this.defaultTextWidth(this.columns[columnIndex].name) + padding;
10271 
10272 		if(getData !is null)
10273 		foreach(row; 0 .. itemCount)
10274 			getData(row, cast(int) columnIndex, (txt) {
10275 				m = mymax(m, this.defaultTextWidth(txt) + padding);
10276 			});
10277 
10278 		if(m < 32)
10279 			m = 32;
10280 
10281 		return m;
10282 	}
10283 
10284 	/++
10285 		History:
10286 			Added February 26, 2025
10287 	+/
10288 	void autoSizeColumnsToContent() {
10289 		version(custom_widgets) {
10290 			foreach(idx, ref c; columns) {
10291 				c.width = getColumnSizeForContent(idx);
10292 			}
10293 			updateCalculatedWidth(false);
10294 			tvwi.updateScrolls();
10295 		} else version(win32_widgets) {
10296 			foreach(i, c; columns)
10297 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, LVSCW_AUTOSIZE); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
10298 		}
10299 	}
10300 
10301 	/++
10302 		History:
10303 			Added March 1, 2025
10304 	+/
10305 	bool supportsPerCellAlignment() {
10306 		version(custom_widgets)
10307 			return true;
10308 		else version(win32_widgets)
10309 			return false;
10310 		return false;
10311 	}
10312 
10313 	private int getActualSetSize(size_t i, bool askWindows) {
10314 		version(win32_widgets)
10315 			if(askWindows)
10316 				return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0);
10317 		auto w = columns[i].width;
10318 		if(w == -1)
10319 			return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME
10320 		return w;
10321 	}
10322 
10323 	private void updateCalculatedWidth(bool informWindows) {
10324 		int padding;
10325 		version(win32_widgets)
10326 			padding = 4;
10327 		int remaining = this.width;
10328 		foreach(i, column; columns)
10329 			remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding;
10330 		remaining -= padding;
10331 		if(remaining < 0)
10332 			remaining = 0;
10333 
10334 		int percentTotal;
10335 		foreach(i, ref column; columns) {
10336 			percentTotal += column.widthPercent;
10337 
10338 			auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100;
10339 
10340 			column.calculatedWidth = c;
10341 
10342 			version(win32_widgets)
10343 			if(informWindows)
10344 				SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg
10345 		}
10346 
10347 		assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100.");
10348 		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).");
10349 
10350 
10351 	}
10352 
10353 	override void registerMovement() {
10354 		super.registerMovement();
10355 
10356 		updateCalculatedWidth(true);
10357 	}
10358 
10359 	/++
10360 		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.
10361 	+/
10362 	void setItemCount(int count) {
10363 		this.itemCount = count;
10364 		version(custom_widgets) {
10365 			tvwi.updateScrolls();
10366 			redraw();
10367 		} else version(win32_widgets) {
10368 			SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0);
10369 		}
10370 	}
10371 
10372 	/++
10373 		Clears all items;
10374 	+/
10375 	void clear() {
10376 		this.itemCount = 0;
10377 		this.columns = null;
10378 		version(custom_widgets) {
10379 			tvwi.header.updateHeaders();
10380 			tvwi.updateScrolls();
10381 			redraw();
10382 		} else version(win32_widgets) {
10383 			SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0);
10384 		}
10385 	}
10386 
10387 	/+
10388 	version(win32_widgets)
10389 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis)
10390 		auto itemId = dis.itemID;
10391 		auto hdc = dis.hDC;
10392 		auto rect = dis.rcItem;
10393 		switch(dis.itemAction) {
10394 			case ODA_DRAWENTIRE:
10395 
10396 				// FIXME: do other items
10397 				// FIXME: do the focus rectangle i guess
10398 				// FIXME: alignment
10399 				// FIXME: column width
10400 				// FIXME: padding left
10401 				// FIXME: check dpi scaling
10402 				// FIXME: don't owner draw unless it is necessary.
10403 
10404 				auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi
10405 				RECT itemRect;
10406 				itemRect.top = 1; // subitem idx, 1-based
10407 				itemRect.left = LVIR_BOUNDS;
10408 
10409 				SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect);
10410 				itemRect.left += padding;
10411 
10412 				getData(itemId, 0, (in char[] data) {
10413 					auto wdata = WCharzBuffer(data);
10414 					DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS);
10415 
10416 				});
10417 			goto case;
10418 			case ODA_FOCUS:
10419 				if(dis.itemState & ODS_FOCUS)
10420 					DrawFocusRect(hdc, &rect);
10421 			break;
10422 			case ODA_SELECT:
10423 				// itemState & ODS_SELECTED
10424 			break;
10425 			default:
10426 		}
10427 		return 1;
10428 	}
10429 	+/
10430 
10431 	version(win32_widgets) {
10432 		CellStyle last;
10433 		COLORREF defaultColor;
10434 		COLORREF defaultBackground;
10435 	}
10436 
10437 	version(win32_widgets)
10438 	override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
10439 		switch(code) {
10440 			case NM_CUSTOMDRAW:
10441 				auto s = cast(NMLVCUSTOMDRAW*) hdr;
10442 				switch(s.nmcd.dwDrawStage) {
10443 					case CDDS_PREPAINT:
10444 						if(getCellStyle is null)
10445 							return 0;
10446 
10447 						mustReturn = true;
10448 						return CDRF_NOTIFYITEMDRAW;
10449 					case CDDS_ITEMPREPAINT:
10450 						mustReturn = true;
10451 						return CDRF_NOTIFYSUBITEMDRAW;
10452 					case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
10453 						mustReturn = true;
10454 
10455 						if(getCellStyle is null) // this SHOULD never happen...
10456 							return 0;
10457 
10458 						if(s.iSubItem == 0) {
10459 							// Windows resets it per row so we'll use item 0 as a chance
10460 							// to capture these for later
10461 							defaultColor = s.clrText;
10462 							defaultBackground = s.clrTextBk;
10463 						}
10464 
10465 						auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem);
10466 						// if no special style and no reset needed...
10467 						if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init))
10468 							return 0; // allow default processing to continue
10469 
10470 						last = style;
10471 
10472 						// might still need to reset or use the preference.
10473 
10474 						if(style.flags & CellStyle.Flags.textColorSet)
10475 							s.clrText = style.textColor.asWindowsColorRef;
10476 						else
10477 							s.clrText = defaultColor; // reset in case it was set from last iteration not a fan
10478 						if(style.flags & CellStyle.Flags.backgroundColorSet)
10479 							s.clrTextBk = style.backgroundColor.asWindowsColorRef;
10480 						else
10481 							s.clrTextBk = defaultBackground; // need to reset it... not a fan of this
10482 
10483 						return CDRF_NEWFONT;
10484 					default:
10485 						return 0;
10486 
10487 				}
10488 			case NM_RETURN: // no need since i subclass keydown
10489 			break;
10490 			case LVN_COLUMNCLICK:
10491 				auto info = cast(LPNMLISTVIEW) hdr;
10492 				this.emit!HeaderClickedEvent(info.iSubItem);
10493 			break;
10494 			case (LVN_FIRST-21) /* LVN_HOTTRACK */:
10495 				// requires LVS_EX_TRACKSELECT
10496 				// sdpyPrintDebugString("here");
10497 				mustReturn = 1; // override Windows' auto selection
10498 			break;
10499 			case NM_CLICK:
10500 				NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr;
10501 				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);
10502 			break;
10503 			case NM_DBLCLK:
10504 				NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr;
10505 				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);
10506 			break;
10507 			case NM_RCLICK:
10508 				NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr;
10509 				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);
10510 			break;
10511 			case NM_RDBLCLK:
10512 				NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr;
10513 				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);
10514 			break;
10515 			case LVN_GETDISPINFO:
10516 				LV_DISPINFO* info = cast(LV_DISPINFO*) hdr;
10517 				if(info.item.mask & LVIF_TEXT) {
10518 					if(getData) {
10519 						getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) {
10520 							auto bfr = WCharzBuffer(dataReceived);
10521 							auto len = info.item.cchTextMax;
10522 							if(bfr.length < len)
10523 								len = cast(typeof(len)) bfr.length;
10524 							info.item.pszText[0 .. len] = bfr.ptr[0 .. len];
10525 							info.item.pszText[len] = 0;
10526 						});
10527 					} else {
10528 						info.item.pszText[0] = 0;
10529 					}
10530 					//info.item.iItem
10531 					//if(info.item.iSubItem)
10532 				}
10533 			break;
10534 			default:
10535 		}
10536 		return 0;
10537 	}
10538 
10539 	// FIXME: this throws off mouse calculations, it should only happen when we're at the top level or something idk
10540 	override bool encapsulatedChildren() {
10541 		return true;
10542 	}
10543 
10544 	/++
10545 		Informs the control that content has changed.
10546 
10547 		History:
10548 			Added November 10, 2021 (dub v10.4)
10549 	+/
10550 	void update() {
10551 		version(custom_widgets)
10552 			redraw();
10553 		else {
10554 			SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0));
10555 			UpdateWindow(hwnd);
10556 		}
10557 
10558 
10559 	}
10560 
10561 	/++
10562 		Called by the system to request the text content of an individual cell. You
10563 		should pass the text into the provided `sink` delegate. This function will be
10564 		called for each visible cell as-needed when drawing.
10565 	+/
10566 	void delegate(int row, int column, scope void delegate(in char[]) sink) getData;
10567 
10568 	/++
10569 		Available per-cell style customization options. Use one of the constructors
10570 		provided to set the values conveniently, or default construct it and set individual
10571 		values yourself. Just remember to set the `flags` so your values are actually used.
10572 		If the flag isn't set, the field is ignored and the system default is used instead.
10573 
10574 		This is returned by the [getCellStyle] delegate.
10575 
10576 		Examples:
10577 			---
10578 			// assumes you have a variables called `my_data` which is an array of arrays of numbers
10579 			auto table = new TableView(window);
10580 			// snip: you would set up columns here
10581 
10582 			// this is how you provide data to the table view class
10583 			table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) {
10584 				import std.conv;
10585 				sink(to!string(my_data[row][column]));
10586 			};
10587 
10588 			// and this is how you customize the colors
10589 			table.getCellStyle = delegate(int row, int column) {
10590 				return (my_data[row][column] < 0) ?
10591 					TableView.CellStyle(Color.red); // make negative numbers red
10592 					: TableView.CellStyle.init; // leave the rest alone
10593 			};
10594 			// snip: you would call table.setItemCount here then continue with the rest of your window setup work
10595 			---
10596 
10597 		History:
10598 			Added November 27, 2021 (dub v10.4)
10599 	+/
10600 	struct CellStyle {
10601 		/// 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.
10602 		this(Color textColor) {
10603 			this.textColor = textColor;
10604 			this.flags |= Flags.textColorSet;
10605 		}
10606 		/// Sets a custom text and background color.
10607 		this(Color textColor, Color backgroundColor) {
10608 			this.textColor = textColor;
10609 			this.backgroundColor = backgroundColor;
10610 			this.flags |= Flags.textColorSet | Flags.backgroundColorSet;
10611 		}
10612 		/++
10613 			Alignment is only supported on some platforms.
10614 		+/
10615 		this(TextAlignment alignment) {
10616 			this.alignment = alignment;
10617 			this.flags |= Flags.alignmentSet;
10618 		}
10619 		/// ditto
10620 		this(TextAlignment alignment, Color textColor) {
10621 			this.alignment = alignment;
10622 			this.textColor = textColor;
10623 			this.flags |= Flags.alignmentSet | Flags.textColorSet;
10624 		}
10625 		/// ditto
10626 		this(TextAlignment alignment, Color textColor, Color backgroundColor) {
10627 			this.alignment = alignment;
10628 			this.textColor = textColor;
10629 			this.backgroundColor = backgroundColor;
10630 			this.flags |= Flags.alignmentSet | Flags.textColorSet | Flags.backgroundColorSet;
10631 		}
10632 
10633 		TextAlignment alignment;
10634 		Color textColor;
10635 		Color backgroundColor;
10636 		int flags; /// bitmask of [Flags]
10637 		/// available options to combine into [flags]
10638 		enum Flags {
10639 			textColorSet = 1 << 0,
10640 			backgroundColorSet = 1 << 1,
10641 			alignmentSet = 1 << 2,
10642 		}
10643 	}
10644 	/++
10645 		Companion delegate to [getData] that allows you to custom style each
10646 		cell of the table.
10647 
10648 		Returns:
10649 			A [CellStyle] structure that describes the desired style for the
10650 			given cell. `return CellStyle.init` if you want the default style.
10651 
10652 		History:
10653 			Added November 27, 2021 (dub v10.4)
10654 	+/
10655 	CellStyle delegate(int row, int column) getCellStyle;
10656 
10657 	// i want to be able to do things like draw little colored things to show red for negative numbers
10658 	// or background color indicators or even in-cell charts
10659 	// void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell;
10660 
10661 	/++
10662 		When the user clicks on a header, this event is emitted. It has a member to identify which header (by index) was clicked.
10663 	+/
10664 	mixin Emits!HeaderClickedEvent;
10665 
10666 	/++
10667 		History:
10668 			Added March 2, 2025
10669 	+/
10670 	mixin Emits!CellClickedEvent;
10671 }
10672 
10673 /++
10674 	This is emitted by the [TableView] when a user clicks on a column header.
10675 
10676 	Its member `columnIndex` has the zero-based index of the column that was clicked.
10677 
10678 	The default behavior of this event is to do nothing, so `preventDefault` has no effect.
10679 
10680 	History:
10681 		Added November 27, 2021 (dub v10.4)
10682 
10683 		Made `final` on January 3, 2025
10684 +/
10685 final class HeaderClickedEvent : Event {
10686 	enum EventString = "HeaderClicked";
10687 	this(Widget target, int columnIndex) {
10688 		this.columnIndex = columnIndex;
10689 		super(EventString, target);
10690 	}
10691 
10692 	/// The index of the column
10693 	int columnIndex;
10694 
10695 	///
10696 	override @property int intValue() {
10697 		return columnIndex;
10698 	}
10699 }
10700 
10701 /++
10702 	History:
10703 		Added March 2, 2025
10704 +/
10705 final class CellClickedEvent : MouseEventBase {
10706 	enum EventString = "CellClicked";
10707 	this(Widget target, int rowIndex, int columnIndex, MouseButton button, MouseButtonLinear mouseButtonLinear, int x, int y, bool altKey, bool ctrlKey, bool shiftKey, bool isDoubleClick) {
10708 		this.rowIndex = rowIndex;
10709 		this.columnIndex = columnIndex;
10710 		this.button = button;
10711 		this.buttonLinear = mouseButtonLinear;
10712 		this.isDoubleClick = isDoubleClick;
10713 		this.clientX = x;
10714 		this.clientY = y;
10715 
10716 		this.altKey = altKey;
10717 		this.ctrlKey = ctrlKey;
10718 		this.shiftKey = shiftKey;
10719 
10720 		// import std.stdio; std.stdio.writeln(rowIndex, "x", columnIndex, " @ ", x, ",", y, " ", button, " ", isDoubleClick, " ", altKey, " ", ctrlKey, " ", shiftKey);
10721 
10722 		// FIXME: x, y, state, altButton etc?
10723 		super(EventString, target);
10724 	}
10725 
10726 	/++
10727 		See also: [button] inherited from the base class.
10728 
10729 		clientX and clientY are irrespective of scrolling - FIXME is that sane?
10730 	+/
10731 	int columnIndex;
10732 
10733 	/// ditto
10734 	int rowIndex;
10735 
10736 	/// ditto
10737 	bool isDoubleClick;
10738 
10739 	/+
10740 	// i could do intValue as a linear index if we know the width
10741 	// and a stringValue with the string in the cell. but idk if worth.
10742 	override @property int intValue() {
10743 		return columnIndex;
10744 	}
10745 	+/
10746 
10747 }
10748 
10749 version(custom_widgets)
10750 private class TableViewWidgetInner : Widget {
10751 
10752 // wrap this thing in a ScrollMessageWidget
10753 
10754 	TableView tvw;
10755 	ScrollMessageWidget smw;
10756 	HeaderWidget header;
10757 
10758 	this(TableView tvw, ScrollMessageWidget smw) {
10759 		this.tvw = tvw;
10760 		this.smw = smw;
10761 		super(smw);
10762 
10763 		this.tabStop = true;
10764 
10765 		header = new HeaderWidget(this, smw.getHeader());
10766 
10767 		smw.addEventListener("scroll", () {
10768 			this.redraw();
10769 			header.redraw();
10770 		});
10771 
10772 
10773 		// I need headers outside the scroll area but rendered on the same line as the up arrow
10774 		// FIXME: add a fixed header to the SMW
10775 	}
10776 
10777 	enum padding = 3;
10778 
10779 	void updateScrolls() {
10780 		int w;
10781 		foreach(idx, column; tvw.columns) {
10782 			w += column.calculatedWidth;
10783 		}
10784 		smw.setTotalArea(w, tvw.itemCount);
10785 		columnsWidth = w;
10786 	}
10787 
10788 	private int columnsWidth;
10789 
10790 	private int lh() { return scaleWithDpi(16); } // FIXME lineHeight
10791 
10792 	override void registerMovement() {
10793 		super.registerMovement();
10794 		// FIXME: actual column width. it might need to be done per-pixel instead of per-column
10795 		smw.setViewableArea(this.width, this.height / lh);
10796 	}
10797 
10798 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
10799 		int x;
10800 		int y;
10801 
10802 		int row = smw.position.y;
10803 
10804 		foreach(lol; 0 .. this.height / lh) {
10805 			if(row >= tvw.itemCount)
10806 				break;
10807 			x = 0;
10808 			foreach(columnNumber, column; tvw.columns) {
10809 				auto x2 = x + column.calculatedWidth;
10810 				auto smwx = smw.position.x;
10811 
10812 				if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) {
10813 					auto startX = x;
10814 					auto endX = x + column.calculatedWidth;
10815 					switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) {
10816 						case TextAlignment.Left: startX += padding; break;
10817 						case TextAlignment.Center: startX += padding; endX -= padding; break;
10818 						case TextAlignment.Right: endX -= padding; break;
10819 						default: /* broken */ break;
10820 					}
10821 					if(column.width != 0) // no point drawing an invisible column
10822 					tvw.getData(row, cast(int) columnNumber, (in char[] info) {
10823 						auto endClip = endX - smw.position.x;
10824 						if(endClip > this.width - padding)
10825 							endClip = this.width - padding;
10826 						auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endClip, y + lh)));
10827 
10828 						void dotext(WidgetPainter painter, TextAlignment alignment) {
10829 							painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x - padding, y + lh), alignment);
10830 						}
10831 
10832 						if(tvw.getCellStyle !is null) {
10833 							auto style = tvw.getCellStyle(row, cast(int) columnNumber);
10834 
10835 							if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) {
10836 								auto tempPainter = painter;
10837 								tempPainter.fillColor = style.backgroundColor;
10838 								tempPainter.outlineColor = style.backgroundColor;
10839 
10840 								tempPainter.drawRectangle(Point(startX - smw.position.x, y),
10841 									Point(endX - smw.position.x, y + lh));
10842 							}
10843 							auto tempPainter = painter;
10844 							if(style.flags & TableView.CellStyle.Flags.textColorSet)
10845 								tempPainter.outlineColor = style.textColor;
10846 
10847 							auto alignment = column.alignment;
10848 							if(style.flags & TableView.CellStyle.Flags.alignmentSet)
10849 								alignment = style.alignment;
10850 							dotext(tempPainter, alignment);
10851 						} else {
10852 							dotext(painter, column.alignment);
10853 						}
10854 					});
10855 				}
10856 
10857 				x += column.calculatedWidth;
10858 			}
10859 			row++;
10860 			y += lh;
10861 		}
10862 		return bounds;
10863 	}
10864 
10865 	static class Style : Widget.Style {
10866 		override WidgetBackground background() {
10867 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
10868 		}
10869 	}
10870 	mixin OverrideStyle!Style;
10871 
10872 	private static class HeaderWidget : Widget {
10873 		/+
10874 			maybe i should do a splitter thing on top of the other widgets
10875 			so the splitter itself isn't really drawn but still replies to mouse events?
10876 		+/
10877 		this(TableViewWidgetInner tvw, Widget parent) {
10878 			super(parent);
10879 			this.tvw = tvw;
10880 
10881 			this.remainder = new Button("", this);
10882 
10883 			this.addEventListener((scope ClickEvent ev) {
10884 				int header = -1;
10885 				foreach(idx, child; this.children[1 .. $]) {
10886 					if(child is ev.target) {
10887 						header = cast(int) idx;
10888 						break;
10889 					}
10890 				}
10891 
10892 				if(header != -1) {
10893 					auto hce = new HeaderClickedEvent(tvw.tvw, header);
10894 					hce.dispatch();
10895 				}
10896 
10897 			});
10898 		}
10899 
10900 		override int minHeight() {
10901 			return defaultLineHeight + 4; // same as Button
10902 		}
10903 
10904 		void updateHeaders() {
10905 			foreach(child; children[1 .. $])
10906 				child.removeWidget();
10907 
10908 			foreach(column; tvw.tvw.columns) {
10909 				// the cast is ok because I dup it above, just the type is never changed.
10910 				// all this is private so it should never get messed up.
10911 				new Button(ImageLabel(cast(string) column.name, column.alignment), this);
10912 			}
10913 		}
10914 
10915 		Button remainder;
10916 		TableViewWidgetInner tvw;
10917 
10918 		override void recomputeChildLayout() {
10919 			registerMovement();
10920 			int pos;
10921 			foreach(idx, child; children[1 .. $]) {
10922 				if(idx >= tvw.tvw.columns.length)
10923 					continue;
10924 				child.x = pos;
10925 				child.y = 0;
10926 				child.width = tvw.tvw.columns[idx].calculatedWidth;
10927 				child.height = scaleWithDpi(16);// this.height;
10928 				pos += child.width;
10929 
10930 				child.recomputeChildLayout();
10931 			}
10932 
10933 			if(remainder is null)
10934 				return;
10935 
10936 			remainder.x = pos;
10937 			remainder.y = 0;
10938 			if(pos < this.width)
10939 				remainder.width = this.width - pos;// + 4;
10940 			else
10941 				remainder.width = 0;
10942 			remainder.height = scaleWithDpi(16);
10943 
10944 			remainder.recomputeChildLayout();
10945 		}
10946 
10947 		// for the scrollable children mixin
10948 		Point scrollOrigin() {
10949 			return Point(tvw.smw.position.x, 0);
10950 		}
10951 		void paintFrameAndBackground(WidgetPainter painter) { }
10952 
10953 		// for mouse event dispatching
10954 		override protected void addScrollPosition(ref int x, ref int y) {
10955 			x += scrollOrigin.x;
10956 			y += scrollOrigin.y;
10957 		}
10958 
10959 		mixin ScrollableChildren;
10960 	}
10961 
10962 	private void emitCellClickedEvent(scope MouseEventBase event, bool isDoubleClick) {
10963 		int mx = event.clientX + smw.position.x;
10964 		int my = event.clientY;
10965 
10966 		Widget par = this;
10967 		while(par && !par.encapsulatedChildren) {
10968 			my -= par.y; // to undo the encapsulatedChildren adjustClientCoordinates effect
10969 			par = par.parent;
10970 		}
10971 		if(par is null)
10972 			my = event.clientY; // encapsulatedChildren not present?
10973 
10974 		int row = my / lh + smw.position.y; // scrolling here is done per-item, not per pixel
10975 		if(row > tvw.itemCount)
10976 			row = -1;
10977 
10978 		int column = -1;
10979 		if(row != -1) {
10980 			int pos;
10981 			foreach(idx, col; tvw.columns) {
10982 				pos += col.calculatedWidth;
10983 				if(mx < pos) {
10984 					column = cast(int) idx;
10985 					break;
10986 				}
10987 			}
10988 		}
10989 
10990 		// wtf are these casts about?
10991 		tvw.emit!CellClickedEvent(row, column, cast(MouseButton) event.button, cast(MouseButtonLinear) event.buttonLinear, event.clientX, event.clientY, event.altKey, event.ctrlKey, event.shiftKey, isDoubleClick);
10992 	}
10993 
10994 	override void defaultEventHandler_click(scope ClickEvent ce) {
10995 		// FIXME: should i filter mouse wheel events? Windows doesn't send them but i can.
10996 		emitCellClickedEvent(ce, false);
10997 	}
10998 
10999 	override void defaultEventHandler_dblclick(scope DoubleClickEvent ce) {
11000 		emitCellClickedEvent(ce, true);
11001 	}
11002 }
11003 
11004 /+
11005 
11006 // given struct / array / number / string / etc, make it viewable and editable
11007 class DataViewerWidget : Widget {
11008 
11009 }
11010 +/
11011 
11012 /++
11013 	A line edit box with an associated label.
11014 
11015 	History:
11016 		On May 17, 2021, the default internal layout was changed from horizontal to vertical.
11017 
11018 		```
11019 		Old: ________
11020 
11021 		New:
11022 		____________
11023 		```
11024 
11025 		To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);`
11026 
11027 		You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a
11028 		horizontal label but left aligned. You may also consider a [GridLayout].
11029 +/
11030 alias LabeledLineEdit = Labeled!LineEdit;
11031 
11032 private int widthThatWouldFitChildLabels(Widget w) {
11033 	if(w is null)
11034 		return 0;
11035 
11036 	int max;
11037 
11038 	if(auto label = cast(TextLabel) w) {
11039 		return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight();
11040 	} else {
11041 		foreach(child; w.children) {
11042 			max = mymax(max, widthThatWouldFitChildLabels(child));
11043 		}
11044 	}
11045 
11046 	return max;
11047 }
11048 
11049 /++
11050 	History:
11051 		Added May 19, 2021
11052 +/
11053 class Labeled(T) : Widget {
11054 	///
11055 	this(string label, Widget parent) {
11056 		super(parent);
11057 		initialize!VerticalLayout(label, TextAlignment.Left, parent);
11058 	}
11059 
11060 	/++
11061 		History:
11062 			The alignment parameter was added May 17, 2021
11063 	+/
11064 	this(string label, TextAlignment alignment, Widget parent) {
11065 		super(parent);
11066 		initialize!HorizontalLayout(label, alignment, parent);
11067 	}
11068 
11069 	private void initialize(L)(string label, TextAlignment alignment, Widget parent) {
11070 		tabStop = false;
11071 		horizontal = is(L == HorizontalLayout);
11072 		auto hl = new L(this);
11073 		if(horizontal) {
11074 			static class SpecialTextLabel : TextLabel {
11075 				Widget outerParent;
11076 
11077 				this(string label, TextAlignment alignment, Widget outerParent, Widget parent) {
11078 					this.outerParent = outerParent;
11079 					super(label, alignment, parent);
11080 				}
11081 
11082 				override int flexBasisWidth() {
11083 					return widthThatWouldFitChildLabels(outerParent);
11084 				}
11085 				/+
11086 				override int widthShrinkiness() { return 0; }
11087 				override int widthStretchiness() { return 1; }
11088 				+/
11089 
11090 				override int paddingRight() { return 6; }
11091 				override int paddingLeft() { return 9; }
11092 
11093 				override int paddingTop() { return 3; }
11094 			}
11095 			this.label = new SpecialTextLabel(label, alignment, parent, hl);
11096 		} else
11097 			this.label = new TextLabel(label, alignment, hl);
11098 		this.lineEdit = new T(hl);
11099 
11100 		this.label.labelFor = this.lineEdit;
11101 	}
11102 
11103 	private bool horizontal;
11104 
11105 	TextLabel label; ///
11106 	T lineEdit; ///
11107 
11108 	override int flexBasisWidth() { return 250; }
11109 	override int widthShrinkiness() { return 1; }
11110 
11111 	override int minHeight() {
11112 		return this.children[0].minHeight;
11113 	}
11114 	override int maxHeight() { return minHeight(); }
11115 	override int marginTop() { return 4; }
11116 	override int marginBottom() { return 4; }
11117 
11118 	// FIXME: i should prolly call it value as well as content tbh
11119 
11120 	///
11121 	@property string content() {
11122 		return lineEdit.content;
11123 	}
11124 	///
11125 	@property void content(string c) {
11126 		return lineEdit.content(c);
11127 	}
11128 
11129 	///
11130 	void selectAll() {
11131 		lineEdit.selectAll();
11132 	}
11133 
11134 	override void focus() {
11135 		lineEdit.focus();
11136 	}
11137 }
11138 
11139 /++
11140 	A labeled password edit.
11141 
11142 	History:
11143 		Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021
11144 
11145 		The default parameters for the constructors were also removed on May 19, 2021
11146 +/
11147 alias LabeledPasswordEdit = Labeled!PasswordEdit;
11148 
11149 private string toMenuLabel(string s) {
11150 	string n;
11151 	n.reserve(s.length);
11152 	foreach(c; s)
11153 		if(c == '_')
11154 			n ~= ' ';
11155 		else
11156 			n ~= c;
11157 	return n;
11158 }
11159 
11160 private void autoExceptionHandler(Exception e) {
11161 	messageBox(e.msg);
11162 }
11163 
11164 void callAsIfClickedFromMenu(alias fn)(auto ref __traits(parent, fn) _this, Window window) {
11165 	makeAutomaticHandler!(fn)(window, &__traits(child, _this, fn))();
11166 }
11167 
11168 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) {
11169 	static if(is(T : void delegate())) {
11170 		return () {
11171 			try
11172 				t();
11173 			catch(Exception e)
11174 				autoExceptionHandler(e);
11175 		};
11176 	} else static if(is(typeof(fn) Params == __parameters)) {
11177 		static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) {
11178 			return () {
11179 				void onOK(string s) {
11180 					member = s;
11181 					try
11182 						t(Params[0](s));
11183 					catch(Exception e)
11184 						autoExceptionHandler(e);
11185 				}
11186 
11187 				if(
11188 					(type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export")))
11189 					|| type == FileDialogType.Save)
11190 				{
11191 					getSaveFileName(window, &onOK, member, filters, null);
11192 				} else
11193 					getOpenFileName(window, &onOK, member, filters, null);
11194 			};
11195 		} else {
11196 			struct S {
11197 				static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) {
11198 					pragma(msg, "warning: automatic handler of params not yet implemented on your compiler");
11199 				} else mixin(q{
11200 				static foreach(idx, ignore; Params) {
11201 					mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";");
11202 				}
11203 				});
11204 			}
11205 			return () {
11206 				dialog(window, (S s) {
11207 					try {
11208 						static if(is(typeof(t) Ret == return)) {
11209 							static if(is(Ret == void)) {
11210 								t(s.tupleof);
11211 							} else {
11212 								auto ret = t(s.tupleof);
11213 								import std.conv;
11214 								messageBox(to!string(ret), "Returned Value");
11215 							}
11216 						}
11217 					} catch(Exception e)
11218 						autoExceptionHandler(e);
11219 				}, null, __traits(identifier, fn));
11220 			};
11221 		}
11222 	}
11223 }
11224 
11225 private template hasAnyRelevantAnnotations(a...) {
11226 	bool helper() {
11227 		bool any;
11228 		foreach(attr; a) {
11229 			static if(is(typeof(attr) == .menu))
11230 				any = true;
11231 			else static if(is(typeof(attr) == .toolbar))
11232 				any = true;
11233 			else static if(is(attr == .separator))
11234 				any = true;
11235 			else static if(is(typeof(attr) == .accelerator))
11236 				any = true;
11237 			else static if(is(typeof(attr) == .hotkey))
11238 				any = true;
11239 			else static if(is(typeof(attr) == .icon))
11240 				any = true;
11241 			else static if(is(typeof(attr) == .label))
11242 				any = true;
11243 			else static if(is(typeof(attr) == .tip))
11244 				any = true;
11245 		}
11246 		return any;
11247 	}
11248 
11249 	enum bool hasAnyRelevantAnnotations = helper();
11250 }
11251 
11252 /++
11253 	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.
11254 +/
11255 class MainWindow : Window {
11256 	///
11257 	this(string title = null, int initialWidth = 500, int initialHeight = 500) {
11258 		super(initialWidth, initialHeight, title);
11259 
11260 		_clientArea = new ClientAreaWidget();
11261 		_clientArea.x = 0;
11262 		_clientArea.y = 0;
11263 		_clientArea.width = this.width;
11264 		_clientArea.height = this.height;
11265 		_clientArea.tabStop = false;
11266 
11267 		super.addChild(_clientArea);
11268 
11269 		statusBar = new StatusBar(this);
11270 	}
11271 
11272 	/++
11273 		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).
11274 
11275 		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')`.
11276 
11277 		You can also use `@separator` to put a separating line in the menu before the function.
11278 
11279 		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.
11280 
11281 		Let's look at a complete example:
11282 
11283 	---
11284 	import arsd.minigui;
11285 
11286 	void main() {
11287 		auto window = new MainWindow();
11288 
11289 		// we can add widgets before or after setting the menu, either way is fine.
11290 		// i'll do it before here so the local variables are available to the commands.
11291 
11292 		auto textEdit = new TextEdit(window);
11293 
11294 		// Remember, in D, you can define structs inside of functions
11295 		// and those structs can access the function's local variables.
11296 		//
11297 		// Of course, you might also want to do this separately, and if you
11298 		// do, make sure you keep a reference to the window as a struct data
11299 		// member so you can refer to it in cases like this Exit function.
11300 		struct Commands {
11301 			// the & in the string indicates that the next letter is the hotkey
11302 			// to access it from the keyboard (so here, alt+f will open the
11303 			// file menu)
11304 			@menu("&File") {
11305 				@accelerator("Ctrl+N")
11306 				@hotkey('n')
11307 				@icon(GenericIcons.New) // add an icon to the action
11308 				@toolbar("File") // adds it to a toolbar.
11309 				// The toolbar name is never visible to the user, but is used to group icons.
11310 				void New() {
11311 					previousFileReferenced = null;
11312 					textEdit.content = "";
11313 				}
11314 
11315 				@icon(GenericIcons.Open)
11316 				@toolbar("File")
11317 				@hotkey('s')
11318 				@accelerator("Ctrl+O")
11319 				void Open(FileName!() filename) {
11320 					import std.file;
11321 					textEdit.content = std.file.readText(filename);
11322 				}
11323 
11324 				@icon(GenericIcons.Save)
11325 				@toolbar("File")
11326 				@accelerator("Ctrl+S")
11327 				@hotkey('s')
11328 				void Save() {
11329 					// these are still functions, so of course you can
11330 					// still call them yourself too
11331 					Save_As(previousFileReferenced);
11332 				}
11333 
11334 				// underscores translate to spaces in the visible name
11335 				@hotkey('a')
11336 				void Save_As(FileName!() filename) {
11337 					import std.file;
11338 					std.file.write(previousFileReferenced, textEdit.content);
11339 				}
11340 
11341 				// you can put the annotations before or after the function name+args and it works the same way
11342 				@separator
11343 				void Exit() @accelerator("Alt+F4") @hotkey('x') {
11344 					window.close();
11345 				}
11346 			}
11347 
11348 			@menu("&Edit") {
11349 				// not putting accelerators here because the text edit widget
11350 				// does it locally, so no need to duplicate it globally.
11351 
11352 				@icon(GenericIcons.Undo)
11353 				void Undo() @toolbar("Undo") {
11354 					textEdit.undo();
11355 				}
11356 
11357 				@separator
11358 
11359 				@icon(GenericIcons.Cut)
11360 				void Cut() @toolbar("Edit") {
11361 					textEdit.cut();
11362 				}
11363 				@icon(GenericIcons.Copy)
11364 				void Copy() @toolbar("Edit") {
11365 					textEdit.copy();
11366 				}
11367 				@icon(GenericIcons.Paste)
11368 				void Paste() @toolbar("Edit") {
11369 					textEdit.paste();
11370 				}
11371 
11372 				@separator
11373 				void Select_All() {
11374 					textEdit.selectAll();
11375 				}
11376 			}
11377 
11378 			@menu("Help") {
11379 				void About() @accelerator("F1") {
11380 					window.messageBox("A minigui sample program.");
11381 				}
11382 
11383 				// @label changes the name in the menu from what is in the code
11384 				@label("In Menu Name")
11385 				void otherNameInCode() {}
11386 			}
11387 		}
11388 
11389 		// declare the object that holds the commands, and set
11390 		// and members you want from it
11391 		Commands commands;
11392 
11393 		// and now tell minigui to do its magic and create the ui for it!
11394 		window.setMenuAndToolbarFromAnnotatedCode(commands);
11395 
11396 		// then, loop the window normally;
11397 		window.loop();
11398 
11399 		// important to note that the `commands` variable must live through the window's whole life cycle,
11400 		// or you can have crashes. If you declare the variable and loop in different functions, make sure
11401 		// you do `new Commands` so the garbage collector can take over management of it for you.
11402 	}
11403 	---
11404 
11405 	Note that you can call this function multiple times and it will add the items in order to the given items.
11406 
11407 	+/
11408 	void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) {
11409 		setMenuAndToolbarFromAnnotatedCode_internal(t);
11410 	}
11411 	/// ditto
11412 	void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) {
11413 		setMenuAndToolbarFromAnnotatedCode_internal(t);
11414 	}
11415 	void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
11416 		auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
11417 		Menu[string] mcs;
11418 
11419 		alias ToolbarSection = ToolBar.ToolbarSection;
11420 		ToolbarSection[] toolbarSections;
11421 
11422 		foreach(menu; menuBar.subMenus) {
11423 			mcs[menu.label] = menu;
11424 		}
11425 
11426 		foreach(memberName; __traits(derivedMembers, T)) {
11427 			static if(memberName != "this")
11428 			static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
11429 				.menu menu;
11430 				.toolbar toolbar;
11431 				bool separator;
11432 				.accelerator accelerator;
11433 				.hotkey hotkey;
11434 				.icon icon;
11435 				string label;
11436 				string tip;
11437 				foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
11438 					static if(is(typeof(attr) == .menu))
11439 						menu = attr;
11440 					else static if(is(typeof(attr) == .toolbar))
11441 						toolbar = attr;
11442 					else static if(is(attr == .separator))
11443 						separator = true;
11444 					else static if(is(typeof(attr) == .accelerator))
11445 						accelerator = attr;
11446 					else static if(is(typeof(attr) == .hotkey))
11447 						hotkey = attr;
11448 					else static if(is(typeof(attr) == .icon))
11449 						icon = attr;
11450 					else static if(is(typeof(attr) == .label))
11451 						label = attr.label;
11452 					else static if(is(typeof(attr) == .tip))
11453 						tip = attr.tip;
11454 				}
11455 
11456 				if(menu !is .menu.init || toolbar !is .toolbar.init) {
11457 					ushort correctIcon = icon.id; // FIXME
11458 					if(label.length == 0)
11459 						label = memberName.toMenuLabel;
11460 
11461 					auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName));
11462 
11463 					auto action = new Action(label, correctIcon, handler);
11464 
11465 					if(accelerator.keyString.length) {
11466 						auto ke = KeyEvent.parse(accelerator.keyString);
11467 						action.accelerator = ke;
11468 						accelerators[ke.toStr] = handler;
11469 					}
11470 
11471 					if(toolbar !is .toolbar.init) {
11472 						bool found;
11473 						foreach(ref section; toolbarSections)
11474 							if(section.name == toolbar.groupName) {
11475 								section.actions ~= action;
11476 								found = true;
11477 								break;
11478 							}
11479 						if(!found) {
11480 							toolbarSections ~= ToolbarSection(toolbar.groupName, [action]);
11481 						}
11482 					}
11483 					if(menu !is .menu.init) {
11484 						Menu mc;
11485 						if(menu.name in mcs) {
11486 							mc = mcs[menu.name];
11487 						} else {
11488 							mc = new Menu(menu.name, this);
11489 							menuBar.addItem(mc);
11490 							mcs[menu.name] = mc;
11491 						}
11492 
11493 						if(separator)
11494 							mc.addSeparator();
11495 						auto mi = mc.addItem(new MenuItem(action));
11496 
11497 						if(hotkey !is .hotkey.init)
11498 							mi.hotkey = hotkey.ch;
11499 					}
11500 				}
11501 			}
11502 		}
11503 
11504 		this.menuBar = menuBar;
11505 
11506 		if(toolbarSections.length) {
11507 			auto tb = new ToolBar(toolbarSections, this);
11508 		}
11509 	}
11510 
11511 	void delegate()[string] accelerators;
11512 
11513 	override void defaultEventHandler_keydown(KeyDownEvent event) {
11514 		auto str = event.originalKeyEvent.toStr;
11515 		if(auto acl = str in accelerators)
11516 			(*acl)();
11517 
11518 		// Windows this this automatically so only on custom need we implement it
11519 		version(custom_widgets) {
11520 			if(event.altKey && this.menuBar) {
11521 				foreach(item; this.menuBar.items) {
11522 					if(item.hotkey == keyToLetterCharAssumingLotsOfThingsThatYouMightBetterNotAssume(event.key)) {
11523 						// FIXME this kinda sucks but meh just pretending to click on it to trigger other existing mediocre code
11524 						item.dynamicState = DynamicState.hover | DynamicState.depressed;
11525 						item.redraw();
11526 						auto e = new MouseDownEvent(item);
11527 						e.dispatch();
11528 						break;
11529 					}
11530 				}
11531 			}
11532 
11533 			if(event.key == Key.Menu) {
11534 				showContextMenu(-1, -1);
11535 			}
11536 		}
11537 
11538 		super.defaultEventHandler_keydown(event);
11539 	}
11540 
11541 	override void defaultEventHandler_mouseover(MouseOverEvent event) {
11542 		super.defaultEventHandler_mouseover(event);
11543 		if(this.statusBar !is null && event.target.statusTip.length)
11544 			this.statusBar.parts[0].content = event.target.statusTip;
11545 		else if(this.statusBar !is null && this.statusTip.length)
11546 			this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString();
11547 	}
11548 
11549 	override void addChild(Widget c, int position = int.max) {
11550 		if(auto tb = cast(ToolBar) c)
11551 			version(win32_widgets)
11552 				super.addChild(c, 0);
11553 			else version(custom_widgets)
11554 				super.addChild(c, menuBar ? 1 : 0);
11555 			else static assert(0);
11556 		else
11557 			clientArea.addChild(c, position);
11558 	}
11559 
11560 	ToolBar _toolBar;
11561 	///
11562 	ToolBar toolBar() { return _toolBar; }
11563 	///
11564 	ToolBar toolBar(ToolBar t) {
11565 		_toolBar = t;
11566 		foreach(child; this.children)
11567 			if(child is t)
11568 				return t;
11569 		version(win32_widgets)
11570 			super.addChild(t, 0);
11571 		else version(custom_widgets)
11572 			super.addChild(t, menuBar ? 1 : 0);
11573 		else static assert(0);
11574 		return t;
11575 	}
11576 
11577 	MenuBar _menu;
11578 	///
11579 	MenuBar menuBar() { return _menu; }
11580 	///
11581 	MenuBar menuBar(MenuBar m) {
11582 		if(m is _menu) {
11583 			version(custom_widgets)
11584 				queueRecomputeChildLayout();
11585 			return m;
11586 		}
11587 
11588 		if(_menu !is null) {
11589 			// make sure it is sanely removed
11590 			// FIXME
11591 		}
11592 
11593 		_menu = m;
11594 
11595 		version(win32_widgets) {
11596 			SetMenu(parentWindow.win.impl.hwnd, m.handle);
11597 		} else version(custom_widgets) {
11598 			super.addChild(m, 0);
11599 
11600 		//	clientArea.y = menu.height;
11601 		//	clientArea.height = this.height - menu.height;
11602 
11603 			queueRecomputeChildLayout();
11604 		} else static assert(false);
11605 
11606 		return _menu;
11607 	}
11608 	private Widget _clientArea;
11609 	///
11610 	@property Widget clientArea() { return _clientArea; }
11611 	protected @property void clientArea(Widget wid) {
11612 		_clientArea = wid;
11613 	}
11614 
11615 	private StatusBar _statusBar;
11616 	/++
11617 		Returns the window's [StatusBar]. Be warned it may be `null`.
11618 	+/
11619 	@property StatusBar statusBar() { return _statusBar; }
11620 	/// ditto
11621 	@property void statusBar(StatusBar bar) {
11622 		if(_statusBar !is null)
11623 			_statusBar.removeWidget();
11624 		_statusBar = bar;
11625 		if(bar !is null)
11626 			super.addChild(_statusBar);
11627 	}
11628 }
11629 
11630 /+
11631 	This is really an implementation detail of [MainWindow]
11632 +/
11633 private class ClientAreaWidget : Widget {
11634 	this() {
11635 		this.tabStop = false;
11636 		super(null);
11637 		//sa = new ScrollableWidget(this);
11638 	}
11639 	/*
11640 	ScrollableWidget sa;
11641 	override void addChild(Widget w, int position) {
11642 		if(sa is null)
11643 			super.addChild(w, position);
11644 		else {
11645 			sa.addChild(w, position);
11646 			sa.setContentSize(this.minWidth + 1, this.minHeight);
11647 			writeln(sa.contentWidth, "x", sa.contentHeight);
11648 		}
11649 	}
11650 	*/
11651 }
11652 
11653 /**
11654 	Toolbars are lists of buttons (typically icons) that appear under the menu.
11655 	Each button ought to correspond to a menu item, represented by [Action] objects.
11656 */
11657 class ToolBar : Widget {
11658 	version(win32_widgets) {
11659 		private int idealHeight;
11660 		override int minHeight() { return idealHeight; }
11661 		override int maxHeight() { return idealHeight; }
11662 	} else version(custom_widgets) {
11663 		override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; }
11664 		override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; }
11665 	} else static assert(false);
11666 	override int heightStretchiness() { return 0; }
11667 
11668 	static struct ToolbarSection {
11669 		string name;
11670 		Action[] actions;
11671 	}
11672 
11673 	version(win32_widgets) {
11674 		HIMAGELIST imageListSmall;
11675 		HIMAGELIST imageListLarge;
11676 	}
11677 
11678 	this(Widget parent) {
11679 		this(cast(ToolbarSection[]) null, parent);
11680 	}
11681 
11682 	version(win32_widgets)
11683 	void changeIconSize(bool useLarge) {
11684 		SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall));
11685 
11686 		/+
11687 		SIZE size;
11688 		import core.sys.windows.commctrl;
11689 		SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size);
11690 		idealHeight = size.cy + 4; // the plus 4 is a hack
11691 		+/
11692 
11693 		idealHeight = useLarge ? 34 : 26;
11694 
11695 		if(parent) {
11696 			parent.queueRecomputeChildLayout();
11697 			parent.redraw();
11698 		}
11699 
11700 		SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4));
11701 		SendMessageW(hwnd, TB_AUTOSIZE, 0, 0);
11702 	}
11703 
11704 	/++
11705 		History:
11706 			The `ToolbarSection` overload was added December 31, 2024
11707 	+/
11708 	this(Action[] actions, Widget parent) {
11709 		this([ToolbarSection(null, actions)], parent);
11710 	}
11711 
11712 	/// ditto
11713 	this(ToolbarSection[] sections, Widget parent) {
11714 		super(parent);
11715 
11716 		tabStop = false;
11717 
11718 		version(win32_widgets) {
11719 			// so i like how the flat thing looks on windows, but not on wine
11720 			// and eh, with windows visual styles enabled it looks cool anyway soooo gonna
11721 			// leave it commented
11722 			createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS);
11723 
11724 			SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/);
11725 
11726 			imageListSmall = ImageList_Create(
11727 				// width, height
11728 				16, 16,
11729 				ILC_COLOR16 | ILC_MASK,
11730 				16 /*numberOfButtons*/, 0);
11731 
11732 			imageListLarge = ImageList_Create(
11733 				// width, height
11734 				24, 24,
11735 				ILC_COLOR16 | ILC_MASK,
11736 				16 /*numberOfButtons*/, 0);
11737 
11738 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall);
11739 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL);
11740 
11741 			SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge);
11742 			SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL);
11743 
11744 			SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0);
11745 
11746 			TBBUTTON[] buttons;
11747 
11748 			// FIXME: I_IMAGENONE is if here is no icon
11749 			foreach(sidx, section; sections) {
11750 				if(sidx)
11751 					buttons ~= TBBUTTON(
11752 						scaleWithDpi(4),
11753 						0,
11754 						TBSTATE_ENABLED, // state
11755 						TBSTYLE_SEP | BTNS_SEP, // style
11756 						0, // reserved array, just zero it out
11757 						0, // dwData
11758 						-1
11759 					);
11760 
11761 				foreach(action; section.actions)
11762 					buttons ~= TBBUTTON(
11763 						MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0),
11764 						action.id,
11765 						TBSTATE_ENABLED, // state
11766 						0, // style
11767 						0, // reserved array, just zero it out
11768 						0, // dwData
11769 						cast(size_t) toWstringzInternal(action.label) // INT_PTR
11770 					);
11771 			}
11772 
11773 			SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0);
11774 			SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr);
11775 
11776 			/*
11777 			RECT rect;
11778 			GetWindowRect(hwnd, &rect);
11779 			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
11780 			*/
11781 
11782 			dpiChanged(); // to load the things calling changeIconSize the first time
11783 
11784 			assert(idealHeight);
11785 		} else version(custom_widgets) {
11786 			foreach(sidx, section; sections) {
11787 				if(sidx)
11788 					new HorizontalSpacer(4, this);
11789 				foreach(action; section.actions)
11790 					new ToolButton(action, this);
11791 			}
11792 		} else static assert(false);
11793 	}
11794 
11795 	override void recomputeChildLayout() {
11796 		.recomputeChildLayout!"width"(this);
11797 	}
11798 
11799 
11800 	version(win32_widgets)
11801 	override protected void dpiChanged() {
11802 		auto sz = scaleWithDpi(16);
11803 		if(sz >= 20)
11804 			changeIconSize(true);
11805 		else
11806 			changeIconSize(false);
11807 	}
11808 }
11809 
11810 /// 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.
11811 class ToolButton : Button {
11812 	///
11813 	this(Action action, Widget parent) {
11814 		super(action.label, parent);
11815 		tabStop = false;
11816 		this.action = action;
11817 	}
11818 
11819 	version(custom_widgets)
11820 	override void defaultEventHandler_click(ClickEvent event) {
11821 		foreach(handler; action.triggered)
11822 			handler();
11823 	}
11824 
11825 	Action action;
11826 
11827 	override int maxWidth() { return toolbarIconSize; }
11828 	override int minWidth() { return toolbarIconSize; }
11829 	override int maxHeight() { return toolbarIconSize; }
11830 	override int minHeight() { return toolbarIconSize; }
11831 
11832 	version(custom_widgets)
11833 	override void paint(WidgetPainter painter) {
11834 	painter.drawThemed(delegate Rectangle (const Rectangle bounds) {
11835 		painter.outlineColor = Color.black;
11836 
11837 		immutable multiplier = toolbarIconSize / 4;
11838 		immutable divisor = 16 / 4;
11839 
11840 		int ScaledNumber(int n) {
11841 			// return n * multiplier / divisor;
11842 			auto s = n * multiplier;
11843 			auto it = s / divisor;
11844 			auto rem = s % divisor;
11845 			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
11846 				it++;
11847 			return it;
11848 		}
11849 
11850 		arsd.color.Point Point(int x, int y) {
11851 			return arsd.color.Point(ScaledNumber(x), ScaledNumber(y));
11852 		}
11853 
11854 		switch(action.iconId) {
11855 			case GenericIcons.New:
11856 				painter.fillColor = Color.white;
11857 				painter.drawPolygon(
11858 					Point(3, 2), Point(3, 13), Point(12, 13), Point(12, 6),
11859 					Point(8, 2), Point(8, 6), Point(12, 6), Point(8, 2),
11860 					Point(3, 2), Point(3, 13)
11861 				);
11862 			break;
11863 			case GenericIcons.Save:
11864 				painter.fillColor = Color.white;
11865 				painter.outlineColor = Color.black;
11866 				painter.drawRectangle(Point(2, 2), Point(13, 13));
11867 
11868 				// the label
11869 				painter.drawRectangle(Point(4, 8), Point(11, 13));
11870 
11871 				// the slider
11872 				painter.fillColor = Color.black;
11873 				painter.outlineColor = Color.black;
11874 				painter.drawRectangle(Point(4, 3), Point(10, 6));
11875 
11876 				painter.fillColor = Color.white;
11877 				painter.outlineColor = Color.white;
11878 				// the disc window
11879 				painter.drawRectangle(Point(5, 3), Point(6, 5));
11880 			break;
11881 			case GenericIcons.Open:
11882 				painter.fillColor = Color.white;
11883 				painter.drawPolygon(
11884 					Point(4, 4), Point(4, 12), Point(13, 12), Point(13, 3),
11885 					Point(9, 3), Point(9, 4), Point(4, 4));
11886 				painter.drawPolygon(
11887 					Point(2, 6), Point(11, 6),
11888 					Point(12, 12), Point(4, 12),
11889 					Point(2, 6));
11890 				//painter.drawLine(Point(9, 6), Point(13, 7));
11891 			break;
11892 			case GenericIcons.Copy:
11893 				painter.fillColor = Color.white;
11894 				painter.drawRectangle(Point(3, 2), Point(9, 10));
11895 				painter.drawRectangle(Point(6, 5), Point(12, 13));
11896 			break;
11897 			case GenericIcons.Cut:
11898 				painter.fillColor = Color.transparent;
11899 				painter.outlineColor = getComputedStyle.foregroundColor();
11900 				painter.drawLine(Point(3, 2), Point(10, 9));
11901 				painter.drawLine(Point(4, 9), Point(11, 2));
11902 				painter.drawRectangle(Point(3, 9), Point(5, 13));
11903 				painter.drawRectangle(Point(9, 9), Point(11, 12));
11904 			break;
11905 			case GenericIcons.Paste:
11906 				painter.fillColor = Color.white;
11907 				painter.drawRectangle(Point(2, 3), Point(11, 11));
11908 				painter.drawRectangle(Point(6, 8), Point(13, 13));
11909 				painter.drawLine(Point(6, 2), Point(4, 5));
11910 				painter.drawLine(Point(6, 2), Point(9, 5));
11911 				painter.fillColor = Color.black;
11912 				painter.drawRectangle(Point(4, 5), Point(9, 6));
11913 			break;
11914 			case GenericIcons.Help:
11915 				painter.outlineColor = getComputedStyle.foregroundColor();
11916 				painter.drawText(arsd.color.Point(0, 0), "?", arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
11917 			break;
11918 			case GenericIcons.Undo:
11919 				painter.fillColor = Color.transparent;
11920 				painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64);
11921 				painter.outlineColor = Color.black;
11922 				painter.fillColor = Color.black;
11923 				painter.drawPolygon(
11924 					Point(4, 4),
11925 					Point(8, 2),
11926 					Point(8, 6),
11927 					Point(4, 4),
11928 				);
11929 			break;
11930 			case GenericIcons.Redo:
11931 				painter.fillColor = Color.transparent;
11932 				painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64);
11933 				painter.outlineColor = Color.black;
11934 				painter.fillColor = Color.black;
11935 				painter.drawPolygon(
11936 					Point(10, 4),
11937 					Point(6, 2),
11938 					Point(6, 6),
11939 					Point(10, 4),
11940 				);
11941 			break;
11942 			default:
11943 				painter.outlineColor = getComputedStyle.foregroundColor;
11944 				painter.drawText(arsd.color.Point(0, 0), action.label, arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter);
11945 		}
11946 		return bounds;
11947 		});
11948 	}
11949 
11950 }
11951 
11952 
11953 /++
11954 	You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode].
11955 +/
11956 class MenuBar : Widget {
11957 	MenuItem[] items;
11958 	Menu[] subMenus;
11959 
11960 	version(win32_widgets) {
11961 		HMENU handle;
11962 		///
11963 		this(Widget parent = null) {
11964 			super(parent);
11965 
11966 			handle = CreateMenu();
11967 			tabStop = false;
11968 		}
11969 	} else version(custom_widgets) {
11970 		///
11971 		this(Widget parent = null) {
11972 			tabStop = false; // these are selected some other way
11973 			super(parent);
11974 		}
11975 
11976 		mixin Padding!q{2};
11977 	} else static assert(false);
11978 
11979 	version(custom_widgets)
11980 	override void paint(WidgetPainter painter) {
11981 		draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color);
11982 	}
11983 
11984 	///
11985 	MenuItem addItem(MenuItem item) {
11986 		this.addChild(item);
11987 		items ~= item;
11988 		version(win32_widgets) {
11989 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
11990 		}
11991 		return item;
11992 	}
11993 
11994 
11995 	///
11996 	Menu addItem(Menu item) {
11997 
11998 		subMenus ~= item;
11999 
12000 		auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane
12001 
12002 		addChild(mbItem);
12003 		items ~= mbItem;
12004 
12005 		version(win32_widgets) {
12006 			AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label));
12007 		} else version(custom_widgets) {
12008 			mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) {
12009 				item.popup(mbItem);
12010 			};
12011 		} else static assert(false);
12012 
12013 		return item;
12014 	}
12015 
12016 	override void recomputeChildLayout() {
12017 		.recomputeChildLayout!"width"(this);
12018 	}
12019 
12020 	override int maxHeight() { return defaultLineHeight + 4; }
12021 	override int minHeight() { return defaultLineHeight + 4; }
12022 }
12023 
12024 
12025 /**
12026 	Status bars appear at the bottom of a MainWindow.
12027 	They are made out of Parts, with a width and content.
12028 
12029 	They can have multiple parts or be in simple mode. FIXME: implement simple mode.
12030 
12031 
12032 	sb.parts[0].content = "Status bar text!";
12033 */
12034 // https://learn.microsoft.com/en-us/windows/win32/controls/status-bars#owner-drawn-status-bars
12035 class StatusBar : Widget {
12036 	private Part[] partsArray;
12037 	///
12038 	struct Parts {
12039 		@disable this();
12040 		this(StatusBar owner) { this.owner = owner; }
12041 		//@disable this(this);
12042 		///
12043 		@property int length() { return cast(int) owner.partsArray.length; }
12044 		private StatusBar owner;
12045 		private this(StatusBar owner, Part[] parts) {
12046 			this.owner.partsArray = parts;
12047 			this.owner = owner;
12048 		}
12049 		///
12050 		Part opIndex(int p) {
12051 			if(owner.partsArray.length == 0)
12052 				this ~= new StatusBar.Part(0);
12053 			return owner.partsArray[p];
12054 		}
12055 
12056 		///
12057 		Part opOpAssign(string op : "~" )(Part p) {
12058 			assert(owner.partsArray.length < 255);
12059 			p.owner = this.owner;
12060 			p.idx = cast(int) owner.partsArray.length;
12061 			owner.partsArray ~= p;
12062 
12063 			owner.queueRecomputeChildLayout();
12064 
12065 			version(win32_widgets) {
12066 				int[256] pos;
12067 				int cpos;
12068 				foreach(idx, part; owner.partsArray) {
12069 					if(idx + 1 == owner.partsArray.length)
12070 						pos[idx] = -1;
12071 					else {
12072 						cpos += part.currentlyAssignedWidth;
12073 						pos[idx] = cpos;
12074 					}
12075 				}
12076 				SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr);
12077 			} else version(custom_widgets) {
12078 				owner.redraw();
12079 			} else static assert(false);
12080 
12081 			return p;
12082 		}
12083 
12084 		/++
12085 			Sets up proportional parts in one function call. You can use negative numbers to indicate device-independent pixels, and positive numbers to indicate proportions.
12086 
12087 			No given item should be 0.
12088 
12089 			History:
12090 				Added December 31, 2024
12091 		+/
12092 		void setSizes(int[] proportions...) {
12093 			assert(this.owner);
12094 			this.owner.partsArray = null;
12095 
12096 			foreach(n; proportions) {
12097 				assert(n, "do not give 0 to statusBar.parts.set, it would make an invisible part. Try 1 instead.");
12098 
12099 				this.opOpAssign!"~"(new StatusBar.Part(n > 0 ? n : -n, n > 0 ? StatusBar.Part.WidthUnits.Proportional : StatusBar.Part.WidthUnits.DeviceIndependentPixels));
12100 			}
12101 
12102 		}
12103 	}
12104 
12105 	private Parts _parts;
12106 	///
12107 	final @property Parts parts() {
12108 		return _parts;
12109 	}
12110 
12111 	/++
12112 
12113 	+/
12114 	static class Part {
12115 		/++
12116 			History:
12117 				Added September 1, 2023 (dub v11.1)
12118 		+/
12119 		enum WidthUnits {
12120 			/++
12121 				Unscaled pixels as they appear on screen.
12122 
12123 				If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui.
12124 			+/
12125 			DeviceDependentPixels,
12126 			/++
12127 				Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui.
12128 			+/
12129 			DeviceIndependentPixels,
12130 			/++
12131 				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`).
12132 			+/
12133 			ApproximateCharacters,
12134 			/++
12135 				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.
12136 
12137 				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.
12138 			+/
12139 			Proportional
12140 		}
12141 		private WidthUnits units;
12142 		private int width;
12143 		private StatusBar owner;
12144 
12145 		private int currentlyAssignedWidth;
12146 
12147 		/++
12148 			History:
12149 				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.
12150 
12151 				It now allows you to provide your own value for [WidthUnits].
12152 
12153 				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`.
12154 		+/
12155 		this(int w, WidthUnits units = WidthUnits.Proportional) {
12156 			this.units = units;
12157 			this.width = w;
12158 		}
12159 
12160 		/// ditto
12161 		this(int w = 0) {
12162 			if(w == 0)
12163 				this(w, WidthUnits.Proportional);
12164 			else
12165 				this(w, WidthUnits.DeviceDependentPixels);
12166 		}
12167 
12168 		private int idx;
12169 		private string _content;
12170 		///
12171 		@property string content() { return _content; }
12172 		///
12173 		@property void content(string s) {
12174 			version(win32_widgets) {
12175 				_content = s;
12176 				WCharzBuffer bfr = WCharzBuffer(s);
12177 				SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr);
12178 			} else version(custom_widgets) {
12179 				if(_content != s) {
12180 					_content = s;
12181 					owner.redraw();
12182 				}
12183 			} else static assert(false);
12184 		}
12185 	}
12186 	string simpleModeContent;
12187 	bool inSimpleMode;
12188 
12189 
12190 	///
12191 	this(Widget parent) {
12192 		super(null); // FIXME
12193 		_parts = Parts(this);
12194 		tabStop = false;
12195 		version(win32_widgets) {
12196 			parentWindow = parent.parentWindow;
12197 			createWin32Window(this, "msctls_statusbar32"w, "", 0);
12198 
12199 			RECT rect;
12200 			GetWindowRect(hwnd, &rect);
12201 			idealHeight = rect.bottom - rect.top;
12202 			assert(idealHeight);
12203 		} else version(custom_widgets) {
12204 		} else static assert(false);
12205 	}
12206 
12207 	override void recomputeChildLayout() {
12208 		int remainingLength = this.width;
12209 
12210 		int proportionalSum;
12211 		int proportionalCount;
12212 		foreach(idx, part; this.partsArray) {
12213 			with(Part.WidthUnits)
12214 			final switch(part.units) {
12215 				case DeviceDependentPixels:
12216 					part.currentlyAssignedWidth = part.width;
12217 					remainingLength -= part.currentlyAssignedWidth;
12218 				break;
12219 				case DeviceIndependentPixels:
12220 					part.currentlyAssignedWidth = scaleWithDpi(part.width);
12221 					remainingLength -= part.currentlyAssignedWidth;
12222 				break;
12223 				case ApproximateCharacters:
12224 					auto cs = getComputedStyle();
12225 					auto font = cs.font;
12226 
12227 					part.currentlyAssignedWidth = castFnumToCnum(font.averageWidth * this.width);
12228 					remainingLength -= part.currentlyAssignedWidth;
12229 				break;
12230 				case Proportional:
12231 					proportionalSum += part.width;
12232 					proportionalCount ++;
12233 				break;
12234 			}
12235 		}
12236 
12237 		foreach(part; this.partsArray) {
12238 			if(part.units == Part.WidthUnits.Proportional) {
12239 				auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width;
12240 				if(proportion == 0)
12241 					proportion = 1;
12242 
12243 				if(proportionalSum == 0)
12244 					proportionalSum = proportionalCount;
12245 
12246 				part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum;
12247 			}
12248 		}
12249 
12250 		super.recomputeChildLayout();
12251 	}
12252 
12253 	version(win32_widgets)
12254 	override protected void dpiChanged() {
12255 		RECT rect;
12256 		GetWindowRect(hwnd, &rect);
12257 		idealHeight = rect.bottom - rect.top;
12258 		assert(idealHeight);
12259 	}
12260 
12261 	version(custom_widgets)
12262 	override void paint(WidgetPainter painter) {
12263 		auto cs = getComputedStyle();
12264 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
12265 		int cpos = 0;
12266 		foreach(idx, part; this.partsArray) {
12267 			auto partWidth = part.currentlyAssignedWidth;
12268 			// part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100);
12269 			painter.setClipRectangle(Point(cpos, 0), partWidth, height);
12270 			draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color);
12271 			painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4);
12272 
12273 			painter.outlineColor = cs.foregroundColor();
12274 			painter.fillColor = cs.foregroundColor();
12275 
12276 			painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter);
12277 			cpos += partWidth;
12278 		}
12279 	}
12280 
12281 
12282 	version(win32_widgets) {
12283 		private int idealHeight;
12284 		override int maxHeight() { return idealHeight; }
12285 		override int minHeight() { return idealHeight; }
12286 	} else version(custom_widgets) {
12287 		override int maxHeight() { return defaultLineHeight + 4; }
12288 		override int minHeight() { return defaultLineHeight + 4; }
12289 	} else static assert(false);
12290 }
12291 
12292 /// Displays an in-progress indicator without known values
12293 version(none)
12294 class IndefiniteProgressBar : Widget {
12295 	version(win32_widgets)
12296 	this(Widget parent) {
12297 		super(parent);
12298 		createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */);
12299 		tabStop = false;
12300 	}
12301 	override int minHeight() { return 10; }
12302 }
12303 
12304 /// A progress bar with a known endpoint and completion amount
12305 class ProgressBar : Widget {
12306 	/++
12307 		History:
12308 			Added March 16, 2022 (dub v10.7)
12309 	+/
12310 	this(int min, int max, Widget parent) {
12311 		this(parent);
12312 		setRange(cast(ushort) min, cast(ushort) max); // FIXME
12313 	}
12314 	this(Widget parent) {
12315 		version(win32_widgets) {
12316 			super(parent);
12317 			createWin32Window(this, "msctls_progress32"w, "", 0);
12318 			tabStop = false;
12319 		} else version(custom_widgets) {
12320 			super(parent);
12321 			max = 100;
12322 			step = 10;
12323 			tabStop = false;
12324 		} else static assert(0);
12325 	}
12326 
12327 	version(custom_widgets)
12328 	override void paint(WidgetPainter painter) {
12329 		auto cs = getComputedStyle();
12330 		this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
12331 		painter.fillColor = cs.progressBarColor;
12332 		painter.drawRectangle(Point(0, 0), width * current / max, height);
12333 	}
12334 
12335 
12336 	version(custom_widgets) {
12337 		int current;
12338 		int max;
12339 		int step;
12340 	}
12341 
12342 	///
12343 	void advanceOneStep() {
12344 		version(win32_widgets)
12345 			SendMessageW(hwnd, PBM_STEPIT, 0, 0);
12346 		else version(custom_widgets)
12347 			addToPosition(step);
12348 		else static assert(false);
12349 	}
12350 
12351 	///
12352 	void setStepIncrement(int increment) {
12353 		version(win32_widgets)
12354 			SendMessageW(hwnd, PBM_SETSTEP, increment, 0);
12355 		else version(custom_widgets)
12356 			step = increment;
12357 		else static assert(false);
12358 	}
12359 
12360 	///
12361 	void addToPosition(int amount) {
12362 		version(win32_widgets)
12363 			SendMessageW(hwnd, PBM_DELTAPOS, amount, 0);
12364 		else version(custom_widgets)
12365 			setPosition(current + amount);
12366 		else static assert(false);
12367 	}
12368 
12369 	///
12370 	void setPosition(int pos) {
12371 		version(win32_widgets)
12372 			SendMessageW(hwnd, PBM_SETPOS, pos, 0);
12373 		else version(custom_widgets) {
12374 			current = pos;
12375 			if(current > max)
12376 				current = max;
12377 			redraw();
12378 		}
12379 		else static assert(false);
12380 	}
12381 
12382 	///
12383 	void setRange(ushort min, ushort max) {
12384 		version(win32_widgets)
12385 			SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max));
12386 		else version(custom_widgets) {
12387 			this.max = max;
12388 		}
12389 		else static assert(false);
12390 	}
12391 
12392 	override int minHeight() { return 10; }
12393 }
12394 
12395 version(custom_widgets)
12396 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) {
12397 	thisLabel.reserve(label.length);
12398 	bool justSawAmpersand;
12399 	foreach(ch; label) {
12400 		if(justSawAmpersand) {
12401 			justSawAmpersand = false;
12402 			if(ch == '&') {
12403 				goto plain;
12404 			}
12405 			thisAccelerator = ch;
12406 		} else {
12407 			if(ch == '&') {
12408 				justSawAmpersand = true;
12409 				continue;
12410 			}
12411 			plain:
12412 			thisLabel ~= ch;
12413 		}
12414 	}
12415 }
12416 
12417 /++
12418 	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.
12419 
12420 
12421 	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
12422 
12423 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
12424 
12425 	History:
12426 		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.
12427 +/
12428 class Fieldset : Widget {
12429 	// FIXME: on Windows,it doesn't draw the background on the label
12430 	// on X, it doesn't fix the clipping rectangle for it
12431 	version(win32_widgets)
12432 		override int paddingTop() { return defaultLineHeight; }
12433 	else version(custom_widgets)
12434 		override int paddingTop() { return defaultLineHeight + 2; }
12435 	else static assert(false);
12436 	override int paddingBottom() { return 6; }
12437 	override int paddingLeft() { return 6; }
12438 	override int paddingRight() { return 6; }
12439 
12440 	override int marginLeft() { return 6; }
12441 	override int marginRight() { return 6; }
12442 	override int marginTop() { return 2; }
12443 	override int marginBottom() { return 2; }
12444 
12445 	string legend;
12446 
12447 	version(custom_widgets) private dchar accelerator;
12448 
12449 	this(string legend, Widget parent) {
12450 		version(win32_widgets) {
12451 			super(parent);
12452 			this.legend = legend;
12453 			createWin32Window(this, "button"w, legend, BS_GROUPBOX);
12454 			tabStop = false;
12455 		} else version(custom_widgets) {
12456 			super(parent);
12457 			tabStop = false;
12458 
12459 			legend.extractWindowsStyleLabel(this.legend, this.accelerator);
12460 		} else static assert(0);
12461 	}
12462 
12463 	version(custom_widgets)
12464 	override void paint(WidgetPainter painter) {
12465 		auto dlh = defaultLineHeight;
12466 
12467 		painter.fillColor = Color.transparent;
12468 		auto cs = getComputedStyle();
12469 		painter.pen = Pen(cs.foregroundColor, 1);
12470 		painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2);
12471 
12472 		auto tx = painter.textSize(legend);
12473 		painter.outlineColor = Color.transparent;
12474 
12475 		version(Windows) {
12476 			auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE));
12477 			painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height);
12478 			SelectObject(painter.impl.hdc, b);
12479 		} else static if(UsingSimpledisplayX11) {
12480 			painter.fillColor = getComputedStyle().windowBackgroundColor;
12481 			painter.drawRectangle(Point(8, 0), tx.width, tx.height);
12482 		}
12483 		painter.outlineColor = cs.foregroundColor;
12484 		painter.drawText(Point(8, 0), legend);
12485 	}
12486 
12487 	override int maxHeight() {
12488 		auto m = paddingTop() + paddingBottom();
12489 		foreach(child; children) {
12490 			auto mh = child.maxHeight();
12491 			if(mh == int.max)
12492 				return int.max;
12493 			m += mh;
12494 			m += child.marginBottom();
12495 			m += child.marginTop();
12496 		}
12497 		m += 6;
12498 		if(m < minHeight)
12499 			return minHeight;
12500 		return m;
12501 	}
12502 
12503 	override int minHeight() {
12504 		auto m = paddingTop() + paddingBottom();
12505 		foreach(child; children) {
12506 			m += child.minHeight();
12507 			m += child.marginBottom();
12508 			m += child.marginTop();
12509 		}
12510 		return m + 6;
12511 	}
12512 
12513 	override int minWidth() {
12514 		return 6 + cast(int) this.legend.length * 7;
12515 	}
12516 }
12517 
12518 /++
12519 	$(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")
12520 	$(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.)
12521 +/
12522 version(minigui_screenshots)
12523 @Screenshot("Fieldset")
12524 unittest {
12525 	auto window = new Window(200, 100);
12526 	auto set = new Fieldset("Baby will", window);
12527 	auto option1 = new Radiobox("Eat", set);
12528 	auto option2 = new Radiobox("Cry", set);
12529 	auto option3 = new Radiobox("Sleep", set);
12530 	window.loop();
12531 }
12532 
12533 /// Draws a line
12534 class HorizontalRule : Widget {
12535 	mixin Margin!q{ 2 };
12536 	override int minHeight() { return 2; }
12537 	override int maxHeight() { return 2; }
12538 
12539 	///
12540 	this(Widget parent) {
12541 		super(parent);
12542 	}
12543 
12544 	override void paint(WidgetPainter painter) {
12545 		auto cs = getComputedStyle();
12546 		painter.outlineColor = cs.darkAccentColor;
12547 		painter.drawLine(Point(0, 0), Point(width, 0));
12548 		painter.outlineColor = cs.lightAccentColor;
12549 		painter.drawLine(Point(0, 1), Point(width, 1));
12550 	}
12551 }
12552 
12553 version(minigui_screenshots)
12554 @Screenshot("HorizontalRule")
12555 /++
12556 	$(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.)
12557 
12558 +/
12559 unittest {
12560 	auto window = new Window(200, 100);
12561 	auto above = new TextLabel("Above the line", TextAlignment.Left, window);
12562 	new HorizontalRule(window);
12563 	auto below = new TextLabel("Below the line", TextAlignment.Left, window);
12564 	window.loop();
12565 }
12566 
12567 /// ditto
12568 class VerticalRule : Widget {
12569 	mixin Margin!q{ 2 };
12570 	override int minWidth() { return 2; }
12571 	override int maxWidth() { return 2; }
12572 
12573 	///
12574 	this(Widget parent) {
12575 		super(parent);
12576 	}
12577 
12578 	override void paint(WidgetPainter painter) {
12579 		auto cs = getComputedStyle();
12580 		painter.outlineColor = cs.darkAccentColor;
12581 		painter.drawLine(Point(0, 0), Point(0, height));
12582 		painter.outlineColor = cs.lightAccentColor;
12583 		painter.drawLine(Point(1, 0), Point(1, height));
12584 	}
12585 }
12586 
12587 
12588 ///
12589 class Menu : Window {
12590 	void remove() {
12591 		foreach(i, child; parentWindow.children)
12592 			if(child is this) {
12593 				parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $];
12594 				break;
12595 			}
12596 		parentWindow.redraw();
12597 
12598 		parentWindow.releaseMouseCapture();
12599 	}
12600 
12601 	///
12602 	void addSeparator() {
12603 		version(win32_widgets)
12604 			AppendMenu(handle, MF_SEPARATOR, 0, null);
12605 		else version(custom_widgets)
12606 			auto hr = new HorizontalRule(this);
12607 		else static assert(0);
12608 	}
12609 
12610 	override int paddingTop() { return 4; }
12611 	override int paddingBottom() { return 4; }
12612 	override int paddingLeft() { return 2; }
12613 	override int paddingRight() { return 2; }
12614 
12615 	version(win32_widgets) {}
12616 	else version(custom_widgets) {
12617 
12618 		Widget previouslyFocusedWidget;
12619 		Widget* previouslyFocusedWidgetBelongsIn;
12620 
12621 		SimpleWindow dropDown;
12622 		Widget menuParent;
12623 		void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
12624 			this.menuParent = parent;
12625 
12626 			previouslyFocusedWidget = parent.parentWindow.focusedWidget;
12627 			previouslyFocusedWidgetBelongsIn = &parent.parentWindow.focusedWidget;
12628 			parent.parentWindow.focusedWidget = this;
12629 
12630 			int w = 150;
12631 			int h = paddingTop + paddingBottom;
12632 			if(this.children.length) {
12633 				// hacking it to get the ideal height out of recomputeChildLayout
12634 				this.width = w;
12635 				this.height = h;
12636 				this.recomputeChildLayoutEntry();
12637 				h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
12638 				h += paddingBottom;
12639 
12640 				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
12641 			}
12642 
12643 			if(offsetY == int.min)
12644 				offsetY = parent.defaultLineHeight;
12645 
12646 			auto coord = parent.globalCoordinates();
12647 			dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
12648 			this.x = 0;
12649 			this.y = 0;
12650 			this.width = dropDown.width;
12651 			this.height = dropDown.height;
12652 			this.drawableWindow = dropDown;
12653 			this.recomputeChildLayoutEntry();
12654 
12655 			static if(UsingSimpledisplayX11)
12656 				XSync(XDisplayConnection.get, 0);
12657 
12658 			dropDown.visibilityChanged = (bool visible) {
12659 				if(visible) {
12660 					this.redraw();
12661 					dropDown.grabInput();
12662 				} else {
12663 					dropDown.releaseInputGrab();
12664 				}
12665 			};
12666 
12667 			dropDown.show();
12668 
12669 			clickListener = this.addEventListener((scope ClickEvent ev) {
12670 				unpopup();
12671 				// need to unlock asap just in case other user handlers block...
12672 				static if(UsingSimpledisplayX11)
12673 					flushGui();
12674 			}, true /* again for asap action */);
12675 		}
12676 
12677 		EventListener clickListener;
12678 	}
12679 	else static assert(false);
12680 
12681 	version(custom_widgets)
12682 	void unpopup() {
12683 		mouseLastOver = mouseLastDownOn = null;
12684 		dropDown.hide();
12685 		if(!menuParent.parentWindow.win.closed) {
12686 			if(auto maw = cast(MouseActivatedWidget) menuParent) {
12687 				maw.setDynamicState(DynamicState.depressed, false);
12688 				maw.setDynamicState(DynamicState.hover, false);
12689 				maw.redraw();
12690 			}
12691 			// menuParent.parentWindow.win.focus();
12692 		}
12693 		clickListener.disconnect();
12694 
12695 		if(previouslyFocusedWidgetBelongsIn)
12696 			*previouslyFocusedWidgetBelongsIn = previouslyFocusedWidget;
12697 	}
12698 
12699 	MenuItem[] items;
12700 
12701 	///
12702 	MenuItem addItem(MenuItem item) {
12703 		addChild(item);
12704 		items ~= item;
12705 		version(win32_widgets) {
12706 			AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label));
12707 		}
12708 		return item;
12709 	}
12710 
12711 	string label;
12712 
12713 	version(win32_widgets) {
12714 		HMENU handle;
12715 		///
12716 		this(string label, Widget parent) {
12717 			// not actually passing the parent since it effs up the drawing
12718 			super(cast(Widget) null);// parent);
12719 			this.label = label;
12720 			handle = CreatePopupMenu();
12721 		}
12722 	} else version(custom_widgets) {
12723 		///
12724 		this(string label, Widget parent) {
12725 
12726 			if(dropDown) {
12727 				dropDown.close();
12728 			}
12729 			dropDown = new SimpleWindow(
12730 				150, 4,
12731 				// FIXME: what if it is a popupMenu ?
12732 				null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null);
12733 
12734 			this.label = label;
12735 
12736 			super(dropDown);
12737 		}
12738 	} else static assert(false);
12739 
12740 	override int maxHeight() { return defaultLineHeight; }
12741 	override int minHeight() { return defaultLineHeight; }
12742 
12743 	version(custom_widgets) {
12744 		Widget currentPlace;
12745 
12746 		void changeCurrentPlace(Widget n) {
12747 			if(currentPlace) {
12748 				currentPlace.dynamicState = 0;
12749 			}
12750 
12751 			if(n) {
12752 				n.dynamicState = DynamicState.hover;
12753 			}
12754 
12755 			currentPlace = n;
12756 		}
12757 
12758 		override void paint(WidgetPainter painter) {
12759 			this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color);
12760 		}
12761 
12762 		override void defaultEventHandler_keydown(KeyDownEvent ke) {
12763 			switch(ke.key) {
12764 				case Key.Down:
12765 					Widget next;
12766 					Widget first;
12767 					foreach(w; this.children) {
12768 						if((cast(MenuItem) w) is null)
12769 							continue;
12770 
12771 						if(first is null)
12772 							first = w;
12773 
12774 						if(next !is null) {
12775 							next = w;
12776 							break;
12777 						}
12778 
12779 						if(currentPlace is null) {
12780 							next = w;
12781 							break;
12782 						}
12783 
12784 						if(w is currentPlace) {
12785 							next = w;
12786 						}
12787 					}
12788 
12789 					if(next is currentPlace)
12790 						next = first;
12791 
12792 					changeCurrentPlace(next);
12793 					break;
12794 				case Key.Up:
12795 					Widget prev;
12796 					foreach(w; this.children) {
12797 						if((cast(MenuItem) w) is null)
12798 							continue;
12799 						if(w is currentPlace) {
12800 							if(prev is null) {
12801 								foreach_reverse(c; this.children) {
12802 									if((cast(MenuItem) c) !is null) {
12803 										prev = c;
12804 										break;
12805 									}
12806 								}
12807 							}
12808 							break;
12809 						}
12810 						prev = w;
12811 					}
12812 					changeCurrentPlace(prev);
12813 					break;
12814 				case Key.Left:
12815 				case Key.Right:
12816 					if(menuParent) {
12817 						Menu first;
12818 						Menu last;
12819 						Menu prev;
12820 						Menu next;
12821 						bool found;
12822 
12823 						size_t prev_idx;
12824 						size_t next_idx;
12825 
12826 						MenuBar mb = cast(MenuBar) menuParent.parent;
12827 
12828 						if(mb) {
12829 							foreach(idx, menu; mb.subMenus) {
12830 								if(first is null)
12831 									first = menu;
12832 								last = menu;
12833 								if(found && next is null) {
12834 									next = menu;
12835 									next_idx = idx;
12836 								}
12837 								if(menu is this)
12838 									found = true;
12839 								if(!found) {
12840 									prev = menu;
12841 									prev_idx = idx;
12842 								}
12843 							}
12844 
12845 							Menu nextMenu;
12846 							size_t nextMenuIdx;
12847 							if(ke.key == Key.Left) {
12848 								nextMenu = prev ? prev : last;
12849 								nextMenuIdx = prev ? prev_idx : mb.subMenus.length - 1;
12850 							} else {
12851 								nextMenu = next ? next : first;
12852 								nextMenuIdx = next ? next_idx : 0;
12853 							}
12854 
12855 							unpopup();
12856 
12857 							auto rent = mb.children[nextMenuIdx]; // FIXME thsi is not necessarily right
12858 							rent.dynamicState = DynamicState.depressed | DynamicState.hover;
12859 							nextMenu.popup(rent);
12860 						}
12861 					}
12862 					break;
12863 				case Key.Enter:
12864 				case Key.PadEnter:
12865 					// because the key up and char events will go back to the other window after we unpopup!
12866 					// we will wait for the char event to come (in the following method)
12867 					break;
12868 				case Key.Escape:
12869 					unpopup();
12870 					break;
12871 				default:
12872 			}
12873 		}
12874 		override void defaultEventHandler_char(CharEvent ke) {
12875 			// if one is selected, enter activates it
12876 			if(currentPlace) {
12877 				if(ke.character == '\n') {
12878 					// enter selects
12879 					auto event = new Event(EventType.triggered, currentPlace);
12880 					event.dispatch();
12881 					unpopup();
12882 					return;
12883 				}
12884 			}
12885 
12886 			// otherwise search for a hotkey
12887 			foreach(item; items) {
12888 				if(item.hotkey == ke.character) {
12889 					auto event = new Event(EventType.triggered, item);
12890 					event.dispatch();
12891 					unpopup();
12892 					return;
12893 				}
12894 			}
12895 		}
12896 		override void defaultEventHandler_mouseover(MouseOverEvent moe) {
12897 			if(moe.target && moe.target.parent is this)
12898 				changeCurrentPlace(moe.target);
12899 		}
12900 	}
12901 }
12902 
12903 /++
12904 	A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked.
12905 +/
12906 class MenuItem : MouseActivatedWidget {
12907 	Menu submenu;
12908 
12909 	Action action;
12910 	string label;
12911 	dchar hotkey;
12912 
12913 	override int paddingLeft() { return 4; }
12914 
12915 	override int maxHeight() { return defaultLineHeight + 4; }
12916 	override int minHeight() { return defaultLineHeight + 4; }
12917 	override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); }
12918 	override int maxWidth() {
12919 		if(cast(MenuBar) parent) {
12920 			return minWidth();
12921 		}
12922 		return int.max;
12923 	}
12924 	/// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu.
12925 	this(string lbl, Widget parent = null) {
12926 		super(parent);
12927 		//label = lbl; // FIXME
12928 		foreach(idx, char ch; lbl) // FIXME
12929 			if(ch != '&') { // FIXME
12930 				label ~= ch; // FIXME
12931 			} else {
12932 				if(idx + 1 < lbl.length) {
12933 					hotkey = lbl[idx + 1];
12934 					if(hotkey >= 'A' && hotkey <= 'Z')
12935 						hotkey += 32;
12936 				}
12937 			}
12938 		tabStop = false; // these are selected some other way
12939 	}
12940 
12941 	///
12942 	this(Action action, Widget parent = null) {
12943 		assert(action !is null);
12944 		this(action.label, parent);
12945 		this.action = action;
12946 		tabStop = false; // these are selected some other way
12947 	}
12948 
12949 	version(custom_widgets)
12950 	override void paint(WidgetPainter painter) {
12951 		auto cs = getComputedStyle();
12952 		if(dynamicState & DynamicState.depressed)
12953 			this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color);
12954 		else {
12955 			if(dynamicState & DynamicState.hover) {
12956 				painter.fillColor = cs.hoveringColor;
12957 				painter.outlineColor = Color.transparent;
12958 			} else {
12959 				painter.fillColor = cs.background.color;
12960 				painter.outlineColor = Color.transparent;
12961 			}
12962 
12963 			painter.drawRectangle(Point(0, 0), Size(this.width, this.height));
12964 		}
12965 
12966 		if(dynamicState & DynamicState.hover)
12967 			painter.outlineColor = cs.activeMenuItemColor;
12968 		else
12969 			painter.outlineColor = cs.foregroundColor;
12970 		painter.fillColor = Color.transparent;
12971 		painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
12972 		if(action && action.accelerator !is KeyEvent.init) {
12973 			painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter);
12974 
12975 		}
12976 	}
12977 
12978 	static class Style : Widget.Style {
12979 		override bool variesWithState(ulong dynamicStateFlags) {
12980 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
12981 		}
12982 	}
12983 	mixin OverrideStyle!Style;
12984 
12985 	override void defaultEventHandler_triggered(Event event) {
12986 		if(action)
12987 		foreach(handler; action.triggered)
12988 			handler();
12989 
12990 		if(auto pmenu = cast(Menu) this.parent)
12991 			pmenu.remove();
12992 
12993 		super.defaultEventHandler_triggered(event);
12994 	}
12995 }
12996 
12997 version(win32_widgets)
12998 /// A "mouse activiated widget" is really just an abstract variant of button.
12999 class MouseActivatedWidget : Widget {
13000 	@property bool isChecked() {
13001 		assert(hwnd);
13002 		return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED;
13003 
13004 	}
13005 	@property void isChecked(bool state) {
13006 		assert(hwnd);
13007 		SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0);
13008 
13009 	}
13010 
13011 	override void handleWmCommand(ushort cmd, ushort id) {
13012 		if(cmd == 0) {
13013 			auto event = new Event(EventType.triggered, this);
13014 			event.dispatch();
13015 		}
13016 	}
13017 
13018 	this(Widget parent) {
13019 		super(parent);
13020 	}
13021 }
13022 else version(custom_widgets)
13023 /// ditto
13024 class MouseActivatedWidget : Widget {
13025 	@property bool isChecked() { return isChecked_; }
13026 	@property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;}
13027 
13028 	private bool isChecked_;
13029 
13030 	this(Widget parent) {
13031 		super(parent);
13032 
13033 		addEventListener((MouseDownEvent ev) {
13034 			if(ev.button == MouseButton.left) {
13035 				setDynamicState(DynamicState.depressed, true);
13036 				setDynamicState(DynamicState.hover, true);
13037 				redraw();
13038 			}
13039 		});
13040 
13041 		addEventListener((MouseUpEvent ev) {
13042 			if(ev.button == MouseButton.left) {
13043 				setDynamicState(DynamicState.depressed, false);
13044 				setDynamicState(DynamicState.hover, false);
13045 				redraw();
13046 			}
13047 		});
13048 
13049 		addEventListener((MouseMoveEvent mme) {
13050 			if(!(mme.state & ModifierState.leftButtonDown)) {
13051 				if(dynamicState_ & DynamicState.depressed) {
13052 					setDynamicState(DynamicState.depressed, false);
13053 					redraw();
13054 				}
13055 			}
13056 		});
13057 	}
13058 
13059 	override void defaultEventHandler_focus(FocusEvent ev) {
13060 		super.defaultEventHandler_focus(ev);
13061 		this.redraw();
13062 	}
13063 	override void defaultEventHandler_blur(BlurEvent ev) {
13064 		super.defaultEventHandler_blur(ev);
13065 		setDynamicState(DynamicState.depressed, false);
13066 		this.redraw();
13067 	}
13068 	override void defaultEventHandler_keydown(KeyDownEvent ev) {
13069 		super.defaultEventHandler_keydown(ev);
13070 		if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) {
13071 			setDynamicState(DynamicState.depressed, true);
13072 			setDynamicState(DynamicState.hover, true);
13073 			this.redraw();
13074 		}
13075 	}
13076 	override void defaultEventHandler_keyup(KeyUpEvent ev) {
13077 		super.defaultEventHandler_keyup(ev);
13078 		if(!(dynamicState & DynamicState.depressed))
13079 			return;
13080 		if(!enabled)
13081 			return;
13082 		setDynamicState(DynamicState.depressed, false);
13083 		setDynamicState(DynamicState.hover, false);
13084 		this.redraw();
13085 
13086 		auto event = new Event(EventType.triggered, this);
13087 		event.sendDirectly();
13088 	}
13089 	override void defaultEventHandler_click(ClickEvent ev) {
13090 		super.defaultEventHandler_click(ev);
13091 		if(ev.button == MouseButton.left && enabled) {
13092 			auto event = new Event(EventType.triggered, this);
13093 			event.sendDirectly();
13094 		}
13095 	}
13096 
13097 }
13098 else static assert(false);
13099 
13100 /*
13101 /++
13102 	Like the tablet thing, it would have a label, a description, and a switch slider thingy.
13103 
13104 	Basically the same as a checkbox.
13105 +/
13106 class OnOffSwitch : MouseActivatedWidget {
13107 
13108 }
13109 */
13110 
13111 /++
13112 	History:
13113 		Added June 15, 2021 (dub v10.1)
13114 +/
13115 struct ImageLabel {
13116 	/++
13117 		Defines a label+image combo used by some widgets.
13118 
13119 		If you provide just a text label, that is all the widget will try to
13120 		display. Or just an image will display just that. If you provide both,
13121 		it may display both text and image side by side or display the image
13122 		and offer text on an input event depending on the widget.
13123 
13124 		History:
13125 			The `alignment` parameter was added on September 27, 2021
13126 	+/
13127 	this(string label, TextAlignment alignment = TextAlignment.Center) {
13128 		this.label = label;
13129 		this.displayFlags = DisplayFlags.displayText;
13130 		this.alignment = alignment;
13131 	}
13132 
13133 	/// ditto
13134 	this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
13135 		this.label = label;
13136 		this.image = image;
13137 		this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
13138 		this.alignment = alignment;
13139 	}
13140 
13141 	/// ditto
13142 	this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) {
13143 		this.image = image;
13144 		this.displayFlags = DisplayFlags.displayImage;
13145 		this.alignment = alignment;
13146 	}
13147 
13148 	/// ditto
13149 	this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) {
13150 		this.label = label;
13151 		this.image = image;
13152 		this.alignment = alignment;
13153 		this.displayFlags = displayFlags;
13154 	}
13155 
13156 	string label;
13157 	MemoryImage image;
13158 
13159 	enum DisplayFlags {
13160 		displayText = 1 << 0,
13161 		displayImage = 1 << 1,
13162 	}
13163 
13164 	int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage;
13165 
13166 	TextAlignment alignment;
13167 }
13168 
13169 /++
13170 	A basic checked or not checked box with an attached label.
13171 
13172 
13173 	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
13174 
13175 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
13176 
13177 	History:
13178 		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.
13179 +/
13180 class Checkbox : MouseActivatedWidget {
13181 	version(win32_widgets) {
13182 		override int maxHeight() { return scaleWithDpi(16); }
13183 		override int minHeight() { return scaleWithDpi(16); }
13184 	} else version(custom_widgets) {
13185 		private enum buttonSize = 16;
13186 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
13187 		override int minHeight() { return maxHeight(); }
13188 	} else static assert(0);
13189 
13190 	override int marginLeft() { return 4; }
13191 
13192 	override int flexBasisWidth() { return 24 + cast(int) label.length * 7; }
13193 
13194 	/++
13195 		Just an alias because I keep typing checked out of web habit.
13196 
13197 		History:
13198 			Added May 31, 2021
13199 	+/
13200 	alias checked = isChecked;
13201 
13202 	private string label;
13203 	private dchar accelerator;
13204 
13205 	/++
13206 	+/
13207 	this(string label, Widget parent) {
13208 		this(ImageLabel(label), Appearance.checkbox, parent);
13209 	}
13210 
13211 	/// ditto
13212 	this(string label, Appearance appearance, Widget parent) {
13213 		this(ImageLabel(label), appearance, parent);
13214 	}
13215 
13216 	/++
13217 		Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific.
13218 
13219 		History:
13220 			Added June 29, 2021 (dub v10.2)
13221 	+/
13222 	enum Appearance {
13223 		checkbox, /// a normal checkbox
13224 		pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad.
13225 		//sliderswitch,
13226 	}
13227 	private Appearance appearance;
13228 
13229 	/// ditto
13230 	private this(ImageLabel label, Appearance appearance, Widget parent) {
13231 		super(parent);
13232 		version(win32_widgets) {
13233 			this.label = label.label;
13234 
13235 			uint extraStyle;
13236 			final switch(appearance) {
13237 				case Appearance.checkbox:
13238 				break;
13239 				case Appearance.pushbutton:
13240 					extraStyle |= BS_PUSHLIKE;
13241 				break;
13242 			}
13243 
13244 			createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle);
13245 		} else version(custom_widgets) {
13246 			label.label.extractWindowsStyleLabel(this.label, this.accelerator);
13247 		} else static assert(0);
13248 	}
13249 
13250 	version(custom_widgets)
13251 	override void paint(WidgetPainter painter) {
13252 		auto cs = getComputedStyle();
13253 		if(isFocused()) {
13254 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
13255 			painter.fillColor = cs.windowBackgroundColor;
13256 			painter.drawRectangle(Point(0, 0), width, height);
13257 			painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
13258 		} else {
13259 			painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid);
13260 			painter.fillColor = cs.windowBackgroundColor;
13261 			painter.drawRectangle(Point(0, 0), width, height);
13262 		}
13263 
13264 
13265 		painter.outlineColor = Color.black;
13266 		painter.fillColor = Color.white;
13267 		enum rectOffset = 2;
13268 		painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset));
13269 
13270 		if(isChecked) {
13271 			auto size = scaleWithDpi(2);
13272 			painter.pen = Pen(Color.black, size);
13273 			// I'm using height so the checkbox is square
13274 			enum padding = 3;
13275 			painter.drawLine(
13276 				scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)),
13277 				scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2)
13278 			);
13279 			painter.drawLine(
13280 				scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0),
13281 				scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 -  size % 2)
13282 			);
13283 
13284 			painter.pen = Pen(Color.black, 1);
13285 		}
13286 
13287 		if(label !is null) {
13288 			painter.outlineColor = cs.foregroundColor();
13289 			painter.fillColor = cs.foregroundColor();
13290 
13291 			// i want the centerline of the text to be aligned with the centerline of the checkbox
13292 			/+
13293 			auto font = cs.font();
13294 			auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2;
13295 			painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label);
13296 			+/
13297 			painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter);
13298 		}
13299 	}
13300 
13301 	override void defaultEventHandler_triggered(Event ev) {
13302 		isChecked = !isChecked;
13303 
13304 		this.emit!(ChangeEvent!bool)(&isChecked);
13305 
13306 		redraw();
13307 	}
13308 
13309 	/// Emits a change event with the checked state
13310 	mixin Emits!(ChangeEvent!bool);
13311 }
13312 
13313 /// Adds empty space to a layout.
13314 class VerticalSpacer : Widget {
13315 	private int mh;
13316 
13317 	/++
13318 		History:
13319 			The overload with `maxHeight` was added on December 31, 2024
13320 	+/
13321 	this(Widget parent) {
13322 		this(0, parent);
13323 	}
13324 
13325 	/// ditto
13326 	this(int maxHeight, Widget parent) {
13327 		this.mh = maxHeight;
13328 		super(parent);
13329 		this.tabStop = false;
13330 	}
13331 
13332 	override int maxHeight() {
13333 		return mh ? scaleWithDpi(mh) : super.maxHeight();
13334 	}
13335 }
13336 
13337 
13338 /// ditto
13339 class HorizontalSpacer : Widget {
13340 	private int mw;
13341 
13342 	/++
13343 		History:
13344 			The overload with `maxWidth` was added on December 31, 2024
13345 	+/
13346 	this(Widget parent) {
13347 		this(0, parent);
13348 	}
13349 
13350 	/// ditto
13351 	this(int maxWidth, Widget parent) {
13352 		this.mw = maxWidth;
13353 		super(parent);
13354 		this.tabStop = false;
13355 	}
13356 
13357 	override int maxWidth() {
13358 		return mw ? scaleWithDpi(mw) : super.maxWidth();
13359 	}
13360 }
13361 
13362 
13363 /++
13364 	Creates a radio button with an associated label. These are usually put inside a [Fieldset].
13365 
13366 
13367 	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
13368 
13369 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
13370 
13371 	History:
13372 		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.
13373 +/
13374 class Radiobox : MouseActivatedWidget {
13375 
13376 	version(win32_widgets) {
13377 		override int maxHeight() { return scaleWithDpi(16); }
13378 		override int minHeight() { return scaleWithDpi(16); }
13379 	} else version(custom_widgets) {
13380 		private enum buttonSize = 16;
13381 		override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); }
13382 		override int minHeight() { return maxHeight(); }
13383 	} else static assert(0);
13384 
13385 	override int marginLeft() { return 4; }
13386 
13387 	// FIXME: make a label getter
13388 	private string label;
13389 	private dchar accelerator;
13390 
13391 	/++
13392 
13393 	+/
13394 	this(string label, Widget parent) {
13395 		super(parent);
13396 		version(win32_widgets) {
13397 			this.label = label;
13398 			createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON);
13399 		} else version(custom_widgets) {
13400 			label.extractWindowsStyleLabel(this.label, this.accelerator);
13401 			height = 16;
13402 			width = height + 4 + cast(int) label.length * 16;
13403 		}
13404 	}
13405 
13406 	version(custom_widgets)
13407 	override void paint(WidgetPainter painter) {
13408 		auto cs = getComputedStyle();
13409 
13410 		if(isFocused) {
13411 			painter.fillColor = cs.windowBackgroundColor;
13412 			painter.pen = Pen(Color.black, 1, Pen.Style.Dotted);
13413 		} else {
13414 			painter.fillColor = cs.windowBackgroundColor;
13415 			painter.outlineColor = cs.windowBackgroundColor;
13416 		}
13417 		painter.drawRectangle(Point(0, 0), width, height);
13418 
13419 		painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
13420 
13421 		painter.outlineColor = Color.black;
13422 		painter.fillColor = Color.white;
13423 		painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2)));
13424 		if(isChecked) {
13425 			painter.outlineColor = Color.black;
13426 			painter.fillColor = Color.black;
13427 			// I'm using height so the checkbox is square
13428 			auto size = scaleWithDpi(2);
13429 			painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2));
13430 		}
13431 
13432 		painter.outlineColor = cs.foregroundColor();
13433 		painter.fillColor = cs.foregroundColor();
13434 
13435 		painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter);
13436 	}
13437 
13438 
13439 	override void defaultEventHandler_triggered(Event ev) {
13440 		isChecked = true;
13441 
13442 		if(this.parent) {
13443 			foreach(child; this.parent.children) {
13444 				if(child is this) continue;
13445 				if(auto rb = cast(Radiobox) child) {
13446 					rb.isChecked = false;
13447 					rb.emit!(ChangeEvent!bool)(&rb.isChecked);
13448 					rb.redraw();
13449 				}
13450 			}
13451 		}
13452 
13453 		this.emit!(ChangeEvent!bool)(&this.isChecked);
13454 
13455 		redraw();
13456 	}
13457 
13458 	/// 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.
13459 	mixin Emits!(ChangeEvent!bool);
13460 }
13461 
13462 
13463 /++
13464 	Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event.
13465 
13466 
13467 	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
13468 
13469 	Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second".
13470 
13471 	History:
13472 		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.
13473 +/
13474 class Button : MouseActivatedWidget {
13475 	override int heightStretchiness() { return 3; }
13476 	override int widthStretchiness() { return 3; }
13477 
13478 	/++
13479 		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.
13480 
13481 		History:
13482 			Added July 2, 2021
13483 	+/
13484 	public bool triggersOnMultiClick;
13485 
13486 	private string label_;
13487 	private TextAlignment alignment;
13488 	private dchar accelerator;
13489 
13490 	///
13491 	string label() { return label_; }
13492 	///
13493 	void label(string l) {
13494 		label_ = l;
13495 		version(win32_widgets) {
13496 			WCharzBuffer bfr = WCharzBuffer(l);
13497 			SetWindowTextW(hwnd, bfr.ptr);
13498 		} else version(custom_widgets) {
13499 			redraw();
13500 		}
13501 	}
13502 
13503 	override void defaultEventHandler_dblclick(DoubleClickEvent ev) {
13504 		super.defaultEventHandler_dblclick(ev);
13505 		if(triggersOnMultiClick) {
13506 			if(ev.button == MouseButton.left) {
13507 				auto event = new Event(EventType.triggered, this);
13508 				event.sendDirectly();
13509 			}
13510 		}
13511 	}
13512 
13513 	private Sprite sprite;
13514 	private int displayFlags;
13515 
13516 	protected bool needsOwnerDraw() {
13517 		return &this.paint !is &Button.paint || &this.useStyleProperties !is &Button.useStyleProperties || &this.paintContent !is &Button.paintContent;
13518 	}
13519 
13520 	version(win32_widgets)
13521 	override int handleWmDrawItem(DRAWITEMSTRUCT* dis) {
13522 		auto itemId = dis.itemID;
13523 		auto hdc = dis.hDC;
13524 		auto rect = dis.rcItem;
13525 		switch(dis.itemAction) {
13526 			// skipping setDynamicState because i don't want to queue the redraw unnecessarily
13527 			case ODA_SELECT:
13528 				dynamicState_ &= ~DynamicState.depressed;
13529 				if(dis.itemState & ODS_SELECTED)
13530 					dynamicState_ |= DynamicState.depressed;
13531 			goto case;
13532 			case ODA_FOCUS:
13533 				dynamicState_ &= ~DynamicState.focus;
13534 				if(dis.itemState & ODS_FOCUS)
13535 					dynamicState_ |= DynamicState.focus;
13536 			goto case;
13537 			case ODA_DRAWENTIRE:
13538 				auto painter = WidgetPainter(this.simpleWindowWrappingHwnd.draw(true), this);
13539 				//painter.impl.hdc = hdc;
13540 				paint(painter);
13541 			break;
13542 			default:
13543 		}
13544 		return 1;
13545 
13546 	}
13547 
13548 	/++
13549 		Creates a push button with the given label, which may be an image or some text.
13550 
13551 		Bugs:
13552 			If the image is bigger than the button, it may not be displayed in the right position on Linux.
13553 
13554 		History:
13555 			The [ImageLabel] overload was added on June 21, 2021 (dub v10.1).
13556 
13557 			The button with label and image will respect requests to show both on Windows as
13558 			of March 28, 2022 iff you provide a manifest file to opt into common controls v6.
13559 	+/
13560 	this(string label, Widget parent) {
13561 		this(ImageLabel(label), parent);
13562 	}
13563 
13564 	/// ditto
13565 	this(ImageLabel label, Widget parent) {
13566 		bool needsImage;
13567 		version(win32_widgets) {
13568 			super(parent);
13569 
13570 			// BS_BITMAP is set when we want image only, so checking for exactly that combination
13571 			enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText;
13572 			auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0;
13573 
13574 			// could also do a virtual method needsOwnerDraw which default returns true and we control it here. typeid(this) == typeid(Button) for override check.
13575 
13576 			if(needsOwnerDraw) {
13577 				extraStyle |= BS_OWNERDRAW;
13578 				needsImage = true;
13579 			}
13580 
13581 			// the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter
13582 			createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 );
13583 
13584 			if(label.image) {
13585 				sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true);
13586 
13587 				SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle);
13588 			}
13589 
13590 			this.label = label.label;
13591 		} else version(custom_widgets) {
13592 			super(parent);
13593 
13594 			label.label.extractWindowsStyleLabel(this.label_, this.accelerator);
13595 			needsImage = true;
13596 		}
13597 
13598 
13599 		if(needsImage && label.image) {
13600 			this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image);
13601 			this.displayFlags = label.displayFlags;
13602 		}
13603 
13604 		this.alignment = label.alignment;
13605 	}
13606 
13607 	override int minHeight() { return defaultLineHeight + 4; }
13608 
13609 	static class Style : Widget.Style {
13610 		override WidgetBackground background() {
13611 			auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive
13612 
13613 			auto pressed = DynamicState.depressed | DynamicState.hover;
13614 			if((widget.dynamicState & pressed) == pressed && widget.enabled) {
13615 				return WidgetBackground(cs.depressedButtonColor());
13616 			} else if(widget.dynamicState & DynamicState.hover && widget.enabled) {
13617 				return WidgetBackground(cs.hoveringColor());
13618 			} else {
13619 				return WidgetBackground(cs.buttonColor());
13620 			}
13621 		}
13622 
13623 		override Color foregroundColor() {
13624 			auto clr = super.foregroundColor();
13625 			if(widget.enabled) return clr;
13626 
13627 			return Color(clr.r, clr.g, clr.b, clr.a / 2);
13628 		}
13629 
13630 		override FrameStyle borderStyle() {
13631 			auto pressed = DynamicState.depressed | DynamicState.hover;
13632 			if((widget.dynamicState & pressed) == pressed && widget.enabled) {
13633 				return FrameStyle.sunk;
13634 			} else {
13635 				return FrameStyle.risen;
13636 			}
13637 
13638 		}
13639 
13640 		override bool variesWithState(ulong dynamicStateFlags) {
13641 			return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover));
13642 		}
13643 	}
13644 	mixin OverrideStyle!Style;
13645 
13646 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
13647 		if(sprite) {
13648 			sprite.drawAt(
13649 				painter,
13650 				bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2),
13651 				Point(0, 0)
13652 			);
13653 		} else {
13654 			Point pos = bounds.upperLeft;
13655 			if(this.height == 16)
13656 				pos.y -= 2; // total hack omg
13657 
13658 			painter.drawText(pos, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter);
13659 		}
13660 		return bounds;
13661 	}
13662 
13663 	override int flexBasisWidth() {
13664 		version(win32_widgets) {
13665 			SIZE size;
13666 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
13667 			if(size.cx == 0)
13668 				goto fallback;
13669 			return size.cx + scaleWithDpi(16);
13670 		}
13671 		fallback:
13672 			return scaleWithDpi(cast(int) label.length * 8 + 16);
13673 	}
13674 
13675 	override int flexBasisHeight() {
13676 		version(win32_widgets) {
13677 			SIZE size;
13678 			SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size);
13679 			if(size.cy == 0)
13680 				goto fallback;
13681 			return size.cy + scaleWithDpi(6);
13682 		}
13683 		fallback:
13684 			return defaultLineHeight + 4;
13685 	}
13686 }
13687 
13688 /++
13689 	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.
13690 
13691 	History:
13692 		Added January 14, 2024
13693 +/
13694 class CustomButton : Button {
13695 	this(ImageLabel label, Widget parent) {
13696 		super(label, parent);
13697 	}
13698 
13699 	this(string label, Widget parent) {
13700 		super(label, parent);
13701 	}
13702 
13703 	version(win32_widgets)
13704 	override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) {
13705 		// paint is driven by handleWmDrawItem instead of minigui's redraw events
13706 		if(hwnd)
13707 			InvalidateRect(hwnd, null, false); // get Windows to trigger the actual redraw
13708 		return;
13709 	}
13710 
13711 	override void paint(WidgetPainter painter) {
13712 		// the parent does `if(hwnd) return;` because
13713 		// normally we don't want to draw on standard controls,
13714 		// but this is an exception if it is an owner drawn button
13715 		// (which is determined in the constructor by testing,
13716 		// at runtime, for the existence of an overridden paint
13717 		// member anyway, so this needed to trigger BS_OWNERDRAW)
13718 		// sdpyPrintDebugString("drawing");
13719 		painter.drawThemed(&paintContent);
13720 	}
13721 }
13722 
13723 /++
13724 	A button with a consistent size, suitable for user commands like OK and CANCEL.
13725 +/
13726 class CommandButton : Button {
13727 	this(string label, Widget parent) {
13728 		super(label, parent);
13729 	}
13730 
13731 	// FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there
13732 
13733 	override int maxHeight() {
13734 		return defaultLineHeight + 4;
13735 	}
13736 
13737 	override int maxWidth() {
13738 		return defaultLineHeight * 4;
13739 	}
13740 
13741 	override int marginLeft() { return 12; }
13742 	override int marginRight() { return 12; }
13743 	override int marginTop() { return 12; }
13744 	override int marginBottom() { return 12; }
13745 }
13746 
13747 ///
13748 enum ArrowDirection {
13749 	left, ///
13750 	right, ///
13751 	up, ///
13752 	down ///
13753 }
13754 
13755 ///
13756 version(custom_widgets)
13757 class ArrowButton : Button {
13758 	///
13759 	this(ArrowDirection direction, Widget parent) {
13760 		super("", parent);
13761 		this.direction = direction;
13762 		triggersOnMultiClick = true;
13763 	}
13764 
13765 	private ArrowDirection direction;
13766 
13767 	override int minHeight() { return scaleWithDpi(16); }
13768 	override int maxHeight() { return scaleWithDpi(16); }
13769 	override int minWidth() { return scaleWithDpi(16); }
13770 	override int maxWidth() { return scaleWithDpi(16); }
13771 
13772 	override void paint(WidgetPainter painter) {
13773 		super.paint(painter);
13774 
13775 		auto cs = getComputedStyle();
13776 
13777 		painter.outlineColor = cs.foregroundColor;
13778 		painter.fillColor = cs.foregroundColor;
13779 
13780 		auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2);
13781 
13782 		final switch(direction) {
13783 			case ArrowDirection.up:
13784 				painter.drawPolygon(
13785 					scaleWithDpi(Point(2, 10) + offset),
13786 					scaleWithDpi(Point(7, 5) + offset),
13787 					scaleWithDpi(Point(12, 10) + offset),
13788 					scaleWithDpi(Point(2, 10) + offset)
13789 				);
13790 			break;
13791 			case ArrowDirection.down:
13792 				painter.drawPolygon(
13793 					scaleWithDpi(Point(2, 6) + offset),
13794 					scaleWithDpi(Point(7, 11) + offset),
13795 					scaleWithDpi(Point(12, 6) + offset),
13796 					scaleWithDpi(Point(2, 6) + offset)
13797 				);
13798 			break;
13799 			case ArrowDirection.left:
13800 				painter.drawPolygon(
13801 					scaleWithDpi(Point(10, 2) + offset),
13802 					scaleWithDpi(Point(5, 7) + offset),
13803 					scaleWithDpi(Point(10, 12) + offset),
13804 					scaleWithDpi(Point(10, 2) + offset)
13805 				);
13806 			break;
13807 			case ArrowDirection.right:
13808 				painter.drawPolygon(
13809 					scaleWithDpi(Point(6, 2) + offset),
13810 					scaleWithDpi(Point(11, 7) + offset),
13811 					scaleWithDpi(Point(6, 12) + offset),
13812 					scaleWithDpi(Point(6, 2) + offset)
13813 				);
13814 			break;
13815 		}
13816 	}
13817 }
13818 
13819 private
13820 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow {
13821 	int x, y;
13822 	Widget par = c;
13823 	while(par) {
13824 		x += par.x;
13825 		y += par.y;
13826 		par = par.parent;
13827 	}
13828 	return [x, y];
13829 }
13830 
13831 version(win32_widgets)
13832 private
13833 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow {
13834 // MapWindowPoints?
13835 	int x, y;
13836 	Widget par = c;
13837 	while(par) {
13838 		x += par.x;
13839 		y += par.y;
13840 		par = par.parent;
13841 		if(par !is null && par.useNativeDrawing())
13842 			break;
13843 	}
13844 	return [x, y];
13845 }
13846 
13847 ///
13848 class ImageBox : Widget {
13849 	private MemoryImage image_;
13850 
13851 	override int widthStretchiness() { return 1; }
13852 	override int heightStretchiness() { return 1; }
13853 	override int widthShrinkiness() { return 1; }
13854 	override int heightShrinkiness() { return 1; }
13855 
13856 	override int flexBasisHeight() {
13857 		return image_.height;
13858 	}
13859 
13860 	override int flexBasisWidth() {
13861 		return image_.width;
13862 	}
13863 
13864 	///
13865 	public void setImage(MemoryImage image){
13866 		this.image_ = image;
13867 		if(this.parentWindow && this.parentWindow.win) {
13868 			if(sprite)
13869 				sprite.dispose();
13870 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
13871 		}
13872 		redraw();
13873 	}
13874 
13875 	/// How to fit the image in the box if they aren't an exact match in size?
13876 	enum HowToFit {
13877 		center, /// centers the image, cropping around all the edges as needed
13878 		crop, /// always draws the image in the upper left, cropping the lower right if needed
13879 		// stretch, /// not implemented
13880 	}
13881 
13882 	private Sprite sprite;
13883 	private HowToFit howToFit_;
13884 
13885 	private Color backgroundColor_;
13886 
13887 	///
13888 	this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) {
13889 		this.image_ = image;
13890 		this.tabStop = false;
13891 		this.howToFit_ = howToFit;
13892 		this.backgroundColor_ = backgroundColor;
13893 		super(parent);
13894 		updateSprite();
13895 	}
13896 
13897 	/// ditto
13898 	this(MemoryImage image, HowToFit howToFit, Widget parent) {
13899 		this(image, howToFit, Color.transparent, parent);
13900 	}
13901 
13902 	private void updateSprite() {
13903 		if(sprite is null && this.parentWindow && this.parentWindow.win) {
13904 			sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true));
13905 		}
13906 	}
13907 
13908 	override void paint(WidgetPainter painter) {
13909 		updateSprite();
13910 		if(backgroundColor_.a) {
13911 			painter.fillColor = backgroundColor_;
13912 			painter.drawRectangle(Point(0, 0), width, height);
13913 		}
13914 		if(howToFit_ == HowToFit.crop)
13915 			sprite.drawAt(painter, Point(0, 0));
13916 		else if(howToFit_ == HowToFit.center) {
13917 			sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2));
13918 		}
13919 	}
13920 }
13921 
13922 ///
13923 class TextLabel : Widget {
13924 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; }
13925 	override int maxHeight() { return minHeight; }
13926 	override int minWidth() { return 32; }
13927 
13928 	override int flexBasisHeight() { return minHeight(); }
13929 	override int flexBasisWidth() { return defaultTextWidth(label); }
13930 
13931 	string label_;
13932 
13933 	/++
13934 		Indicates which other control this label is here for. Similar to HTML `for` attribute.
13935 
13936 		In practice this means a click on the label will focus the `labelFor`. In future versions
13937 		it will also set screen reader hints but that is not yet implemented.
13938 
13939 		History:
13940 			Added October 3, 2021 (dub v10.4)
13941 	+/
13942 	Widget labelFor;
13943 
13944 	///
13945 	@scriptable
13946 	string label() { return label_; }
13947 
13948 	///
13949 	@scriptable
13950 	void label(string l) {
13951 		label_ = l;
13952 		version(win32_widgets) {
13953 			WCharzBuffer bfr = WCharzBuffer(l);
13954 			SetWindowTextW(hwnd, bfr.ptr);
13955 		} else version(custom_widgets)
13956 			redraw();
13957 	}
13958 
13959 	override void defaultEventHandler_click(scope ClickEvent ce) {
13960 		if(this.labelFor !is null)
13961 			this.labelFor.focus();
13962 	}
13963 
13964 	/++
13965 		WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version.
13966 		For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly.
13967 	+/
13968 	this(string label, TextAlignment alignment, Widget parent) {
13969 		this.label_ = label;
13970 		this.alignment = alignment;
13971 		this.tabStop = false;
13972 		super(parent);
13973 
13974 		version(win32_widgets)
13975 		createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT);
13976 	}
13977 
13978 	/// ditto
13979 	this(string label, Widget parent) {
13980 		this(label, TextAlignment.Right, parent);
13981 	}
13982 
13983 	TextAlignment alignment;
13984 
13985 	version(custom_widgets)
13986 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
13987 		painter.outlineColor = getComputedStyle().foregroundColor;
13988 		painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment);
13989 		return bounds;
13990 	}
13991 }
13992 
13993 class TextDisplayHelper : Widget {
13994 	protected TextLayouter l;
13995 	protected ScrollMessageWidget smw;
13996 
13997 	private const(TextLayouter.State)*[] undoStack;
13998 	private const(TextLayouter.State)*[] redoStack;
13999 
14000 	private string preservedPrimaryText;
14001 	protected void selectionChanged() {
14002 		// sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString());
14003 		static if(UsingSimpledisplayX11)
14004 		with(l.selection()) {
14005 			if(!isEmpty()) {
14006 				//sdpyPrintDebugString("!isEmpty");
14007 
14008 				getPrimarySelection(parentWindow.win, (in char[] txt) {
14009 					// sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")");
14010 					// import std.stdio; writeln("txt: ", txt, " sel: ", getContentString);
14011 					if(txt.length) {
14012 						preservedPrimaryText = txt.idup;
14013 						// writeln(preservedPrimaryText);
14014 					}
14015 
14016 					setPrimarySelection(parentWindow.win, getContentString());
14017 				});
14018 			}
14019 		}
14020 	}
14021 
14022 	final TextLayouter layouter() {
14023 		return l;
14024 	}
14025 
14026 	bool readonly;
14027 	bool caretNavigation; // scroll lock can flip this
14028 	bool singleLine;
14029 	bool acceptsTabInput;
14030 
14031 	private Menu ctx;
14032 	override Menu contextMenu(int x, int y) {
14033 		if(ctx is null) {
14034 			ctx = new Menu("Actions", this);
14035 			if(!readonly) {
14036 				ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo)));
14037 				ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo)));
14038 				ctx.addSeparator();
14039 			}
14040 			if(!readonly)
14041 				ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut)));
14042 			ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, &copy)));
14043 			if(!readonly)
14044 				ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste)));
14045 			if(!readonly)
14046 				ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection)));
14047 			ctx.addSeparator();
14048 			ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll)));
14049 		}
14050 		return ctx;
14051 	}
14052 
14053 	override void defaultEventHandler_blur(BlurEvent ev) {
14054 		super.defaultEventHandler_blur(ev);
14055 		if(l.wasMutated()) {
14056 			auto evt = new ChangeEvent!string(this, &this.content);
14057 			evt.dispatch();
14058 			l.clearWasMutatedFlag();
14059 		}
14060 	}
14061 
14062 	private string content() {
14063 		return l.getTextString();
14064 	}
14065 
14066 	void undo() {
14067 		if(readonly) return;
14068 		if(undoStack.length) {
14069 			auto state = undoStack[$-1];
14070 			undoStack = undoStack[0 .. $-1];
14071 			undoStack.assumeSafeAppend();
14072 			redoStack ~= l.saveState();
14073 			l.restoreState(state);
14074 			adjustScrollbarSizes();
14075 			scrollForCaret();
14076 			redraw();
14077 			stateCheckpoint = true;
14078 		}
14079 	}
14080 
14081 	void redo() {
14082 		if(readonly) return;
14083 		if(redoStack.length) {
14084 			doStateCheckpoint();
14085 			auto state = redoStack[$-1];
14086 			redoStack = redoStack[0 .. $-1];
14087 			redoStack.assumeSafeAppend();
14088 			l.restoreState(state);
14089 			adjustScrollbarSizes();
14090 			scrollForCaret();
14091 			redraw();
14092 			stateCheckpoint = true;
14093 		}
14094 	}
14095 
14096 	void cut() {
14097 		if(readonly) return;
14098 		with(l.selection()) {
14099 			if(!isEmpty()) {
14100 				setClipboardText(parentWindow.win, getContentString());
14101 				doStateCheckpoint();
14102 				replaceContent("");
14103 				adjustScrollbarSizes();
14104 				scrollForCaret();
14105 				this.redraw();
14106 			}
14107 		}
14108 
14109 	}
14110 
14111 	void copy() {
14112 		with(l.selection()) {
14113 			if(!isEmpty()) {
14114 				setClipboardText(parentWindow.win, getContentString());
14115 				this.redraw();
14116 			}
14117 		}
14118 	}
14119 
14120 	void paste() {
14121 		if(readonly) return;
14122 		getClipboardText(parentWindow.win, (txt) {
14123 			doStateCheckpoint();
14124 			if(singleLine)
14125 				l.selection.replaceContent(txt.stripInternal());
14126 			else
14127 				l.selection.replaceContent(txt);
14128 			adjustScrollbarSizes();
14129 			scrollForCaret();
14130 			this.redraw();
14131 		});
14132 	}
14133 
14134 	void deleteContentOfSelection() {
14135 		if(readonly) return;
14136 		doStateCheckpoint();
14137 		l.selection.replaceContent("");
14138 		l.selection.setUserXCoordinate();
14139 		adjustScrollbarSizes();
14140 		scrollForCaret();
14141 		redraw();
14142 	}
14143 
14144 	void selectAll() {
14145 		with(l.selection) {
14146 			moveToStartOfDocument();
14147 			setAnchor();
14148 			moveToEndOfDocument();
14149 			setFocus();
14150 
14151 			selectionChanged();
14152 		}
14153 		redraw();
14154 	}
14155 
14156 	protected bool stateCheckpoint = true;
14157 
14158 	protected void doStateCheckpoint() {
14159 		if(stateCheckpoint) {
14160 			undoStack ~= l.saveState();
14161 			stateCheckpoint = false;
14162 		}
14163 	}
14164 
14165 	protected void adjustScrollbarSizes() {
14166 		// FIXME: will want a content area helper function instead of doing all these subtractions myself
14167 		auto borderWidth = 2;
14168 		this.smw.setTotalArea(l.width, l.height);
14169 		this.smw.setViewableArea(
14170 			this.width - this.paddingLeft - this.paddingRight - borderWidth * 2,
14171 			this.height - this.paddingTop - this.paddingBottom - borderWidth * 2);
14172 	}
14173 
14174 	protected void scrollForCaret() {
14175 		// writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom);
14176 		smw.scrollIntoView(l.selection.focusBoundingBox());
14177 	}
14178 
14179 	// FIXME: this should be a theme changed event listener instead
14180 	private BaseVisualTheme currentTheme;
14181 	override void recomputeChildLayout() {
14182 		if(currentTheme is null)
14183 			currentTheme = WidgetPainter.visualTheme;
14184 		if(WidgetPainter.visualTheme !is currentTheme) {
14185 			currentTheme = WidgetPainter.visualTheme;
14186 			auto ds = this.l.defaultStyle;
14187 			if(auto ms = cast(MyTextStyle) ds) {
14188 				auto cs = getComputedStyle();
14189 				auto font = cs.font();
14190 				if(font !is null)
14191 					ms.font_ = font;
14192 				else {
14193 					auto osc = new OperatingSystemFont();
14194 					osc.loadDefault;
14195 					ms.font_ = osc;
14196 				}
14197 			}
14198 		}
14199 		super.recomputeChildLayout();
14200 	}
14201 
14202 	private Point adjustForSingleLine(Point p) {
14203 		if(singleLine)
14204 			return Point(p.x, this.height / 2);
14205 		else
14206 			return p;
14207 	}
14208 
14209 	private bool wordWrapEnabled_;
14210 
14211 	this(TextLayouter l, ScrollMessageWidget parent) {
14212 		this.smw = parent;
14213 
14214 		smw.addDefaultWheelListeners(16, 16, 8);
14215 		smw.movementPerButtonClick(16, 16);
14216 
14217 		this.defaultPadding = Rectangle(2, 2, 2, 2);
14218 
14219 		this.l = l;
14220 		super(parent);
14221 
14222 		smw.addEventListener((scope ScrollEvent se) {
14223 			this.redraw();
14224 		});
14225 
14226 		this.addEventListener((scope ResizeEvent re) {
14227 			// FIXME: I should add a method to give this client area width thing
14228 			if(wordWrapEnabled_)
14229 				this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight;
14230 
14231 			adjustScrollbarSizes();
14232 			scrollForCaret();
14233 
14234 			this.redraw();
14235 		});
14236 
14237 	}
14238 
14239 	private {
14240 		bool mouseDown;
14241 		bool mouseActuallyMoved;
14242 
14243 		Point downAt;
14244 
14245 		Timer autoscrollTimer;
14246 		int autoscrollDirection;
14247 		int autoscrollAmount;
14248 
14249 		void autoscroll() {
14250 			switch(autoscrollDirection) {
14251 				case 0: smw.scrollUp(autoscrollAmount); break;
14252 				case 1: smw.scrollDown(autoscrollAmount); break;
14253 				case 2: smw.scrollLeft(autoscrollAmount); break;
14254 				case 3: smw.scrollRight(autoscrollAmount); break;
14255 				default: assert(0);
14256 			}
14257 
14258 			this.redraw();
14259 		}
14260 
14261 		void setAutoscrollTimer(int direction, int amount) {
14262 			if(autoscrollTimer is null) {
14263 				autoscrollTimer = new Timer(1000 / 60, &autoscroll);
14264 			}
14265 
14266 			autoscrollDirection = direction;
14267 			autoscrollAmount = amount;
14268 		}
14269 
14270 		void stopAutoscrollTimer() {
14271 			if(autoscrollTimer !is null) {
14272 				autoscrollTimer.dispose();
14273 				autoscrollTimer = null;
14274 			}
14275 			autoscrollAmount = 0;
14276 			autoscrollDirection = 0;
14277 		}
14278 	}
14279 
14280 	override void defaultEventHandler_mousemove(scope MouseMoveEvent ce) {
14281 		if(mouseDown) {
14282 			auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
14283 
14284 			// FIXME: when scrolling i actually do want a timer.
14285 			// i also want a zone near the sides of the window where i can auto scroll
14286 
14287 			auto scrollMultiplier = scaleWithDpi(16);
14288 			auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster
14289 
14290 			if(!singleLine && movedTo.y < 4) {
14291 				setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor);
14292 			} else
14293 			if(!singleLine && (movedTo.y + 6) > this.height) {
14294 				setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor);
14295 			} else
14296 			if(movedTo.x < 4) {
14297 				setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor);
14298 			} else
14299 			if((movedTo.x + 6) > this.width) {
14300 				setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor);
14301 			} else
14302 				stopAutoscrollTimer();
14303 
14304 			l.selection.moveTo(adjustForSingleLine(smw.position + movedTo));
14305 			l.selection.setFocus();
14306 			mouseActuallyMoved = true;
14307 			this.redraw();
14308 		}
14309 
14310 		super.defaultEventHandler_mousemove(ce);
14311 	}
14312 
14313 	override void defaultEventHandler_mouseup(scope MouseUpEvent ce) {
14314 		// FIXME: assert primary selection
14315 		if(mouseDown && ce.button == MouseButton.left) {
14316 			stateCheckpoint = true;
14317 			//l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop)));
14318 			//l.selection.setFocus();
14319 			mouseDown = false;
14320 			parentWindow.releaseMouseCapture();
14321 			stopAutoscrollTimer();
14322 			this.redraw();
14323 
14324 			if(mouseActuallyMoved)
14325 				selectionChanged();
14326 		}
14327 		//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
14328 
14329 		super.defaultEventHandler_mouseup(ce);
14330 	}
14331 
14332 	static if(UsingSimpledisplayX11)
14333 	override void defaultEventHandler_click(scope ClickEvent ce) {
14334 		if(ce.button == MouseButton.middle) {
14335 			parentWindow.win.getPrimarySelection((txt) {
14336 				doStateCheckpoint();
14337 
14338 				// import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText);
14339 
14340 				if(txt == l.selection.getContentString && preservedPrimaryText.length)
14341 					l.selection.replaceContent(preservedPrimaryText);
14342 				else
14343 					l.selection.replaceContent(txt);
14344 				redraw();
14345 			});
14346 		}
14347 
14348 		super.defaultEventHandler_click(ce);
14349 	}
14350 
14351 	override void defaultEventHandler_dblclick(scope DoubleClickEvent dce) {
14352 		if(dce.button == MouseButton.left) {
14353 			with(l.selection()) {
14354 				// FIXME: for a url or file picker i might wanna use / as a separator intead
14355 				scope dg = delegate const(char)[] (scope return const(char)[] ch) {
14356 					if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r")
14357 						return ch;
14358 					return null;
14359 				};
14360 				find(dg, 1, true).moveToEnd.setAnchor;
14361 				find(dg, 1, false).moveTo.setFocus;
14362 				selectionChanged();
14363 				redraw();
14364 			}
14365 		}
14366 
14367 		super.defaultEventHandler_dblclick(dce);
14368 	}
14369 
14370 	override void defaultEventHandler_mousedown(scope MouseDownEvent ce) {
14371 		if(ce.button == MouseButton.left) {
14372 			downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop);
14373 			l.selection.moveTo(adjustForSingleLine(smw.position + downAt));
14374 			if(ce.shiftKey)
14375 				l.selection.setFocus();
14376 			else
14377 				l.selection.setAnchor();
14378 			mouseDown = true;
14379 			mouseActuallyMoved = false;
14380 			parentWindow.captureMouse(this);
14381 			this.redraw();
14382 		}
14383 		//writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY)));
14384 
14385 		super.defaultEventHandler_mousedown(ce);
14386 	}
14387 
14388 	override void defaultEventHandler_char(scope CharEvent ce) {
14389 		super.defaultEventHandler_char(ce);
14390 
14391 		if(readonly)
14392 			return;
14393 		if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b')
14394 			return; // skip the ctrl+x characters we don't care about as plain text
14395 
14396 		if(singleLine && ce.character == '\n')
14397 			return;
14398 		if(!acceptsTabInput && ce.character == '\t')
14399 			return;
14400 
14401 		doStateCheckpoint();
14402 
14403 		char[4] buffer;
14404 		import arsd.core;
14405 		auto stride = encodeUtf8(buffer, ce.character);
14406 		l.selection.replaceContent(buffer[0 .. stride]);
14407 		l.selection.setUserXCoordinate();
14408 		adjustScrollbarSizes();
14409 		scrollForCaret();
14410 		redraw();
14411 
14412 	}
14413 
14414 	override void defaultEventHandler_keydown(scope KeyDownEvent kde) {
14415 		switch(kde.key) {
14416 			case Key.Up, Key.Down, Key.Left, Key.Right:
14417 			case Key.Home, Key.End:
14418 				stateCheckpoint = true;
14419 				bool setPosition = false;
14420 				switch(kde.key) {
14421 					case Key.Up: l.selection.moveUp(); break;
14422 					case Key.Down: l.selection.moveDown(); break;
14423 					case Key.Left: l.selection.moveLeft(); setPosition = true; break;
14424 					case Key.Right: l.selection.moveRight(); setPosition = true; break;
14425 					case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break;
14426 					case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break;
14427 					default: assert(0);
14428 				}
14429 
14430 				if(kde.shiftKey)
14431 					l.selection.setFocus();
14432 				else
14433 					l.selection.setAnchor();
14434 
14435 				selectionChanged();
14436 
14437 				if(setPosition)
14438 					l.selection.setUserXCoordinate();
14439 				scrollForCaret();
14440 				redraw();
14441 			break;
14442 			case Key.PageUp, Key.PageDown:
14443 				// want to act like the user clicked on the caret again
14444 				// after the scroll operation completed, so it would remain at
14445 				// about the same place on the viewport
14446 				auto oldY = smw.vsb.position;
14447 				smw.defaultKeyboardListener(kde);
14448 				auto newY = smw.vsb.position;
14449 				with(l.selection) {
14450 					auto uc = getUserCoordinate();
14451 					uc.y += newY - oldY;
14452 					moveTo(uc);
14453 
14454 					if(kde.shiftKey)
14455 						setFocus();
14456 					else
14457 						setAnchor();
14458 				}
14459 			break;
14460 			case Key.Delete:
14461 				if(l.selection.isEmpty()) {
14462 					l.selection.setAnchor();
14463 					l.selection.moveRight();
14464 					l.selection.setFocus();
14465 				}
14466 				deleteContentOfSelection();
14467 				adjustScrollbarSizes();
14468 				scrollForCaret();
14469 			break;
14470 			case Key.Insert:
14471 			break;
14472 			case Key.A:
14473 				if(kde.ctrlKey)
14474 					selectAll();
14475 			break;
14476 			case Key.F:
14477 				// find
14478 			break;
14479 			case Key.Z:
14480 				if(kde.ctrlKey)
14481 					undo();
14482 			break;
14483 			case Key.R:
14484 				if(kde.ctrlKey)
14485 					redo();
14486 			break;
14487 			case Key.X:
14488 				if(kde.ctrlKey)
14489 					cut();
14490 			break;
14491 			case Key.C:
14492 				if(kde.ctrlKey)
14493 					copy();
14494 			break;
14495 			case Key.V:
14496 				if(kde.ctrlKey)
14497 					paste();
14498 			break;
14499 			case Key.F1:
14500 				with(l.selection()) {
14501 					moveToStartOfLine();
14502 					setAnchor();
14503 					moveToEndOfLine();
14504 					moveToIncludeAdjacentEndOfLineMarker();
14505 					setFocus();
14506 					replaceContent("");
14507 				}
14508 
14509 				redraw();
14510 			break;
14511 			/*
14512 			case Key.F2:
14513 				l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle(
14514 					//(cast(MyTextStyle) old).font,
14515 					font2,
14516 					Color.red)));
14517 				redraw();
14518 			break;
14519 			*/
14520 			case Key.Tab:
14521 				// we process the char event, so don't want to change focus on it, unless the user overrides that with ctrl
14522 				if(acceptsTabInput && !kde.ctrlKey)
14523 					kde.preventDefault();
14524 			break;
14525 			default:
14526 		}
14527 
14528 		if(!kde.defaultPrevented)
14529 			super.defaultEventHandler_keydown(kde);
14530 	}
14531 
14532 	// we want to delegate all the Widget.Style stuff up to the other class that the user can see
14533 	override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) {
14534 		// this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidget Parent
14535 		if(parent && parent.parent && parent.parent.parent)
14536 			parent.parent.parent.useStyleProperties(dg);
14537 		else
14538 			super.useStyleProperties(dg);
14539 	}
14540 
14541 	override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; }
14542 	override int maxHeight() {
14543 		if(singleLine)
14544 			return minHeight;
14545 		else
14546 			return super.maxHeight();
14547 	}
14548 
14549 	void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
14550 		painter.setFont(myStyle.font);
14551 		painter.drawText(upperLeft, text);
14552 	}
14553 
14554 	override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) {
14555 		//painter.setFont(font);
14556 
14557 		auto cs = getComputedStyle();
14558 		auto defaultColor = cs.foregroundColor;
14559 
14560 		auto old = painter.setClipRectangleForWidget(bounds.upperLeft, bounds.width, bounds.height);
14561 		scope(exit) painter.setClipRectangleForWidget(old.upperLeft, old.width, old.height);
14562 
14563 		l.getDrawableText(delegate bool(txt, style, info, carets...) {
14564 			//writeln("Segment: ", txt);
14565 			assert(style !is null);
14566 
14567 			if(info.selections && info.boundingBox.width > 0) {
14568 				auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode
14569 				painter.fillColor = color;
14570 				painter.outlineColor = color;
14571 				painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size));
14572 				painter.outlineColor = cs.selectionForegroundColor;
14573 				//painter.fillColor = Color.white;
14574 			} else {
14575 				painter.outlineColor = defaultColor;
14576 			}
14577 
14578 			if(this.isFocused)
14579 			foreach(idx, caret; carets) {
14580 				if(idx == 0)
14581 					painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height);
14582 				painter.drawLine(
14583 					caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(),
14584 					bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position()
14585 				);
14586 			}
14587 
14588 			if(txt.stripInternal.length) {
14589 				// defaultColor = myStyle.color; // FIXME: so wrong
14590 				if(auto myStyle = cast(MyTextStyle) style)
14591 					drawTextSegment(myStyle, painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
14592 				else if(auto myStyle = cast(MyImageStyle) style)
14593 					myStyle.draw(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal);
14594 			}
14595 
14596 			if(info.boundingBox.upperLeft.y - smw.position().y > this.height) {
14597 				return false;
14598 			} else {
14599 				return true;
14600 			}
14601 		}, Rectangle(smw.position(), bounds.size));
14602 
14603 		/+
14604 		int place = 0;
14605 		int y = 75;
14606 		foreach(width; widths) {
14607 			painter.fillColor = Color.red;
14608 			painter.drawRectangle(Point(place, y), Size(width, 75));
14609 			//y += 15;
14610 			place += width;
14611 		}
14612 		+/
14613 
14614 		return bounds;
14615 	}
14616 
14617 	static class MyTextStyle : TextStyle {
14618 		OperatingSystemFont font_;
14619 		this(OperatingSystemFont font, bool passwordMode = false) {
14620 			this.font_ = font;
14621 		}
14622 
14623 		override OperatingSystemFont font() {
14624 			return font_;
14625 		}
14626 
14627 		bool foregroundColorOverridden;
14628 		bool backgroundColorOverridden;
14629 		Color foregroundColor;
14630 		Color backgroundColor; // should this be inline segment or the whole paragraph block?
14631 		bool italic;
14632 		bool bold;
14633 		bool underline;
14634 		bool strikeout;
14635 		bool subscript;
14636 		bool superscript;
14637 	}
14638 
14639 	static class MyImageStyle : TextStyle, MeasurableFont {
14640 		MemoryImage image_;
14641 		Image converted;
14642 		this(MemoryImage image) {
14643 			this.image_ =  image;
14644 			this.converted = Image.fromMemoryImage(image);
14645 		}
14646 
14647 		bool isMonospace() { return false; }
14648 		fnum averageWidth() { return image_.width; }
14649 		fnum height() { return image_.height; }
14650 		fnum ascent() { return image_.height; }
14651 		fnum descent() { return 0; }
14652 
14653 		fnum stringWidth(scope const(char)[] s, SimpleWindow window = null) {
14654 			return image_.width;
14655 		}
14656 
14657 		override MeasurableFont font() {
14658 			return this;
14659 		}
14660 
14661 		void draw(WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
14662 			painter.drawImage(upperLeft, converted);
14663 		}
14664 	}
14665 }
14666 
14667 /+
14668 class TextWidget : Widget {
14669 	TextLayouter l;
14670 	ScrollMessageWidget smw;
14671 	TextDisplayHelper helper;
14672 	this(TextLayouter l, Widget parent) {
14673 		this.l = l;
14674 		super(parent);
14675 
14676 		smw = new ScrollMessageWidget(this);
14677 		//smw.horizontalScrollBar.hide;
14678 		//smw.verticalScrollBar.hide;
14679 		smw.addDefaultWheelListeners(16, 16, 8);
14680 		smw.movementPerButtonClick(16, 16);
14681 		helper = new TextDisplayHelper(l, smw);
14682 
14683 		// no need to do this here since there's gonna be a resize
14684 		// event immediately before any drawing
14685 		// smw.setTotalArea(l.width, l.height);
14686 		smw.setViewableArea(
14687 			this.width - this.paddingLeft - this.paddingRight,
14688 			this.height - this.paddingTop - this.paddingBottom);
14689 
14690 		/+
14691 		writeln(l.width, "x", l.height);
14692 		+/
14693 	}
14694 }
14695 +/
14696 
14697 
14698 
14699 
14700 /+
14701 	make sure it calls parentWindow.inputProxy.setIMEPopupLocation too
14702 +/
14703 
14704 /++
14705 	Contains the implementation of text editing and shared basic api. You should construct one of the child classes instead, like [TextEdit], [LineEdit], or [PasswordEdit].
14706 +/
14707 abstract class EditableTextWidget : Widget {
14708 	protected this(Widget parent) {
14709 		version(custom_widgets)
14710 			this(true, parent);
14711 		else
14712 			this(false, parent);
14713 	}
14714 
14715 	private bool useCustomWidget;
14716 
14717 	protected this(bool useCustomWidget, Widget parent) {
14718 		this.useCustomWidget = useCustomWidget;
14719 
14720 		super(parent);
14721 
14722 		if(useCustomWidget)
14723 			setupCustomTextEditing();
14724 	}
14725 
14726 	private bool wordWrapEnabled_;
14727 	/++
14728 		Enables or disables wrapping of long lines on word boundaries.
14729 	+/
14730 	void wordWrapEnabled(bool enabled) {
14731 		if(useCustomWidget) {
14732 			wordWrapEnabled_ = enabled;
14733 			if(tdh)
14734 				tdh.wordWrapEnabled_ = true;
14735 			textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME
14736 		} else version(win32_widgets) {
14737 			SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0);
14738 		}
14739 	}
14740 
14741 	override int minWidth() { return scaleWithDpi(16); }
14742 	override int widthStretchiness() { return 7; }
14743 	override int widthShrinkiness() { return 1; }
14744 
14745 	override int maxHeight() {
14746 		if(useCustomWidget)
14747 			return tdh.maxHeight;
14748 		else
14749 			return super.maxHeight();
14750 	}
14751 
14752 	override void focus() {
14753 		if(useCustomWidget && tdh)
14754 			tdh.focus();
14755 		else
14756 			super.focus();
14757 	}
14758 
14759 	override void defaultEventHandler_focusout(FocusOutEvent foe) {
14760 		if(tdh !is null && foe.target is tdh)
14761 			tdh.redraw();
14762 	}
14763 
14764 	override void defaultEventHandler_focusin(FocusInEvent foe) {
14765 		if(tdh !is null && foe.target is tdh)
14766 			tdh.redraw();
14767 	}
14768 
14769 
14770 	/++
14771 		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.
14772 	+/
14773 	void selectAll() {
14774 		if(useCustomWidget) {
14775 			tdh.selectAll();
14776 		} else version(win32_widgets) {
14777 			SendMessage(hwnd, EM_SETSEL, 0, -1);
14778 		}
14779 	}
14780 
14781 	/++
14782 		Basic clipboard operations.
14783 
14784 		History:
14785 			Added December 31, 2024
14786 	+/
14787 	void copy() {
14788 		if(useCustomWidget) {
14789 			tdh.copy();
14790 		} else version(win32_widgets) {
14791 			SendMessage(hwnd, WM_COPY, 0, 0);
14792 		}
14793 	}
14794 
14795 	/// ditto
14796 	void cut() {
14797 		if(useCustomWidget) {
14798 			tdh.cut();
14799 		} else version(win32_widgets) {
14800 			SendMessage(hwnd, WM_CUT, 0, 0);
14801 		}
14802 	}
14803 
14804 	/// ditto
14805 	void paste() {
14806 		if(useCustomWidget) {
14807 			tdh.paste();
14808 		} else version(win32_widgets) {
14809 			SendMessage(hwnd, WM_PASTE, 0, 0);
14810 		}
14811 	}
14812 
14813 	///
14814 	void undo() {
14815 		if(useCustomWidget) {
14816 			tdh.undo();
14817 		} else version(win32_widgets) {
14818 			SendMessage(hwnd, EM_UNDO, 0, 0);
14819 		}
14820 	}
14821 
14822 	// note that WM_CLEAR deletes the selection without copying it to the clipboard
14823 	// also windows supports margins, modified flag, and much more
14824 
14825 	// EM_UNDO and EM_CANUNDO. EM_REDO is only supported in rich text boxes here
14826 
14827 	// EM_GETSEL, EM_REPLACESEL, and EM_SETSEL might be usable for find etc.
14828 
14829 
14830 
14831 	/*protected*/ TextDisplayHelper tdh;
14832 	/*protected*/ TextLayouter textLayout;
14833 
14834 	/++
14835 		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.
14836 	+/
14837 	@property string content() {
14838 		if(useCustomWidget) {
14839 			return textLayout.getTextString();
14840 		} else version(win32_widgets) {
14841 			wchar[4096] bufferstack;
14842 			wchar[] buffer;
14843 			auto len = GetWindowTextLength(hwnd);
14844 			if(len < bufferstack.length)
14845 				buffer = bufferstack[0 .. len + 1];
14846 			else
14847 				buffer = new wchar[](len + 1);
14848 
14849 			auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length);
14850 			if(l >= 0)
14851 				return makeUtf8StringFromWindowsString(buffer[0 .. l]);
14852 			else
14853 				return null;
14854 		}
14855 
14856 		assert(0);
14857 	}
14858 	/// ditto
14859 	@property void content(string s) {
14860 		if(useCustomWidget) {
14861 			with(textLayout.selection) {
14862 				moveToStartOfDocument();
14863 				setAnchor();
14864 				moveToEndOfDocument();
14865 				setFocus();
14866 				replaceContent(s);
14867 			}
14868 
14869 			tdh.adjustScrollbarSizes();
14870 			// these don't seem to help
14871 			// tdh.smw.setPosition(0, 0);
14872 			// tdh.scrollForCaret();
14873 
14874 			redraw();
14875 		} else version(win32_widgets) {
14876 			WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines);
14877 			SetWindowTextW(hwnd, bfr.ptr);
14878 		}
14879 	}
14880 
14881 	/++
14882 		Appends some text to the widget at the end, without affecting the user selection or cursor position.
14883 	+/
14884 	void addText(string txt) {
14885 		if(useCustomWidget) {
14886 			textLayout.appendText(txt);
14887 			tdh.adjustScrollbarSizes();
14888 			redraw();
14889 		} else version(win32_widgets) {
14890 			// get the current selection
14891 			DWORD StartPos, EndPos;
14892 			SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) );
14893 
14894 			// move the caret to the end of the text
14895 			int outLength = GetWindowTextLengthW(hwnd);
14896 			SendMessageW( hwnd, EM_SETSEL, outLength, outLength );
14897 
14898 			// insert the text at the new caret position
14899 			WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines);
14900 			SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr );
14901 
14902 			// restore the previous selection
14903 			SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos );
14904 		}
14905 	}
14906 
14907 	// EM_SCROLLCARET scrolls the caret into view
14908 
14909 	void scrollToBottom() {
14910 		if(useCustomWidget) {
14911 			tdh.smw.scrollDown(int.max);
14912 		} else version(win32_widgets) {
14913 			SendMessageW( hwnd, EM_LINESCROLL, 0, int.max );
14914 		}
14915 	}
14916 
14917 	protected TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
14918 		return new TextDisplayHelper(textLayout, smw);
14919 	}
14920 
14921 	protected TextStyle defaultTextStyle() {
14922 		return new TextDisplayHelper.MyTextStyle(getUsedFont());
14923 	}
14924 
14925 	private OperatingSystemFont getUsedFont() {
14926 		auto cs = getComputedStyle();
14927 		auto font = cs.font;
14928 		if(font is null) {
14929 			font = new OperatingSystemFont;
14930 			font.loadDefault();
14931 		}
14932 		return font;
14933 	}
14934 
14935 	protected void setupCustomTextEditing() {
14936 		textLayout = new TextLayouter(defaultTextStyle());
14937 
14938 		auto smw = new ScrollMessageWidget(this);
14939 		if(!showingHorizontalScroll)
14940 			smw.horizontalScrollBar.hide();
14941 		if(!showingVerticalScroll)
14942 			smw.verticalScrollBar.hide();
14943 		this.tabStop = false;
14944 		smw.tabStop = false;
14945 		tdh = textDisplayHelperFactory(textLayout, smw);
14946 	}
14947 
14948 	override void newParentWindow(Window old, Window n) {
14949 		if(n is null) return;
14950 		this.parentWindow.addEventListener((scope DpiChangedEvent dce) {
14951 			if(textLayout) {
14952 				if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) {
14953 					// the dpi change can change the font, so this informs the layouter that it has changed too
14954 					style.font_ = getUsedFont();
14955 
14956 					// arsd.core.writeln(this.parentWindow.win.actualDpi);
14957 				}
14958 			}
14959 		});
14960 	}
14961 
14962 	static class Style : Widget.Style {
14963 		override WidgetBackground background() {
14964 			return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor);
14965 		}
14966 
14967 		override Color foregroundColor() {
14968 			return WidgetPainter.visualTheme.foregroundColor;
14969 		}
14970 
14971 		override FrameStyle borderStyle() {
14972 			return FrameStyle.sunk;
14973 		}
14974 
14975 		override MouseCursor cursor() {
14976 			return GenericCursor.Text;
14977 		}
14978 	}
14979 	mixin OverrideStyle!Style;
14980 
14981 	version(win32_widgets) {
14982 		private string lastContentBlur;
14983 
14984 		override void defaultEventHandler_blur(BlurEvent ev) {
14985 			super.defaultEventHandler_blur(ev);
14986 
14987 			if(!useCustomWidget)
14988 			if(this.content != lastContentBlur) {
14989 				auto evt = new ChangeEvent!string(this, &this.content);
14990 				evt.dispatch();
14991 				lastContentBlur = this.content;
14992 			}
14993 		}
14994 	}
14995 
14996 
14997 	bool showingVerticalScroll() { return true; }
14998 	bool showingHorizontalScroll() { return true; }
14999 }
15000 
15001 /++
15002 	A `LineEdit` is an editor of a single line of text, comparable to a HTML `<input type="text" />`.
15003 
15004 	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.
15005 
15006 	See_Also:
15007 		[PasswordEdit] for a `LineEdit` that obscures its input.
15008 
15009 		[TextEdit] for a multi-line plain text editor widget.
15010 
15011 		[TextLabel] for a single line piece of static text.
15012 
15013 		[TextDisplay] for a read-only display of a larger piece of plain text.
15014 +/
15015 class LineEdit : EditableTextWidget {
15016 	override bool showingVerticalScroll() { return false; }
15017 	override bool showingHorizontalScroll() { return false; }
15018 
15019 	override int flexBasisWidth() { return 250; }
15020 	override int widthShrinkiness() { return 10; }
15021 
15022 	///
15023 	this(Widget parent) {
15024 		super(parent);
15025 		version(win32_widgets) {
15026 			createWin32Window(this, "edit"w, "",
15027 				0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
15028 		} else version(custom_widgets) {
15029 		} else static assert(false);
15030 	}
15031 
15032 	private this(bool useCustomWidget, Widget parent) {
15033 		if(!useCustomWidget)
15034 			this(parent);
15035 		else
15036 			super(true, parent);
15037 	}
15038 
15039 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
15040 		auto tdh = new TextDisplayHelper(textLayout, smw);
15041 		tdh.singleLine = true;
15042 		return tdh;
15043 	}
15044 
15045 	version(win32_widgets) {
15046 		mixin Padding!q{0};
15047 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
15048 		override int maxHeight() { return minHeight; }
15049 	}
15050 
15051 	/+
15052 	@property void passwordMode(bool p) {
15053 		SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD);
15054 	}
15055 	+/
15056 }
15057 
15058 /// ditto
15059 class CustomLineEdit : LineEdit {
15060 	this(Widget parent) {
15061 		super(true, parent);
15062 	}
15063 }
15064 
15065 /++
15066 	A [LineEdit] that displays `*` in place of the actual characters.
15067 
15068 	Alas, Windows requires the window to be created differently to use this style,
15069 	so it had to be a new class instead of a toggle on and off on an existing object.
15070 
15071 	History:
15072 		Added January 24, 2021
15073 
15074 		Implemented on Linux on January 31, 2023.
15075 +/
15076 class PasswordEdit : EditableTextWidget {
15077 	override bool showingVerticalScroll() { return false; }
15078 	override bool showingHorizontalScroll() { return false; }
15079 
15080 	override int flexBasisWidth() { return 250; }
15081 
15082 	override TextStyle defaultTextStyle() {
15083 		auto cs = getComputedStyle();
15084 
15085 		auto osf = new class OperatingSystemFont {
15086 			this() {
15087 				super(cs.font);
15088 			}
15089 			override fnum stringWidth(scope const(char)[] text, SimpleWindow window = null) {
15090 				int count = 0;
15091 				foreach(dchar ch; text)
15092 					count++;
15093 				return count * super.stringWidth("*", window);
15094 			}
15095 		};
15096 
15097 		return new TextDisplayHelper.MyTextStyle(osf);
15098 	}
15099 
15100 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
15101 		static class TDH : TextDisplayHelper {
15102 			this(TextLayouter textLayout, ScrollMessageWidget smw) {
15103 				singleLine = true;
15104 				super(textLayout, smw);
15105 			}
15106 
15107 			override void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) {
15108 				char[256] buffer = void;
15109 				int bufferLength = 0;
15110 				foreach(dchar ch; text)
15111 					buffer[bufferLength++] = '*';
15112 				painter.setFont(myStyle.font);
15113 				painter.drawText(upperLeft, buffer[0..bufferLength]);
15114 			}
15115 		}
15116 
15117 		return new TDH(textLayout, smw);
15118 	}
15119 
15120 	///
15121 	this(Widget parent) {
15122 		super(parent);
15123 		version(win32_widgets) {
15124 			createWin32Window(this, "edit"w, "",
15125 				ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL);
15126 		} else version(custom_widgets) {
15127 		} else static assert(false);
15128 	}
15129 
15130 	private this(bool useCustomWidget, Widget parent) {
15131 		if(!useCustomWidget)
15132 			this(parent);
15133 		else
15134 			super(true, parent);
15135 	}
15136 
15137 	version(win32_widgets) {
15138 		mixin Padding!q{2};
15139 		override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; }
15140 		override int maxHeight() { return minHeight; }
15141 	}
15142 }
15143 
15144 /// ditto
15145 class CustomPasswordEdit : PasswordEdit {
15146 	this(Widget parent) {
15147 		super(true, parent);
15148 	}
15149 }
15150 
15151 
15152 /++
15153 	A `TextEdit` is a multi-line plain text editor, comparable to a HTML `<textarea>`.
15154 
15155 	See_Also:
15156 		[TextDisplay] for a read-only text display.
15157 
15158 		[LineEdit] for a single line text editor.
15159 
15160 		[PasswordEdit] for a single line text editor that obscures its input.
15161 +/
15162 class TextEdit : EditableTextWidget {
15163 	///
15164 	this(Widget parent) {
15165 		super(parent);
15166 		version(win32_widgets) {
15167 			createWin32Window(this, "edit"w, "",
15168 				0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE);
15169 		} else version(custom_widgets) {
15170 		} else static assert(false);
15171 	}
15172 
15173 	private this(bool useCustomWidget, Widget parent) {
15174 		if(!useCustomWidget)
15175 			this(parent);
15176 		else
15177 			super(true, parent);
15178 	}
15179 
15180 	override int maxHeight() { return int.max; }
15181 	override int heightStretchiness() { return 7; }
15182 
15183 	override int flexBasisWidth() { return 250; }
15184 	override int flexBasisHeight() { return 25; }
15185 }
15186 
15187 /// ditto
15188 class CustomTextEdit : TextEdit {
15189 	this(Widget parent) {
15190 		super(true, parent);
15191 	}
15192 }
15193 
15194 /+
15195 /++
15196 
15197 +/
15198 version(none)
15199 class RichTextDisplay : Widget {
15200 	@property void content(string c) {}
15201 	void appendContent(string c) {}
15202 }
15203 +/
15204 
15205 /++
15206 	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.
15207 
15208 	History:
15209 		Added October 31, 2023 (dub v11.3)
15210 +/
15211 class TextDisplay : EditableTextWidget {
15212 	this(string text, Widget parent) {
15213 		super(true, parent);
15214 		this.content = text;
15215 	}
15216 
15217 	override int maxHeight() { return int.max; }
15218 	override int minHeight() { return Window.defaultLineHeight; }
15219 	override int heightStretchiness() { return 7; }
15220 	override int heightShrinkiness() { return 2; }
15221 
15222 	override int flexBasisWidth() {
15223 		return scaleWithDpi(250);
15224 	}
15225 	override int flexBasisHeight() {
15226 		if(textLayout is null || this.tdh is null)
15227 			return Window.defaultLineHeight;
15228 
15229 		auto textHeight = borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textLayout.height))).height;
15230 		return this.tdh.borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textHeight))).height;
15231 	}
15232 
15233 	override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) {
15234 		return new MyTextDisplayHelper(textLayout, smw);
15235 	}
15236 
15237 	override void registerMovement() {
15238 		super.registerMovement();
15239 		this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally
15240 	}
15241 
15242 	static class MyTextDisplayHelper : TextDisplayHelper {
15243 		this(TextLayouter textLayout, ScrollMessageWidget smw) {
15244 			smw.verticalScrollBar.hide();
15245 			smw.horizontalScrollBar.hide();
15246 			super(textLayout, smw);
15247 			this.readonly = true;
15248 		}
15249 
15250 		override void registerMovement() {
15251 			super.registerMovement();
15252 
15253 			// FIXME: do the horizontal one too as needed and make sure that it does
15254 			// wordwrapping again
15255 			if(l.height + smw.horizontalScrollBar.height > this.height)
15256 				smw.verticalScrollBar.show();
15257 			else
15258 				smw.verticalScrollBar.hide();
15259 
15260 			l.wordWrapWidth = this.width;
15261 
15262 			smw.verticalScrollBar.setPosition = 0;
15263 		}
15264 	}
15265 
15266 	static class Style : Widget.Style {
15267 		// just want the generic look for these
15268 	}
15269 
15270 	mixin OverrideStyle!Style;
15271 }
15272 
15273 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active
15274 /++
15275 	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.
15276 
15277 
15278 	When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`.
15279 
15280 	Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand.
15281 
15282 	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.
15283 
15284 	History:
15285 		Added August 12, 2024 (dub v11.6)
15286 +/
15287 abstract class GenericListViewWidget : Widget {
15288 	/++
15289 
15290 	+/
15291 	this(Widget parent) {
15292 		super(parent);
15293 
15294 		smw = new ScrollMessageWidget(this);
15295 		smw.addDefaultKeyboardListeners(itemSize.height, itemSize.width);
15296 		smw.addDefaultWheelListeners(itemSize.height, itemSize.width);
15297 		smw.hsb.hide(); // FIXME: this might actually be useful but we can't really communicate that yet
15298 
15299 		inner = new GenericListViewWidgetInner(this, smw, new GenericListViewInnerContainer(smw));
15300 		inner.tabStop = this.tabStop;
15301 		this.tabStop = false;
15302 	}
15303 
15304 	private ScrollMessageWidget smw;
15305 	private GenericListViewWidgetInner inner;
15306 
15307 	/++
15308 
15309 	+/
15310 	abstract GenericListViewItem itemFactory(Widget parent);
15311 	// in device-dependent pixels
15312 	/++
15313 
15314 	+/
15315 	abstract Size itemSize(); // use 0 to indicate it can stretch?
15316 
15317 	enum LayoutMode {
15318 		rows,
15319 		columns,
15320 		gridRowsFirst,
15321 		gridColumnsFirst
15322 	}
15323 	LayoutMode layoutMode() {
15324 		return LayoutMode.rows;
15325 	}
15326 
15327 	private int itemCount_;
15328 
15329 	/++
15330 		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.
15331 	+/
15332 	void setItemCount(int count) {
15333 		smw.setTotalArea(inner.width, count * itemSize().height);
15334 		smw.setViewableArea(inner.width, inner.height);
15335 		this.itemCount_ = count;
15336 	}
15337 
15338 	/++
15339 		Returns the current count of items expected to available in the list.
15340 	+/
15341 	int itemCount() {
15342 		return this.itemCount_;
15343 	}
15344 
15345 	/++
15346 		Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data.
15347 
15348 		Note you must $(I also) call [setItemCount] if the total item count has changed.
15349 	+/
15350 	void notifyItemsChanged(int index, int count = 1) {
15351 	}
15352 	/// ditto
15353 	void notifyItemsInserted(int index, int count = 1) {
15354 	}
15355 	/// ditto
15356 	void notifyItemsRemoved(int index, int count = 1) {
15357 	}
15358 	/// ditto
15359 	void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) {
15360 	}
15361 
15362 	/++
15363 		History:
15364 			Added January 1, 2025
15365 	+/
15366 	void ensureItemVisibleInScroll(int index) {
15367 		auto itemPos = index * itemSize().height;
15368 		auto vsb = smw.verticalScrollBar;
15369 		auto viewable = vsb.viewableArea_;
15370 
15371 		if(viewable == 0) {
15372 			// viewable == 0 isn't actually supposed to happen, this means
15373 			// this method is being called before having our size assigned, it should
15374 			// probably just queue it up for later.
15375 			queuedScroll = index;
15376 			return;
15377 		}
15378 
15379 		queuedScroll = int.min;
15380 
15381 		if(itemPos < vsb.position) {
15382 			// scroll up to it
15383 			vsb.setPosition(itemPos);
15384 			smw.notify();
15385 		} else if(itemPos + itemSize().height > (vsb.position + viewable)) {
15386 			// scroll down to it, so it is at the bottom
15387 
15388 			auto lastViewableItemPosition = (viewable - itemSize.height) / itemSize.height * itemSize.height;
15389 			// need the itemPos to be at the lastViewableItemPosition after scrolling, so subtraction does it
15390 
15391 			vsb.setPosition(itemPos - lastViewableItemPosition);
15392 			smw.notify();
15393 		}
15394 	}
15395 
15396 	/++
15397 		History:
15398 			Added January 1, 2025;
15399 	+/
15400 	int numberOfCurrentlyFullyVisibleItems() {
15401 		return smw.verticalScrollBar.viewableArea_ / itemSize.height;
15402 	}
15403 
15404 	private int queuedScroll = int.min;
15405 
15406 	override void recomputeChildLayout() {
15407 		super.recomputeChildLayout();
15408 		if(queuedScroll != int.min)
15409 			ensureItemVisibleInScroll(queuedScroll);
15410 	}
15411 
15412 	private GenericListViewItem[] items;
15413 
15414 	override void paint(WidgetPainter painter) {}
15415 }
15416 
15417 /// ditto
15418 abstract class GenericListViewItem : Widget {
15419 	/++
15420 	+/
15421 	this(Widget parent) {
15422 		super(parent);
15423 	}
15424 
15425 	private int _currentIndex = -1;
15426 
15427 	private void showItemPrivate(int idx) {
15428 		showItem(idx);
15429 		_currentIndex = idx;
15430 	}
15431 
15432 	/++
15433 		Implement this to show an item from your data backing to the list.
15434 
15435 		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.
15436 	+/
15437 	abstract void showItem(int idx);
15438 
15439 	/++
15440 		Maintained by the library after calling [showItem] so the object knows which data index it currently has.
15441 
15442 		It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent).
15443 
15444 		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.
15445 	+/
15446 	final int currentIndexLoaded() {
15447 		return _currentIndex;
15448 	}
15449 }
15450 
15451 ///
15452 unittest {
15453 	import arsd.minigui;
15454 
15455 	import std.conv;
15456 
15457 	void main() {
15458 		auto mw = new MainWindow();
15459 
15460 		static class MyListViewItem : GenericListViewItem {
15461 			this(Widget parent) {
15462 				super(parent);
15463 
15464 				label = new TextLabel("unloaded", TextAlignment.Left, this);
15465 				button = new Button("Click", this);
15466 
15467 				button.addEventListener("triggered", (){
15468 					messageBox(text("clicked ", currentIndexLoaded()));
15469 				});
15470 			}
15471 			override void showItem(int idx) {
15472 				label.label = "Item " ~ to!string(idx);
15473 			}
15474 
15475 			TextLabel label;
15476 			Button button;
15477 		}
15478 
15479 		auto widget = new class GenericListViewWidget {
15480 			this() {
15481 				super(mw);
15482 			}
15483 			override GenericListViewItem itemFactory(Widget parent) {
15484 				return new MyListViewItem(parent);
15485 			}
15486 			override Size itemSize() {
15487 				return Size(0, scaleWithDpi(80));
15488 			}
15489 		};
15490 
15491 		widget.setItemCount(5000);
15492 
15493 		mw.loop();
15494 	}
15495 }
15496 
15497 // this exists just to wrap the actual GenericListViewWidgetInner so borders
15498 // and padding and stuff can work
15499 private class GenericListViewInnerContainer : Widget {
15500 	this(Widget parent) {
15501 		super(parent);
15502 		this.tabStop = false;
15503 	}
15504 
15505 	override void recomputeChildLayout() {
15506 		registerMovement();
15507 
15508 		auto cs = getComputedStyle();
15509 		auto bw = getBorderWidth(cs.borderStyle);
15510 
15511 		assert(children.length < 2);
15512 		foreach(child; children) {
15513 			child.x = bw + paddingLeft();
15514 			child.y = bw + paddingTop();
15515 			child.width = this.width.NonOverflowingUint - bw - bw - paddingLeft() - paddingRight();
15516 			child.height = this.height.NonOverflowingUint - bw - bw - paddingTop() - paddingBottom();
15517 
15518 			child.recomputeChildLayout();
15519 		}
15520 	}
15521 
15522 	override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) {
15523 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15524 			return parent.parent.parent.useStyleProperties(dg);
15525 		else
15526 			return super.useStyleProperties(dg);
15527 	}
15528 
15529 	override int paddingTop() {
15530 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15531 			return parent.parent.parent.paddingTop();
15532 		else
15533 			return super.paddingTop();
15534 	}
15535 
15536 	override int paddingBottom() {
15537 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15538 			return parent.parent.parent.paddingBottom();
15539 		else
15540 			return super.paddingBottom();
15541 	}
15542 
15543 	override int paddingLeft() {
15544 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15545 			return parent.parent.parent.paddingLeft();
15546 		else
15547 			return super.paddingLeft();
15548 	}
15549 
15550 	override int paddingRight() {
15551 		if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget
15552 			return parent.parent.parent.paddingRight();
15553 		else
15554 			return super.paddingRight();
15555 	}
15556 
15557 
15558 }
15559 
15560 private class GenericListViewWidgetInner : Widget {
15561 	this(GenericListViewWidget glvw, ScrollMessageWidget smw, GenericListViewInnerContainer parent) {
15562 		super(parent);
15563 		this.glvw = glvw;
15564 
15565 		reloadVisible();
15566 
15567 		smw.addEventListener("scroll", () {
15568 			reloadVisible();
15569 		});
15570 	}
15571 
15572 	override void registerMovement() {
15573 		super.registerMovement();
15574 		if(glvw && glvw.smw)
15575 			glvw.smw.setViewableArea(this.width, this.height);
15576 	}
15577 
15578 	void reloadVisible() {
15579 		auto y = glvw.smw.position.y / glvw.itemSize.height;
15580 
15581 		// idk why i had this here it doesn't seem to be ueful and actually made last items diasppear
15582 		//int offset = glvw.smw.position.y % glvw.itemSize.height;
15583 		//if(offset || y >= glvw.itemCount())
15584 			//y--;
15585 
15586 		if(y < 0)
15587 			y = 0;
15588 
15589 		recomputeChildLayout();
15590 
15591 		foreach(item; glvw.items) {
15592 			if(y < glvw.itemCount()) {
15593 				item.showItemPrivate(y);
15594 				item.show();
15595 			} else {
15596 				item.hide();
15597 			}
15598 			y++;
15599 		}
15600 
15601 		this.redraw();
15602 	}
15603 
15604 	private GenericListViewWidget glvw;
15605 
15606 	private bool inRcl;
15607 	override void recomputeChildLayout() {
15608 		if(inRcl)
15609 			return;
15610 		inRcl = true;
15611 		scope(exit)
15612 			inRcl = false;
15613 
15614 		registerMovement();
15615 
15616 		auto ih = glvw.itemSize().height;
15617 
15618 		auto itemCount = this.height / ih + 2; // extra for partial display before and after
15619 		bool hadNew;
15620 		while(glvw.items.length < itemCount) {
15621 			// FIXME: free the old items? maybe just set length
15622 			glvw.items ~= glvw.itemFactory(this);
15623 			hadNew = true;
15624 		}
15625 
15626 		if(hadNew)
15627 			reloadVisible();
15628 
15629 		int y = -(glvw.smw.position.y % ih) + this.paddingTop();
15630 		foreach(child; children) {
15631 			child.x = this.paddingLeft();
15632 			child.y = y;
15633 			y += glvw.itemSize().height;
15634 			child.width = this.width.NonOverflowingUint - this.paddingLeft() - this.paddingRight();
15635 			child.height = ih;
15636 
15637 			child.recomputeChildLayout();
15638 		}
15639 	}
15640 }
15641 
15642 
15643 
15644 /++
15645 	History:
15646 		It was a child of Window before, but as of September 29, 2024, it is now a child of `Dialog`.
15647 +/
15648 class MessageBox : Dialog {
15649 	private string message;
15650 	MessageBoxButton buttonPressed = MessageBoxButton.None;
15651 	/++
15652 
15653 		History:
15654 		The overload that takes `Window originator` was added on September 29, 2024.
15655 	+/
15656 	this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
15657 		this(null, message, buttons, buttonIds);
15658 	}
15659 	/// ditto
15660 	this(Window originator, string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) {
15661 		message = message.stripRightInternal;
15662 		int mainWidth;
15663 
15664 		// estimate longest line
15665 		int count;
15666 		foreach(ch; message) {
15667 			if(ch == '\n') {
15668 				if(count > mainWidth)
15669 					mainWidth = count;
15670 				count = 0;
15671 			} else {
15672 				count++;
15673 			}
15674 		}
15675 		mainWidth *= 8;
15676 		if(mainWidth < 300)
15677 			mainWidth = 300;
15678 		if(mainWidth > 600)
15679 			mainWidth = 600;
15680 
15681 		super(originator, mainWidth, 100);
15682 
15683 		assert(buttons.length);
15684 		assert(buttons.length ==  buttonIds.length);
15685 
15686 		this.message = message;
15687 
15688 		auto label = new TextDisplay(message, this);
15689 
15690 		auto hl = new HorizontalLayout(this);
15691 		auto spacer = new HorizontalSpacer(hl); // to right align
15692 
15693 		foreach(idx, buttonText; buttons) {
15694 			auto button = new CommandButton(buttonText, hl);
15695 
15696 			button.addEventListener(EventType.triggered, ((size_t idx) { return () {
15697 				this.buttonPressed = buttonIds[idx];
15698 				win.close();
15699 			}; })(idx));
15700 
15701 			if(idx == 0)
15702 				button.focus();
15703 		}
15704 
15705 		if(buttons.length == 1)
15706 			auto spacer2 = new HorizontalSpacer(hl); // to center it
15707 
15708 		auto size = label.flexBasisHeight() + hl.minHeight() + this.paddingTop + this.paddingBottom;
15709 		auto max = scaleWithDpi(600); // random max height
15710 		if(size > max)
15711 			size = max;
15712 
15713 		win.resize(scaleWithDpi(mainWidth), size);
15714 
15715 		win.show();
15716 		redraw();
15717 	}
15718 
15719 	override void OK() {
15720 		this.win.close();
15721 	}
15722 
15723 	mixin Padding!q{16};
15724 }
15725 
15726 ///
15727 enum MessageBoxStyle {
15728 	OK, ///
15729 	OKCancel, ///
15730 	RetryCancel, ///
15731 	YesNo, ///
15732 	YesNoCancel, ///
15733 	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.
15734 }
15735 
15736 ///
15737 enum MessageBoxIcon {
15738 	None, ///
15739 	Info, ///
15740 	Warning, ///
15741 	Error ///
15742 }
15743 
15744 /// Identifies the button the user pressed on a message box.
15745 enum MessageBoxButton {
15746 	None, /// The user closed the message box without clicking any of the buttons.
15747 	OK, ///
15748 	Cancel, ///
15749 	Retry, ///
15750 	Yes, ///
15751 	No, ///
15752 	Continue ///
15753 }
15754 
15755 
15756 /++
15757 	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.
15758 
15759 	Returns: the button pressed.
15760 +/
15761 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15762 	return messageBox(null, title, message, style, icon);
15763 }
15764 
15765 /// ditto
15766 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15767 	return messageBox(null, null, message, style, icon);
15768 }
15769 
15770 /++
15771 
15772 +/
15773 MessageBoxButton messageBox(Window originator, string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15774 	version(win32_widgets) {
15775 		WCharzBuffer t = WCharzBuffer(title);
15776 		WCharzBuffer m = WCharzBuffer(message);
15777 		UINT type;
15778 		with(MessageBoxStyle)
15779 		final switch(style) {
15780 			case OK: type |= MB_OK; break;
15781 			case OKCancel: type |= MB_OKCANCEL; break;
15782 			case RetryCancel: type |= MB_RETRYCANCEL; break;
15783 			case YesNo: type |= MB_YESNO; break;
15784 			case YesNoCancel: type |= MB_YESNOCANCEL; break;
15785 			case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break;
15786 		}
15787 		with(MessageBoxIcon)
15788 		final switch(icon) {
15789 			case None: break;
15790 			case Info: type |= MB_ICONINFORMATION; break;
15791 			case Warning: type |= MB_ICONWARNING; break;
15792 			case Error: type |= MB_ICONERROR; break;
15793 		}
15794 		switch(MessageBoxW(originator is null ? null : originator.win.hwnd, m.ptr, t.ptr, type)) {
15795 			case IDOK: return MessageBoxButton.OK;
15796 			case IDCANCEL: return MessageBoxButton.Cancel;
15797 			case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry;
15798 			case IDYES: return MessageBoxButton.Yes;
15799 			case IDNO: return MessageBoxButton.No;
15800 			case IDCONTINUE: return MessageBoxButton.Continue;
15801 			default: return MessageBoxButton.None;
15802 		}
15803 	} else {
15804 		string[] buttons;
15805 		MessageBoxButton[] buttonIds;
15806 		with(MessageBoxStyle)
15807 		final switch(style) {
15808 			case OK:
15809 				buttons = ["OK"];
15810 				buttonIds = [MessageBoxButton.OK];
15811 			break;
15812 			case OKCancel:
15813 				buttons = ["OK", "Cancel"];
15814 				buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel];
15815 			break;
15816 			case RetryCancel:
15817 				buttons = ["Retry", "Cancel"];
15818 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel];
15819 			break;
15820 			case YesNo:
15821 				buttons = ["Yes", "No"];
15822 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No];
15823 			break;
15824 			case YesNoCancel:
15825 				buttons = ["Yes", "No", "Cancel"];
15826 				buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel];
15827 			break;
15828 			case RetryCancelContinue:
15829 				buttons = ["Try Again", "Cancel", "Continue"];
15830 				buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue];
15831 			break;
15832 		}
15833 		auto mb = new MessageBox(originator, message, buttons, buttonIds);
15834 		EventLoop el = EventLoop.get;
15835 		el.run(() { return !mb.win.closed; });
15836 		return mb.buttonPressed;
15837 	}
15838 
15839 }
15840 
15841 /// ditto
15842 int messageBox(Window originator, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) {
15843 	return messageBox(originator, null, message, style, icon);
15844 }
15845 
15846 
15847 ///
15848 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler;
15849 
15850 /++
15851 	This is an opaque type you can use to disconnect an event handler when you're no longer interested.
15852 
15853 	History:
15854 		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.
15855 +/
15856 struct EventListener {
15857 	private Widget widget;
15858 	private string event;
15859 	private EventHandler handler;
15860 	private bool useCapture;
15861 
15862 	///
15863 	void disconnect() {
15864 		if(widget !is null && handler !is null)
15865 			widget.removeEventListener(this);
15866 	}
15867 }
15868 
15869 /++
15870 	The purpose of this enum was to give a compile-time checked version of various standard event strings.
15871 
15872 	Now, I recommend you use a statically typed event object instead.
15873 
15874 	See_Also: [Event]
15875 +/
15876 enum EventType : string {
15877 	click = "click", ///
15878 
15879 	mouseenter = "mouseenter", ///
15880 	mouseleave = "mouseleave", ///
15881 	mousein = "mousein", ///
15882 	mouseout = "mouseout", ///
15883 	mouseup = "mouseup", ///
15884 	mousedown = "mousedown", ///
15885 	mousemove = "mousemove", ///
15886 
15887 	keydown = "keydown", ///
15888 	keyup = "keyup", ///
15889 	char_ = "char", ///
15890 
15891 	focus = "focus", ///
15892 	blur = "blur", ///
15893 
15894 	triggered = "triggered", ///
15895 
15896 	change = "change", ///
15897 }
15898 
15899 /++
15900 	Represents an event that is currently being processed.
15901 
15902 
15903 	Minigui's event model is based on the web browser. An event has a name, a target,
15904 	and an associated data object. It starts from the window and works its way down through
15905 	the target through all intermediate [Widget]s, triggering capture phase handlers as it goes,
15906 	then goes back up again all the way back to the window, triggering bubble phase handlers. At
15907 	the end, if [Event.preventDefault] has not been called, it calls the target widget's default
15908 	handlers for the event (please note that default handlers will be called even if [Event.stopPropagation]
15909 	was called; that just stops it from calling other handlers in the widget tree, but the default happens
15910 	whenever propagation is done, not only if it gets to the end of the chain).
15911 
15912 	This model has several nice points:
15913 
15914 	$(LIST
15915 		* It is easy to delegate dynamic handlers to a parent. You can have a parent container
15916 		  with event handlers set, then add/remove children as much as you want without needing
15917 		  to manage the event handlers on them - the parent alone can manage everything.
15918 
15919 		* It is easy to create new custom events in your application.
15920 
15921 		* It is familiar to many web developers.
15922 	)
15923 
15924 	There's a few downsides though:
15925 
15926 	$(LIST
15927 		* There's not a lot of type safety.
15928 
15929 		* You don't get a static list of what events a widget can emit.
15930 
15931 		* Tracing where an event got cancelled along the chain can get difficult; the downside of
15932 		  the central delegation benefit is it can be lead to debugging of action at a distance.
15933 	)
15934 
15935 	In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript
15936 	while keeping the benefits - and most compatibility with - the existing model. The main idea is
15937 	to simply use a D object type which provides a static interface as well as a built-in event name.
15938 	Then, a new static interface allows you to see what an event can emit and attach handlers to it
15939 	similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still
15940 	delegate to a parent and use custom events as well as using the runtime dynamic access, in addition
15941 	to having a little more help from the D compiler and documentation generator.
15942 
15943 	Your code would change like this:
15944 
15945 	---
15946 	// old
15947 	widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture );
15948 
15949 	// new
15950 	widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture );
15951 	---
15952 
15953 	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.
15954 
15955 	All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class.
15956 
15957 	Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you!
15958 
15959 	Thus the family of functions are:
15960 
15961 	[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.
15962 
15963 	[Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate.
15964 
15965 	[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.
15966 
15967 	Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property:
15968 
15969 	---
15970 	class MyCheckbox : Widget {
15971 		/// This gives a chance to document it and generates a convenience function to send it and attach handlers.
15972 		/// It is NOT actually required but should be used whenever possible.
15973 		mixin Emits!(ChangeEvent!bool);
15974 
15975 		this(Widget parent) {
15976 			super(parent);
15977 			setDefaultEventHandler((ClickEvent) { checked = !checked; });
15978 		}
15979 
15980 		private bool _checked;
15981 		@property bool checked() { return _checked; }
15982 		@property void checked(bool set) {
15983 			_checked = set;
15984 			emit!(ChangeEvent!bool)(&checked);
15985 		}
15986 	}
15987 	---
15988 
15989 	## Creating Your Own Events
15990 
15991 	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.
15992 
15993 	---
15994 	final class MyEvent : Event {
15995 		this(Widget target) { super(EventString, target); }
15996 		mixin Register; // adds EventString and other reflection information
15997 	}
15998 	---
15999 
16000 	Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it.
16001 
16002 	History:
16003 		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.
16004 
16005 		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.
16006 +/
16007 /+
16008 
16009 	## General Conventions
16010 
16011 	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.
16012 
16013 
16014 	## Qt-style signals and slots
16015 
16016 	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.
16017 
16018 	The intention is for events to be used when
16019 
16020 	---
16021 	class Demo : Widget {
16022 		this() {
16023 			myPropertyChanged = Signal!int(this);
16024 		}
16025 		@property myProperty(int v) {
16026 			myPropertyChanged.emit(v);
16027 		}
16028 
16029 		Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name...
16030 		// but it can just genuinely not care about `this` since that's not really passed.
16031 	}
16032 
16033 	class Foo : Widget {
16034 		// the slot uda is not necessary, but it helps the script and ui builder find it.
16035 		@slot void setValue(int v) { ... }
16036 	}
16037 
16038 	demo.myPropertyChanged.connect(&foo.setValue);
16039 	---
16040 
16041 	The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor.
16042 
16043 	Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically.
16044 
16045 	class StringChangeEvent : ChangeEvent, Signal!string {
16046 		mixin SignalImpl
16047 	}
16048 
16049 +/
16050 class Event : ReflectableProperties {
16051 	/// Creates an event without populating any members and without sending it. See [dispatch]
16052 	this(string eventName, Widget emittedBy) {
16053 		this.eventName = eventName;
16054 		this.srcElement = emittedBy;
16055 	}
16056 
16057 
16058 	/// Implementations for the [ReflectableProperties] interface/
16059 	void getPropertiesList(scope void delegate(string name) sink) const {}
16060 	/// ditto
16061 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { }
16062 	/// ditto
16063 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) {
16064 		return SetPropertyResult.notPermitted;
16065 	}
16066 
16067 
16068 	/+
16069 	/++
16070 		This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice.
16071 
16072 		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.
16073 	+/
16074 	protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) {
16075 		if(value.length == 0) {
16076 			finalSink(memberName, `""`);
16077 			return;
16078 		}
16079 
16080 		char[1024] bufferBacking;
16081 		char[] buffer = bufferBacking;
16082 		int bufferPosition;
16083 
16084 		void sink(char ch) {
16085 			if(bufferPosition >= buffer.length)
16086 				buffer.length = buffer.length + 1024;
16087 			buffer[bufferPosition++] = ch;
16088 		}
16089 
16090 		sink('"');
16091 
16092 		foreach(ch; value) {
16093 			switch(ch) {
16094 				case '\\':
16095 					sink('\\'); sink('\\');
16096 				break;
16097 				case '"':
16098 					sink('\\'); sink('"');
16099 				break;
16100 				case '\n':
16101 					sink('\\'); sink('n');
16102 				break;
16103 				case '\r':
16104 					sink('\\'); sink('r');
16105 				break;
16106 				case '\t':
16107 					sink('\\'); sink('t');
16108 				break;
16109 				default:
16110 					sink(ch);
16111 			}
16112 		}
16113 
16114 		sink('"');
16115 
16116 		finalSink(memberName, buffer[0 .. bufferPosition]);
16117 	}
16118 	+/
16119 
16120 	/+
16121 	enum EventInitiator {
16122 		system,
16123 		minigui,
16124 		user
16125 	}
16126 
16127 	immutable EventInitiator; initiatedBy;
16128 	+/
16129 
16130 	/++
16131 		Events should generally follow the propagation model, but there's some exceptions
16132 		to that rule. If so, they should override this to return false. In that case, only
16133 		bubbling event handlers on the target itself and capturing event handlers on the containing
16134 		window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal
16135 		capture -> target -> bubble process.)
16136 
16137 		History:
16138 			Added May 12, 2021
16139 	+/
16140 	bool propagates() const pure nothrow @nogc @safe {
16141 		return true;
16142 	}
16143 
16144 	/++
16145 		hints as to whether preventDefault will actually do anything. not entirely reliable.
16146 
16147 		History:
16148 			Added May 14, 2021
16149 	+/
16150 	bool cancelable() const pure nothrow @nogc @safe {
16151 		return true;
16152 	}
16153 
16154 	/++
16155 		You can mix this into child class to register some boilerplate. It includes the `EventString`
16156 		member, a constructor, and implementations of the dynamic get data interfaces.
16157 
16158 		If you fail to do this, your event will probably not have full compatibility but it might still work for you.
16159 
16160 
16161 		You can override the default EventString by simply providing your own in the form of
16162 		`enum string EventString = "some.name";` The default is the name of your class and its parent entity
16163 		which provides some namespace protection against conflicts in other libraries while still being fairly
16164 		easy to use.
16165 
16166 		If you provide your own constructor, it will override the default constructor provided here. A constructor
16167 		must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the
16168 		first argument to your constructor.
16169 
16170 		History:
16171 			Added May 13, 2021.
16172 	+/
16173 	protected static mixin template Register() {
16174 		public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~  __traits(identifier, typeof(this));
16175 		this(Widget target) { super(EventString, target); }
16176 
16177 		mixin ReflectableProperties.RegisterGetters;
16178 	}
16179 
16180 	/++
16181 		This is the widget that emitted the event.
16182 
16183 
16184 		The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms.
16185 
16186 		History:
16187 			The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms,
16188 			but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web
16189 			so I don't intend to remove these aliases.
16190 	+/
16191 	Widget source;
16192 	/// ditto
16193 	alias source target;
16194 	/// ditto
16195 	alias source srcElement;
16196 
16197 	Widget relatedTarget; /// Note: likely to be deprecated at some point.
16198 
16199 	/// Prevents the default event handler (if there is one) from being called
16200 	void preventDefault() {
16201 		lastDefaultPrevented = true;
16202 		defaultPrevented = true;
16203 	}
16204 
16205 	/// Stops the event propagation immediately.
16206 	void stopPropagation() {
16207 		propagationStopped = true;
16208 	}
16209 
16210 	private bool defaultPrevented;
16211 	private bool propagationStopped;
16212 	private string eventName;
16213 
16214 	private bool isBubbling;
16215 
16216 	/// 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.
16217 	protected void adjustClientCoordinates(int deltaX, int deltaY) { }
16218 
16219 	/++
16220 		this sends it only to the target. If you want propagation, use dispatch() instead.
16221 
16222 		This should be made private!!!
16223 
16224 	+/
16225 	void sendDirectly() {
16226 		if(srcElement is null)
16227 			return;
16228 
16229 		// 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.
16230 
16231 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
16232 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement);
16233 
16234 		if(auto e = target.parentWindow) {
16235 			if(auto handlers = "*" in e.capturingEventHandlers)
16236 			foreach(handler; *handlers)
16237 				if(handler) handler(e, this);
16238 			if(auto handlers = eventName in e.capturingEventHandlers)
16239 			foreach(handler; *handlers)
16240 				if(handler) handler(e, this);
16241 		}
16242 
16243 		auto e = srcElement;
16244 
16245 		if(auto handlers = eventName in e.bubblingEventHandlers)
16246 		foreach(handler; *handlers)
16247 			if(handler) handler(e, this);
16248 
16249 		if(auto handlers = "*" in e.bubblingEventHandlers)
16250 		foreach(handler; *handlers)
16251 			if(handler) handler(e, this);
16252 
16253 		// there's never a default for a catch-all event
16254 		if(!defaultPrevented)
16255 			if(eventName in e.defaultEventHandlers)
16256 				e.defaultEventHandlers[eventName](e, this);
16257 	}
16258 
16259 	/// this dispatches the element using the capture -> target -> bubble process
16260 	void dispatch() {
16261 		if(srcElement is null)
16262 			return;
16263 
16264 		if(!propagates) {
16265 			sendDirectly;
16266 			return;
16267 		}
16268 
16269 		//debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools)
16270 			//target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement);
16271 
16272 		// first capture, then bubble
16273 
16274 		Widget[] chain;
16275 		Widget curr = srcElement;
16276 		while(curr) {
16277 			auto l = curr;
16278 			chain ~= l;
16279 			curr = curr.parent;
16280 		}
16281 
16282 		isBubbling = false;
16283 
16284 		foreach_reverse(e; chain) {
16285 			if(auto handlers = "*" in e.capturingEventHandlers)
16286 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
16287 
16288 			if(propagationStopped)
16289 				break;
16290 
16291 			if(auto handlers = eventName in e.capturingEventHandlers)
16292 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
16293 
16294 			// the default on capture should really be to always do nothing
16295 
16296 			//if(!defaultPrevented)
16297 			//	if(eventName in e.defaultEventHandlers)
16298 			//		e.defaultEventHandlers[eventName](e.element, this);
16299 
16300 			if(propagationStopped)
16301 				break;
16302 		}
16303 
16304 		int adjustX;
16305 		int adjustY;
16306 
16307 		isBubbling = true;
16308 		if(!propagationStopped)
16309 		foreach(e; chain) {
16310 			if(auto handlers = eventName in e.bubblingEventHandlers)
16311 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
16312 
16313 			if(propagationStopped)
16314 				break;
16315 
16316 			if(auto handlers = "*" in e.bubblingEventHandlers)
16317 				foreach(handler; *handlers) if(handler !is null) handler(e, this);
16318 
16319 			if(propagationStopped)
16320 				break;
16321 
16322 			if(e.encapsulatedChildren()) {
16323 				adjustClientCoordinates(adjustX, adjustY);
16324 				target = e;
16325 			} else {
16326 				adjustX += e.x;
16327 				adjustY += e.y;
16328 			}
16329 		}
16330 
16331 		if(!defaultPrevented)
16332 		foreach(e; chain) {
16333 			if(eventName in e.defaultEventHandlers)
16334 				e.defaultEventHandlers[eventName](e, this);
16335 		}
16336 	}
16337 
16338 
16339 	/* old compatibility things */
16340 	deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!")
16341 	final @property {
16342 		Key key() { return (cast(KeyEventBase) this).key; }
16343 		KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; }
16344 
16345 		bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; }
16346 		bool altKey() { return (cast(KeyEventBase) this).altKey; }
16347 		bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; }
16348 	}
16349 
16350 	deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!")
16351 	final @property {
16352 		int clientX() { return (cast(MouseEventBase) this).clientX; }
16353 		int clientY() { return (cast(MouseEventBase) this).clientY; }
16354 
16355 		int viewportX() { return (cast(MouseEventBase) this).viewportX; }
16356 		int viewportY() { return (cast(MouseEventBase) this).viewportY; }
16357 
16358 		int button() { return (cast(MouseEventBase) this).button; }
16359 		int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; }
16360 	}
16361 
16362 	deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward")
16363 	final @property {
16364 		int state() {
16365 			if(auto meb = cast(MouseEventBase) this)
16366 				return meb.state;
16367 			if(auto keb = cast(KeyEventBase) this)
16368 				return keb.state;
16369 			assert(0);
16370 		}
16371 	}
16372 
16373 	deprecated("Use a CharEvent instead of Event in your handler going forward")
16374 	final @property {
16375 		dchar character() {
16376 			if(auto ce = cast(CharEvent) this)
16377 				return ce.character;
16378 			return dchar.init;
16379 		}
16380 	}
16381 
16382 	// for change events
16383 	@property {
16384 		///
16385 		int intValue() { return 0; }
16386 		///
16387 		string stringValue() { return null; }
16388 	}
16389 }
16390 
16391 /++
16392 	This lets you statically verify you send the events you claim you send and gives you a hook to document them.
16393 
16394 	Please note that a widget may send events not listed as Emits. You can always construct and dispatch
16395 	dynamic and custom events, but the static list helps ensure you get them right.
16396 
16397 	If this is declared, you can use [Widget.emit] to send the event.
16398 
16399 	All events work the same way though, following the capture->widget->bubble model described under [Event].
16400 
16401 	History:
16402 		Added May 4, 2021
16403 +/
16404 mixin template Emits(EventType) {
16405 	import arsd.minigui : EventString;
16406 	static if(is(EventType : Event) && !is(EventType == Event))
16407 		mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";");
16408 	else
16409 		static assert(0, "You can only emit subclasses of Event");
16410 }
16411 
16412 /// ditto
16413 mixin template Emits(string eventString) {
16414 	mixin("private Event[0] emits_" ~ eventString ~";");
16415 }
16416 
16417 /*
16418 class SignalEvent(string name) : Event {
16419 
16420 }
16421 */
16422 
16423 /++
16424 	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".
16425 
16426 
16427 	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.
16428 
16429 	History:
16430 		Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things.
16431 +/
16432 class CommandEvent : Event {
16433 	enum EventString = "command";
16434 	this(Widget source, string CommandString = EventString) {
16435 		super(CommandString, source);
16436 	}
16437 }
16438 
16439 /++
16440 	A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments.
16441 +/
16442 class CommandEventWithArgs(Args...) : CommandEvent {
16443 	this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; }
16444 	Args args;
16445 }
16446 
16447 /++
16448 	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.
16449 
16450 	See [CommandEvent] for more information.
16451 
16452 	Returns:
16453 		The [EventListener] you can use to remove the handler.
16454 +/
16455 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) {
16456 	return w.addEventListener(CommandString, (Event ev) {
16457 		if(ev.target is w)
16458 			return; // it does not consume its own commands!
16459 		if(auto cev = cast(CommandEventWithArgs!Args) ev) {
16460 			handler(cev.args);
16461 			ev.stopPropagation();
16462 		}
16463 	});
16464 }
16465 
16466 /++
16467 	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.
16468 +/
16469 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) {
16470 	auto event = new CommandEventWithArgs!Args(w, CommandString, args);
16471 	event.dispatch();
16472 }
16473 
16474 /++
16475 	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.
16476 
16477 	If you need to know the old size, you need to store it yourself.
16478 
16479 	History:
16480 		Made final on January 3, 2025 (dub v12.0)
16481 +/
16482 final class ResizeEvent : Event {
16483 	enum EventString = "resize";
16484 
16485 	this(Widget target) { super(EventString, target); }
16486 
16487 	override bool propagates() const { return false; }
16488 }
16489 
16490 /++
16491 	ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close.
16492 
16493 	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.
16494 
16495 	History:
16496 		Added June 21, 2021 (dub v10.1)
16497 
16498 		Made final on January 3, 2025 (dub v12.0)
16499 +/
16500 final class ClosingEvent : Event {
16501 	enum EventString = "closing";
16502 
16503 	this(Widget target) { super(EventString, target); }
16504 
16505 	override bool propagates() const { return false; }
16506 	override bool cancelable() const { return true; }
16507 }
16508 
16509 /// ditto
16510 final class ClosedEvent : Event {
16511 	enum EventString = "closed";
16512 
16513 	this(Widget target) { super(EventString, target); }
16514 
16515 	override bool propagates() const { return false; }
16516 	override bool cancelable() const { return false; }
16517 }
16518 
16519 ///
16520 final class BlurEvent : Event {
16521 	enum EventString = "blur";
16522 
16523 	// FIXME: related target?
16524 	this(Widget target) { super(EventString, target); }
16525 
16526 	override bool propagates() const { return false; }
16527 }
16528 
16529 ///
16530 final class FocusEvent : Event {
16531 	enum EventString = "focus";
16532 
16533 	// FIXME: related target?
16534 	this(Widget target) { super(EventString, target); }
16535 
16536 	override bool propagates() const { return false; }
16537 }
16538 
16539 /++
16540 	FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates.
16541 
16542 	History:
16543 		Added July 3, 2021
16544 +/
16545 final class FocusInEvent : Event {
16546 	enum EventString = "focusin";
16547 
16548 	// FIXME: related target?
16549 	this(Widget target) { super(EventString, target); }
16550 
16551 	override bool cancelable() const { return false; }
16552 }
16553 
16554 /// ditto
16555 final class FocusOutEvent : Event {
16556 	enum EventString = "focusout";
16557 
16558 	// FIXME: related target?
16559 	this(Widget target) { super(EventString, target); }
16560 
16561 	override bool cancelable() const { return false; }
16562 }
16563 
16564 ///
16565 final class ScrollEvent : Event {
16566 	enum EventString = "scroll";
16567 	this(Widget target) { super(EventString, target); }
16568 
16569 	override bool cancelable() const { return false; }
16570 }
16571 
16572 /++
16573 	Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget.
16574 
16575 	History:
16576 		Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class.
16577 +/
16578 final class CharEvent : Event {
16579 	enum EventString = "char";
16580 	this(Widget target, dchar ch) {
16581 		character = ch;
16582 		super(EventString, target);
16583 	}
16584 
16585 	immutable dchar character;
16586 }
16587 
16588 /++
16589 	You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information.
16590 +/
16591 abstract class ChangeEventBase : Event {
16592 	enum EventString = "change";
16593 	this(Widget target) {
16594 		super(EventString, target);
16595 	}
16596 
16597 	/+
16598 		// idk where or how exactly i want to do this.
16599 		// i might come back to it later.
16600 
16601 	// If a widget itself broadcasts one of theses itself, it stops propagation going down
16602 	// this way the source doesn't get too confused (think of a nested scroll widget)
16603 	//
16604 	// the idea is like the scroll bar emits a command event saying like "scroll left one line"
16605 	// then you consume that command and change you scroll x position to whatever. then you do
16606 	// some kind of change event that is broadcast back to the children and any horizontal scroll
16607 	// listeners are now able to update, without having an explicit connection between them.
16608 	void broadcastToChildren(string fieldName) {
16609 
16610 	}
16611 	+/
16612 }
16613 
16614 /++
16615 	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.
16616 
16617 
16618 	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).
16619 
16620 	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);`
16621 
16622 	Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything.
16623 
16624 	History:
16625 		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.
16626 +/
16627 final class ChangeEvent(T) : ChangeEventBase {
16628 	this(Widget target, T delegate() getNewValue) {
16629 		assert(getNewValue !is null);
16630 		this.getNewValue = getNewValue;
16631 		super(target);
16632 	}
16633 
16634 	private T delegate() getNewValue;
16635 
16636 	/++
16637 		Gets the new value that just changed.
16638 	+/
16639 	@property T value() {
16640 		return getNewValue();
16641 	}
16642 
16643 	/// compatibility method for old generic Events
16644 	static if(is(immutable T == immutable int))
16645 		override int intValue() { return value; }
16646 	/// ditto
16647 	static if(is(immutable T == immutable string))
16648 		override string stringValue() { return value; }
16649 }
16650 
16651 /++
16652 	Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s.
16653 
16654 
16655 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16656 
16657 	History:
16658 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
16659 +/
16660 abstract class KeyEventBase : Event {
16661 	this(string name, Widget target) {
16662 		super(name, target);
16663 	}
16664 
16665 	// for key events
16666 	Key key; ///
16667 
16668 	KeyEvent originalKeyEvent;
16669 
16670 	/++
16671 		Indicates the current state of the given keyboard modifier keys.
16672 
16673 		History:
16674 			Added to events on April 15, 2020.
16675 	+/
16676 	bool ctrlKey;
16677 
16678 	/// ditto
16679 	bool altKey;
16680 
16681 	/// ditto
16682 	bool shiftKey;
16683 
16684 	/++
16685 		The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey].
16686 
16687 		See [arsd.simpledisplay.ModifierState] for other possible flags.
16688 	+/
16689 	int state;
16690 
16691 	mixin Register;
16692 }
16693 
16694 /++
16695 	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].
16696 
16697 
16698 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16699 
16700 	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.
16701 
16702 	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.
16703 
16704 	See_Also: [KeyUpEvent], [CharEvent]
16705 
16706 	History:
16707 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners.
16708 +/
16709 final class KeyDownEvent : KeyEventBase {
16710 	enum EventString = "keydown";
16711 	this(Widget target) { super(EventString, target); }
16712 }
16713 
16714 /++
16715 	Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase].
16716 
16717 
16718 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16719 
16720 	See_Also: [KeyDownEvent], [CharEvent]
16721 
16722 	History:
16723 		Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners.
16724 +/
16725 final class KeyUpEvent : KeyEventBase {
16726 	enum EventString = "keyup";
16727 	this(Widget target) { super(EventString, target); }
16728 }
16729 
16730 /++
16731 	Contains shared properties for various mouse events;
16732 
16733 
16734 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16735 
16736 	History:
16737 		Added May 2, 2021. Previously, its properties were members of the [Event] base class.
16738 +/
16739 abstract class MouseEventBase : Event {
16740 	this(string name, Widget target) {
16741 		super(name, target);
16742 	}
16743 
16744 	// for mouse events
16745 	int clientX; /// The mouse event location relative to the target widget
16746 	int clientY; /// ditto
16747 
16748 	int viewportX; /// The mouse event location relative to the window origin
16749 	int viewportY; /// ditto
16750 
16751 	int button; /// See: [MouseEvent.button]
16752 	int buttonLinear; /// See: [MouseEvent.buttonLinear]
16753 
16754 	/++
16755 		Indicates the current state of the given keyboard modifier keys.
16756 
16757 		History:
16758 			Added to mouse events on September 28, 2010.
16759 	+/
16760 	bool ctrlKey;
16761 
16762 	/// ditto
16763 	bool altKey;
16764 
16765 	/// ditto
16766 	bool shiftKey;
16767 
16768 
16769 
16770 	int state; ///
16771 
16772 	/++
16773 		for consistent names with key event.
16774 
16775 		History:
16776 			Added September 28, 2021 (dub v10.3)
16777 	+/
16778 	alias modifierState = state;
16779 
16780 	/++
16781 		Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out.
16782 
16783 		History:
16784 			Added May 15, 2021
16785 	+/
16786 	bool isMouseWheel() {
16787 		return button == MouseButton.wheelUp || button == MouseButton.wheelDown;
16788 	}
16789 
16790 	// private
16791 	override void adjustClientCoordinates(int deltaX, int deltaY) {
16792 		clientX += deltaX;
16793 		clientY += deltaY;
16794 	}
16795 
16796 	mixin Register;
16797 }
16798 
16799 /++
16800 	Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase].
16801 
16802 
16803 	$(WARNING
16804 		Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and
16805 		for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct
16806 		behavior.
16807 
16808 		Use [MouseEventBase.isMouseWheel] to filter wheel events while keeping others.
16809 	)
16810 
16811 	[MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement.
16812 
16813 	[MouseUpEvent] is sent when the user releases a mouse button.
16814 
16815 	[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.)
16816 
16817 	[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.
16818 
16819 	[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.
16820 
16821 	[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.
16822 
16823 	[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.
16824 
16825 	[MouseEnterEvent] is sent when the mouse enters the bounding box of a widget.
16826 
16827 	[MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget.
16828 
16829 	You can construct these yourself, but generally the system will send them to you and there's little need to emit your own.
16830 
16831 	Rationale:
16832 
16833 		If you only want to do drag, mousedown/up works just fine being consistently sent.
16834 
16835 		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).
16836 
16837 		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.
16838 
16839 	History:
16840 		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.
16841 +/
16842 final class MouseUpEvent : MouseEventBase {
16843 	enum EventString = "mouseup"; ///
16844 	this(Widget target) { super(EventString, target); }
16845 }
16846 /// ditto
16847 final class MouseDownEvent : MouseEventBase {
16848 	enum EventString = "mousedown"; ///
16849 	this(Widget target) { super(EventString, target); }
16850 }
16851 /// ditto
16852 final class MouseMoveEvent : MouseEventBase {
16853 	enum EventString = "mousemove"; ///
16854 	this(Widget target) { super(EventString, target); }
16855 }
16856 /// ditto
16857 final class ClickEvent : MouseEventBase {
16858 	enum EventString = "click"; ///
16859 	this(Widget target) { super(EventString, target); }
16860 }
16861 /// ditto
16862 final class DoubleClickEvent : MouseEventBase {
16863 	enum EventString = "dblclick"; ///
16864 	this(Widget target) { super(EventString, target); }
16865 }
16866 /// ditto
16867 final class MouseOverEvent : Event {
16868 	enum EventString = "mouseover"; ///
16869 	this(Widget target) { super(EventString, target); }
16870 }
16871 /// ditto
16872 final class MouseOutEvent : Event {
16873 	enum EventString = "mouseout"; ///
16874 	this(Widget target) { super(EventString, target); }
16875 }
16876 /// ditto
16877 final class MouseEnterEvent : Event {
16878 	enum EventString = "mouseenter"; ///
16879 	this(Widget target) { super(EventString, target); }
16880 
16881 	override bool propagates() const { return false; }
16882 }
16883 /// ditto
16884 final class MouseLeaveEvent : Event {
16885 	enum EventString = "mouseleave"; ///
16886 	this(Widget target) { super(EventString, target); }
16887 
16888 	override bool propagates() const { return false; }
16889 }
16890 
16891 private bool isAParentOf(Widget a, Widget b) {
16892 	if(a is null || b is null)
16893 		return false;
16894 
16895 	while(b !is null) {
16896 		if(a is b)
16897 			return true;
16898 		b = b.parent;
16899 	}
16900 
16901 	return false;
16902 }
16903 
16904 private struct WidgetAtPointResponse {
16905 	Widget widget;
16906 
16907 	// x, y relative to the widget in the response.
16908 	int x;
16909 	int y;
16910 }
16911 
16912 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) {
16913 	assert(starting !is null);
16914 
16915 	starting.addScrollPosition(x, y);
16916 
16917 	auto child = starting.getChildAtPosition(x, y);
16918 	while(child) {
16919 		if(child.hidden)
16920 			continue;
16921 		starting = child;
16922 		x -= child.x;
16923 		y -= child.y;
16924 		auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y);
16925 		child = r.widget;
16926 		if(child is starting)
16927 			break;
16928 	}
16929 	return WidgetAtPointResponse(starting, x, y);
16930 }
16931 
16932 version(win32_widgets) {
16933 private:
16934 	import core.sys.windows.commctrl;
16935 
16936 	pragma(lib, "comctl32");
16937 	shared static this() {
16938 		// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx
16939 		INITCOMMONCONTROLSEX ic;
16940 		ic.dwSize = cast(DWORD) ic.sizeof;
16941 		ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES;
16942 		if(!InitCommonControlsEx(&ic)) {
16943 			//writeln("ICC failed");
16944 		}
16945 	}
16946 
16947 
16948 	// everything from here is just win32 headers copy pasta
16949 private:
16950 extern(Windows):
16951 
16952 	alias HANDLE HMENU;
16953 	HMENU CreateMenu();
16954 	bool SetMenu(HWND, HMENU);
16955 	HMENU CreatePopupMenu();
16956 	enum MF_POPUP = 0x10;
16957 	enum MF_STRING = 0;
16958 
16959 
16960 	BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*);
16961 	struct INITCOMMONCONTROLSEX {
16962 		DWORD dwSize;
16963 		DWORD dwICC;
16964 	}
16965 	enum HINST_COMMCTRL = cast(HINSTANCE) (-1);
16966 enum {
16967         IDB_STD_SMALL_COLOR,
16968         IDB_STD_LARGE_COLOR,
16969         IDB_VIEW_SMALL_COLOR = 4,
16970         IDB_VIEW_LARGE_COLOR = 5
16971 }
16972 enum {
16973         STD_CUT,
16974         STD_COPY,
16975         STD_PASTE,
16976         STD_UNDO,
16977         STD_REDOW,
16978         STD_DELETE,
16979         STD_FILENEW,
16980         STD_FILEOPEN,
16981         STD_FILESAVE,
16982         STD_PRINTPRE,
16983         STD_PROPERTIES,
16984         STD_HELP,
16985         STD_FIND,
16986         STD_REPLACE,
16987         STD_PRINT // = 14
16988 }
16989 
16990 alias HANDLE HIMAGELIST;
16991 	HIMAGELIST ImageList_Create(int, int, UINT, int, int);
16992 	int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP);
16993         BOOL ImageList_Destroy(HIMAGELIST);
16994 
16995 uint MAKELONG(ushort a, ushort b) {
16996         return cast(uint) ((b << 16) | a);
16997 }
16998 
16999 
17000 struct TBBUTTON {
17001 	int   iBitmap;
17002 	int   idCommand;
17003 	BYTE  fsState;
17004 	BYTE  fsStyle;
17005 	version(Win64)
17006 	BYTE[6] bReserved;
17007 	else
17008 	BYTE[2]  bReserved;
17009 	DWORD dwData;
17010 	INT_PTR   iString;
17011 }
17012 
17013 	enum {
17014 		TB_ADDBUTTONSA   = WM_USER + 20,
17015 		TB_INSERTBUTTONA = WM_USER + 21,
17016 		TB_GETIDEALSIZE = WM_USER + 99,
17017 	}
17018 
17019 struct SIZE {
17020 	LONG cx;
17021 	LONG cy;
17022 }
17023 
17024 
17025 enum {
17026 	TBSTATE_CHECKED       = 1,
17027 	TBSTATE_PRESSED       = 2,
17028 	TBSTATE_ENABLED       = 4,
17029 	TBSTATE_HIDDEN        = 8,
17030 	TBSTATE_INDETERMINATE = 16,
17031 	TBSTATE_WRAP          = 32
17032 }
17033 
17034 
17035 
17036 enum {
17037 	ILC_COLOR    = 0,
17038 	ILC_COLOR4   = 4,
17039 	ILC_COLOR8   = 8,
17040 	ILC_COLOR16  = 16,
17041 	ILC_COLOR24  = 24,
17042 	ILC_COLOR32  = 32,
17043 	ILC_COLORDDB = 254,
17044 	ILC_MASK     = 1,
17045 	ILC_PALETTE  = 2048
17046 }
17047 
17048 
17049 alias TBBUTTON*       PTBBUTTON, LPTBBUTTON;
17050 
17051 
17052 enum {
17053 	TB_ENABLEBUTTON          = WM_USER + 1,
17054 	TB_CHECKBUTTON,
17055 	TB_PRESSBUTTON,
17056 	TB_HIDEBUTTON,
17057 	TB_INDETERMINATE, //     = WM_USER + 5,
17058 	TB_ISBUTTONENABLED       = WM_USER + 9,
17059 	TB_ISBUTTONCHECKED,
17060 	TB_ISBUTTONPRESSED,
17061 	TB_ISBUTTONHIDDEN,
17062 	TB_ISBUTTONINDETERMINATE, // = WM_USER + 13,
17063 	TB_SETSTATE              = WM_USER + 17,
17064 	TB_GETSTATE              = WM_USER + 18,
17065 	TB_ADDBITMAP             = WM_USER + 19,
17066 	TB_DELETEBUTTON          = WM_USER + 22,
17067 	TB_GETBUTTON,
17068 	TB_BUTTONCOUNT,
17069 	TB_COMMANDTOINDEX,
17070 	TB_SAVERESTOREA,
17071 	TB_CUSTOMIZE,
17072 	TB_ADDSTRINGA,
17073 	TB_GETITEMRECT,
17074 	TB_BUTTONSTRUCTSIZE,
17075 	TB_SETBUTTONSIZE,
17076 	TB_SETBITMAPSIZE,
17077 	TB_AUTOSIZE, //          = WM_USER + 33,
17078 	TB_GETTOOLTIPS           = WM_USER + 35,
17079 	TB_SETTOOLTIPS           = WM_USER + 36,
17080 	TB_SETPARENT             = WM_USER + 37,
17081 	TB_SETROWS               = WM_USER + 39,
17082 	TB_GETROWS,
17083 	TB_GETBITMAPFLAGS,
17084 	TB_SETCMDID,
17085 	TB_CHANGEBITMAP,
17086 	TB_GETBITMAP,
17087 	TB_GETBUTTONTEXTA,
17088 	TB_REPLACEBITMAP, //     = WM_USER + 46,
17089 	TB_GETBUTTONSIZE         = WM_USER + 58,
17090 	TB_SETBUTTONWIDTH        = WM_USER + 59,
17091 	TB_GETBUTTONTEXTW        = WM_USER + 75,
17092 	TB_SAVERESTOREW          = WM_USER + 76,
17093 	TB_ADDSTRINGW            = WM_USER + 77,
17094 }
17095 
17096 extern(Windows)
17097 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM);
17098 
17099 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC;
17100 
17101 
17102 	enum {
17103 		TB_SETINDENT = WM_USER + 47,
17104 		TB_SETIMAGELIST,
17105 		TB_GETIMAGELIST,
17106 		TB_LOADIMAGES,
17107 		TB_GETRECT,
17108 		TB_SETHOTIMAGELIST,
17109 		TB_GETHOTIMAGELIST,
17110 		TB_SETDISABLEDIMAGELIST,
17111 		TB_GETDISABLEDIMAGELIST,
17112 		TB_SETSTYLE,
17113 		TB_GETSTYLE,
17114 		//TB_GETBUTTONSIZE,
17115 		//TB_SETBUTTONWIDTH,
17116 		TB_SETMAXTEXTROWS,
17117 		TB_GETTEXTROWS // = WM_USER + 61
17118 	}
17119 
17120 enum {
17121 	CCM_FIRST            = 0x2000,
17122 	CCM_LAST             = CCM_FIRST + 0x200,
17123 	CCM_SETBKCOLOR       = 8193,
17124 	CCM_SETCOLORSCHEME   = 8194,
17125 	CCM_GETCOLORSCHEME   = 8195,
17126 	CCM_GETDROPTARGET    = 8196,
17127 	CCM_SETUNICODEFORMAT = 8197,
17128 	CCM_GETUNICODEFORMAT = 8198,
17129 	CCM_SETVERSION       = 0x2007,
17130 	CCM_GETVERSION       = 0x2008,
17131 	CCM_SETNOTIFYWINDOW  = 0x2009
17132 }
17133 
17134 
17135 enum {
17136 	PBM_SETRANGE     = WM_USER + 1,
17137 	PBM_SETPOS,
17138 	PBM_DELTAPOS,
17139 	PBM_SETSTEP,
17140 	PBM_STEPIT,   // = WM_USER + 5
17141 	PBM_SETRANGE32   = 1030,
17142 	PBM_GETRANGE,
17143 	PBM_GETPOS,
17144 	PBM_SETBARCOLOR, // = 1033
17145 	PBM_SETBKCOLOR   = CCM_SETBKCOLOR
17146 }
17147 
17148 enum {
17149 	PBS_SMOOTH   = 1,
17150 	PBS_VERTICAL = 4
17151 }
17152 
17153 enum {
17154         ICC_LISTVIEW_CLASSES = 1,
17155         ICC_TREEVIEW_CLASSES = 2,
17156         ICC_BAR_CLASSES      = 4,
17157         ICC_TAB_CLASSES      = 8,
17158         ICC_UPDOWN_CLASS     = 16,
17159         ICC_PROGRESS_CLASS   = 32,
17160         ICC_HOTKEY_CLASS     = 64,
17161         ICC_ANIMATE_CLASS    = 128,
17162         ICC_WIN95_CLASSES    = 255,
17163         ICC_DATE_CLASSES     = 256,
17164         ICC_USEREX_CLASSES   = 512,
17165         ICC_COOL_CLASSES     = 1024,
17166 	ICC_STANDARD_CLASSES = 0x00004000,
17167 }
17168 
17169 	enum WM_USER = 1024;
17170 }
17171 
17172 version(win32_widgets)
17173 	pragma(lib, "comdlg32");
17174 
17175 
17176 ///
17177 enum GenericIcons : ushort {
17178 	None, ///
17179 	// these happen to match the win32 std icons numerically if you just subtract one from the value
17180 	Cut, ///
17181 	Copy, ///
17182 	Paste, ///
17183 	Undo, ///
17184 	Redo, ///
17185 	Delete, ///
17186 	New, ///
17187 	Open, ///
17188 	Save, ///
17189 	PrintPreview, ///
17190 	Properties, ///
17191 	Help, ///
17192 	Find, ///
17193 	Replace, ///
17194 	Print, ///
17195 }
17196 
17197 enum FileDialogType {
17198 	Automatic,
17199 	Open,
17200 	Save
17201 }
17202 
17203 /++
17204 	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.
17205 +/
17206 string previousFileReferenced;
17207 
17208 /++
17209 	Used in automatic menu functions to indicate that the user should be able to browse for a file.
17210 
17211 	Params:
17212 		storage = an alias to a `static string` variable that stores the last file referenced. It will
17213 		use this to pre-fill the dialog with a suggestion.
17214 
17215 		Please note that it MUST be `static` or you will get compile errors.
17216 
17217 		filters = the filters param to [getFileName]
17218 
17219 		type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will
17220 		guess based on the function name. If it has the word "Save" or "Export" in it, it will show
17221 		a save dialog box. Otherwise, it will show an open dialog box.
17222 +/
17223 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) {
17224 	string name;
17225 	alias name this;
17226 
17227 	@implicit this(string name) {
17228 		this.name = name;
17229 	}
17230 }
17231 
17232 /++
17233 	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.
17234 
17235 	History:
17236 		onCancel was added November 6, 2021.
17237 
17238 		The dialog itself on Linux was modified on December 2, 2021 to include
17239 		a directory picker in addition to the command line completion view.
17240 
17241 		The `initialDirectory` argument was added November 9, 2022 (dub v10.10)
17242 
17243 		The `owner` argument was added September 29, 2024. The overloads without this argument are likely to be deprecated in the next major version.
17244 	Future_directions:
17245 		I want to add some kind of custom preview and maybe thumbnail thing in the future,
17246 		at least on Linux, maybe on Windows too.
17247 +/
17248 void getOpenFileName(
17249 	Window owner,
17250 	void delegate(string) onOK,
17251 	string prefilledName = null,
17252 	string[] filters = null,
17253 	void delegate() onCancel = null,
17254 	string initialDirectory = null,
17255 )
17256 {
17257 	return getFileName(owner, true, onOK, prefilledName, filters, onCancel, initialDirectory);
17258 }
17259 
17260 /// ditto
17261 void getSaveFileName(
17262 	Window owner,
17263 	void delegate(string) onOK,
17264 	string prefilledName = null,
17265 	string[] filters = null,
17266 	void delegate() onCancel = null,
17267 	string initialDirectory = null,
17268 )
17269 {
17270 	return getFileName(owner, false, onOK, prefilledName, filters, onCancel, initialDirectory);
17271 }
17272 
17273 // 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.")
17274 /// ditto
17275 void getOpenFileName(
17276 	void delegate(string) onOK,
17277 	string prefilledName = null,
17278 	string[] filters = null,
17279 	void delegate() onCancel = null,
17280 	string initialDirectory = null,
17281 )
17282 {
17283 	return getFileName(null, true, onOK, prefilledName, filters, onCancel, initialDirectory);
17284 }
17285 
17286 /// ditto
17287 void getSaveFileName(
17288 	void delegate(string) onOK,
17289 	string prefilledName = null,
17290 	string[] filters = null,
17291 	void delegate() onCancel = null,
17292 	string initialDirectory = null,
17293 )
17294 {
17295 	return getFileName(null, false, onOK, prefilledName, filters, onCancel, initialDirectory);
17296 }
17297 
17298 /++
17299 	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.
17300 
17301 	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.
17302 
17303 	History:
17304 		Added January 1, 2025
17305 +/
17306 class FileDialogDelegate {
17307 
17308 	/++
17309 
17310 	+/
17311 	static abstract class PreviewWidget : Widget {
17312 		/// Call this from your subclass' constructor
17313 		this(Widget parent) {
17314 			super(parent);
17315 		}
17316 
17317 		/// Load the file given to you and show its preview inside the widget here
17318 		abstract void previewFile(string filename);
17319 	}
17320 
17321 	/++
17322 		Override this to add preview capabilities to the dialog for certain files.
17323 	+/
17324 	protected PreviewWidget makePreviewWidget(Widget parent) {
17325 		return null;
17326 	}
17327 
17328 	/++
17329 		Override this to change the dialog entirely.
17330 
17331 		This function IS allowed to block, but is NOT required to.
17332 	+/
17333 	protected void getFileName(
17334 		Window owner,
17335 		bool openOrSave, // true if open, false if save
17336 		void delegate(string) onOK,
17337 		string prefilledName,
17338 		string[] filters, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
17339 		void delegate() onCancel,
17340 		string initialDirectory,
17341 	)
17342 	{
17343 
17344 		version(win32_widgets) {
17345 			import core.sys.windows.commdlg;
17346 		/*
17347 		Ofn.lStructSize = sizeof(OPENFILENAME);
17348 		Ofn.hwndOwner = hWnd;
17349 		Ofn.lpstrFilter = szFilter;
17350 		Ofn.lpstrFile= szFile;
17351 		Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile);
17352 		Ofn.lpstrFileTitle = szFileTitle;
17353 		Ofn.nMaxFileTitle = sizeof(szFileTitle);
17354 		Ofn.lpstrInitialDir = (LPSTR)NULL;
17355 		Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT;
17356 		Ofn.lpstrTitle = szTitle;
17357 		 */
17358 
17359 
17360 			wchar[1024] file = 0;
17361 			wchar[1024] filterBuffer = 0;
17362 			makeWindowsString(prefilledName, file[]);
17363 			OPENFILENAME ofn;
17364 			ofn.lStructSize = ofn.sizeof;
17365 			ofn.hwndOwner = owner is null ? null : owner.win.hwnd;
17366 			if(filters.length) {
17367 				string filter;
17368 				foreach(i, f; filters) {
17369 					filter ~= f;
17370 					filter ~= "\0";
17371 				}
17372 				filter ~= "\0";
17373 				ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr;
17374 			}
17375 			ofn.lpstrFile = file.ptr;
17376 			ofn.nMaxFile = file.length;
17377 
17378 			wchar[1024] initialDir = 0;
17379 			if(initialDirectory !is null) {
17380 				makeWindowsString(initialDirectory, initialDir[]);
17381 				ofn.lpstrInitialDir = file.ptr;
17382 			}
17383 
17384 			if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn))
17385 			{
17386 				string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile);
17387 				if(okString.length && okString[$-1] == '\0')
17388 					okString = okString[0..$-1];
17389 				onOK(okString);
17390 			} else {
17391 				if(onCancel)
17392 					onCancel();
17393 			}
17394 		} else version(custom_widgets) {
17395 			filters ~= ["All Files\0*.*"];
17396 			auto picker = new FilePicker(openOrSave, prefilledName, filters, initialDirectory, owner);
17397 			picker.onOK = onOK;
17398 			picker.onCancel = onCancel;
17399 			picker.show();
17400 		}
17401 	}
17402 
17403 }
17404 
17405 /// ditto
17406 FileDialogDelegate fileDialogDelegate() {
17407 	if(fileDialogDelegate_ is null)
17408 		fileDialogDelegate_ = new FileDialogDelegate();
17409 	return fileDialogDelegate_;
17410 }
17411 
17412 /// ditto
17413 void fileDialogDelegate(FileDialogDelegate replacement) {
17414 	fileDialogDelegate_ = replacement;
17415 }
17416 
17417 private FileDialogDelegate fileDialogDelegate_;
17418 
17419 struct FileNameFilter {
17420 	string description;
17421 	string[] globPatterns;
17422 
17423 	string toString() {
17424 		string ret;
17425 		ret ~= description;
17426 		ret ~= " (";
17427 		foreach(idx, pattern; globPatterns) {
17428 			if(idx)
17429 				ret ~= "; ";
17430 			ret ~= pattern;
17431 		}
17432 		ret ~= ")";
17433 
17434 		return ret;
17435 	}
17436 
17437 	static FileNameFilter fromString(string s) {
17438 		size_t end = s.length;
17439 		size_t start = 0;
17440 		foreach_reverse(idx, ch; s) {
17441 			if(ch == ')' && end == s.length)
17442 				end = idx;
17443 			else if(ch == '(' && end != s.length) {
17444 				start = idx + 1;
17445 				break;
17446 			}
17447 		}
17448 
17449 		FileNameFilter fnf;
17450 		fnf.description = s[0 .. start ? start - 1 : 0];
17451 		size_t globStart = 0;
17452 		s = s[start .. end];
17453 		foreach(idx, ch; s)
17454 			if(ch == ';') {
17455 				auto ptn = stripInternal(s[globStart .. idx]);
17456 				if(ptn.length)
17457 					fnf.globPatterns ~= ptn;
17458 				globStart = idx + 1;
17459 
17460 			}
17461 		auto ptn = stripInternal(s[globStart .. $]);
17462 		if(ptn.length)
17463 			fnf.globPatterns ~= ptn;
17464 		return fnf;
17465 	}
17466 }
17467 
17468 struct FileNameFilterSet {
17469 	FileNameFilter[] filters;
17470 
17471 	static FileNameFilterSet fromWindowsFileNameFilterDescription(string[] filters) {
17472 		FileNameFilter[] ret;
17473 
17474 		foreach(filter; filters) {
17475 			FileNameFilter fnf;
17476 			size_t filterStartPoint;
17477 			foreach(idx, ch; filter) {
17478 				if(ch == 0) {
17479 					fnf.description = filter[0 .. idx];
17480 					filterStartPoint = idx + 1;
17481 				} else if(filterStartPoint && ch == ';') {
17482 					fnf.globPatterns ~= filter[filterStartPoint .. idx];
17483 					filterStartPoint = idx + 1;
17484 				}
17485 			}
17486 			fnf.globPatterns ~= filter[filterStartPoint .. $];
17487 
17488 			ret ~= fnf;
17489 		}
17490 
17491 		return FileNameFilterSet(ret);
17492 	}
17493 }
17494 
17495 void getFileName(
17496 	Window owner,
17497 	bool openOrSave,
17498 	void delegate(string) onOK,
17499 	string prefilledName = null,
17500 	string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
17501 	void delegate() onCancel = null,
17502 	string initialDirectory = null,
17503 )
17504 {
17505 	return fileDialogDelegate().getFileName(owner, openOrSave, onOK, prefilledName, filters, onCancel, initialDirectory);
17506 }
17507 
17508 version(custom_widgets)
17509 private
17510 class FilePicker : Dialog {
17511 	void delegate(string) onOK;
17512 	void delegate() onCancel;
17513 	LabeledLineEdit lineEdit;
17514 	bool isOpenDialogInsteadOfSave;
17515 
17516 	static struct HistoryItem {
17517 		string cwd;
17518 		FileNameFilter filters;
17519 	}
17520 	HistoryItem[] historyStack;
17521 	size_t historyStackPosition;
17522 
17523 	void back() {
17524 		if(historyStackPosition) {
17525 			historyStackPosition--;
17526 			currentDirectory = historyStack[historyStackPosition].cwd;
17527 			currentFilter = historyStack[historyStackPosition].filters;
17528 			filesOfType.content = currentFilter.toString();
17529 			loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true);
17530 			lineEdit.focus();
17531 		}
17532 	}
17533 
17534 	void forward() {
17535 		if(historyStackPosition + 1 < historyStack.length) {
17536 			historyStackPosition++;
17537 			currentDirectory = historyStack[historyStackPosition].cwd;
17538 			currentFilter = historyStack[historyStackPosition].filters;
17539 			filesOfType.content = currentFilter.toString();
17540 			loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true);
17541 			lineEdit.focus();
17542 		}
17543 	}
17544 
17545 	void up() {
17546 		currentDirectory = currentDirectory ~ "..";
17547 		loadFiles(currentDirectory, currentFilter);
17548 		lineEdit.focus();
17549 	}
17550 
17551 	void refresh() {
17552 		loadFiles(currentDirectory, currentFilter);
17553 		lineEdit.focus();
17554 	}
17555 
17556 	// returns common prefix
17557 	static struct CommonPrefixInfo {
17558 		string commonPrefix;
17559 		int fileCount;
17560 		string exactMatch;
17561 	}
17562 	CommonPrefixInfo loadFiles(string cwd, FileNameFilter filters, bool comingFromHistory = false) {
17563 
17564 		if(!comingFromHistory) {
17565 			if(historyStack.length) {
17566 				historyStack = historyStack[0 .. historyStackPosition + 1];
17567 				historyStack.assumeSafeAppend();
17568 			}
17569 			historyStack ~= HistoryItem(cwd, filters);
17570 			historyStackPosition = historyStack.length - 1;
17571 		}
17572 
17573 		string[] files;
17574 		string[] dirs;
17575 
17576 		dirs ~= "$HOME";
17577 		dirs ~= "$PWD";
17578 
17579 		string commonPrefix;
17580 		int commonPrefixCount;
17581 		string exactMatch;
17582 
17583 		bool matchesFilter(string name) {
17584 			foreach(filter; filters.globPatterns) {
17585 			if(
17586 				filter.length <= 1 ||
17587 				filter == "*.*" || // we always treat *.* the same as *, but it is a bit different than .*
17588 				(filter[0] == '*' && name.endsWith(filter[1 .. $])) ||
17589 				(filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1]))
17590 			)
17591 			{
17592 				if(name.length > 1 && name[0] == '.')
17593 					if(filter.length == 0 || filter[0] != '.')
17594 						return false;
17595 
17596 				return true;
17597 			}
17598 			}
17599 
17600 			return false;
17601 		}
17602 
17603 		void considerCommonPrefix(string name, bool prefiltered) {
17604 			if(!prefiltered && !matchesFilter(name))
17605 				return;
17606 
17607 			if(commonPrefix is null) {
17608 				commonPrefix = name;
17609 				commonPrefixCount = 1;
17610 				exactMatch = commonPrefix;
17611 			} else {
17612 				foreach(idx, char i; name) {
17613 					if(idx >= commonPrefix.length || i != commonPrefix[idx]) {
17614 						commonPrefix = commonPrefix[0 .. idx];
17615 						commonPrefixCount ++;
17616 						exactMatch = null;
17617 						break;
17618 					}
17619 				}
17620 			}
17621 		}
17622 
17623 		bool applyFilterToDirectories = true;
17624 		bool showDotFiles = false;
17625 		foreach(filter; filters.globPatterns) {
17626 			if(filter == ".*")
17627 				showDotFiles = true;
17628 			else foreach(ch; filter)
17629 				if(ch == '.') {
17630 					// a filter like *.exe should not apply to the directory
17631 					applyFilterToDirectories = false;
17632 					break;
17633 				}
17634 		}
17635 
17636 		try
17637 		getFiles(cwd, (string name, bool isDirectory) {
17638 			if(name == ".")
17639 				return; // skip this as unnecessary
17640 			if(isDirectory) {
17641 				if(applyFilterToDirectories) {
17642 					if(matchesFilter(name)) {
17643 						dirs ~= name;
17644 						considerCommonPrefix(name, false);
17645 					}
17646 				} else if(name != ".." && name.length > 1 && name[0] == '.') {
17647 					if(showDotFiles) {
17648 						dirs ~= name;
17649 						considerCommonPrefix(name, false);
17650 					}
17651 				} else {
17652 					dirs ~= name;
17653 					considerCommonPrefix(name, false);
17654 				}
17655 			} else {
17656 				if(matchesFilter(name)) {
17657 					files ~= name;
17658 
17659 					//if(filter.length > 0 && filter[$-1] == '*') {
17660 						considerCommonPrefix(name, true);
17661 					//}
17662 				}
17663 			}
17664 		});
17665 		catch(ArsdExceptionBase e) {
17666 			messageBox("Unable to read requested directory");
17667 			// FIXME: give them a chance to create it? or at least go back?
17668 			/+
17669 			comingFromHistory = true;
17670 			back();
17671 			return null;
17672 			+/
17673 		}
17674 
17675 		extern(C) static int comparator(scope const void* a, scope const void* b) {
17676 			auto sa = *cast(string*) a;
17677 			auto sb = *cast(string*) b;
17678 
17679 			/+
17680 				Goal here:
17681 
17682 				Dot first. This puts `foo.d` before `foo2.d`
17683 				Then numbers , natural sort order (so 9 comes before 10) for positive numbers
17684 				Then letters, in order Aa, Bb, Cc
17685 				Then other symbols in ascii order
17686 			+/
17687 			static int nextPiece(ref string whole) {
17688 				if(whole.length == 0)
17689 					return -1;
17690 
17691 				enum specialZoneSize = 1;
17692 
17693 				char current = whole[0];
17694 				if(current >= '0' && current <= '9') {
17695 					int accumulator;
17696 					do {
17697 						whole = whole[1 .. $];
17698 						accumulator *= 10;
17699 						accumulator += current - '0';
17700 						current = whole.length ? whole[0] : 0;
17701 					} while (current >= '0' && current <= '9');
17702 
17703 					return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols
17704 				} else {
17705 					whole = whole[1 .. $];
17706 
17707 					if(current == '.')
17708 						return 0; // the special case to put it before numbers
17709 
17710 					// anything above should be < specialZoneSize
17711 
17712 					int letterZoneSize = 26 * 2;
17713 					int base = int.max - letterZoneSize - char.max; // leaves space at end for symbols too if we want them after chars
17714 
17715 					if(current >= 'A' && current <= 'Z')
17716 						return base + (current - 'A') * 2;
17717 					if(current >= 'a' && current <= 'z')
17718 						return base + (current - 'a') * 2 + 1;
17719 					// return base + letterZoneSize + current; // would put symbols after numbers and letters
17720 					return specialZoneSize + current; // puts symbols before numbers and letters, but after the special zone
17721 				}
17722 			}
17723 
17724 			while(sa.length || sb.length) {
17725 				auto pa = nextPiece(sa);
17726 				auto pb = nextPiece(sb);
17727 
17728 				auto diff = pa - pb;
17729 				if(diff)
17730 					return diff;
17731 			}
17732 
17733 			return 0;
17734 		}
17735 
17736 		nonPhobosSort(files, &comparator);
17737 		nonPhobosSort(dirs, &comparator);
17738 
17739 		listWidget.clear();
17740 		dirWidget.clear();
17741 		foreach(name; dirs)
17742 			dirWidget.addOption(name);
17743 		foreach(name; files)
17744 			listWidget.addOption(name);
17745 
17746 		return CommonPrefixInfo(commonPrefix, commonPrefixCount, exactMatch);
17747 	}
17748 
17749 	ListWidget listWidget;
17750 	ListWidget dirWidget;
17751 
17752 	FreeEntrySelection filesOfType;
17753 	LineEdit directoryHolder;
17754 
17755 	string currentDirectory_;
17756 	FileNameFilter currentNonTabFilter;
17757 	FileNameFilter currentFilter;
17758 	FileNameFilterSet filterOptions;
17759 
17760 	void currentDirectory(string s) {
17761 		currentDirectory_ = FilePath(s).makeAbsolute(getCurrentWorkingDirectory()).toString();
17762 		directoryHolder.content = currentDirectory_;
17763 	}
17764 	string currentDirectory() {
17765 		return currentDirectory_;
17766 	}
17767 
17768 	private string getUserHomeDir() {
17769 		import core.stdc.stdlib;
17770 		version(Windows)
17771 			return (stringz(getenv("HOMEDRIVE")).borrow ~ stringz(getenv("HOMEPATH")).borrow).idup;
17772 		else
17773 			return (stringz(getenv("HOME")).borrow).idup;
17774 	}
17775 
17776 	private string expandTilde(string s) {
17777 		// FIXME: cannot look up other user dirs
17778 		if(s.length == 1 && s == "~")
17779 			return getUserHomeDir();
17780 		if(s.length > 1 && s[0] == '~' && s[1] == '/')
17781 			return getUserHomeDir() ~ s[1 .. $];
17782 		return s;
17783 	}
17784 
17785 	// FIXME: allow many files to be picked too sometimes
17786 
17787 	//string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"]
17788 	this(bool isOpenDialogInsteadOfSave, string prefilledName, string[] filtersInWindowsFormat, string initialDirectory, Window owner = null) {
17789 		this.filterOptions = FileNameFilterSet.fromWindowsFileNameFilterDescription(filtersInWindowsFormat);
17790 		this.isOpenDialogInsteadOfSave = isOpenDialogInsteadOfSave;
17791 		super(owner, 500, 400, "Choose File..."); // owner);
17792 
17793 		{
17794 			auto navbar = new HorizontalLayout(24, this);
17795 			auto backButton = new ToolButton(new Action("<", 0, &this.back), navbar);
17796 			auto forwardButton = new ToolButton(new Action(">", 0, &this.forward), navbar);
17797 			auto upButton = new ToolButton(new Action("^", 0, &this.up), navbar); // hmm with .. in the dir list we don't really need an up button
17798 
17799 			directoryHolder = new LineEdit(navbar);
17800 
17801 			directoryHolder.addEventListener(delegate(scope KeyDownEvent kde) {
17802 				if(kde.key == Key.Enter || kde.key == Key.PadEnter) {
17803 					kde.stopPropagation();
17804 
17805 					currentDirectory = directoryHolder.content;
17806 					loadFiles(currentDirectory, currentFilter);
17807 
17808 					lineEdit.focus();
17809 				}
17810 			});
17811 
17812 			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.
17813 
17814 			/+
17815 			auto newDirectoryButton = new ToolButton(new Action("N"), navbar);
17816 
17817 			// FIXME: make sure putting `.` in the dir filter goes back to the CWD
17818 			// and that ~ goes back to the home dir
17819 			// and blanking it goes back to the suggested dir
17820 
17821 			auto homeButton = new ToolButton(new Action("H"), navbar);
17822 			auto cwdButton = new ToolButton(new Action("."), navbar);
17823 			auto suggestedDirectoryButton = new ToolButton(new Action("*"), navbar);
17824 			+/
17825 
17826 			filesOfType = new class FreeEntrySelection {
17827 				this() {
17828 					string[] opt;
17829 					foreach(option; filterOptions.filters)
17830 						opt ~=  option.toString;
17831 					super(opt, navbar);
17832 				}
17833 				override int flexBasisWidth() {
17834 					return scaleWithDpi(150);
17835 				}
17836 				override int widthStretchiness() {
17837 					return 1;//super.widthStretchiness() / 2;
17838 				}
17839 			};
17840 			filesOfType.setSelection(0);
17841 			currentFilter = filterOptions.filters[0];
17842 			currentNonTabFilter = currentFilter;
17843 		}
17844 
17845 		{
17846 			auto mainGrid = new GridLayout(4, 1, this);
17847 
17848 			dirWidget = new ListWidget(mainGrid);
17849 			listWidget = new ListWidget(mainGrid);
17850 			listWidget.tabStop = false;
17851 			dirWidget.tabStop = false;
17852 
17853 			FileDialogDelegate.PreviewWidget previewWidget = fileDialogDelegate.makePreviewWidget(mainGrid);
17854 
17855 			mainGrid.setChildPosition(dirWidget, 0, 0, 1, 1);
17856 			mainGrid.setChildPosition(listWidget, 1, 0, previewWidget !is null ? 2 : 3, 1);
17857 			if(previewWidget)
17858 				mainGrid.setChildPosition(previewWidget, 2, 0, 1, 1);
17859 
17860 			// double click events normally trigger something else but
17861 			// here user might be clicking kinda fast and we'd rather just
17862 			// keep it
17863 			dirWidget.addEventListener((scope DoubleClickEvent dev) {
17864 				auto ce = new ChangeEvent!void(dirWidget, () {});
17865 				ce.dispatch();
17866 				lineEdit.focus();
17867 			});
17868 
17869 			dirWidget.addEventListener((scope ChangeEvent!void sce) {
17870 				string v;
17871 				foreach(o; dirWidget.options)
17872 					if(o.selected) {
17873 						v = o.label;
17874 						break;
17875 					}
17876 				if(v.length) {
17877 					if(v == "$HOME")
17878 						currentDirectory = getUserHomeDir();
17879 					else if(v == "$PWD")
17880 						currentDirectory = ".";
17881 					else
17882 						currentDirectory = currentDirectory ~ "/" ~ v;
17883 					loadFiles(currentDirectory, currentFilter);
17884 				}
17885 
17886 				dirWidget.focusOn = -1;
17887 				lineEdit.focus();
17888 			});
17889 
17890 			// double click here, on the other hand, selects the file
17891 			// and moves on
17892 			listWidget.addEventListener((scope DoubleClickEvent dev) {
17893 				OK();
17894 			});
17895 		}
17896 
17897 		lineEdit = new LabeledLineEdit("File name:", TextAlignment.Right, this);
17898 		lineEdit.focus();
17899 		lineEdit.addEventListener(delegate(CharEvent event) {
17900 			if(event.character == '\t' || event.character == '\n')
17901 				event.preventDefault();
17902 		});
17903 
17904 		listWidget.addEventListener(EventType.change, () {
17905 			foreach(o; listWidget.options)
17906 				if(o.selected)
17907 					lineEdit.content = o.label;
17908 		});
17909 
17910 		currentDirectory = initialDirectory is null ? "." : initialDirectory;
17911 
17912 		auto prefilledPath = FilePath(expandTilde(prefilledName)).makeAbsolute(FilePath(currentDirectory));
17913 		currentDirectory = prefilledPath.directoryName;
17914 		prefilledName = prefilledPath.filename;
17915 		loadFiles(currentDirectory, currentFilter);
17916 
17917 		filesOfType.addEventListener(delegate (FreeEntrySelection.SelectionChangedEvent ce) {
17918 			currentFilter = FileNameFilter.fromString(ce.stringValue);
17919 			currentNonTabFilter = currentFilter;
17920 			loadFiles(currentDirectory, currentFilter);
17921 			// lineEdit.focus(); // this causes a recursive crash.....
17922 		});
17923 
17924 		filesOfType.addEventListener(delegate(KeyDownEvent event) {
17925 			if(event.key == Key.Enter) {
17926 				currentFilter = FileNameFilter.fromString(filesOfType.content);
17927 				currentNonTabFilter = currentFilter;
17928 				loadFiles(currentDirectory, currentFilter);
17929 				event.stopPropagation();
17930 				// FIXME: refocus on the line edit
17931 			}
17932 		});
17933 
17934 		lineEdit.addEventListener((KeyDownEvent event) {
17935 			if(event.key == Key.Tab && !event.ctrlKey && !event.shiftKey) {
17936 
17937 				auto path = FilePath(expandTilde(lineEdit.content)).makeAbsolute(FilePath(currentDirectory));
17938 				currentDirectory = path.directoryName;
17939 				auto current = path.filename;
17940 
17941 				auto newFilter = current;
17942 				if(current.length && current[0] != '*' && current[$-1] != '*')
17943 					newFilter ~= "*";
17944 				else if(newFilter.length == 0)
17945 					newFilter = "*";
17946 
17947 				auto newFilterObj = FileNameFilter("Custom filter", [newFilter]);
17948 
17949 				CommonPrefixInfo commonPrefix = loadFiles(currentDirectory, newFilterObj);
17950 				if(commonPrefix.fileCount == 1) {
17951 					// exactly one file, let's see what it is
17952 					auto specificFile = FilePath(commonPrefix.exactMatch).makeAbsolute(FilePath(currentDirectory));
17953 					if(getFileType(specificFile.toString) == FileType.dir) {
17954 						// a directory means we should change to it and keep the old filter
17955 						currentDirectory = specificFile.toString();
17956 						lineEdit.content = specificFile.toString() ~ "/";
17957 						loadFiles(currentDirectory, currentFilter);
17958 					} else {
17959 						// any other file should be selected in the list
17960 						currentDirectory = specificFile.directoryName;
17961 						current = specificFile.filename;
17962 						lineEdit.content = current;
17963 						loadFiles(currentDirectory, currentFilter);
17964 					}
17965 				} else if(commonPrefix.fileCount > 1) {
17966 					currentFilter = newFilterObj;
17967 					filesOfType.content = currentFilter.toString();
17968 					lineEdit.content = commonPrefix.commonPrefix;
17969 				} else {
17970 					// if there were no files, we don't really want to change the filter..
17971 					//sdpyPrintDebugString("no files");
17972 				}
17973 
17974 				// FIXME: if that is a directory, add the slash? or even go inside?
17975 
17976 				event.preventDefault();
17977 			}
17978 			else if(event.key == Key.Left && event.altKey) {
17979 				this.back();
17980 				event.preventDefault();
17981 			}
17982 			else if(event.key == Key.Right && event.altKey) {
17983 				this.forward();
17984 				event.preventDefault();
17985 			}
17986 		});
17987 
17988 
17989 		lineEdit.content = prefilledName;
17990 
17991 		auto hl = new HorizontalLayout(60, this);
17992 		auto cancelButton = new Button("Cancel", hl);
17993 		auto okButton = new Button(isOpenDialogInsteadOfSave ? "Open" : "Save"/*"OK"*/, hl);
17994 
17995 		cancelButton.addEventListener(EventType.triggered, &Cancel);
17996 		okButton.addEventListener(EventType.triggered, &OK);
17997 
17998 		this.addEventListener((KeyDownEvent event) {
17999 			if(event.key == Key.Enter || event.key == Key.PadEnter) {
18000 				event.preventDefault();
18001 				OK();
18002 			}
18003 			else if(event.key == Key.Escape)
18004 				Cancel();
18005 			else if(event.key == Key.F5)
18006 				refresh();
18007 			else if(event.key == Key.Up && event.altKey)
18008 				up(); // ditto
18009 			else if(event.key == Key.Left && event.altKey)
18010 				back(); // FIXME: it sends the key to the line edit too
18011 			else if(event.key == Key.Right && event.altKey)
18012 				forward(); // ditto
18013 			else if(event.key == Key.Up)
18014 				listWidget.setSelection(listWidget.getSelection() - 1);
18015 			else if(event.key == Key.Down)
18016 				listWidget.setSelection(listWidget.getSelection() + 1);
18017 		});
18018 
18019 		// FIXME: set the list view's focusOn to -1 on most interactions so it doesn't keep a thing highlighted
18020 		// FIXME: button to create new directory
18021 		// FIXME: show dirs in the files list too? idk.
18022 
18023 		// FIXME: support ~ as alias for home in the input
18024 		// FIXME: tab complete ought to be able to change+complete dir too
18025 	}
18026 
18027 	override void OK() {
18028 		if(lineEdit.content.length) {
18029 			auto c = expandTilde(lineEdit.content);
18030 
18031 			FilePath accepted = FilePath(c).makeAbsolute(FilePath(currentDirectory));
18032 
18033 			auto ft = getFileType(accepted.toString);
18034 
18035 			if(ft == FileType.error && isOpenDialogInsteadOfSave) {
18036 				// FIXME: tell the user why
18037 				messageBox("Cannot open file: " ~ accepted.toString ~ "\nTry another or cancel.");
18038 				lineEdit.focus();
18039 				return;
18040 
18041 			}
18042 
18043 			// FIXME: symlinks to dirs should prolly also get this behavior
18044 			if(ft == FileType.dir) {
18045 				currentDirectory = accepted.toString;
18046 
18047 				currentFilter = currentNonTabFilter;
18048 				filesOfType.content = currentFilter.toString();
18049 
18050 				loadFiles(currentDirectory, currentFilter);
18051 				lineEdit.content = "";
18052 
18053 				lineEdit.focus();
18054 
18055 				return;
18056 			}
18057 
18058 			if(onOK)
18059 				onOK(accepted.toString);
18060 		}
18061 		close();
18062 	}
18063 
18064 	override void Cancel() {
18065 		if(onCancel)
18066 			onCancel();
18067 		close();
18068 	}
18069 }
18070 
18071 private enum FileType {
18072 	error,
18073 	dir,
18074 	other
18075 }
18076 
18077 private FileType getFileType(string name) {
18078 	version(Windows) {
18079 		auto ws = WCharzBuffer(name);
18080 		auto ret = GetFileAttributesW(ws.ptr);
18081 		if(ret == INVALID_FILE_ATTRIBUTES)
18082 			return FileType.error;
18083 		return ((ret & FILE_ATTRIBUTE_DIRECTORY) != 0) ? FileType.dir : FileType.other;
18084 	} else version(Posix) {
18085 		import core.sys.posix.sys.stat;
18086 		stat_t buf;
18087 		auto ret = stat((name ~ '\0').ptr, &buf);
18088 		if(ret == -1)
18089 			return FileType.error;
18090 		return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other;
18091 	} else assert(0, "Not implemented");
18092 }
18093 
18094 /*
18095 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes
18096 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx
18097 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx
18098 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx
18099 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx
18100 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box
18101 http://www.sbin.org/doc/Xlib/chapt_03.html
18102 
18103 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx
18104 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx
18105 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx
18106 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx
18107 */
18108 
18109 
18110 // These are all for setMenuAndToolbarFromAnnotatedCode
18111 /// This item in the menu will be preceded by a separator line
18112 /// Group: generating_from_code
18113 struct separator {}
18114 deprecated("It was misspelled, use separator instead") alias seperator = separator;
18115 /// Program-wide keyboard shortcut to trigger the action
18116 /// Group: generating_from_code
18117 struct accelerator { string keyString; } // FIXME: allow multiple aliases here
18118 /// tells which menu the action will be on
18119 /// Group: generating_from_code
18120 struct menu { string name; }
18121 /// Describes which toolbar section the action appears on
18122 /// Group: generating_from_code
18123 struct toolbar { string groupName; }
18124 ///
18125 /// Group: generating_from_code
18126 struct icon { ushort id; }
18127 ///
18128 /// Group: generating_from_code
18129 struct label { string label; }
18130 ///
18131 /// Group: generating_from_code
18132 struct hotkey { dchar ch; }
18133 ///
18134 /// Group: generating_from_code
18135 struct tip { string tip; }
18136 ///
18137 /// Group: generating_from_code
18138 enum context_menu = menu.init;
18139 /++
18140 	// FIXME: the options should have both a label and a value
18141 
18142 	if label is null, it will try to just stringify value.
18143 
18144 	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.
18145 +/
18146 /// Group: generating_from_code
18147 Choices!T choices(T)(T[] options, bool allowCustom = false, bool allowReordering = true, bool allowDuplicates = true) {
18148 	return Choices!T(() => options, allowCustom, allowReordering, allowDuplicates);
18149 }
18150 /// ditto
18151 Choices!T choices(T)(T[] delegate() options, bool allowCustom = false, bool allowReordering = true, bool allowDuplicates = true) {
18152 	return Choices!T(options, allowCustom, allowReordering, allowDuplicates);
18153 }
18154 /// ditto
18155 struct Choices(T) {
18156 	///
18157 	T[] delegate() options;
18158 	bool allowCustom = false;
18159 	/// only relevant if attached to an array
18160 	bool allowReordering = true;
18161 	/// ditto
18162 	bool allowDuplicates = true;
18163 	/// makes no sense on a set
18164 	bool requireAll = false;
18165 }
18166 
18167 
18168 /++
18169 	Observes and allows inspection of an object via automatic gui
18170 +/
18171 /// Group: generating_from_code
18172 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) {
18173 	return new ObjectInspectionWindowImpl!(T)(t);
18174 }
18175 
18176 class ObjectInspectionWindow : Window {
18177 	this(int a, int b, string c) {
18178 		super(a, b, c);
18179 	}
18180 
18181 	abstract void readUpdatesFromObject();
18182 }
18183 
18184 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow {
18185 	T t;
18186 	this(T t) {
18187 		this.t = t;
18188 
18189 		super(300, 400, "ObjectInspectionWindow - " ~ T.stringof);
18190 
18191 		foreach(memberName; __traits(derivedMembers, T)) {{
18192 			alias member = I!(__traits(getMember, t, memberName))[0];
18193 			alias type = typeof(member);
18194 			static if(is(type == int)) {
18195 				auto le = new LabeledLineEdit(memberName ~ ": ", this);
18196 				//le.addEventListener("char", (Event ev) {
18197 					//if((ev.character < '0' || ev.character > '9') && ev.character != '-')
18198 						//ev.preventDefault();
18199 				//});
18200 				le.addEventListener(EventType.change, (Event ev) {
18201 					__traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue);
18202 				});
18203 
18204 				updateMemberDelegates[memberName] = () {
18205 					le.content = toInternal!string(__traits(getMember, t, memberName));
18206 				};
18207 			}
18208 		}}
18209 	}
18210 
18211 	void delegate()[string] updateMemberDelegates;
18212 
18213 	override void readUpdatesFromObject() {
18214 		foreach(k, v; updateMemberDelegates)
18215 			v();
18216 	}
18217 }
18218 
18219 /++
18220 	Creates a dialog based on a data structure.
18221 
18222 	---
18223 	dialog(window, (YourStructure value) {
18224 		// the user filled in the struct and clicked OK,
18225 		// you can check the members now
18226 	});
18227 	---
18228 
18229 	Params:
18230 		initialData = the initial value to show in the dialog. It will not modify this unless
18231 		it is a class then it might, no promises.
18232 
18233 	History:
18234 		The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5)
18235 
18236 		The overloads with `parent` were added September 29, 2024. The ones without it are likely to
18237 		be deprecated soon.
18238 +/
18239 /// Group: generating_from_code
18240 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18241 	dialog(null, T.init, onOK, onCancel, title);
18242 }
18243 /// ditto
18244 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18245 	dialog(null, T.init, onOK, onCancel, title);
18246 }
18247 /// ditto
18248 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18249 	dialog(parent, T.init, onOK, onCancel, title);
18250 }
18251 /// ditto
18252 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18253 	dialog(parent, initialData, onOK, onCancel, title);
18254 }
18255 /// ditto
18256 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) {
18257 	auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title);
18258 	dg.show();
18259 }
18260 
18261 private static template I(T...) { alias I = T; }
18262 
18263 
18264 private string beautify(string name, char space = ' ', bool allLowerCase = false) {
18265 	if(name == "id")
18266 		return allLowerCase ? name : "ID";
18267 
18268 	char[160] buffer;
18269 	int bufferIndex = 0;
18270 	bool shouldCap = true;
18271 	bool shouldSpace;
18272 	bool lastWasCap;
18273 	foreach(idx, char ch; name) {
18274 		if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
18275 
18276 		if((ch >= 'A' && ch <= 'Z') || ch == '_') {
18277 			if(lastWasCap) {
18278 				// two caps in a row, don't change. Prolly acronym.
18279 			} else {
18280 				if(idx)
18281 					shouldSpace = true; // new word, add space
18282 			}
18283 
18284 			lastWasCap = true;
18285 		} else {
18286 			lastWasCap = false;
18287 		}
18288 
18289 		if(shouldSpace) {
18290 			buffer[bufferIndex++] = space;
18291 			if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
18292 			shouldSpace = false;
18293 		}
18294 		if(shouldCap) {
18295 			if(ch >= 'a' && ch <= 'z')
18296 				ch -= 32;
18297 			shouldCap = false;
18298 		}
18299 		if(allLowerCase && ch >= 'A' && ch <= 'Z')
18300 			ch += 32;
18301 		buffer[bufferIndex++] = ch;
18302 	}
18303 	return buffer[0 .. bufferIndex].idup;
18304 }
18305 
18306 /++
18307 	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.
18308 +/
18309 class AutomaticDialog(T) : Dialog {
18310 	T t;
18311 
18312 	void delegate(T) onOK;
18313 	void delegate() onCancel;
18314 
18315 	override int paddingTop() { return defaultLineHeight; }
18316 	override int paddingBottom() { return defaultLineHeight; }
18317 	override int paddingRight() { return defaultLineHeight; }
18318 	override int paddingLeft() { return defaultLineHeight; }
18319 
18320 	this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) {
18321 		assert(onOK !is null);
18322 
18323 		t = initialData;
18324 
18325 		static if(is(T == class)) {
18326 			if(t is null)
18327 				t = new T();
18328 		}
18329 		this.onOK = onOK;
18330 		this.onCancel = onCancel;
18331 		super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title);
18332 
18333 		static if(is(T == class))
18334 			this.addDataControllerWidget(t);
18335 		else
18336 			this.addDataControllerWidget(&t);
18337 
18338 		auto hl = new HorizontalLayout(this);
18339 		auto stretch = new HorizontalSpacer(hl); // to right align
18340 		auto ok = new CommandButton("OK", hl);
18341 		auto cancel = new CommandButton("Cancel", hl);
18342 		ok.addEventListener(EventType.triggered, &OK);
18343 		cancel.addEventListener(EventType.triggered, &Cancel);
18344 
18345 		this.addEventListener((KeyDownEvent ev) {
18346 			if(ev.key == Key.Enter || ev.key == Key.PadEnter) {
18347 				ok.focus();
18348 				OK();
18349 				ev.preventDefault();
18350 			}
18351 			if(ev.key == Key.Escape) {
18352 				Cancel();
18353 				ev.preventDefault();
18354 			}
18355 		});
18356 
18357 		this.addEventListener((scope ClosedEvent ce) {
18358 			if(onCancel)
18359 				onCancel();
18360 		});
18361 
18362 		//this.children[0].focus();
18363 	}
18364 
18365 	override void OK() {
18366 		onOK(t);
18367 		close();
18368 	}
18369 
18370 	override void Cancel() {
18371 		if(onCancel)
18372 			onCancel();
18373 		close();
18374 	}
18375 }
18376 
18377 private template baseClassCount(Class) {
18378 	private int helper() {
18379 		int count = 0;
18380 		static if(is(Class bases == super)) {
18381 			foreach(base; bases)
18382 				static if(is(base == class))
18383 					count += 1 + baseClassCount!base;
18384 		}
18385 		return count;
18386 	}
18387 
18388 	enum int baseClassCount = helper();
18389 }
18390 
18391 private long stringToLong(string s) {
18392 	long ret;
18393 	if(s.length == 0)
18394 		return ret;
18395 	bool negative = s[0] == '-';
18396 	if(negative)
18397 		s = s[1 .. $];
18398 	foreach(ch; s) {
18399 		if(ch >= '0' && ch <= '9') {
18400 			ret *= 10;
18401 			ret += ch - '0';
18402 		}
18403 	}
18404 	if(negative)
18405 		ret = -ret;
18406 	return ret;
18407 }
18408 
18409 
18410 interface ReflectableProperties {
18411 	/++
18412 		Iterates the event's properties as strings. Note that keys may be repeated and a get property request may
18413 		call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by
18414 		json in the current implementation.
18415 
18416 		This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have
18417 		properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself
18418 		as of the June 2, 2021 release.
18419 
18420 		History:
18421 			Added June 2, 2021.
18422 
18423 		See_Also: [getPropertyAsString], [setPropertyFromString]
18424 	+/
18425 	void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow;
18426 	/++
18427 		Requests a property to be delivered to you as a string, through your `sink` delegate.
18428 
18429 		If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should
18430 		be interpreted as json, otherwise, it is just a plain string.
18431 
18432 		The sink should always be called exactly once for each call (it is basically a return value, but it might
18433 		use a local buffer it maintains instead of allocating a return value).
18434 
18435 		History:
18436 			Added June 2, 2021.
18437 
18438 		See_Also: [getPropertiesList], [setPropertyFromString]
18439 	+/
18440 	void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink);
18441 	/++
18442 		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.
18443 
18444 		History:
18445 			Added June 2, 2021.
18446 
18447 		See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult]
18448 	+/
18449 	SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson);
18450 
18451 	/// [setPropertyFromString] possible return values
18452 	enum SetPropertyResult {
18453 		success = 0, /// the property has been successfully set to the request value
18454 		notPermitted = -1, /// the property exists but it cannot be changed at this time
18455 		notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist)
18456 		noSuchProperty = -3, /// there is no property by that name
18457 		wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value
18458 		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)
18459 	}
18460 
18461 	/++
18462 		You can mix this in to get an implementation in child classes. This does [setPropertyFromString].
18463 
18464 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
18465 
18466 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
18467 		rarely need to use these building blocks directly.
18468 	+/
18469 	mixin template RegisterSetters() {
18470 		override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) {
18471 			switch(name) {
18472 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
18473 					case memberName:
18474 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
18475 							if(value != "true" && value != "false")
18476 								return SetPropertyResult.wrongFormat;
18477 							__traits(getMember, this, memberName) = value == "true" ? true : false;
18478 							return SetPropertyResult.success;
18479 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
18480 							import core.stdc.stdlib;
18481 							char[128] zero = 0;
18482 							if(buffer.length + 1 >= zero.length)
18483 								return SetPropertyResult.wrongFormat;
18484 							zero[0 .. buffer.length] = buffer[];
18485 							__traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10);
18486 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
18487 							import core.stdc.stdlib;
18488 							char[128] zero = 0;
18489 							if(buffer.length + 1 >= zero.length)
18490 								return SetPropertyResult.wrongFormat;
18491 							zero[0 .. buffer.length] = buffer[];
18492 							__traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10);
18493 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
18494 							__traits(getMember, this, memberName) = value.idup;
18495 						} else {
18496 							return SetPropertyResult.notImplemented;
18497 						}
18498 
18499 				}
18500 				default:
18501 					return super.setPropertyFromString(name, value, valueIsJson);
18502 			}
18503 		}
18504 	}
18505 
18506 	/++
18507 		You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList].
18508 
18509 		Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand.
18510 
18511 		For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should
18512 		rarely need to use these building blocks directly.
18513 	+/
18514 	mixin template RegisterGetters() {
18515 		override void getPropertiesList(scope void delegate(string name) sink) const {
18516 			super.getPropertiesList(sink);
18517 
18518 			foreach(memberName; __traits(derivedMembers, typeof(this))) {
18519 				sink(memberName);
18520 			}
18521 		}
18522 		override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) {
18523 			switch(name) {
18524 				foreach(memberName; __traits(derivedMembers, typeof(this))) {
18525 					case memberName:
18526 						static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) {
18527 							sink(name, __traits(getMember, this, memberName) ? "true" : "false", true);
18528 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) {
18529 							import core.stdc.stdio;
18530 							char[32] buffer;
18531 							auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName));
18532 							sink(name, buffer[0 .. len], true);
18533 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) {
18534 							import core.stdc.stdio;
18535 							char[32] buffer;
18536 							auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName));
18537 							sink(name, buffer[0 .. len], true);
18538 						} else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) {
18539 							sink(name, __traits(getMember, this, memberName), false);
18540 							//sinkJsonString(memberName, __traits(getMember, this, memberName), sink);
18541 						} else {
18542 							sink(name, null, true);
18543 						}
18544 
18545 					return;
18546 				}
18547 				default:
18548 					return super.getPropertyAsString(name, sink);
18549 			}
18550 		}
18551 	}
18552 }
18553 
18554 private struct Stack(T) {
18555 	this(int maxSize) {
18556 		internalLength = 0;
18557 		arr = initialBuffer[];
18558 	}
18559 
18560 	///.
18561 	void push(T t) {
18562 		if(internalLength >= arr.length) {
18563 			auto oldarr = arr;
18564 			if(arr.length < 4096)
18565 				arr = new T[arr.length * 2];
18566 			else
18567 				arr = new T[arr.length + 4096];
18568 			arr[0 .. oldarr.length] = oldarr[];
18569 		}
18570 
18571 		arr[internalLength] = t;
18572 		internalLength++;
18573 	}
18574 
18575 	///.
18576 	T pop() {
18577 		assert(internalLength);
18578 		internalLength--;
18579 		return arr[internalLength];
18580 	}
18581 
18582 	///.
18583 	T peek() {
18584 		assert(internalLength);
18585 		return arr[internalLength - 1];
18586 	}
18587 
18588 	///.
18589 	@property bool empty() {
18590 		return internalLength ? false : true;
18591 	}
18592 
18593 	///.
18594 	private T[] arr;
18595 	private size_t internalLength;
18596 	private T[64] initialBuffer;
18597 	// 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),
18598 	// using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push()
18599 	// function thanks to this, and push() was actually one of the slowest individual functions in the code!
18600 }
18601 
18602 /// 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.
18603 private struct WidgetStream {
18604 
18605 	///.
18606 	@property Widget front() {
18607 		return current.widget;
18608 	}
18609 
18610 	/// Use Widget.tree instead.
18611 	this(Widget start) {
18612 		current.widget = start;
18613 		current.childPosition = -1;
18614 		isEmpty = false;
18615 		stack = typeof(stack)(0);
18616 	}
18617 
18618 	/*
18619 		Handle it
18620 		handle its children
18621 
18622 	*/
18623 
18624 	///.
18625 	void popFront() {
18626 	    more:
18627 	    	if(isEmpty) return;
18628 
18629 		// FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times)
18630 
18631 		current.childPosition++;
18632 		if(current.childPosition >= current.widget.children.length) {
18633 			if(stack.empty())
18634 				isEmpty = true;
18635 			else {
18636 				current = stack.pop();
18637 				goto more;
18638 			}
18639 		} else {
18640 			stack.push(current);
18641 			current.widget = current.widget.children[current.childPosition];
18642 			current.childPosition = -1;
18643 		}
18644 	}
18645 
18646 	///.
18647 	@property bool empty() {
18648 		return isEmpty;
18649 	}
18650 
18651 	private:
18652 
18653 	struct Current {
18654 		Widget widget;
18655 		int childPosition;
18656 	}
18657 
18658 	Current current;
18659 
18660 	Stack!(Current) stack;
18661 
18662 	bool isEmpty;
18663 }
18664 
18665 
18666 /+
18667 
18668 	I could fix up the hierarchy kinda like this
18669 
18670 	class Widget {
18671 		Widget[] children() { return null; }
18672 	}
18673 	interface WidgetContainer {
18674 		Widget asWidget();
18675 		void addChild(Widget w);
18676 
18677 		// alias asWidget this; // but meh
18678 	}
18679 
18680 	Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead.
18681 
18682 	class Layout : Widget, WidgetContainer {}
18683 
18684 	class Window : WidgetContainer {}
18685 
18686 
18687 	All constructors that previously took Widgets should now take WidgetContainers instead
18688 
18689 
18690 
18691 	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".
18692 +/
18693 
18694 /+
18695 	LAYOUTS 2.0
18696 
18697 	can just be assigned as a function. assigning a new one will cause it to be immediately called.
18698 
18699 	they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one.
18700 
18701 	recomputeChildLayout only really needs a property accessor proxy... just the layout info too.
18702 
18703 	and even Paint can just use computedStyle...
18704 
18705 		background color
18706 		font
18707 		border color and style
18708 
18709 	And actually the style proxy can offer some helper routines to draw these like the draw 3d box
18710 		please note that many widgets and in some modes will completely ignore properties as they will.
18711 		they are just hints you set, not promises.
18712 
18713 
18714 
18715 
18716 
18717 	So generally the existing virtual functions are just the default for the class. But individual objects
18718 	or stylesheets can override this. The virtual ones count as tag-level specificity in css.
18719 +/
18720 
18721 /++
18722 	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.
18723 
18724 	History:
18725 		Added May 24, 2021.
18726 +/
18727 struct WidgetBackground {
18728 	/++
18729 		A background with the given solid color.
18730 	+/
18731 	this(Color color) {
18732 		this.color = color;
18733 	}
18734 
18735 	this(WidgetBackground bg) {
18736 		this = bg;
18737 	}
18738 
18739 	/++
18740 		Creates a widget from the string.
18741 
18742 		Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css.
18743 	+/
18744 	static WidgetBackground fromString(string s) {
18745 		return WidgetBackground(Color.fromString(s));
18746 	}
18747 
18748 	/++
18749 		The background is not necessarily a solid color, but you can always specify a color as a fallback.
18750 
18751 		History:
18752 			Made `public` on December 18, 2022 (dub v10.10).
18753 	+/
18754 	Color color;
18755 }
18756 
18757 /++
18758 	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!)
18759 
18760 	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.
18761 
18762 	You should not inherit from this directly, but instead use [VisualTheme].
18763 
18764 	History:
18765 		Added May 8, 2021
18766 +/
18767 abstract class BaseVisualTheme {
18768 	/// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override.
18769 	abstract void doPaint(Widget widget, WidgetPainter painter);
18770 
18771 	/+
18772 	/// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override.
18773 	abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg);
18774 	+/
18775 
18776 	/++
18777 		Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css,
18778 		where the interpretation of the string varies for each property and may include things like measurement units.
18779 	+/
18780 	abstract string getPropertyString(Widget widget, string propertyName);
18781 
18782 	/++
18783 		Default background color of the window. Widgets also use this to simulate transparency.
18784 
18785 		Probably some shade of grey.
18786 	+/
18787 	abstract Color windowBackgroundColor();
18788 	abstract Color widgetBackgroundColor();
18789 	abstract Color foregroundColor();
18790 	abstract Color lightAccentColor();
18791 	abstract Color darkAccentColor();
18792 
18793 	/++
18794 		Colors used to indicate active selections in lists and text boxes, etc.
18795 	+/
18796 	abstract Color selectionForegroundColor();
18797 	/// ditto
18798 	abstract Color selectionBackgroundColor();
18799 
18800 	deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); }
18801 
18802 	/++
18803 		If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally.
18804 	+/
18805 	abstract OperatingSystemFont defaultFont(int dpi);
18806 
18807 	private OperatingSystemFont[int] defaultFontCache_;
18808 	private OperatingSystemFont defaultFontCached(int dpi) {
18809 		if(dpi !in defaultFontCache_) {
18810 			// FIXME: set this to false if X disconnect or if visual theme changes
18811 			defaultFontCache_[dpi] = defaultFont(dpi);
18812 		}
18813 		return defaultFontCache_[dpi];
18814 	}
18815 }
18816 
18817 /+
18818 	A widget should have:
18819 		classList
18820 		dataset
18821 		attributes
18822 		computedStyles
18823 		state (persistent)
18824 		dynamic state (focused, hover, etc)
18825 +/
18826 
18827 // visualTheme.computedStyle(this).paddingLeft
18828 
18829 
18830 /++
18831 	This is your entry point to create your own visual theme for custom widgets.
18832 
18833 	You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods.
18834 
18835 	Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating.
18836 +/
18837 abstract class VisualTheme(CRTP) : BaseVisualTheme {
18838 	override string getPropertyString(Widget widget, string propertyName) {
18839 		return null;
18840 	}
18841 
18842 	/+
18843 		mixin StyleOverride!Widget
18844 	final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) {
18845 		w.useStyleProperties(dg);
18846 	}
18847 	+/
18848 
18849 	final override void doPaint(Widget widget, WidgetPainter painter) {
18850 		auto derived = cast(CRTP) cast(void*) this;
18851 
18852 		scope void delegate(Widget, WidgetPainter) bestMatch;
18853 		int bestMatchScore;
18854 
18855 		static if(__traits(hasMember, CRTP, "paint"))
18856 		foreach(overload; __traits(getOverloads, CRTP, "paint")) {
18857 			static if(is(typeof(overload) Params == __parameters)) {
18858 				static assert(Params.length == 2);
18859 				static assert(is(Params[0] : Widget));
18860 				static assert(is(Params[1] == WidgetPainter));
18861 				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);
18862 
18863 				alias type = Params[0];
18864 				if(cast(type) widget) {
18865 					auto score = baseClassCount!type;
18866 
18867 					if(score > bestMatchScore) {
18868 						bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload);
18869 						bestMatchScore = score;
18870 					}
18871 				}
18872 			} else static assert(0, "paint should be a method.");
18873 		}
18874 
18875 		if(bestMatch)
18876 			bestMatch(widget, painter);
18877 		else
18878 			widget.paint(painter);
18879 	}
18880 
18881 	deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; }
18882 
18883 	// 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
18884 	// mixin Beautiful95Theme;
18885 	mixin DefaultLightTheme;
18886 
18887 	private static struct Cached {
18888 		// i prolly want to do this
18889 	}
18890 }
18891 
18892 /// ditto
18893 mixin template Beautiful95Theme() {
18894 	override Color windowBackgroundColor() { return Color(212, 212, 212); }
18895 	override Color widgetBackgroundColor() { return Color.white; }
18896 	override Color foregroundColor() { return Color.black; }
18897 	override Color darkAccentColor() { return Color(172, 172, 172); }
18898 	override Color lightAccentColor() { return Color(223, 223, 223); }
18899 	override Color selectionForegroundColor() { return Color.white; }
18900 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
18901 	override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr
18902 }
18903 
18904 /// ditto
18905 mixin template DefaultLightTheme() {
18906 	override Color windowBackgroundColor() { return Color(232, 232, 232); }
18907 	override Color widgetBackgroundColor() { return Color.white; }
18908 	override Color foregroundColor() { return Color.black; }
18909 	override Color darkAccentColor() { return Color(172, 172, 172); }
18910 	override Color lightAccentColor() { return Color(223, 223, 223); }
18911 	override Color selectionForegroundColor() { return Color.white; }
18912 	override Color selectionBackgroundColor() { return Color(0, 0, 128); }
18913 	override OperatingSystemFont defaultFont(int dpi) {
18914 		version(Windows)
18915 			return new OperatingSystemFont("Segoe UI");
18916 		else static if(UsingSimpledisplayCocoa) {
18917 			return (new OperatingSystemFont()).loadDefault;
18918 		} else {
18919 			// FIXME: undo xft's scaling so we don't end up double scaled
18920 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
18921 		}
18922 	}
18923 }
18924 
18925 /// ditto
18926 mixin template DefaultDarkTheme() {
18927 	override Color windowBackgroundColor() { return Color(64, 64, 64); }
18928 	override Color widgetBackgroundColor() { return Color.black; }
18929 	override Color foregroundColor() { return Color.white; }
18930 	override Color darkAccentColor() { return Color(20, 20, 20); }
18931 	override Color lightAccentColor() { return Color(80, 80, 80); }
18932 	override Color selectionForegroundColor() { return Color.white; }
18933 	override Color selectionBackgroundColor() { return Color(128, 0, 128); }
18934 	override OperatingSystemFont defaultFont(int dpi) {
18935 		version(Windows)
18936 			return new OperatingSystemFont("Segoe UI", 12);
18937 		else static if(UsingSimpledisplayCocoa) {
18938 			return (new OperatingSystemFont()).loadDefault;
18939 		} else {
18940 			return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96);
18941 		}
18942 	}
18943 }
18944 
18945 /// ditto
18946 alias DefaultTheme = DefaultLightTheme;
18947 
18948 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme {
18949 	/+
18950 	OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); }
18951 	Color windowBackgroundColor() { return Color(242, 242, 242); }
18952 	Color darkAccentColor() { return windowBackgroundColor; }
18953 	Color lightAccentColor() { return windowBackgroundColor; }
18954 	+/
18955 }
18956 
18957 /++
18958 	Event fired when an [Observable] variable changes. You will want to add an event listener referencing
18959 	the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });`
18960 
18961 	History:
18962 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
18963 
18964 		Made `final` on January 3, 2025
18965 +/
18966 final class StateChanged(alias field) : Event {
18967 	enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change";
18968 	override bool cancelable() const { return false; }
18969 	this(Widget target, typeof(field) newValue) {
18970 		this.newValue = newValue;
18971 		super(EventString, target);
18972 	}
18973 
18974 	typeof(field) newValue;
18975 }
18976 
18977 /++
18978 	Convenience function to add a `triggered` event listener.
18979 
18980 	Its implementation is simply `w.addEventListener("triggered", dg);`
18981 
18982 	History:
18983 		Added November 27, 2021 (dub v10.4)
18984 +/
18985 void addWhenTriggered(Widget w, void delegate() dg) {
18986 	w.addEventListener("triggered", dg);
18987 }
18988 
18989 /++
18990 	Observable variables can be added to widgets and when they are changed, it fires
18991 	off a [StateChanged] event so you can react to it.
18992 
18993 	It is implemented as a getter and setter property, along with another helper you
18994 	can use to subscribe with is `name_changed`. You can also subscribe to the [StateChanged]
18995 	event through the usual means. Just give the name of the variable. See [StateChanged] for an
18996 	example.
18997 
18998 	To get an `ObservableReference` to the observable, use `&yourname_changed`.
18999 
19000 	History:
19001 		Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4)
19002 
19003 		As of March 5, 2025, the changed function now returns an [EventListener] handle, which
19004 		you can use to disconnect the observer.
19005 +/
19006 mixin template Observable(T, string name) {
19007 	private T backing;
19008 
19009 	mixin(q{
19010 		EventListener } ~ name ~ q{_changed (void delegate(T) dg) {
19011 			return this.addEventListener((StateChanged!this_thing ev) {
19012 				dg(ev.newValue);
19013 			});
19014 		}
19015 
19016 		@property T } ~ name ~ q{ () {
19017 			return backing;
19018 		}
19019 
19020 		@property void } ~ name ~ q{ (T t) {
19021 			backing = t;
19022 			auto event = new StateChanged!this_thing(this, t);
19023 			event.dispatch();
19024 		}
19025 	});
19026 
19027 	mixin("private alias this_thing = " ~ name ~ ";");
19028 }
19029 
19030 /// ditto
19031 alias ObservableReference(T) = EventListener delegate(void delegate(T));
19032 
19033 private bool startsWith(string test, string thing) {
19034 	if(test.length < thing.length)
19035 		return false;
19036 	return test[0 .. thing.length] == thing;
19037 }
19038 
19039 private bool endsWith(string test, string thing) {
19040 	if(test.length < thing.length)
19041 		return false;
19042 	return test[$ - thing.length .. $] == thing;
19043 }
19044 
19045 /++
19046 	Context menus can have `@hotkey`, `@label`, `@tip`, `@separator`, and `@icon`
19047 
19048 	Note they can NOT have accelerators or toolbars; those annotations will be ignored.
19049 
19050 	Mark the functions callable from it with `@context_menu { ... }` Presence of other `@menu(...)` annotations will exclude it from the context menu at this time.
19051 
19052 	See_Also:
19053 		[Widget.setMenuAndToolbarFromAnnotatedCode]
19054 +/
19055 Menu createContextMenuFromAnnotatedCode(TWidget)(TWidget w) if(is(TWidget : Widget)) {
19056 	return createContextMenuFromAnnotatedCode(w, w);
19057 }
19058 
19059 /// ditto
19060 Menu createContextMenuFromAnnotatedCode(T)(Widget w, ref T t) if(!is(T == class) && !is(T == interface)) {
19061 	return createContextMenuFromAnnotatedCode_internal(w, t);
19062 }
19063 /// ditto
19064 Menu createContextMenuFromAnnotatedCode(T)(Widget w, T t) if(is(T == class) || is(T == interface)) {
19065 	return createContextMenuFromAnnotatedCode_internal(w, t);
19066 }
19067 Menu createContextMenuFromAnnotatedCode_internal(T)(Widget w, ref T t) {
19068 	Menu ret = new Menu("", w);
19069 
19070 	foreach(memberName; __traits(derivedMembers, T)) {
19071 		static if(memberName != "this")
19072 		static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) {
19073 			.menu menu;
19074 			bool separator;
19075 			.hotkey hotkey;
19076 			.icon icon;
19077 			string label;
19078 			string tip;
19079 			foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {
19080 				static if(is(typeof(attr) == .menu))
19081 					menu = attr;
19082 				else static if(is(attr == .separator))
19083 					separator = true;
19084 				else static if(is(typeof(attr) == .hotkey))
19085 					hotkey = attr;
19086 				else static if(is(typeof(attr) == .icon))
19087 					icon = attr;
19088 				else static if(is(typeof(attr) == .label))
19089 					label = attr.label;
19090 				else static if(is(typeof(attr) == .tip))
19091 					tip = attr.tip;
19092 			}
19093 
19094 			if(menu is .menu.init) {
19095 				ushort correctIcon = icon.id; // FIXME
19096 				if(label.length == 0)
19097 					label = memberName.toMenuLabel;
19098 
19099 				auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(w.parentWindow, &__traits(getMember, t, memberName));
19100 
19101 				auto action = new Action(label, correctIcon, handler);
19102 
19103 				if(separator)
19104 					ret.addSeparator();
19105 					ret.addItem(new MenuItem(action));
19106 			}
19107 		}
19108 	}
19109 
19110 	return ret;
19111 }
19112 
19113 // still do layout delegation
19114 // and... split off Window from Widget.
19115 
19116 version(minigui_screenshots)
19117 struct Screenshot {
19118 	string name;
19119 }
19120 
19121 version(minigui_screenshots)
19122 static if(__VERSION__ > 2092)
19123 mixin(q{
19124 shared static this() {
19125 	import core.runtime;
19126 
19127 	static UnitTestResult screenshotMagic() {
19128 		string name;
19129 
19130 		import arsd.png;
19131 
19132 		auto results = new Window();
19133 		auto button = new Button("do it", results);
19134 
19135 		Window.newWindowCreated = delegate(Window w) {
19136 			Timer timer;
19137 			timer = new Timer(250, {
19138 				auto img = w.win.takeScreenshot();
19139 				timer.destroy();
19140 
19141 				version(Windows)
19142 					writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img);
19143 				else
19144 					writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img);
19145 
19146 				w.close();
19147 			});
19148 		};
19149 
19150 		button.addWhenTriggered( {
19151 
19152 		foreach(test; __traits(getUnitTests, mixin("arsd.minigui"))) {
19153 			name = null;
19154 			static foreach(attr; __traits(getAttributes, test)) {
19155 				static if(is(typeof(attr) == Screenshot))
19156 					name = attr.name;
19157 			}
19158 			if(name.length) {
19159 				test();
19160 			}
19161 		}
19162 
19163 		});
19164 
19165 		results.loop();
19166 
19167 		return UnitTestResult(0, 0, false, false);
19168 	}
19169 
19170 
19171 	Runtime.extendedModuleUnitTester = &screenshotMagic;
19172 }
19173 });
19174 version(minigui_screenshots) {
19175 	version(unittest)
19176 		void main() {}
19177 	else static assert(0, "dont forget the -unittest flag to dmd");
19178 }
19179 
19180 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut.
19181 // FIXME: make multiple accelerators disambiguate based ona rgs
19182 // FIXME: MainWindow ctor should have same arg order as Window
19183 // FIXME: mainwindow ctor w/ client area size instead of total size.
19184 // 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.
19185 // FIXME: tri-state checkbox
19186 // FIXME: subordinate controls grouping...