1 /+ 2 BreakpointSplitter 3 - if not all widgets fit, it collapses to tabs 4 - if they do, you get a splitter 5 - you set priority to display things first and optional breakpoint (otherwise it uses flex basis and min width) 6 +/ 7 8 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx 9 10 // if doing nested menus, make sure the straight line from where it pops up to any destination on the new popup is not going to disappear the menu until at least a delay 11 12 // me@arsd:~/.kde/share/config$ vim kdeglobals 13 14 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too. 15 16 // https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/ 17 18 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever. 19 20 // responsive minigui, menu search, and file open with a preview hook on the side. 21 22 // FIXME: add menu checkbox and menu icon eventually 23 24 // FOXME: look at Windows rebar control too 25 26 /* 27 28 im tempted to add some css kind of thing to minigui. i've not done in the past cuz i have a lot of virtual functins i use but i think i have an evil plan 29 30 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 31 */ 32 33 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places 34 35 // FIXME: text label must be copyable to the clipboard, at least as a full chunk. 36 37 // FIXME: opt-in file picker widget with image support 38 39 // FIXME: number widget 40 41 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm 42 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles 43 44 // osx style menu search. 45 46 // would be cool for a scroll bar to have marking capabilities 47 // kinda like vim's marks just on clicks etc and visual representation 48 // generically. may be cool to add an up arrow to the bottom too 49 // 50 // leave a shadow of where you last were for going back easily 51 52 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 53 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 54 // the window. 55 56 // so what about context menus? 57 58 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 59 60 // FIXME: make the scroll thing go to bottom when the content changes. 61 62 // add a knob slider view... you click and go up and down so basically same as a vertical slider, just presented as a round image 63 64 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 65 66 67 // FIXME: add a command search thingy built in and implement tip. 68 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 69 70 // On Windows: 71 // FIXME: various labels look broken in high contrast mode 72 // FIXME: changing themes while the program is upen doesn't trigger a redraw 73 74 // add note about manifest to documentation. also icons. 75 76 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 77 // FIXME: clear the corner of scrollbars if they pop up 78 79 // minigui needs to have a stdout redirection for gui mode on windows writeln 80 81 // I kinda wanna do state reacting. sort of. idk tho 82 83 // need a viewer widget that works like a web page - arrows scroll down consistently 84 85 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 86 87 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 88 // and help info about menu items. 89 // and search in menus? 90 91 // FIXME: a scroll area event signaling when a thing comes into view might be good 92 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 93 94 // FIXME: unify Windows style line endings 95 96 /* 97 TODO: 98 99 pie menu 100 101 class Form with submit behavior -- see AutomaticDialog 102 103 disabled widgets and menu items 104 105 event cleanup 106 tooltips. 107 api improvements 108 109 margins are kinda broken, they don't collapse like they should. at least. 110 111 a table form btw would be a horizontal layout of vertical layouts holding each column 112 that would give the same width things 113 */ 114 115 /* 116 117 1(15:19:48) NotSpooky: Menus, text entry, label, notebook, box, frame, file dialogs and layout (this one is very useful because I can draw lines between its child widgets 118 */ 119 120 /++ 121 minigui is a smallish GUI widget library, aiming to be on par with at least 122 HTML4 forms and a few other expected gui components. It uses native controls 123 on Windows and does its own thing on Linux (Mac is not currently supported but 124 I'm slowly working on it). 125 126 127 $(H3 Conceptual Overviews) 128 129 A gui application is made out of widgets laid out in windows that display information and respond to events from the user. They also typically have actions available in menus, and you might also want to customize the appearance. How do we do these things with minigui? Let's break it down into several categories. 130 131 $(H4 Code structure) 132 133 You will typically want to create the ui, prepare event handlers, then run an event loop. The event loop drives the program, calling your methods to respond to user activity. 134 135 --- 136 import arsd.minigui; 137 138 void main() { 139 // first, create a window, the (optional) string here is its title 140 auto window = new MainWindow("Hello, World!"); 141 142 // lay out some widgets inside the window to create the ui 143 auto name = new LabeledLineEdit("What is your name?", window); 144 auto button = new Button("Say Hello", window); 145 146 // prepare event handlers 147 button.addEventListener(EventType.triggered, () { 148 window.messageBox("Hello, " ~ name.content ~ "!"); 149 }); 150 151 // show the window and run the event loop until this window is closed 152 window.loop(); 153 } 154 --- 155 156 To compile, run `opend hello.d`, then run the generated `hello` program. 157 158 While the specifics will change, nearly all minigui applications will roughly follow this pattern. 159 160 $(TIP 161 There are two other ways to run event loops: `arsd.simpledisplay.EventLoop.get.run();` and `arsd.core.getThisThreadEventLoop().run();`. They all call the same underlying functions, but have different exit conditions - the `EventLoop.get.run()` keeps running until all top-level windows are closed, and `getThisThreadEventLoop().run` keeps running until all "tasks are resolved"; it is more abstract, supporting more than just windows. 162 163 You may call this if you don't have a single main window. 164 165 Even a basic minigui window can benefit from these if you don't have a single main window: 166 167 --- 168 import arsd.minigui; 169 170 void main() { 171 // create a struct to hold gathered info 172 struct Hello { string name; } 173 // let minigui create a dialog box to get that 174 // info from the user. If you have a main window, 175 // you'd pass that here, but it is not required 176 dialog((Hello info) { 177 // inline handler of the "OK" button 178 messageBox("Hello, " ~ info.name); 179 }); 180 181 // since there is no main window to loop on, 182 // we instead call the event loop singleton ourselves 183 EventLoop.get.run; 184 } 185 --- 186 187 This is also useful when your programs lives as a notification area (aka systray) icon instead of as a window. But let's not get too far ahead of ourselves! 188 ) 189 190 $(H4 How to lay out widgets) 191 192 To better understand the details of layout algorithms and see more available included classes, see [Layout]. 193 194 $(H5 Default layouts) 195 196 minigui windows default to a flexible vertical layout, where widgets are added, from top to bottom on the window, in the same order of you creating them, then they are sized according to layout hints on the widget itself to fill the available space. This gives a reasonably usable setup but you'll probably want to customize it. 197 198 $(TIP 199 minigui's default [VerticalLayout] and [HorizontalLayout] are roughly based on css flexbox with wrap turned off. 200 ) 201 202 Generally speaking, there are two ways to customize layouts: either subclass the widget and change its hints, or wrap it in another layout widget. You can also create your own layout classes and do it all yourself, but that's fairly complicated. Wrapping existing widgets in other layout widgets is usually the easiest way to make things work. 203 204 $(NOTE 205 minigui widgets are not supposed to overlap, but can contain children, and are always rectangular. Children are laid out as rectangles inside the parent's rectangular area. 206 ) 207 208 For example, to display two widgets side-by-side, you can wrap them in a [HorizontalLayout]: 209 210 --- 211 import arsd.minigui; 212 void main() { 213 auto window = new MainWindow(); 214 215 // make the layout a child of our window 216 auto hl = new HorizontalLayout(window); 217 218 // then make the widgets children of the layout 219 auto leftButton = new Button("Left", hl); 220 auto rightButton = new Button("Right", hl); 221 222 window.loop(); 223 } 224 --- 225 226 A [HorizontalLayout] works just like the default [VerticalLayout], except in the other direction. These two buttons will take up all the available vertical space, then split available horizontal space equally. 227 228 $(H5 Nesting layouts) 229 230 Nesting layouts lets you carve up the rectangle in different ways. 231 232 $(EMBED_UNITTEST layout-example) 233 234 $(H5 Special layouts) 235 236 [TabWidget] can show pages of layouts as tabs. 237 238 See [ScrollableWidget] but be warned that it is weird. You might want to consider something like [GenericListViewWidget] instead. 239 240 $(H5 Other common layout classes) 241 242 [HorizontalLayout], [VerticalLayout], [InlineBlockLayout], [GridLayout] 243 244 $(H4 How to respond to widget events) 245 246 To better understanding the underlying event system, see [Event]. 247 248 Each widget emits its own events, which propagate up through their parents until they reach their top-level window. 249 250 $(H4 How to do overall ui - title, icons, menus, toolbar, hotkeys, statuses, etc.) 251 252 We started this series with a [MainWindow], but only added widgets to it. MainWindows also support menus and toolbars with various keyboard shortcuts. You can construct these menus by constructing classes and calling methods, but minigui also lets you just write functions in a command object and it does the rest! 253 254 See [MainWindow.setMenuAndToolbarFromAnnotatedCode] for an example. 255 256 Note that toggleable menu or toolbar items are not yet implemented, but on the todolist. Submenus and disabled items are also not supported at this time and not currently on the work list (but if you need it, let me know and MAYBE we can work something out. Emphasis on $(I maybe)). 257 258 $(TIP 259 The automatic dialog box logic is also available for you to invoke on demand with [dialog] and the data setting logic can be used with a child widget inside an existing window [addDataControllerWidget], which also has annotation-based layout capabilities. 260 ) 261 262 All windows also have titles. You can change this at any time with the `window.title = "string";` property. 263 264 Windows also have icons, which can be set with the `window.icon` property. It takes a [arsd.color.MemoryImage] object, which is an in-memory bitmap. [arsd.image] can load common file formats into these objects, or you can make one yourself. The default icon on Windows is the icon of your exe, which you can set through a resource file. (FIXME: explain how to do this easily.) 265 266 The `MainWindow` also provides a status bar across the bottom. These aren't so common in new applications, but I love them - on my own computer, I even have a global status bar for my whole desktop! I suggest you use it: a status bar is a consistent place to put information and notifications that will never overlap other content. 267 268 A status bar has parts, and the parts have content. The first part's content is assumed to change frequently; the default mouse over event will set it to [Widget.statusTip], a public `string` you can assign to any widget you want at any time. 269 270 Other parts can be added by you and are under your control. You add them with: 271 272 --- 273 window.statusBar.parts ~= StatusBar.Part(optional_size, optional_units); 274 --- 275 276 The size can be in a variety of units and what you get with mixes can get complicated. The rule is: explicit pixel sizes are used first. Then, proportional sizes are applied to the remaining space. Then, finally, if there is any space left, any items without an explicit size split them equally. 277 278 You may prefer to set them all at once, with: 279 280 --- 281 window.statusBar.parts.setSizes(1, 1, 1); 282 --- 283 284 This makes a three-part status bar, each with the same size - they all take the same proportion of the total size. Negative numbers here will use auto-scaled pixels. 285 286 You should call this right after creating your `MainWindow` as part of your setup code. 287 288 Once you make parts, you can explicitly change their content with `window.statusBar.parts[index].content = "some string";` 289 290 $(NOTE 291 I'm thinking about making the other parts do other things by default too, but if I do change it, I'll try not to break any explicitly set things you do anyway. 292 ) 293 294 If you really don't want a status bar on your main window, you can remove it with `window.statusBar = null;` Make sure you don't try to use it again, or your program will likely crash! 295 296 Status bars, at this time, cannot hold non-text content, but I do want to change that. They also cannot have event listeners at this time, but again, that is likely to change. I have something in mind where they can hold clickable messages with a history and maybe icons, but haven't implemented any of that yet. Right now, they're just a (still very useful!) display area. 297 298 $(H4 How to do custom styles) 299 300 Minigui's custom widgets support styling parameters on the level of individual widgets, or application-wide with [VisualTheme]s. 301 302 $(WARNING 303 These don't apply to non-custom widgets! They will use the operating system's native theme unless the documentation for that specific class says otherwise. 304 305 At this time, custom widgets gain capability in styling, but lose capability in terms of keeping all the right integrated details of the user experience and availability to accessibility and other automation tools. Evaluate if the benefit is worth the costs before making your decision. 306 307 I'd like to erase more and more of these gaps, but no promises as to when - or even if - that will ever actually happen. 308 ) 309 310 See [Widget.Style] for more information. 311 312 $(H4 Selection of categorized widgets) 313 314 $(LIST 315 * Buttons: [Button] 316 * Text display widgets: [TextLabel], [TextDisplay] 317 * Text edit widgets: [LineEdit] (and [LabeledLineEdit]), [PasswordEdit] (and [LabeledPasswordEdit]), [TextEdit] 318 * Selecting multiple on/off options: [Checkbox] 319 * Selecting just one from a list of options: [Fieldset], [Radiobox], [DropDownSelection] 320 * Getting rough numeric input: [HorizontalSlider], [VerticalSlider] 321 * Displaying data: [ImageBox], [ProgressBar], [TableView] 322 * Showing a list of editable items: [GenericListViewWidget] 323 * Helpers for building your own widgets: [OpenGlWidget], [ScrollMessageWidget] 324 ) 325 326 And more. See [#members] until I write up more of this later and also be aware of the package [arsd.minigui_addons]. 327 328 If none of these do what you need, you'll want to write your own. More on that in the following section. 329 330 $(H4 custom widgets - how to write your own) 331 332 See some example programs: https://github.com/adamdruppe/minigui-samples 333 334 When you can't build your application out of existing widgets, you'll want to make your own. The general pattern is to subclass [Widget], write a constructor that takes a `Widget` parent argument you pass to `super`, then set some values, override methods you want to customize, and maybe add child widgets and events as appropriate. You might also be able to subclass an existing other Widget and customize that way. 335 336 To get more specific, let's consider a few illustrative examples, then we'll come back to some principles. 337 338 $(H5 Custom Widget Examples) 339 340 $(H5 More notes) 341 342 See [Widget]. 343 344 If you override [Widget.recomputeChildLayout], don't forget to call `registerMovement()` at the top of it, then call recomputeChildLayout of all its children too! 345 346 If you need a nested OS level window, see [NestedChildWindowWidget]. Use [Widget.scaleWithDpi] to convert logical pixels to physical pixels, as required. 347 348 See [Widget.OverrideStyle], [Widget.paintContent], [Widget.dynamicState] for some useful starting points. 349 350 You may also want to provide layout and style hints by overriding things like [Widget.flexBasisWidth], [Widget.flexBasisHeight], [Widget.minHeight], yada, yada, yada. 351 352 You might make a compound widget out of other widgets. [Widget.encapsulatedChildren] can help hide this from the outside world (though is not necessary and might hurt some debugging!) 353 354 $(TIP 355 Compile your application with the `-debug` switch and press F12 in your window to open a web-browser-inspired debug window. It sucks right now and doesn't do a lot, but is sometimes better than nothing. 356 ) 357 358 $(H5 Timers and animations) 359 360 The [Timer] class is available and you can call `widget.redraw();` to trigger a redraw from a timer handler. 361 362 I generally don't like animations in my programs, so it hasn't been a priority for me to do more than this. I also hate uis that move outside of explicit user action, so minigui kinda supports this but I'd rather you didn't. I kinda wanna do something like `requestAnimationFrame` or something but haven't yet so it is just the `Timer` class. 363 364 $(H5 Clipboard integrations, drag and drop) 365 366 GUI application users tend to expect integration with their system, so clipboard support is basically a must, and drag and drop is nice to offer too. The functions for these are provided in [arsd.simpledisplay], which is public imported from minigui, and thus available to you here too. 367 368 I'd like to think of some better abstractions to make this more automagic, but you must do it yourself when implementing your custom widgets right now. 369 370 See: [draggable], [DropHandler], [setClipboardText], [setClipboardImage], [getClipboardText], [getClipboardImage], [setPrimarySelection], and others from simpledisplay. 371 372 $(H5 Context menus) 373 374 Override [Widget.contextMenu] in your subclass. 375 376 $(H4 Coming later) 377 378 Among the unfinished features: unified selections, translateable strings, external integrations. 379 380 $(H2 Running minigui programs) 381 382 Note the environment variable ARSD_SCALING_FACTOR on Linux can set multi-monitor scaling factors. I should also read it from a root window property so it easier to do with migrations... maybe a default theme selector from there too. 383 384 $(H2 Building minigui programs) 385 386 minigui's only required dependencies are [arsd.simpledisplay], [arsd.color], and 387 [arsd.textlayouter], on which it is built. simpledisplay provides the low-level 388 interfaces and minigui builds the concept of widgets inside the windows on top of it. 389 390 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 391 It isn't hugely concerned with appearance - on Windows, it just uses the native 392 controls and native theme, and on Linux, it keeps it simple and I may change that 393 at any time, though after May 2021, you can customize some things with css-inspired 394 [Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`, 395 you can use the custom implementation there too, but... you shouldn't.) 396 397 The event model is similar to what you use in the browser with Javascript and the 398 layout engine tries to automatically fit things in, similar to a css flexbox. 399 400 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 401 `-L/SUBSYSTEM:WINDOWS` and -L/entry:mainCRTStartup`. If using ldc instead 402 of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w". 403 404 Otherwise you'll get a console and possibly other visual bugs. But if you do use 405 the subsystem:windows, note that Phobos' writeln will crash the program! 406 407 HTML_To_Classes: 408 $(SMALL_TABLE 409 HTML Code | Minigui Class 410 411 `<input type="text">` | [LineEdit] 412 `<input type="password">` | [PasswordEdit] 413 `<textarea>` | [TextEdit] 414 `<select>` | [DropDownSelection] 415 `<input type="checkbox">` | [Checkbox] 416 `<input type="radio">` | [Radiobox] 417 `<button>` | [Button] 418 ) 419 420 421 Stretchiness: 422 The default is 4. You can use larger numbers for things that should 423 consume a lot of space, and lower numbers for ones that are better at 424 smaller sizes. 425 426 Overlapped_input: 427 COMING EVENTUALLY: 428 minigui will include a little bit of I/O functionality that just works 429 with the event loop. If you want to get fancy, I suggest spinning up 430 another thread and posting events back and forth. 431 432 $(H2 Add ons) 433 See the `minigui_addons` directory in the arsd repo for some add on widgets 434 you can import separately too. 435 436 $(H3 XML definitions) 437 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 438 439 $(H3 Scriptability) 440 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 441 in this documentation, it means you can call it from the script language. 442 443 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 444 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 445 446 --- 447 import arsd.minigui_xml; 448 import arsd.script; 449 450 var globals = var.emptyObject; 451 globals.makeWidgetFromString = &makeWidgetFromString; 452 453 // this now works 454 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 455 --- 456 457 More to come. 458 459 Widget_tree_notes: 460 minigui doesn't really formalize these distinctions, but in practice, there are multiple types of widgets: 461 462 $(LIST 463 * Containers - a widget that holds other widgets directly, generally [Layout]s. [WidgetContainer] is an attempt to formalize this but is nothing really special. 464 465 * Reparenting containers - a widget that holds other widgets inside a different one of their parents. [MainWindow] is an example - any time you try to add a child to the main window, it actually goes to a special container one layer deeper. [ScrollMessageWidget] also works this way. 466 467 --- 468 auto child = new Widget(mainWindow); 469 assert(child.parent is mainWindow); // fails, its actual parent is mainWindow's inner container instead. 470 --- 471 472 * Limiting containers - a widget that can only hold children of a particular type. See [TabWidget], which can only hold [TabWidgetPage]s. 473 474 * Simple controls - a widget that cannot have children, but instead does a specific job. 475 476 * Compound controls - a widget that is comprised of children internally to help it do a specific job, but externally acts like a simple control that does not allow any more children. Ideally, this is encapsulated, but in practice, it leaks right now. 477 ) 478 479 In practice, all of these are [Widget]s right now, but this violates the OOP principles of substitutability since some operations are not actually valid on all subclasses. 480 481 Future breaking changes might be related to making this more structured but im not sure it is that important to actually break stuff over. 482 483 My_UI_Guidelines: 484 Note that the Linux custom widgets generally aim to be efficient on remote X network connections. 485 486 In a perfect world, you'd achieve all the following goals: 487 488 $(LIST 489 * All operations are present in the menu 490 * The operations the user wants at the moment are right where they want them 491 * All operations can be scripted 492 * The UI does not move any elements without explicit user action 493 * All numbers can be seen and typed in if wanted, even if the ui usually hides them 494 ) 495 496 $(H2 Future Directions) 497 498 I want to do some newer ideas that might not be easy to keep working fully on Windows, like adding a menu search feature and scrollbar custom marks and typing in numbers. I might make them a default part of the widget with custom, and let you provide them through a menu or something elsewhere. 499 500 History: 501 In January 2025 (dub v12.0), minigui got a few more breaking changes: 502 503 $(LIST 504 * `defaultEventHandler_*` functions take more specific objects. So if you see errors like: 505 506 --- 507 Error: function `void arsd.minigui.EditableTextWidget.defaultEventHandler_focusin(Event foe)` does not override any function, did you mean to override `void arsd.minigui.Widget.defaultEventHandler_focusin(arsd.minigui.FocusInEvent event)`? 508 --- 509 510 Go to the file+line number from the error message and change `Event` to `FocusInEvent` (or whatever one it tells you in the "did you mean" part of the error) and recompile. No other changes should be necessary to be compatible with this change. 511 512 * Most event classes, except those explicitly used as a base class, are now marked `final`. If you depended on this subclassing, let me know and I'll see what I can do, but I expect there's little use of it. I now recommend all event classes the `final` unless you are specifically planning on extending it. 513 ) 514 515 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 516 517 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 518 tag this as version 2.0. 519 520 Among the changes: 521 $(LIST 522 * The event model changed to prefer strongly-typed events, though the Javascript string style ones still work, using properties off them is deprecated. It will still compile and function, but you should change the handler to use the classes in its argument list. I adapted my code to use the new model in just a few minutes, so it shouldn't too hard. 523 524 See [Event] for details. 525 526 * A [DoubleClickEvent] was added. Previously, you'd get two rapidly repeated click events. Now, you get one click event followed by a double click event. If you must recreate the old way exactly, you can listen for a DoubleClickEvent, set a flag upon receiving one, then send yourself a synthetic ClickEvent on the next MouseUpEvent, but your program might be better served just working with [MouseDownEvent]s instead. 527 528 See [DoubleClickEvent] for details. 529 530 * Styling hints were added, and the few that existed before have been moved to a new helper class. Deprecated forwarders exist for the (few) old properties to help you transition. Note that most of these only affect a `custom_events` build, which is the default on Linux, but opt in only on Windows. 531 532 See [Widget.Style] for details. 533 534 * Widgets now draw their keyboard focus by default instead of opt in. You may wish to set `tabStop = false;` if it wasn't supposed to receive it. 535 536 * Most Widget constructors no longer have a default `parent` argument. You must pass the parent to almost all widgets, or in rare cases, an explict `null`, but more often than not, you need the parent so the default argument was not very useful at best and misleading to a crash at worst. 537 538 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 539 540 * Several conversions of public fields to properties, deprecated, or made private. It is unlikely this will affect you, but the compiler will tell you if it does. 541 542 * Various non-breaking additions. 543 ) 544 +/ 545 module arsd.minigui; 546 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 547 548 /++ 549 This hello world sample will have an oversized button, but that's ok, you see your first window! 550 +/ 551 version(Demo) 552 unittest { 553 import arsd.minigui; 554 555 void main() { 556 auto window = new MainWindow(); 557 558 // note the parent widget is almost always passed as the last argument to a constructor 559 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 560 auto button = new Button("Close", window); 561 button.addWhenTriggered({ 562 window.close(); 563 }); 564 565 window.loop(); 566 } 567 568 main(); // exclude from docs 569 } 570 571 /++ 572 $(ID layout-example) 573 574 This example shows one way you can partition your window into a header 575 and sidebar. Here, the header and sidebar have a fixed width, while the 576 rest of the content sizes with the window. 577 578 It might be a new way of thinking about window layout to do things this 579 way - perhaps [GridLayout] more matches your style of thought - but the 580 concept here is to partition the window into sub-boxes with a particular 581 size, then partition those boxes into further boxes. 582 583 $(IMG //arsdnet.net/minigui-screenshots/windows/layout.png, The example window has a header across the top, then below it a sidebar to the left and a content area to the right.) 584 585 So to make the header, start with a child layout that has a max height. 586 It will use that space from the top, then the remaining children will 587 split the remaining area, meaning you can think of is as just being another 588 box you can split again. Keep splitting until you have the look you desire. 589 +/ 590 // https://github.com/adamdruppe/arsd/issues/310 591 version(minigui_screenshots) 592 @Screenshot("layout") 593 unittest { 594 import arsd.minigui; 595 596 // This helper class is just to help make the layout boxes visible. 597 // think of it like a <div style="background-color: whatever;"></div> in HTML. 598 class ColorWidget : Widget { 599 this(Color color, Widget parent) { 600 this.color = color; 601 super(parent); 602 } 603 Color color; 604 class Style : Widget.Style { 605 override WidgetBackground background() { return WidgetBackground(color); } 606 } 607 mixin OverrideStyle!Style; 608 } 609 610 void main() { 611 auto window = new Window; 612 613 // the key is to give it a max height. This is one way to do it: 614 auto header = new class HorizontalLayout { 615 this() { super(window); } 616 override int maxHeight() { return 50; } 617 }; 618 // this next line is a shortcut way of doing it too, but it only works 619 // for HorizontalLayout and VerticalLayout, and is less explicit, so it 620 // is good to know how to make a new class like above anyway. 621 // auto header = new HorizontalLayout(50, window); 622 623 auto bar = new HorizontalLayout(window); 624 625 // or since this is so common, VerticalLayout and HorizontalLayout both 626 // can just take an argument in their constructor for max width/height respectively 627 628 // (could have tone this above too, but I wanted to demo both techniques) 629 auto left = new VerticalLayout(100, bar); 630 631 // and this is the main section's container. A plain Widget instance is good enough here. 632 auto container = new Widget(bar); 633 634 // and these just add color to the containers we made above for the screenshot. 635 // in a real application, you can just add your actual controls instead of these. 636 auto headerColorBox = new ColorWidget(Color.teal, header); 637 auto leftColorBox = new ColorWidget(Color.green, left); 638 auto rightColorBox = new ColorWidget(Color.purple, container); 639 640 window.loop(); 641 } 642 643 main(); // exclude from docs 644 } 645 646 647 import arsd.core; 648 import arsd.textlayouter; 649 650 alias Timer = arsd.simpledisplay.Timer; 651 public import arsd.simpledisplay; 652 /++ 653 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 654 655 History: 656 Was private until May 15, 2021. 657 +/ 658 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 659 660 version(Windows) { 661 import core.sys.windows.winnls; 662 import core.sys.windows.windef; 663 import core.sys.windows.basetyps; 664 import core.sys.windows.winbase; 665 import core.sys.windows.winuser; 666 import core.sys.windows.wingdi; 667 static import gdi = core.sys.windows.wingdi; 668 } 669 670 version(Windows) { 671 version(minigui_manifest) {} else version=minigui_no_manifest; 672 673 version(minigui_no_manifest) {} else 674 static if(__VERSION__ >= 2_083) 675 version(CRuntime_Microsoft) { // FIXME: mingw? 676 // assume we want commctrl6 whenever possible since there's really no reason not to 677 // and this avoids some of the manifest hassle 678 pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""); 679 } 680 } 681 682 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 683 private bool lastDefaultPrevented; 684 685 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 686 alias scriptable = arsd_jsvar_compatible; 687 688 version(Windows) { 689 // use native widgets when available unless specifically asked otherwise 690 version(custom_widgets) { 691 enum bool UsingCustomWidgets = true; 692 enum bool UsingWin32Widgets = false; 693 } else { 694 version = win32_widgets; 695 enum bool UsingCustomWidgets = false; 696 enum bool UsingWin32Widgets = true; 697 } 698 // and native theming when needed 699 //version = win32_theming; 700 } else { 701 enum bool UsingCustomWidgets = true; 702 enum bool UsingWin32Widgets = false; 703 version=custom_widgets; 704 } 705 706 707 708 /* 709 710 The main goals of minigui.d are to: 711 1) Provide basic widgets that just work in a lightweight lib. 712 I basically want things comparable to a plain HTML form, 713 plus the easy and obvious things you expect from Windows 714 apps like a menu. 715 2) Use native things when possible for best functionality with 716 least library weight. 717 3) Give building blocks to provide easy extension for your 718 custom widgets, or hooking into additional native widgets 719 I didn't wrap. 720 4) Provide interfaces for easy interaction between third 721 party minigui extensions. (event model, perhaps 722 signals/slots, drop-in ease of use bits.) 723 5) Zero non-system dependencies, including Phobos as much as 724 I reasonably can. It must only import arsd.color and 725 my simpledisplay.d. If you need more, it will have to be 726 an extension module. 727 6) An easy layout system that generally works. 728 729 A stretch goal is to make it easy to make gui forms with code, 730 some kind of resource file (xml?) and even a wysiwyg designer. 731 732 Another stretch goal is to make it easy to hook data into the gui, 733 including from reflection. So like auto-generate a form from a 734 function signature or struct definition, or show a list from an 735 array that automatically updates as the array is changed. Then, 736 your program focuses on the data more than the gui interaction. 737 738 739 740 STILL NEEDED: 741 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 742 * slider 743 * listbox 744 * spinner 745 * label? 746 * rich text 747 */ 748 749 750 /+ 751 enum LayoutMethods { 752 verticalFlex, 753 horizontalFlex, 754 inlineBlock, // left to right, no stretch, goes to next line as needed 755 static, // just set to x, y 756 verticalNoStretch, // browser style default 757 758 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 759 760 grid, // magic 761 } 762 +/ 763 764 /++ 765 The `Widget` is the base class for minigui's functionality, ranging from UI components like checkboxes or text displays to abstract groupings of other widgets like a layout container or a html `<div>`. You will likely want to use pre-made widgets as well as creating your own. 766 767 768 To create your own widget, you must inherit from it and create a constructor that passes a parent to `super`. Everything else after that is optional. 769 770 --- 771 class MinimalWidget : Widget { 772 this(Widget parent) { 773 super(parent); 774 } 775 } 776 --- 777 778 $(SIDEBAR 779 I'm not entirely happy with leaf, container, and windows all coming from the same base Widget class, but I so far haven't thought of a better solution that's good enough to justify the breakage of a transition. It hasn't been a major problem in practice anyway. 780 ) 781 782 Broadly, there's two kinds of widgets: leaf widgets, which are intended to be the direct user-interactive components, and container widgets, which organize, lay out, and aggregate other widgets in the object tree. A special case of a container widget is [Window], which represents a separate top-level window on the screen. Both leaf and container widgets inherit from `Widget`, so this distinction is more conventional than formal. 783 784 Among the things you'll most likely want to change in your custom widget: 785 786 $(LIST 787 * In your constructor, set `tabStop = false;` if the widget is not supposed to receive keyboard focus. (Please note its childen still can, so `tabStop = false;` is appropriate on most container widgets.) 788 789 You may explicitly set `tabStop = true;` to ensure you get it, even against future changes to the library, though that's the default right now. 790 791 Do this $(I after) calling the `super` constructor. 792 793 * Override [paint] if you want full control of the widget's drawing area (except the area obscured by children!), or [paintContent] if you want to participate in the styling engine's system. You'll also possibly want to make a subclass of [Style] and use [OverrideStyle] to change the default hints given to the styling engine for widget. 794 795 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 796 797 * Override default event handlers with your behavior. For example [defaultEventHandler_click] may be overridden to make clicks do something. Again, this is generally a job for leaf widgets rather than containers; most events are dispatched to the lowest leaf on the widget tree, but they also pass through all their parents. See [Event] for more details about the event model. 798 799 * You may also want to override the various layout hints like [minWidth], [maxHeight], etc. In particular [Padding] and [Margin] are often relevant for both container and leaf widgets and the default values of 0 are often not what you want. 800 ) 801 802 On Microsoft Windows, many widgets are also based on native controls. You can also do this if `static if(UsingWin32Widgets)` passes. You should use the helper function [createWin32Window] to create the window and let minigui do what it needs to do to create its bridge structures. This will populate [Widget.hwnd] which you can access later for communcating with the native window. You may also consider overriding [Widget.handleWmCommand] and [Widget.handleWmNotify] for the widget to translate those messages into appropriate minigui [Event]s. 803 804 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 805 806 Your own custom-drawn and native system controls can exist side-by-side. 807 808 Later I'll add more complete examples, but for now [TextLabel] and [LabeledPasswordEdit] are both simple widgets you can view implementation to get some ideas. 809 +/ 810 class Widget : ReflectableProperties { 811 812 private int toolbarIconSize() { 813 return scaleWithDpi(24); 814 } 815 816 817 /++ 818 Returns the current size of the widget. 819 820 History: 821 Added January 3, 2025 822 +/ 823 final Size size() const { 824 return Size(width, height); 825 } 826 827 private bool willDraw() { 828 return true; 829 } 830 831 /+ 832 /++ 833 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 834 835 History: 836 Added September 15, 2021 837 implemented.... ??? 838 +/ 839 void prepareReflection(this This)() { 840 841 } 842 +/ 843 844 private bool _enabled = true; 845 846 /++ 847 Determines whether the control is marked enabled. Disabled controls are generally displayed as greyed out and clicking on them does nothing. It is also possible for a control to be disabled because its parent is disabled, in which case this will still return `true`, but setting `enabled = true` may have no effect. Check [disabledBy] to see which parent caused it to be disabled. 848 849 I also recommend you set a [disabledReason] if you chose to set `enabled = false` to tell the user why the control does not work and what they can do to enable it. 850 851 History: 852 Added November 23, 2021 (dub v10.4) 853 854 Warning: the specific behavior of disabling with parents may change in the future. 855 Bugs: 856 Currently only implemented for widgets backed by native Windows controls. 857 858 See_Also: [disabledReason], [disabledBy] 859 +/ 860 @property bool enabled() { 861 return disabledBy() is null; 862 } 863 864 /// ditto 865 @property void enabled(bool yes) { 866 _enabled = yes; 867 version(win32_widgets) { 868 if(hwnd) 869 EnableWindow(hwnd, yes); 870 } 871 setDynamicState(DynamicState.disabled, yes); 872 } 873 874 private string disabledReason_; 875 876 /++ 877 If the widget is not [enabled] this string may be presented to the user when they try to use it. The exact manner and time it gets displayed is up to the implementation of the control. 878 879 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 880 881 History: 882 Added November 23, 2021 (dub v10.4) 883 See_Also: [enabled], [disabledBy] 884 +/ 885 @property string disabledReason() { 886 auto w = disabledBy(); 887 return (w is null) ? null : w.disabledReason_; 888 } 889 890 /// ditto 891 @property void disabledReason(string reason) { 892 disabledReason_ = reason; 893 } 894 895 /++ 896 Returns the widget that disabled this. It might be this or one of its parents all the way up the chain, or `null` if the widget is not disabled by anything. You can check [disabledReason] on the return value (after the null check!) to get a hint to display to the user. 897 898 History: 899 Added November 25, 2021 (dub v10.4) 900 See_Also: [enabled], [disabledReason] 901 +/ 902 Widget disabledBy() { 903 Widget p = this; 904 while(p) { 905 if(!p._enabled) 906 return p; 907 p = p.parent; 908 } 909 return null; 910 } 911 912 /// Implementations of [ReflectableProperties] interface. See the interface for details. 913 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 914 if(valueIsJson) 915 return SetPropertyResult.wrongFormat; 916 switch(name) { 917 case "name": 918 this.name = value.idup; 919 return SetPropertyResult.success; 920 case "statusTip": 921 this.statusTip = value.idup; 922 return SetPropertyResult.success; 923 default: 924 return SetPropertyResult.noSuchProperty; 925 } 926 } 927 /// ditto 928 void getPropertiesList(scope void delegate(string name) sink) const { 929 sink("name"); 930 sink("statusTip"); 931 } 932 /// ditto 933 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 934 switch(name) { 935 case "name": 936 sink(name, this.name, false); 937 return; 938 case "statusTip": 939 sink(name, this.statusTip, false); 940 return; 941 default: 942 sink(name, null, true); 943 } 944 } 945 946 /++ 947 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 948 949 History: 950 Added November 25, 2021 (dub v10.5) 951 `Point` overload added January 12, 2022 (dub v10.6) 952 +/ 953 int scaleWithDpi(int value, int assumedDpi = 96) { 954 // avoid potential overflow with common special values 955 if(value == int.max) 956 return int.max; 957 if(value == int.min) 958 return int.min; 959 if(value == 0) 960 return 0; 961 return value * currentDpi(assumedDpi) / assumedDpi; 962 } 963 964 /// ditto 965 Point scaleWithDpi(Point value, int assumedDpi = 96) { 966 return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi)); 967 } 968 969 /++ 970 Returns the current scaling factor as a logical dpi value for this widget. Generally speaking, this divided by 96 gives you the user scaling factor. 971 972 Not entirely stable. 973 974 History: 975 Added August 25, 2023 (dub v11.1) 976 +/ 977 final int currentDpi(int assumedDpi = 96) { 978 // assert(parentWindow !is null); 979 // assert(parentWindow.win !is null); 980 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 981 //divide = 138; // to test 1.5x 982 // for lower values it is something i don't really want changed anyway since it is an old monitor and you don't want to scale down. 983 // this also covers the case when actualDpi returns 0. 984 if(divide < 96) 985 divide = 96; 986 return divide; 987 } 988 989 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 990 // I'll think up something better eventually 991 992 // FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight. 993 protected final int defaultLineHeight() { 994 auto cs = getComputedStyle(); 995 if(cs.font && !cs.font.isNull) 996 return cs.font.height() * 5 / 4; 997 else 998 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4); 999 } 1000 1001 /++ 1002 1003 History: 1004 Added August 25, 2023 (dub v11.1) 1005 +/ 1006 protected final int defaultTextHeight(int numberOfLines = 1) { 1007 auto cs = getComputedStyle(); 1008 if(cs.font && !cs.font.isNull) 1009 return cs.font.height() * numberOfLines; 1010 else 1011 return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines; 1012 } 1013 1014 protected final int defaultTextWidth(const(char)[] text) { 1015 auto cs = getComputedStyle(); 1016 if(cs.font && !cs.font.isNull) 1017 return cs.font.stringWidth(text); 1018 else 1019 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2); 1020 } 1021 1022 /++ 1023 If `encapsulatedChildren` returns true, it changes the event handling mechanism to act as if events from the child widgets are actually targeted on this widget. 1024 1025 The idea is then you can use child widgets as part of your implementation, but not expose those details through the event system; if someone checks the mouse coordinates and target of the event once it bubbles past you, it will show as it it came from you. 1026 1027 History: 1028 Added May 22, 2021 1029 +/ 1030 protected bool encapsulatedChildren() { 1031 return false; 1032 } 1033 1034 private void privateDpiChanged() { 1035 dpiChanged(); 1036 foreach(child; children) 1037 child.privateDpiChanged(); 1038 } 1039 1040 /++ 1041 Virtual hook to update any caches or fonts you need on the event of a dpi scaling change. 1042 1043 History: 1044 Added January 12, 2022 (dub v10.6) 1045 +/ 1046 protected void dpiChanged() { 1047 1048 } 1049 1050 // Default layout properties { 1051 1052 int minWidth() { return 0; } 1053 int minHeight() { 1054 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 1055 int sum = this.paddingTop + this.paddingBottom; 1056 foreach(child; children) { 1057 if(child.hidden) 1058 continue; 1059 sum += child.minHeight(); 1060 sum += child.marginTop(); 1061 sum += child.marginBottom(); 1062 } 1063 1064 return sum; 1065 } 1066 int maxWidth() { return int.max; } 1067 int maxHeight() { return int.max; } 1068 int widthStretchiness() { return 4; } 1069 int heightStretchiness() { return 4; } 1070 1071 /++ 1072 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 1073 1074 History: 1075 Added June 15, 2021 (dub v10.1) 1076 +/ 1077 int widthShrinkiness() { return 0; } 1078 /// ditto 1079 int heightShrinkiness() { return 0; } 1080 1081 /++ 1082 The initial size of the widget for layout calculations. Default is 0. 1083 1084 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 1085 1086 History: 1087 Added June 15, 2021 (dub v10.1) 1088 +/ 1089 int flexBasisWidth() { return 0; } 1090 /// ditto 1091 int flexBasisHeight() { return 0; } 1092 1093 /++ 1094 Not stable. 1095 1096 Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored. 1097 1098 So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6. 1099 1100 History: 1101 Added January 5, 2023 1102 +/ 1103 Rectangle defaultMargin; 1104 /// ditto 1105 Rectangle defaultPadding; 1106 1107 int marginLeft() { return scaleWithDpi(defaultMargin.left); } 1108 int marginRight() { return scaleWithDpi(defaultMargin.right); } 1109 int marginTop() { return scaleWithDpi(defaultMargin.top); } 1110 int marginBottom() { return scaleWithDpi(defaultMargin.bottom); } 1111 int paddingLeft() { return scaleWithDpi(defaultPadding.left); } 1112 int paddingRight() { return scaleWithDpi(defaultPadding.right); } 1113 int paddingTop() { return scaleWithDpi(defaultPadding.top); } 1114 int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); } 1115 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 1116 1117 private bool recomputeChildLayoutRequired = true; 1118 private static class RecomputeEvent {} 1119 private __gshared rce = new RecomputeEvent(); 1120 protected final void queueRecomputeChildLayout() { 1121 recomputeChildLayoutRequired = true; 1122 1123 if(this.parentWindow) { 1124 auto sw = this.parentWindow.win; 1125 assert(sw !is null); 1126 if(!sw.eventQueued!RecomputeEvent) { 1127 sw.postEvent(rce); 1128 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1129 } 1130 } 1131 1132 } 1133 1134 protected final void recomputeChildLayoutEntry() { 1135 if(recomputeChildLayoutRequired) { 1136 recomputeChildLayout(); 1137 recomputeChildLayoutRequired = false; 1138 redraw(); 1139 } else { 1140 // I still need to check the tree just in case one of them was queued up 1141 // and the event came up here instead of there. 1142 foreach(child; children) 1143 child.recomputeChildLayoutEntry(); 1144 } 1145 } 1146 1147 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 1148 void recomputeChildLayout() { 1149 .recomputeChildLayout!"height"(this); 1150 } 1151 1152 // } 1153 1154 1155 /++ 1156 Returns the style's tag name string this object uses. 1157 1158 The default is to use the typeid() name trimmed down to whatever is after the last dot which is typically the identifier of the class. 1159 1160 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 1161 1162 History: 1163 Added May 10, 2021 1164 +/ 1165 string styleTagName() const { 1166 string n = typeid(this).name; 1167 foreach_reverse(idx, ch; n) 1168 if(ch == '.') { 1169 n = n[idx + 1 .. $]; 1170 break; 1171 } 1172 return n; 1173 } 1174 1175 /// API for the [styleClassList] 1176 static struct ClassList { 1177 private Widget widget; 1178 1179 /// 1180 void add(string s) { 1181 widget.styleClassList_ ~= s; 1182 } 1183 1184 /// 1185 void remove(string s) { 1186 foreach(idx, s1; widget.styleClassList_) 1187 if(s1 == s) { 1188 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 1189 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 1190 widget.styleClassList_.assumeSafeAppend(); 1191 return; 1192 } 1193 } 1194 1195 /// Returns true if it was added, false if it was removed. 1196 bool toggle(string s) { 1197 if(contains(s)) { 1198 remove(s); 1199 return false; 1200 } else { 1201 add(s); 1202 return true; 1203 } 1204 } 1205 1206 /// 1207 bool contains(string s) const { 1208 foreach(s1; widget.styleClassList_) 1209 if(s1 == s) 1210 return true; 1211 return false; 1212 1213 } 1214 } 1215 1216 private string[] styleClassList_; 1217 1218 /++ 1219 Returns a "class list" that can be used by the visual theme's style engine via [VisualTheme.getPropertyString] if it chooses to do something like CSS. 1220 1221 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 1222 1223 History: 1224 Added May 10, 2021 1225 +/ 1226 inout(ClassList) styleClassList() inout { 1227 return cast(inout(ClassList)) ClassList(cast() this); 1228 } 1229 1230 /++ 1231 List of dynamic states made available to the style engine, for cases like CSS pseudo-classes and also used by default paint methods. It is stored in a 64 bit variable attached to the widget that you can update. The style cache is aware of the fact that these can frequently change. 1232 1233 The lower 32 bits are defined here or reserved for future use by the library. You should keep these updated if you reasonably can on custom widgets if they apply to you, but don't use them for a purpose they aren't defined for. 1234 1235 The upper 32 bits are available for your own extensions. 1236 1237 History: 1238 Added May 10, 2021 1239 1240 Examples: 1241 1242 --- 1243 addEventListener((MouseUpEvent ev) { 1244 if(ev.button == MouseButton.left) { 1245 // the first arg is the state to modify, the second arg is what to set it to 1246 setDynamicState(DynamicState.depressed, false); 1247 } 1248 }); 1249 --- 1250 1251 +/ 1252 enum DynamicState : ulong { 1253 focus = (1 << 0), /// the widget currently has the keyboard focus 1254 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 1255 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if no validation has been performed!) 1256 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if no validation has been performed!) 1257 checked = (1 << 4), /// the widget is toggleable and currently toggled on 1258 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 1259 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 1260 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 1261 depressed = (1 << 8), /// the widget is being actively pressed or clicked (compare to css `:active`). Can be combined with hover to visually indicate if a mouse up would result in a click event. 1262 1263 USER_BEGIN = (1UL << 32), 1264 } 1265 1266 // I want to add the primary and cancel styles to buttons at least at some point somehow. 1267 1268 /// ditto 1269 @property ulong dynamicState() { return dynamicState_; } 1270 /// ditto 1271 @property ulong dynamicState(ulong newValue) { 1272 if(dynamicState != newValue) { 1273 auto old = dynamicState_; 1274 dynamicState_ = newValue; 1275 1276 useStyleProperties((scope Widget.Style s) { 1277 if(s.variesWithState(old ^ newValue)) 1278 redraw(); 1279 }); 1280 } 1281 return dynamicState_; 1282 } 1283 1284 /// ditto 1285 void setDynamicState(ulong flags, bool state) { 1286 auto ds = dynamicState_; 1287 if(state) 1288 ds |= flags; 1289 else 1290 ds &= ~flags; 1291 1292 dynamicState = ds; 1293 } 1294 1295 private ulong dynamicState_; 1296 1297 deprecated("Use dynamic styles instead now") { 1298 Color backgroundColor() { return backgroundColor_; } 1299 void backgroundColor(Color c){ this.backgroundColor_ = c; } 1300 1301 MouseCursor cursor() { return GenericCursor.Default; } 1302 } private Color backgroundColor_ = Color.transparent; 1303 1304 1305 /++ 1306 Style properties are defined as an accessory class so they can be referenced and overridden independently, but they are nested so you can refer to them easily by name (e.g. generic `Widget.Style` vs `Button.Style` and such). 1307 1308 It is here so there can be a specificity switch. 1309 1310 See [OverrideStyle] for a helper function to use your own. 1311 1312 History: 1313 Added May 11, 2021 1314 +/ 1315 static class Style/* : StyleProperties*/ { 1316 public Widget widget; // public because the mixin template needs access to it 1317 1318 /++ 1319 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 1320 1321 History: 1322 Added May 11, 2021, but changed on July 2, 2021 to return false by default. You MUST override this if you want declarative hover effects etc to take effect. 1323 +/ 1324 bool variesWithState(ulong dynamicStateFlags) { 1325 version(win32_widgets) { 1326 if(widget.hwnd) 1327 return false; 1328 } 1329 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 1330 } 1331 1332 /// 1333 Color foregroundColor() { 1334 return WidgetPainter.visualTheme.foregroundColor; 1335 } 1336 1337 /// 1338 WidgetBackground background() { 1339 // the default is a "transparent" background, which means 1340 // it goes as far up as it can to get the color 1341 if (widget.backgroundColor_ != Color.transparent) 1342 return WidgetBackground(widget.backgroundColor_); 1343 if (widget.parent) 1344 return widget.parent.getComputedStyle.background; 1345 return WidgetBackground(widget.backgroundColor_); 1346 } 1347 1348 private static OperatingSystemFont fontCached_; 1349 private OperatingSystemFont fontCached() { 1350 if(fontCached_ is null) 1351 fontCached_ = font(); 1352 return fontCached_; 1353 } 1354 1355 /++ 1356 Returns the default font to be used with this widget. The return value will be cached by the library, so you can not expect live updates. 1357 +/ 1358 OperatingSystemFont font() { 1359 return null; 1360 } 1361 1362 /++ 1363 Returns the cursor that should be used over this widget. You may change this and updates will be reflected next time the mouse enters the widget. 1364 1365 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 1366 1367 History: 1368 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 1369 +/ 1370 MouseCursor cursor() { 1371 return GenericCursor.Default; 1372 } 1373 1374 FrameStyle borderStyle() { 1375 return FrameStyle.none; 1376 } 1377 1378 /++ 1379 +/ 1380 Color borderColor() { 1381 return Color.transparent; 1382 } 1383 1384 FrameStyle outlineStyle() { 1385 if(widget.dynamicState & DynamicState.focus) 1386 return FrameStyle.dotted; 1387 else 1388 return FrameStyle.none; 1389 } 1390 1391 Color outlineColor() { 1392 return foregroundColor; 1393 } 1394 } 1395 1396 /++ 1397 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 1398 The basic usage is simple: 1399 1400 --- 1401 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 1402 // override style hints as-needed here 1403 } 1404 OverrideStyle!Style; // add the method 1405 --- 1406 1407 $(TIP 1408 While the class is not forced to be `static`, for best results, it should be. A non-static class 1409 can not be inherited by other objects whereas the static one can. A property on the base class, 1410 called [Widget.Style.widget|widget], is available for you to access its properties. 1411 ) 1412 1413 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 1414 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 1415 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 1416 1417 1418 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 1419 You may also just override `variesWithState` when you use this flag. 1420 1421 --- 1422 mixin OverrideStyle!( 1423 DynamicState.focus, YourFocusedStyle, 1424 DynamicState.hover, YourHoverStyle, 1425 YourDefaultStyle 1426 ) 1427 --- 1428 1429 It checks if `dynamicState` matches the state and if so, returns the object given. 1430 1431 If there is no state mask given, the next one matches everything. The first match given is used. 1432 1433 However, since in most cases you'll want check state inside your individual methods, you probably won't 1434 find much use for this whole-class swap out. 1435 1436 History: 1437 Added May 16, 2021 1438 +/ 1439 static protected mixin template OverrideStyle(S...) { 1440 static import amg = arsd.minigui; 1441 override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) { 1442 ulong mask = 0; 1443 foreach(idx, thing; S) { 1444 static if(is(typeof(thing) : ulong)) { 1445 mask = thing; 1446 } else { 1447 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 1448 //static assert(!__traits(isNested, thing), thing.stringof ~ " is a nested class. For best results, mark it `static`. You can still access the widget through a `widget` variable inside the Style class."); 1449 scope amg.Widget.Style s = new thing(); 1450 s.widget = this; 1451 dg(s); 1452 return; 1453 } 1454 } 1455 } 1456 } 1457 } 1458 /++ 1459 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 1460 +/ 1461 void useStyleProperties(scope void delegate(scope Style props) dg) { 1462 scope Style s = new Style(); 1463 s.widget = this; 1464 dg(s); 1465 } 1466 1467 1468 protected void sendResizeEvent() { 1469 this.emit!ResizeEvent(); 1470 } 1471 1472 /++ 1473 Override this to provide a custom context menu for your widget. (x, y) is where the menu was requested. If x == -1 && y == -1, the menu was triggered by the keyboard instead of the mouse and it should use the current cursor, selection, or whatever would make sense for where a keyboard user's attention would currently be. 1474 1475 It should return an instance of the [Menu] object. You may choose to cache this object. To construct one, either make `new Menu("", this);` (the empty string there is the menu's label, but for a context menu, that is not important), then call the `menu.addItem(new Action("Label Text", 0 /* icon id */, () { on clicked handler }), menu);` and `menu.addSeparator() methods, or use `return createContextMenuFromAnnotatedCode(this, some_command_struct);` 1476 1477 Context menus are automatically triggered by default by the keyboard menu key, mouse right click, and possibly other conventions per platform. You can also invoke one by calling the [showContextMenu] method. 1478 1479 See_Also: 1480 [createContextMenuFromAnnotatedCode] 1481 +/ 1482 Menu contextMenu(int x, int y) { return null; } 1483 1484 /++ 1485 Shows the widget's context menu, as if the user right clicked at the x, y position. You should rarely, if ever, have to call this, since default event handlers will do it for you automatically. To control what menu shows up, override [contextMenu] instead. 1486 +/ 1487 final bool showContextMenu(int x, int y) { 1488 return showContextMenu(x, y, -2, -2); 1489 } 1490 1491 private final bool showContextMenu(int x, int y, int screenX, int screenY) { 1492 if(parentWindow is null || parentWindow.win is null) return false; 1493 1494 auto menu = this.contextMenu(x, y); 1495 if(menu is null) 1496 return false; 1497 1498 version(win32_widgets) { 1499 // FIXME: if it is -1, -1, do it at the current selection location instead 1500 // tho the corner of the window, which it does now, isn't the literal worst. 1501 1502 // i see notepad just seems to put it in the center of the window so idk 1503 1504 if(screenX < 0 && screenY < 0) { 1505 auto p = this.globalCoordinates(); 1506 if(screenX == -2) 1507 p.x += x; 1508 if(screenY == -2) 1509 p.y += y; 1510 1511 screenX = p.x; 1512 screenY = p.y; 1513 } 1514 1515 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1516 throw new Exception("TrackContextMenuEx"); 1517 } else version(custom_widgets) { 1518 menu.popup(this, x, y); 1519 } 1520 1521 return true; 1522 } 1523 1524 /++ 1525 Removes this widget from its parent. 1526 1527 History: 1528 `removeWidget` was made `final` on May 11, 2021. 1529 +/ 1530 @scriptable 1531 final void removeWidget() { 1532 auto p = this.parent; 1533 if(p) { 1534 int item; 1535 for(item = 0; item < p._children.length; item++) 1536 if(p._children[item] is this) 1537 break; 1538 auto idx = item; 1539 for(; item < p._children.length - 1; item++) 1540 p._children[item] = p._children[item + 1]; 1541 p._children = p._children[0 .. $-1]; 1542 1543 this.parent.widgetRemoved(idx, this); 1544 //this.parent = null; 1545 1546 p.queueRecomputeChildLayout(); 1547 } 1548 version(win32_widgets) { 1549 removeAllChildren(); 1550 if(hwnd) { 1551 DestroyWindow(hwnd); 1552 hwnd = null; 1553 } 1554 } 1555 } 1556 1557 /++ 1558 Notifies the subclass that a widget was removed. If you keep auxillary data about your children, you can override this to help keep that data in sync. 1559 1560 History: 1561 Added September 19, 2021 1562 +/ 1563 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 1564 1565 /++ 1566 Removes all child widgets from `this`. You should not use the removed widgets again. 1567 1568 Note that on Windows, it also destroys the native handles for the removed children recursively. 1569 1570 History: 1571 Added July 1, 2021 (dub v10.2) 1572 +/ 1573 void removeAllChildren() { 1574 version(win32_widgets) 1575 foreach(child; _children) { 1576 child.removeAllChildren(); 1577 if(child.hwnd) { 1578 DestroyWindow(child.hwnd); 1579 child.hwnd = null; 1580 } 1581 } 1582 auto orig = this._children; 1583 this._children = null; 1584 foreach(idx, w; orig) 1585 this.widgetRemoved(idx, w); 1586 1587 queueRecomputeChildLayout(); 1588 } 1589 1590 /++ 1591 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1592 +/ 1593 @scriptable 1594 Widget getChildByName(string name) { 1595 return getByName(name); 1596 } 1597 /++ 1598 Finds the nearest descendant with the requested type and [name]. May return `this`. 1599 +/ 1600 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1601 if(this.name == name) 1602 if(auto c = cast(WidgetClass) this) 1603 return c; 1604 foreach(child; children) { 1605 auto w = child.getByName(name); 1606 if(auto c = cast(WidgetClass) w) 1607 return c; 1608 } 1609 return null; 1610 } 1611 1612 /++ 1613 The name is a string tag that is used to reference the widget from scripts, gui loaders, declarative ui templates, etc. Similar to a HTML id attribute. 1614 Names should be unique in a window. 1615 1616 See_Also: [getByName], [getChildByName] 1617 +/ 1618 @scriptable string name; 1619 1620 private EventHandler[][string] bubblingEventHandlers; 1621 private EventHandler[][string] capturingEventHandlers; 1622 1623 /++ 1624 Default event handlers. These are called on the appropriate 1625 event unless [Event.preventDefault] is called on the event at 1626 some point through the bubbling process. 1627 1628 1629 If you are implementing your own widget and want to add custom 1630 events, you should follow the same pattern here: create a virtual 1631 function named `defaultEventHandler_eventname` with the implementation, 1632 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1633 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1634 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1635 This ensures virtual dispatch based on the correct subclass. 1636 1637 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1638 overridden version. 1639 1640 You only need to do that on parent classes adding NEW event types. If you 1641 just want to change the default behavior of an existing event type in a subclass, 1642 you override the function (and optionally call `super.method_name`) like normal. 1643 1644 History: 1645 Some of the events changed to take specific subclasses instead of generic `Event` 1646 on January 3, 2025. 1647 1648 +/ 1649 protected EventHandler[string] defaultEventHandlers; 1650 1651 /// ditto 1652 void setupDefaultEventHandlers() { 1653 defaultEventHandlers["click"] = (Widget t, Event event) { if(auto e = cast(ClickEvent) event) t.defaultEventHandler_click(e); }; 1654 defaultEventHandlers["dblclick"] = (Widget t, Event event) { if(auto e = cast(DoubleClickEvent) event) t.defaultEventHandler_dblclick(e); }; 1655 defaultEventHandlers["keydown"] = (Widget t, Event event) { if(auto e = cast(KeyDownEvent) event) t.defaultEventHandler_keydown(e); }; 1656 defaultEventHandlers["keyup"] = (Widget t, Event event) { if(auto e = cast(KeyUpEvent) event) t.defaultEventHandler_keyup(e); }; 1657 defaultEventHandlers["mouseover"] = (Widget t, Event event) { if(auto e = cast(MouseOverEvent) event) t.defaultEventHandler_mouseover(e); }; 1658 defaultEventHandlers["mouseout"] = (Widget t, Event event) { if(auto e = cast(MouseOutEvent) event) t.defaultEventHandler_mouseout(e); }; 1659 defaultEventHandlers["mousedown"] = (Widget t, Event event) { if(auto e = cast(MouseDownEvent) event) t.defaultEventHandler_mousedown(e); }; 1660 defaultEventHandlers["mouseup"] = (Widget t, Event event) { if(auto e = cast(MouseUpEvent) event) t.defaultEventHandler_mouseup(e); }; 1661 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { if(auto e = cast(MouseEnterEvent) event) t.defaultEventHandler_mouseenter(e); }; 1662 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { if(auto e = cast(MouseLeaveEvent) event) t.defaultEventHandler_mouseleave(e); }; 1663 defaultEventHandlers["mousemove"] = (Widget t, Event event) { if(auto e = cast(MouseMoveEvent) event) t.defaultEventHandler_mousemove(e); }; 1664 defaultEventHandlers["char"] = (Widget t, Event event) { if(auto e = cast(CharEvent) event) t.defaultEventHandler_char(e); }; 1665 defaultEventHandlers["triggered"] = (Widget t, Event event) { if(auto e = cast(Event) event) t.defaultEventHandler_triggered(e); }; 1666 defaultEventHandlers["change"] = (Widget t, Event event) { if(auto e = cast(ChangeEventBase) event) t.defaultEventHandler_change(e); }; 1667 defaultEventHandlers["focus"] = (Widget t, Event event) { if(auto e = cast(FocusEvent) event) t.defaultEventHandler_focus(e); }; 1668 defaultEventHandlers["blur"] = (Widget t, Event event) { if(auto e = cast(BlurEvent) event) t.defaultEventHandler_blur(e); }; 1669 defaultEventHandlers["focusin"] = (Widget t, Event event) { if(auto e = cast(FocusInEvent) event) t.defaultEventHandler_focusin(e); }; 1670 defaultEventHandlers["focusout"] = (Widget t, Event event) { if(auto e = cast(FocusOutEvent) event) t.defaultEventHandler_focusout(e); }; 1671 } 1672 1673 /// ditto 1674 void defaultEventHandler_click(ClickEvent event) {} 1675 /// ditto 1676 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1677 /// ditto 1678 void defaultEventHandler_keydown(KeyDownEvent event) {} 1679 /// ditto 1680 void defaultEventHandler_keyup(KeyUpEvent event) {} 1681 /// ditto 1682 void defaultEventHandler_mousedown(MouseDownEvent event) { 1683 if(event.button == MouseButton.left) { 1684 if(this.tabStop) { 1685 this.focus(); 1686 } 1687 } else if(event.button == MouseButton.right) { 1688 showContextMenu(event.clientX, event.clientY); 1689 } 1690 } 1691 /// ditto 1692 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1693 /// ditto 1694 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1695 /// ditto 1696 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1697 /// ditto 1698 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1699 /// ditto 1700 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1701 /// ditto 1702 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1703 /// ditto 1704 void defaultEventHandler_char(CharEvent event) {} 1705 /// ditto 1706 void defaultEventHandler_triggered(Event event) {} 1707 /// ditto 1708 void defaultEventHandler_change(ChangeEventBase event) {} 1709 /// ditto 1710 void defaultEventHandler_focus(FocusEvent event) {} 1711 /// ditto 1712 void defaultEventHandler_blur(BlurEvent event) {} 1713 /// ditto 1714 void defaultEventHandler_focusin(FocusInEvent event) {} 1715 /// ditto 1716 void defaultEventHandler_focusout(FocusOutEvent event) {} 1717 1718 /++ 1719 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1720 1721 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1722 1723 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1724 of participating in handler delegation. 1725 1726 $(TIP 1727 Use `scope` on your handlers when you can. While it currently does nothing, this will future-proof your code against future optimizations I want to do. Instead of copying whole event objects out if you do need to store them, just copy the properties you need. 1728 ) 1729 +/ 1730 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1731 return addEventListener(event, (Widget, scope Event e) { 1732 if(e.srcElement is this) 1733 handler(); 1734 }, useCapture); 1735 } 1736 1737 /// ditto 1738 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1739 return addEventListener(event, (Widget, Event e) { 1740 if(e.srcElement is this) 1741 handler(e); 1742 }, useCapture); 1743 } 1744 1745 /// ditto 1746 EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) { 1747 static if(is(Handler Fn == delegate)) { 1748 static if(is(Fn Params == __parameters)) { 1749 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1750 if(e.srcElement !is this) 1751 return; 1752 auto ty = cast(Params[0]) e; 1753 if(ty !is null) 1754 handler(ty); 1755 }, useCapture); 1756 } else static assert(0); 1757 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1758 } 1759 1760 /// ditto 1761 @scriptable 1762 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1763 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1764 } 1765 1766 /// ditto 1767 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1768 static if(is(Handler Fn == delegate)) { 1769 static if(is(Fn Params == __parameters)) { 1770 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1771 auto ty = cast(Params[0]) e; 1772 if(ty !is null) 1773 handler(ty); 1774 }, useCapture); 1775 } else static assert(0); 1776 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1777 } 1778 1779 /// ditto 1780 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1781 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1782 } 1783 1784 /// ditto 1785 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1786 if(event.length > 2 && event[0..2] == "on") 1787 event = event[2 .. $]; 1788 1789 if(useCapture) 1790 capturingEventHandlers[event] ~= handler; 1791 else 1792 bubblingEventHandlers[event] ~= handler; 1793 1794 return EventListener(this, event, handler, useCapture); 1795 } 1796 1797 /// ditto 1798 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1799 if(event.length > 2 && event[0..2] == "on") 1800 event = event[2 .. $]; 1801 1802 if(useCapture) { 1803 if(event in capturingEventHandlers) 1804 foreach(ref evt; capturingEventHandlers[event]) 1805 if(evt is handler) evt = null; 1806 } else { 1807 if(event in bubblingEventHandlers) 1808 foreach(ref evt; bubblingEventHandlers[event]) 1809 if(evt is handler) evt = null; 1810 } 1811 } 1812 1813 /// ditto 1814 void removeEventListener(EventListener listener) { 1815 removeEventListener(listener.event, listener.handler, listener.useCapture); 1816 } 1817 1818 static if(UsingSimpledisplayX11) { 1819 void discardXConnectionState() { 1820 foreach(child; children) 1821 child.discardXConnectionState(); 1822 } 1823 1824 void recreateXConnectionState() { 1825 foreach(child; children) 1826 child.recreateXConnectionState(); 1827 redraw(); 1828 } 1829 } 1830 1831 /++ 1832 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1833 1834 History: 1835 `globalCoordinates` was made `final` on May 11, 2021. 1836 +/ 1837 Point globalCoordinates() { 1838 int x = this.x; 1839 int y = this.y; 1840 auto p = this.parent; 1841 while(p) { 1842 x += p.x; 1843 y += p.y; 1844 p = p.parent; 1845 } 1846 1847 static if(UsingSimpledisplayX11) { 1848 auto dpy = XDisplayConnection.get; 1849 arsd.simpledisplay.Window dummyw; 1850 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1851 } else version(Windows) { 1852 POINT pt; 1853 pt.x = x; 1854 pt.y = y; 1855 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1856 x = pt.x; 1857 y = pt.y; 1858 } else { 1859 featureNotImplemented(); 1860 } 1861 1862 return Point(x, y); 1863 } 1864 1865 version(win32_widgets) 1866 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1867 1868 version(win32_widgets) 1869 /// Called when a WM_COMMAND is sent to the associated hwnd. 1870 void handleWmCommand(ushort cmd, ushort id) {} 1871 1872 version(win32_widgets) 1873 /++ 1874 Called when a WM_NOTIFY is sent to the associated hwnd. 1875 1876 History: 1877 +/ 1878 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1879 1880 version(win32_widgets) 1881 deprecated("This overload is problematic since it is liable to discard return values. Add the `out int mustReturn` to your override as the last parameter and set it to 1 when you must forward the return value to Windows. Otherwise, you can just add the parameter then ignore it and use the default value of 0 to maintain the status quo.") int handleWmNotify(NMHDR* hdr, int code) { int ignored; return handleWmNotify(hdr, code, ignored); } 1882 1883 /++ 1884 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1885 1886 Updates to this variable will only be made visible on the next mouse enter event. 1887 +/ 1888 @scriptable string statusTip; 1889 // string toolTip; 1890 // string helpText; 1891 1892 /++ 1893 If true, this widget can be focused via keyboard control with the tab key. 1894 1895 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1896 +/ 1897 bool tabStop = true; 1898 /++ 1899 The tab key cycles through widgets by the order of a.tabOrder < b.tabOrder. If they are equal, it does them in child order (which is typically the order they were added to the widget.) 1900 +/ 1901 int tabOrder; 1902 1903 version(win32_widgets) { 1904 static Widget[HWND] nativeMapping; 1905 /// The native handle, if there is one. 1906 HWND hwnd; 1907 WNDPROC originalWindowProcedure; 1908 1909 SimpleWindow simpleWindowWrappingHwnd; 1910 1911 // please note it IGNORES your return value and does NOT forward it to Windows! 1912 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1913 return 0; 1914 } 1915 } 1916 private bool implicitlyCreated; 1917 1918 /// Child's position relative to the parent's origin. only the layout manager should be modifying this and even reading it is of limited utility. It may be made `private` at some point in the future without advance notice. Do NOT depend on it being available unless you are writing a layout manager. 1919 int x; 1920 /// ditto 1921 int y; 1922 private int _width; 1923 private int _height; 1924 private Widget[] _children; 1925 private Widget _parent; 1926 private Window _parentWindow; 1927 1928 /++ 1929 Returns the window to which this widget is attached. 1930 1931 History: 1932 Prior to May 11, 2021, the `Window parentWindow` variable was directly available. Now, only this property getter is available and the actual store is private. 1933 +/ 1934 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1935 private @property void parentWindow(Window parent) { 1936 auto old = _parentWindow; 1937 _parentWindow = parent; 1938 newParentWindow(old, _parentWindow); 1939 foreach(child; children) 1940 child.parentWindow = parent; // please note that this is recursive 1941 } 1942 1943 /++ 1944 Called when the widget has been added to or remove from a parent window. 1945 1946 Note that either oldParent and/or newParent may be null any time this is called. 1947 1948 History: 1949 Added September 13, 2024 1950 +/ 1951 protected void newParentWindow(Window oldParent, Window newParent) {} 1952 1953 /++ 1954 Returns the list of the widget's children. 1955 1956 History: 1957 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1958 1959 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1960 +/ 1961 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1962 1963 /++ 1964 Returns the widget's parent. 1965 1966 History: 1967 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1968 1969 The parent should only be managed by the [addChild] and [removeWidget] method. 1970 +/ 1971 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1972 1973 /// The widget's current size. 1974 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1975 /// ditto 1976 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1977 1978 /// Only the layout manager should be calling these. 1979 final protected @property int width(int a) @safe { return _width = a; } 1980 /// ditto 1981 final protected @property int height(int a) @safe { return _height = a; } 1982 1983 /++ 1984 This function is called by the layout engine after it has updated the position (in variables `x` and `y`) and the size (in properties `width` and `height`) to give you a chance to update the actual position of the native child window (if there is one) or whatever. 1985 1986 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 1987 +/ 1988 protected void registerMovement() { 1989 version(win32_widgets) { 1990 if(hwnd) { 1991 auto pos = getChildPositionRelativeToParentHwnd(this); 1992 MoveWindow(hwnd, pos[0], pos[1], width, height, true); // setting this to false can sometimes speed things up but only if it is actually drawn later and that's kinda iffy to do right here so being slower but safer rn 1993 this.redraw(); 1994 } 1995 } 1996 sendResizeEvent(); 1997 } 1998 1999 /// Creates the widget and adds it to the parent. 2000 this(Widget parent) { 2001 if(parent !is null) 2002 parent.addChild(this); 2003 setupDefaultEventHandlers(); 2004 } 2005 2006 /// Returns true if this is the current focused widget inside the parent window. Please note it may return `true` when the window itself is unfocused. In that case, it indicates this widget will receive focuse again when the window does. 2007 @scriptable 2008 bool isFocused() { 2009 return parentWindow && parentWindow.focusedWidget is this; 2010 } 2011 2012 private bool showing_ = true; 2013 /// 2014 bool showing() const { return showing_; } 2015 /// 2016 bool hidden() const { return !showing_; } 2017 /++ 2018 Shows or hides the window. Meant to be assigned as a property. If `recalculate` is true (the default), it recalculates the layout of the parent widget to use the space this widget being hidden frees up or make space for this widget to appear again. 2019 2020 Note that a widget only ever shows if all its parents are showing too. 2021 +/ 2022 void showing(bool s, bool recalculate = true) { 2023 if(s != showing_) { 2024 showing_ = s; 2025 // writeln(typeid(this).toString, " ", this.parent ? typeid(this.parent).toString : "null", " ", s); 2026 2027 showNativeWindowChildren(s); 2028 2029 if(parent && recalculate) { 2030 parent.queueRecomputeChildLayout(); 2031 parent.redraw(); 2032 } 2033 2034 if(s) { 2035 queueRecomputeChildLayout(); 2036 redraw(); 2037 } 2038 } 2039 } 2040 /// Convenience method for `showing = true` 2041 @scriptable 2042 void show() { 2043 showing = true; 2044 } 2045 /// Convenience method for `showing = false` 2046 @scriptable 2047 void hide() { 2048 showing = false; 2049 } 2050 2051 /++ 2052 If you are a native window, show/hide it based on shouldShow and return `true`. 2053 2054 Otherwise, do nothing and return false. 2055 +/ 2056 protected bool showOrHideIfNativeWindow(bool shouldShow) { 2057 version(win32_widgets) { 2058 if(hwnd) { 2059 ShowWindow(hwnd, shouldShow ? SW_SHOW : SW_HIDE); 2060 return true; 2061 } else { 2062 return false; 2063 } 2064 } else { 2065 return false; 2066 } 2067 } 2068 2069 private void showNativeWindowChildren(bool s) { 2070 if(!showOrHideIfNativeWindow(s && showing)) 2071 foreach(child; children) 2072 child.showNativeWindowChildren(s); 2073 } 2074 2075 /// 2076 @scriptable 2077 void focus() { 2078 assert(parentWindow !is null); 2079 if(isFocused()) 2080 return; 2081 2082 if(parentWindow.focusedWidget) { 2083 // FIXME: more details here? like from and to 2084 auto from = parentWindow.focusedWidget; 2085 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 2086 parentWindow.focusedWidget = null; 2087 from.emit!BlurEvent(); 2088 from.emit!FocusOutEvent(); 2089 } 2090 2091 2092 version(win32_widgets) { 2093 if(this.hwnd !is null) 2094 SetFocus(this.hwnd); 2095 } 2096 //else static if(UsingSimpledisplayX11) 2097 //this.parentWindow.win.focus(); 2098 2099 parentWindow.focusedWidget = this; 2100 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 2101 this.emit!FocusEvent(); 2102 this.emit!FocusInEvent(); 2103 } 2104 2105 /+ 2106 /++ 2107 Unfocuses the widget. This may reset 2108 +/ 2109 @scriptable 2110 void blur() { 2111 2112 } 2113 +/ 2114 2115 2116 /++ 2117 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 2118 2119 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 2120 +/ 2121 void attachedToWindow(Window w) {} 2122 /++ 2123 Callback when the widget is added to another widget. 2124 2125 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 2126 +/ 2127 void addedTo(Widget w) {} 2128 2129 /++ 2130 Adds a child to the given position. This is `protected` because you generally shouldn't be calling this directly. Instead, construct widgets with the parent directly. 2131 2132 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 2133 +/ 2134 protected void addChild(Widget w, int position = int.max) { 2135 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 2136 assert(w !is this, "Child cannot be its own parent!"); 2137 w._parent = this; 2138 if(position == int.max || position == children.length) { 2139 _children ~= w; 2140 } else { 2141 assert(position < _children.length); 2142 _children.length = _children.length + 1; 2143 for(int i = cast(int) _children.length - 1; i > position; i--) 2144 _children[i] = _children[i - 1]; 2145 _children[position] = w; 2146 } 2147 2148 this.parentWindow = this._parentWindow; 2149 2150 w.addedTo(this); 2151 2152 bool parentIsNative; 2153 version(win32_widgets) { 2154 parentIsNative = hwnd !is null; 2155 } 2156 if(!parentIsNative && !showing) 2157 w.showOrHideIfNativeWindow(false); 2158 2159 if(parentWindow !is null) { 2160 w.attachedToWindow(parentWindow); 2161 parentWindow.queueRecomputeChildLayout(); 2162 parentWindow.redraw(); 2163 } 2164 } 2165 2166 /++ 2167 Finds the child at the top of the z-order at the given coordinates (relative to the `this` widget's origin), or null if none are found. 2168 +/ 2169 Widget getChildAtPosition(int x, int y) { 2170 // it goes backward so the last one to show gets picked first 2171 // might use z-index later 2172 foreach_reverse(child; children) { 2173 if(child.hidden) 2174 continue; 2175 if(child.x <= x && child.y <= y 2176 && ((x - child.x) < child.width) 2177 && ((y - child.y) < child.height)) 2178 { 2179 return child; 2180 } 2181 } 2182 2183 return null; 2184 } 2185 2186 /++ 2187 If the widget is a scrollable container, this should add the current scroll position to the given coordinates so the mouse events can be dispatched correctly. 2188 2189 History: 2190 Added July 2, 2021 (v10.2) 2191 +/ 2192 protected void addScrollPosition(ref int x, ref int y) {} 2193 2194 /++ 2195 Responsible for actually painting the widget to the screen. The clip rectangle and coordinate translation in the [WidgetPainter] are pre-configured so you can draw independently. 2196 2197 This function paints the entire widget, including styled borders, backgrounds, etc. You are also responsible for displaying any important active state to the user, including if you hold the active keyboard focus. If you only want to be responsible for the content while letting the style engine draw the rest, override [paintContent] instead. 2198 2199 [paint] is not called for system widgets as the OS library draws them instead. 2200 2201 2202 The default implementation forwards to [WidgetPainter.drawThemed], passing [paintContent] as the delegate. If you override this, you might use those same functions or you can do your own thing. 2203 2204 You should also look at [WidgetPainter.visualTheme] to be theme aware. 2205 2206 History: 2207 Prior to May 15, 2021, the default implementation was empty. Now, it is `painter.drawThemed(&paintContent);`. You may wish to override [paintContent] instead of [paint] to take advantage of the new styling engine. 2208 +/ 2209 void paint(WidgetPainter painter) { 2210 version(win32_widgets) 2211 if(hwnd) { 2212 return; 2213 } 2214 painter.drawThemed(&paintContent); // note this refers to the following overload 2215 } 2216 2217 /++ 2218 Responsible for drawing the content as the theme engine is responsible for other elements. 2219 2220 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 2221 2222 Params: 2223 painter = your painter (forwarded from [paint]) for drawing on the widget. The clip rectangle and coordinate translation are prepared for you ahead of time so you can use widget coordinates. It also has the theme foreground preloaded into the painter outline color, the theme font preloaded as the painter's active font, and the theme background preloaded as the painter's fill color. 2224 2225 bounds = the bounds, inside the widget, where your content should be drawn. This is the rectangle inside the border and padding (if any). The stuff outside is not clipped - it is still part of your widget - but you should respect these bounds for visual consistency and respecting the theme's area. 2226 2227 If you do want to clip it, you can of course call `auto oldClip = painter.setClipRectangle(bounds); scope(exit) painter.setClipRectangle(oldClip);` to modify it and return to the previous setting when you return. 2228 2229 Returns: 2230 The rectangle representing your actual content. Typically, this is simply `return bounds;`. The theme engine uses this return value to determine where the outline and overlay should be. 2231 2232 History: 2233 Added May 15, 2021 2234 +/ 2235 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2236 return bounds; 2237 } 2238 2239 deprecated("Change ScreenPainter to WidgetPainter") 2240 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 2241 2242 /// I don't actually like the name of this 2243 /// this draws a background on it 2244 void erase(WidgetPainter painter) { 2245 version(win32_widgets) 2246 if(hwnd) return; // Windows will do it. I think. 2247 2248 auto c = getComputedStyle().background.color; 2249 painter.fillColor = c; 2250 painter.outlineColor = c; 2251 2252 version(win32_widgets) { 2253 HANDLE b, p; 2254 if(c.a == 0 && parent is parentWindow) { 2255 // I don't remember why I had this really... 2256 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 2257 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 2258 } 2259 } 2260 painter.drawRectangle(Point(0, 0), width, height); 2261 version(win32_widgets) { 2262 if(c.a == 0 && parent is parentWindow) { 2263 SelectObject(painter.impl.hdc, p); 2264 SelectObject(painter.impl.hdc, b); 2265 } 2266 } 2267 } 2268 2269 /// 2270 WidgetPainter draw() { 2271 int x = this.x, y = this.y; 2272 auto parent = this.parent; 2273 while(parent) { 2274 x += parent.x; 2275 y += parent.y; 2276 parent = parent.parent; 2277 } 2278 2279 auto painter = parentWindow.win.draw(true); 2280 painter.originX = x; 2281 painter.originY = y; 2282 painter.setClipRectangle(Point(0, 0), width, height); 2283 return WidgetPainter(painter, this); 2284 } 2285 2286 /// This can be overridden by scroll things. It is responsible for actually calling [paint]. Do not override unless you've studied minigui.d's source code. There are no stability guarantees if you do override this; it can (and likely will) break without notice. 2287 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 2288 if(hidden) 2289 return; 2290 2291 int paintX = x; 2292 int paintY = y; 2293 if(this.useNativeDrawing()) { 2294 paintX = 0; 2295 paintY = 0; 2296 lox = 0; 2297 loy = 0; 2298 containment = Rectangle(0, 0, int.max, int.max); 2299 } 2300 2301 painter.originX = lox + paintX; 2302 painter.originY = loy + paintY; 2303 2304 bool actuallyPainted = false; 2305 2306 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 2307 if(clip == Rectangle.init) { 2308 // writeln(this, " clipped out"); 2309 return; 2310 } 2311 2312 bool invalidateChildren = invalidate; 2313 2314 if(redrawRequested || force) { 2315 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 2316 2317 painter.drawingUpon = this; 2318 2319 erase(painter); 2320 if(painter.visualTheme) 2321 painter.visualTheme.doPaint(this, painter); 2322 else 2323 paint(painter); 2324 2325 if(invalidate) { 2326 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 2327 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 2328 painter.invalidateRect(region); 2329 // children are contained inside this, so no need to do extra work 2330 invalidateChildren = false; 2331 } 2332 2333 redrawRequested = false; 2334 actuallyPainted = true; 2335 } 2336 2337 foreach(child; children) { 2338 version(win32_widgets) 2339 if(child.useNativeDrawing()) continue; 2340 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 2341 } 2342 2343 version(win32_widgets) 2344 foreach(child; children) { 2345 if(child.useNativeDrawing) { 2346 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 2347 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, true); // have to reset the invalidate flag since these are not necessarily affected the same way, being native children with a clip 2348 } 2349 } 2350 } 2351 2352 protected bool useNativeDrawing() nothrow { 2353 version(win32_widgets) 2354 return hwnd !is null; 2355 else 2356 return false; 2357 } 2358 2359 private static class RedrawEvent {} 2360 private __gshared re = new RedrawEvent(); 2361 2362 private bool redrawRequested; 2363 /// 2364 final void redraw(string file = __FILE__, size_t line = __LINE__) { 2365 redrawRequested = true; 2366 2367 if(this.parentWindow) { 2368 auto sw = this.parentWindow.win; 2369 assert(sw !is null); 2370 if(!sw.eventQueued!RedrawEvent) { 2371 sw.postEvent(re); 2372 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 2373 } 2374 } 2375 } 2376 2377 private SimpleWindow drawableWindow; 2378 2379 /++ 2380 Allows a class to easily dispatch its own statically-declared event (see [Emits]). The main benefit of using this over constructing an event yourself is simply that you ensure you haven't sent something you haven't documented you can send. 2381 2382 Returns: 2383 `true` if you should do your default behavior. 2384 2385 History: 2386 Added May 5, 2021 2387 2388 Bugs: 2389 It does not do the static checks on gdc right now. 2390 +/ 2391 final protected bool emit(EventType, this This, Args...)(Args args) { 2392 version(GNU) {} else 2393 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 2394 auto e = new EventType(this, args); 2395 e.dispatch(); 2396 return !e.defaultPrevented; 2397 } 2398 /// ditto 2399 final protected bool emit(string eventString, this This)() { 2400 auto e = new Event(eventString, this); 2401 e.dispatch(); 2402 return !e.defaultPrevented; 2403 } 2404 2405 /++ 2406 Does the same as [addEventListener]'s delegate overload, but adds an additional check to ensure the event you are subscribing to is actually emitted by the static type you are using. Since it works on static types, if you have a generic [Widget], this can only subscribe to events declared as [Emits] inside [Widget] itself, not any child classes nor any child elements. If this is too restrictive, simply use [addEventListener] instead. 2407 2408 History: 2409 Added May 5, 2021 2410 +/ 2411 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 2412 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 2413 return addEventListener(handler); 2414 } 2415 2416 /++ 2417 Gets the computed style properties from the visual theme. 2418 2419 You should use this in your paint and layout functions instead of the direct properties on the widget if you want to be style aware. (But when setting defaults in your classes, overriding is the right thing to do. Override to set defaults, but then read out of [getComputedStyle].) 2420 2421 History: 2422 Added May 8, 2021 2423 +/ 2424 final StyleInformation getComputedStyle() { 2425 return StyleInformation(this); 2426 } 2427 2428 int focusableWidgets(scope int delegate(Widget) dg) { 2429 foreach(widget; WidgetStream(this)) { 2430 if(widget.tabStop && !widget.hidden) { 2431 int result = dg(widget); 2432 if (result) 2433 return result; 2434 } 2435 } 2436 return 0; 2437 } 2438 2439 /++ 2440 Calculates the border box (that is, the full width/height of the widget, from border edge to border edge) 2441 for the given content box (the area between the padding) 2442 2443 History: 2444 Added January 4, 2023 (dub v11.0) 2445 +/ 2446 Rectangle borderBoxForContentBox(Rectangle contentBox) { 2447 auto cs = getComputedStyle(); 2448 2449 auto borderWidth = getBorderWidth(cs.borderStyle); 2450 2451 auto rect = contentBox; 2452 2453 rect.left -= borderWidth; 2454 rect.right += borderWidth; 2455 rect.top -= borderWidth; 2456 rect.bottom += borderWidth; 2457 2458 auto insideBorderRect = rect; 2459 2460 rect.left -= cs.paddingLeft; 2461 rect.right += cs.paddingRight; 2462 rect.top -= cs.paddingTop; 2463 rect.bottom += cs.paddingBottom; 2464 2465 return rect; 2466 } 2467 2468 2469 // FIXME: I kinda want to hide events from implementation widgets 2470 // so it just catches them all and stops propagation... 2471 // i guess i can do it with a event listener on star. 2472 2473 mixin Emits!KeyDownEvent; /// 2474 mixin Emits!KeyUpEvent; /// 2475 mixin Emits!CharEvent; /// 2476 2477 mixin Emits!MouseDownEvent; /// 2478 mixin Emits!MouseUpEvent; /// 2479 mixin Emits!ClickEvent; /// 2480 mixin Emits!DoubleClickEvent; /// 2481 mixin Emits!MouseMoveEvent; /// 2482 mixin Emits!MouseOverEvent; /// 2483 mixin Emits!MouseOutEvent; /// 2484 mixin Emits!MouseEnterEvent; /// 2485 mixin Emits!MouseLeaveEvent; /// 2486 2487 mixin Emits!ResizeEvent; /// 2488 2489 mixin Emits!BlurEvent; /// 2490 mixin Emits!FocusEvent; /// 2491 2492 mixin Emits!FocusInEvent; /// 2493 mixin Emits!FocusOutEvent; /// 2494 } 2495 2496 /+ 2497 /++ 2498 Interface to indicate that the widget has a simple value property. 2499 2500 History: 2501 Added August 26, 2021 2502 +/ 2503 interface HasValue!T { 2504 /// Getter 2505 @property T value(); 2506 /// Setter 2507 @property void value(T); 2508 } 2509 2510 /++ 2511 Interface to indicate that the widget has a range of possible values for its simple value property. 2512 This would be present on something like a slider or possibly a number picker. 2513 2514 History: 2515 Added September 11, 2021 2516 +/ 2517 interface HasRangeOfValues!T : HasValue!T { 2518 /// The minimum and maximum values in the range, inclusive. 2519 @property T minValue(); 2520 @property void minValue(T); /// ditto 2521 @property T maxValue(); /// ditto 2522 @property void maxValue(T); /// ditto 2523 2524 /// The smallest step the user interface allows. User may still type in values without this limitation. 2525 @property void step(T); 2526 @property T step(); /// ditto 2527 } 2528 2529 /++ 2530 Interface to indicate that the widget has a list of possible values the user can choose from. 2531 This would be present on something like a drop-down selector. 2532 2533 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2534 combobox. 2535 2536 History: 2537 Added September 11, 2021 2538 +/ 2539 interface HasListOfValues!T : HasValue!T { 2540 @property T[] values; 2541 @property void values(T[]); 2542 2543 @property int selectedIndex(); // note it may return -1! 2544 @property void selectedIndex(int); 2545 } 2546 +/ 2547 2548 /++ 2549 History: 2550 Added September 2021 (dub v10.4) 2551 +/ 2552 class GridLayout : Layout { 2553 2554 // FIXME: grid padding around edges and also cell spacing between units. even though you could do that by just specifying some gutter yourself in the layout. 2555 2556 /++ 2557 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2558 +/ 2559 enum Gravity { 2560 Center = 0, 2561 NorthWest = North | West, 2562 North = 0b10_00, 2563 NorthEast = North | East, 2564 West = 0b00_10, 2565 East = 0b00_01, 2566 SouthWest = South | West, 2567 South = 0b01_00, 2568 SouthEast = South | East, 2569 } 2570 2571 /++ 2572 The width and height are in some proportional units and can often just be 12. 2573 +/ 2574 this(int width, int height, Widget parent) { 2575 this.gridWidth = width; 2576 this.gridHeight = height; 2577 super(parent); 2578 } 2579 2580 /++ 2581 Sets the position of the given child. 2582 2583 The units of these arguments are in the proportional grid units you set in the constructor. 2584 +/ 2585 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2586 // ensure it is in bounds 2587 // then ensure no overlaps 2588 2589 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2590 2591 foreach(ref position; positions) { 2592 if(position.widget is child) { 2593 position = p; 2594 goto set; 2595 } 2596 } 2597 2598 positions ~= p; 2599 2600 set: 2601 2602 // FIXME: should this batch? 2603 queueRecomputeChildLayout(); 2604 2605 return child; 2606 } 2607 2608 override void addChild(Widget w, int position = int.max) { 2609 super.addChild(w, position); 2610 //positions ~= ChildPosition(w); 2611 if(position != int.max) { 2612 // FIXME: align it so they actually match. 2613 } 2614 } 2615 2616 override void widgetRemoved(size_t idx, Widget w) { 2617 // FIXME: keep the positions array aligned 2618 // positions[idx].widget = null; 2619 } 2620 2621 override void recomputeChildLayout() { 2622 registerMovement(); 2623 int onGrid = cast(int) positions.length; 2624 c: foreach(child; children) { 2625 // just snap it to the grid 2626 if(onGrid) 2627 foreach(position; positions) 2628 if(position.widget is child) { 2629 child.x = this.width * position.x / this.gridWidth; 2630 child.y = this.height * position.y / this.gridHeight; 2631 child.width = this.width * position.width / this.gridWidth; 2632 child.height = this.height * position.height / this.gridHeight; 2633 2634 auto diff = child.width - child.maxWidth(); 2635 // FIXME: gravity? 2636 if(diff > 0) { 2637 child.width = child.width - diff; 2638 2639 if(position.gravity & Gravity.West) { 2640 // nothing needed, already aligned 2641 } else if(position.gravity & Gravity.East) { 2642 child.x += diff; 2643 } else { 2644 child.x += diff / 2; 2645 } 2646 } 2647 2648 diff = child.height - child.maxHeight(); 2649 // FIXME: gravity? 2650 if(diff > 0) { 2651 child.height = child.height - diff; 2652 2653 if(position.gravity & Gravity.North) { 2654 // nothing needed, already aligned 2655 } else if(position.gravity & Gravity.South) { 2656 child.y += diff; 2657 } else { 2658 child.y += diff / 2; 2659 } 2660 } 2661 child.recomputeChildLayout(); 2662 onGrid--; 2663 continue c; 2664 } 2665 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2666 } 2667 } 2668 2669 private struct ChildPosition { 2670 Widget widget; 2671 int x; 2672 int y; 2673 int width; 2674 int height; 2675 Gravity gravity; 2676 } 2677 private ChildPosition[] positions; 2678 2679 int gridWidth = 12; 2680 int gridHeight = 12; 2681 } 2682 2683 /// 2684 abstract class ComboboxBase : Widget { 2685 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2686 // or to always show the list, we want CBS_SIMPLE == 1 2687 version(win32_widgets) 2688 this(uint style, Widget parent) { 2689 super(parent); 2690 createWin32Window(this, "ComboBox"w, null, style); 2691 } 2692 else version(custom_widgets) 2693 this(Widget parent) { 2694 super(parent); 2695 2696 addEventListener((KeyDownEvent event) { 2697 if(event.key == Key.Up) { 2698 setSelection(selection_-1); 2699 event.preventDefault(); 2700 } 2701 if(event.key == Key.Down) { 2702 setSelection(selection_+1); 2703 event.preventDefault(); 2704 } 2705 2706 }); 2707 2708 } 2709 else static assert(false); 2710 2711 protected void scrollSelectionIntoView() {} 2712 2713 /++ 2714 Returns the current list of options in the selection. 2715 2716 History: 2717 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2718 +/ 2719 final @property string[] options() const { 2720 return cast(string[]) options_; 2721 } 2722 2723 /++ 2724 Replaces the list of options in the box. Note that calling this will also reset the selection. 2725 2726 History: 2727 Added December, 29 2024 2728 +/ 2729 final @property void options(string[] options) { 2730 version(win32_widgets) 2731 SendMessageW(hwnd, 331 /*CB_RESETCONTENT*/, 0, 0); 2732 selection_ = -1; 2733 options_ = null; 2734 foreach(opt; options) 2735 addOption(opt); 2736 2737 version(custom_widgets) 2738 redraw(); 2739 } 2740 2741 private string[] options_; 2742 private int selection_ = -1; 2743 2744 /++ 2745 Adds an option to the end of options array. 2746 +/ 2747 void addOption(string s) { 2748 options_ ~= s; 2749 version(win32_widgets) 2750 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2751 } 2752 2753 /++ 2754 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2755 +/ 2756 int getSelection() { 2757 return selection_; 2758 } 2759 2760 /++ 2761 Returns the current selection as a string. 2762 2763 History: 2764 Added November 17, 2021 2765 +/ 2766 string getSelectionString() { 2767 return selection_ == -1 ? null : options[selection_]; 2768 } 2769 2770 /++ 2771 Sets the current selection to an index in the options array, or to the given option if present. 2772 Please note that the string version may do a linear lookup. 2773 2774 Returns: 2775 the index you passed in 2776 2777 History: 2778 The `string` based overload was added on March 1, 2022 (dub v10.7). 2779 2780 The return value was `void` prior to March 1, 2022. 2781 +/ 2782 int setSelection(int idx) { 2783 if(idx < -1) 2784 idx = -1; 2785 if(idx + 1 > options.length) 2786 idx = cast(int) options.length - 1; 2787 2788 selection_ = idx; 2789 2790 version(win32_widgets) 2791 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2792 2793 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2794 t.dispatch(); 2795 2796 scrollSelectionIntoView(); 2797 2798 return idx; 2799 } 2800 2801 /// ditto 2802 int setSelection(string s) { 2803 if(s !is null) 2804 foreach(idx, item; options) 2805 if(item == s) { 2806 return setSelection(cast(int) idx); 2807 } 2808 return setSelection(-1); 2809 } 2810 2811 /++ 2812 This event is fired when the selection changes. Both [Event.stringValue] and 2813 [Event.intValue] are filled in - `stringValue` is the text in the selection 2814 and `intValue` is the index of the selection. If the combo box allows multiple 2815 selection, these values will include only one of the selected items - for those, 2816 you should loop through the values and check their selected flag instead. 2817 2818 (I know that sucks, but it is how it is right now.) 2819 2820 History: 2821 It originally inherited from `ChangeEvent!String`, but now does from [ChangeEventBase] as of January 3, 2025. 2822 This shouldn't break anything if you used it through either its own name `SelectionChangedEvent` or through the 2823 base `Event`, only if you specifically used `ChangeEvent!string` - those handlers may now get `null` or fail to 2824 be called. If you did do this, just change it to generic `Event`, as `stringValue` and `intValue` are already there. 2825 +/ 2826 static final class SelectionChangedEvent : ChangeEventBase { 2827 this(Widget target, int iv, string sv) { 2828 super(target); 2829 this.iv = iv; 2830 this.sv = sv; 2831 } 2832 immutable int iv; 2833 immutable string sv; 2834 2835 deprecated("Use stringValue or intValue instead") @property string value() { 2836 return sv; 2837 } 2838 2839 override @property string stringValue() { return sv; } 2840 override @property int intValue() { return iv; } 2841 } 2842 2843 version(win32_widgets) 2844 override void handleWmCommand(ushort cmd, ushort id) { 2845 if(cmd == CBN_SELCHANGE) { 2846 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2847 fireChangeEvent(); 2848 } 2849 } 2850 2851 private void fireChangeEvent() { 2852 if(selection_ >= options.length) 2853 selection_ = -1; 2854 2855 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2856 t.dispatch(); 2857 } 2858 2859 override int minWidth() { return scaleWithDpi(32); } 2860 2861 version(win32_widgets) { 2862 override int minHeight() { return defaultLineHeight + 6; } 2863 override int maxHeight() { return defaultLineHeight + 6; } 2864 } else { 2865 override int minHeight() { return defaultLineHeight + 4; } 2866 override int maxHeight() { return defaultLineHeight + 4; } 2867 } 2868 2869 version(custom_widgets) 2870 void popup() { 2871 CustomComboBoxPopup popup = new CustomComboBoxPopup(this); 2872 } 2873 2874 } 2875 2876 private class CustomComboBoxPopup : Window { 2877 private ComboboxBase associatedWidget; 2878 private ListWidget lw; 2879 private bool cancelled; 2880 2881 this(ComboboxBase associatedWidget) { 2882 this.associatedWidget = associatedWidget; 2883 2884 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2885 2886 auto w = associatedWidget.width; 2887 // FIXME: suggestedDropdownHeight see below 2888 auto h = cast(int) associatedWidget.options.length * associatedWidget.defaultLineHeight + associatedWidget.scaleWithDpi(8); 2889 2890 // FIXME: this sux 2891 if(h > associatedWidget.parentWindow.height) 2892 h = associatedWidget.parentWindow.height; 2893 2894 auto mh = associatedWidget.scaleWithDpi(16 + 16 + 32); // to make the scrollbar look ok 2895 if(h < mh) 2896 h = mh; 2897 2898 auto coord = associatedWidget.globalCoordinates(); 2899 auto dropDown = new SimpleWindow( 2900 w, h, 2901 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, associatedWidget.parentWindow ? associatedWidget.parentWindow.win : null); 2902 2903 super(dropDown); 2904 2905 dropDown.move(coord.x, coord.y + associatedWidget.height); 2906 2907 this.lw = new ListWidget(this); 2908 version(custom_widgets) 2909 lw.multiSelect = false; 2910 foreach(option; associatedWidget.options) 2911 lw.addOption(option); 2912 2913 auto originalSelection = associatedWidget.getSelection; 2914 lw.setSelection(originalSelection); 2915 lw.scrollSelectionIntoView(); 2916 2917 /+ 2918 { 2919 auto cs = getComputedStyle(); 2920 auto painter = dropDown.draw(); 2921 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2922 auto p = Point(4, 4); 2923 painter.outlineColor = cs.foregroundColor; 2924 foreach(option; associatedWidget.options) { 2925 painter.drawText(p, option); 2926 p.y += defaultLineHeight; 2927 } 2928 } 2929 2930 dropDown.setEventHandlers( 2931 (MouseEvent event) { 2932 if(event.type == MouseEventType.buttonReleased) { 2933 dropDown.close(); 2934 auto element = (event.y - 4) / defaultLineHeight; 2935 if(element >= 0 && element <= associatedWidget.options.length) { 2936 associatedWidget.selection_ = element; 2937 2938 associatedWidget.fireChangeEvent(); 2939 } 2940 } 2941 } 2942 ); 2943 +/ 2944 2945 Widget previouslyFocusedWidget; 2946 2947 dropDown.visibilityChanged = (bool visible) { 2948 if(visible) { 2949 this.redraw(); 2950 captureMouse(this); 2951 //dropDown.grabInput(); 2952 2953 if(previouslyFocusedWidget is null) 2954 previouslyFocusedWidget = associatedWidget.parentWindow.focusedWidget; 2955 associatedWidget.parentWindow.focusedWidget = lw; 2956 } else { 2957 //dropDown.releaseInputGrab(); 2958 releaseMouseCapture(); 2959 2960 if(!cancelled) 2961 associatedWidget.setSelection(lw.getSelection); 2962 2963 associatedWidget.parentWindow.focusedWidget = previouslyFocusedWidget; 2964 } 2965 }; 2966 2967 dropDown.show(); 2968 } 2969 2970 private bool shouldCloseIfClicked(Widget w) { 2971 if(w is this) 2972 return true; 2973 version(custom_widgets) 2974 if(cast(TextListViewWidget.TextListViewItem) w) 2975 return true; 2976 return false; 2977 } 2978 2979 override void defaultEventHandler_click(ClickEvent ce) { 2980 if(ce.button == MouseButton.left && shouldCloseIfClicked(ce.target)) { 2981 this.win.close(); 2982 } 2983 } 2984 2985 override void defaultEventHandler_char(CharEvent ce) { 2986 if(ce.character == '\n') 2987 this.win.close(); 2988 } 2989 2990 override void defaultEventHandler_keydown(KeyDownEvent kde) { 2991 if(kde.key == Key.Escape) { 2992 cancelled = true; 2993 this.win.close(); 2994 }/+ else if(kde.key == Key.Up || kde.key == Key.Down) 2995 {} // intentionally blank, the list view handles these 2996 // separately from the scroll message widget default handler 2997 else if(lw && lw.glvw && lw.glvw.smw) 2998 lw.glvw.smw.defaultKeyboardListener(kde);+/ 2999 } 3000 } 3001 3002 /++ 3003 A drop-down list where the user must select one of the 3004 given options. Like `<select>` in HTML. 3005 3006 The current selection is given as a string or an index. 3007 It emits a SelectionChangedEvent when it changes. 3008 +/ 3009 class DropDownSelection : ComboboxBase { 3010 /++ 3011 Creates a drop down selection, optionally passing its initial list of options. 3012 3013 History: 3014 The overload with the `options` parameter was added December 29, 2024. 3015 +/ 3016 this(Widget parent) { 3017 version(win32_widgets) 3018 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 3019 else version(custom_widgets) { 3020 super(parent); 3021 3022 addEventListener("focus", () { this.redraw; }); 3023 addEventListener("blur", () { this.redraw; }); 3024 addEventListener(EventType.change, () { this.redraw; }); 3025 addEventListener("mousedown", () { this.focus(); this.popup(); }); 3026 addEventListener((KeyDownEvent event) { 3027 if(event.key == Key.Space) 3028 popup(); 3029 }); 3030 } else static assert(false); 3031 } 3032 3033 /// ditto 3034 this(string[] options, Widget parent) { 3035 this(parent); 3036 this.options = options; 3037 } 3038 3039 mixin Padding!q{2}; 3040 static class Style : Widget.Style { 3041 override FrameStyle borderStyle() { return FrameStyle.risen; } 3042 } 3043 mixin OverrideStyle!Style; 3044 3045 version(custom_widgets) 3046 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 3047 auto cs = getComputedStyle(); 3048 3049 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 3050 3051 painter.outlineColor = cs.foregroundColor; 3052 painter.fillColor = cs.foregroundColor; 3053 3054 /+ 3055 Point[4] triangle; 3056 enum padding = 6; 3057 enum paddingV = 7; 3058 enum triangleWidth = 10; 3059 triangle[0] = Point(width - padding - triangleWidth, paddingV); 3060 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 3061 triangle[2] = Point(width - padding - 0, paddingV); 3062 triangle[3] = triangle[0]; 3063 painter.drawPolygon(triangle[]); 3064 +/ 3065 3066 auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2); 3067 3068 painter.drawPolygon( 3069 scaleWithDpi(Point(2, 6) + offset), 3070 scaleWithDpi(Point(7, 11) + offset), 3071 scaleWithDpi(Point(12, 6) + offset), 3072 scaleWithDpi(Point(2, 6) + offset) 3073 ); 3074 3075 3076 return bounds; 3077 } 3078 3079 version(win32_widgets) 3080 override void registerMovement() { 3081 version(win32_widgets) { 3082 if(hwnd) { 3083 auto pos = getChildPositionRelativeToParentHwnd(this); 3084 // the height given to this from Windows' perspective is supposed 3085 // to include the drop down's height. so I add to it to give some 3086 // room for that. 3087 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 3088 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 3089 } 3090 } 3091 sendResizeEvent(); 3092 } 3093 } 3094 3095 /++ 3096 A text box with a drop down arrow listing selections. 3097 The user can choose from the list, or type their own. 3098 +/ 3099 class FreeEntrySelection : ComboboxBase { 3100 this(Widget parent) { 3101 this(null, parent); 3102 } 3103 3104 this(string[] options, Widget parent) { 3105 version(win32_widgets) 3106 super(2 /* CBS_DROPDOWN */, parent); 3107 else version(custom_widgets) { 3108 super(parent); 3109 auto hl = new HorizontalLayout(this); 3110 lineEdit = new LineEdit(hl); 3111 3112 tabStop = false; 3113 3114 // lineEdit.addEventListener((FocusEvent fe) { lineEdit.selectAll(); } ); 3115 3116 auto btn = new class ArrowButton { 3117 this() { 3118 super(ArrowDirection.down, hl); 3119 } 3120 override int heightStretchiness() { 3121 return 1; 3122 } 3123 override int heightShrinkiness() { 3124 return 1; 3125 } 3126 override int maxHeight() { 3127 return lineEdit.maxHeight; 3128 } 3129 }; 3130 //btn.addDirectEventListener("focus", &lineEdit.focus); 3131 btn.addEventListener("triggered", &this.popup); 3132 addEventListener(EventType.change, (Event event) { 3133 lineEdit.content = event.stringValue; 3134 lineEdit.focus(); 3135 redraw(); 3136 }); 3137 } 3138 else static assert(false); 3139 3140 this.options = options; 3141 } 3142 3143 string content() { 3144 version(win32_widgets) 3145 assert(0, "not implemented"); 3146 else version(custom_widgets) 3147 return lineEdit.content; 3148 else static assert(0); 3149 } 3150 3151 void content(string s) { 3152 version(win32_widgets) 3153 assert(0, "not implemented"); 3154 else version(custom_widgets) 3155 lineEdit.content = s; 3156 else static assert(0); 3157 } 3158 3159 version(custom_widgets) { 3160 LineEdit lineEdit; 3161 3162 override int widthStretchiness() { 3163 return lineEdit ? lineEdit.widthStretchiness : super.widthStretchiness; 3164 } 3165 override int flexBasisWidth() { 3166 return lineEdit ? lineEdit.flexBasisWidth : super.flexBasisWidth; 3167 } 3168 } 3169 } 3170 3171 /++ 3172 A combination of free entry with a list below it. 3173 +/ 3174 class ComboBox : ComboboxBase { 3175 this(Widget parent) { 3176 version(win32_widgets) 3177 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 3178 else version(custom_widgets) { 3179 super(parent); 3180 lineEdit = new LineEdit(this); 3181 listWidget = new ListWidget(this); 3182 listWidget.multiSelect = false; 3183 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 3184 string c = null; 3185 foreach(option; listWidget.options) 3186 if(option.selected) { 3187 c = option.label; 3188 break; 3189 } 3190 lineEdit.content = c; 3191 }); 3192 3193 listWidget.tabStop = false; 3194 this.tabStop = false; 3195 listWidget.addEventListener("focusin", &lineEdit.focus); 3196 this.addEventListener("focusin", &lineEdit.focus); 3197 3198 addDirectEventListener(EventType.change, { 3199 listWidget.setSelection(selection_); 3200 if(selection_ != -1) 3201 lineEdit.content = options[selection_]; 3202 lineEdit.focus(); 3203 redraw(); 3204 }); 3205 3206 lineEdit.addEventListener("focusin", &lineEdit.selectAll); 3207 3208 listWidget.addDirectEventListener(EventType.change, { 3209 int set = -1; 3210 foreach(idx, opt; listWidget.options) 3211 if(opt.selected) { 3212 set = cast(int) idx; 3213 break; 3214 } 3215 if(set != selection_) 3216 this.setSelection(set); 3217 }); 3218 } else static assert(false); 3219 } 3220 3221 override int minHeight() { return defaultLineHeight * 3; } 3222 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 3223 override int heightStretchiness() { return 5; } 3224 3225 version(custom_widgets) { 3226 LineEdit lineEdit; 3227 ListWidget listWidget; 3228 3229 override void addOption(string s) { 3230 listWidget.addOption(s); 3231 ComboboxBase.addOption(s); 3232 } 3233 3234 override void scrollSelectionIntoView() { 3235 listWidget.scrollSelectionIntoView(); 3236 } 3237 } 3238 } 3239 3240 /+ 3241 class Spinner : Widget { 3242 version(win32_widgets) 3243 this(Widget parent) { 3244 super(parent); 3245 parentWindow = parent.parentWindow; 3246 auto hlayout = new HorizontalLayout(this); 3247 lineEdit = new LineEdit(hlayout); 3248 upDownControl = new UpDownControl(hlayout); 3249 } 3250 3251 LineEdit lineEdit; 3252 UpDownControl upDownControl; 3253 } 3254 3255 class UpDownControl : Widget { 3256 version(win32_widgets) 3257 this(Widget parent) { 3258 super(parent); 3259 parentWindow = parent.parentWindow; 3260 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 3261 } 3262 3263 override int minHeight() { return defaultLineHeight; } 3264 override int maxHeight() { return defaultLineHeight * 3/2; } 3265 3266 override int minWidth() { return defaultLineHeight * 3/2; } 3267 override int maxWidth() { return defaultLineHeight * 3/2; } 3268 } 3269 +/ 3270 3271 /+ 3272 class DataView : Widget { 3273 // this is the omnibus data viewer 3274 // the internal data layout is something like: 3275 // string[string][] but also each node can have parents 3276 } 3277 +/ 3278 3279 3280 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 3281 3282 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 3283 3284 // FIXME: menus should prolly capture the mouse. ugh i kno. 3285 /* 3286 TextEdit needs: 3287 3288 * caret manipulation 3289 * selection control 3290 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 3291 3292 For example: 3293 3294 connect(paste, &textEdit.insertTextAtCaret); 3295 3296 would be nice. 3297 3298 3299 3300 I kinda want an omnibus dataview that combines list, tree, 3301 and table - it can be switched dynamically between them. 3302 3303 Flattening policy: only show top level, show recursive, show grouped 3304 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 3305 3306 Single select, multi select, organization, drag+drop 3307 */ 3308 3309 //static if(UsingSimpledisplayX11) 3310 version(win32_widgets) {} 3311 else version(custom_widgets) { 3312 enum scrollClickRepeatInterval = 50; 3313 3314 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 3315 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 3316 enum activeTabColor = lightAccentColor; 3317 enum hoveringColor = Color(228, 228, 228); 3318 enum buttonColor = windowBackgroundColor; 3319 enum depressedButtonColor = darkAccentColor; 3320 enum activeListXorColor = Color(255, 255, 127); 3321 enum progressBarColor = Color(0, 0, 128); 3322 enum activeMenuItemColor = Color(0, 0, 128); 3323 3324 }} 3325 else static assert(false); 3326 deprecated("Get these properties off the `visualTheme` instead.") { 3327 // these are used by horizontal rule so not just custom_widgets. for now at least. 3328 enum darkAccentColor = Color(172, 172, 172); 3329 enum lightAccentColor = Color(223, 223, 223); // used to be 223 3330 } 3331 3332 private const(wchar)* toWstringzInternal(in char[] s) { 3333 wchar[] str; 3334 str.reserve(s.length + 1); 3335 foreach(dchar ch; s) 3336 str ~= ch; 3337 str ~= '\0'; 3338 return str.ptr; 3339 } 3340 3341 static if(SimpledisplayTimerAvailable) 3342 void setClickRepeat(Widget w, int interval, int delay = 250) { 3343 Timer timer; 3344 int delayRemaining = delay / interval; 3345 if(delayRemaining <= 1) 3346 delayRemaining = 2; 3347 3348 immutable originalDelayRemaining = delayRemaining; 3349 3350 w.addDirectEventListener((scope MouseDownEvent ev) { 3351 if(ev.srcElement !is w) 3352 return; 3353 if(timer !is null) { 3354 timer.destroy(); 3355 timer = null; 3356 } 3357 delayRemaining = originalDelayRemaining; 3358 timer = new Timer(interval, () { 3359 if(delayRemaining > 0) 3360 delayRemaining--; 3361 else { 3362 auto ev = new Event("triggered", w); 3363 ev.sendDirectly(); 3364 } 3365 }); 3366 }); 3367 3368 w.addDirectEventListener((scope MouseUpEvent ev) { 3369 if(ev.srcElement !is w) 3370 return; 3371 if(timer !is null) { 3372 timer.destroy(); 3373 timer = null; 3374 } 3375 }); 3376 3377 w.addDirectEventListener((scope MouseLeaveEvent ev) { 3378 if(ev.srcElement !is w) 3379 return; 3380 if(timer !is null) { 3381 timer.destroy(); 3382 timer = null; 3383 } 3384 }); 3385 3386 } 3387 else 3388 void setClickRepeat(Widget w, int interval, int delay = 250) {} 3389 3390 enum FrameStyle { 3391 none, /// 3392 risen, /// a 3d pop-out effect (think Windows 95 button) 3393 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 3394 solid, /// 3395 dotted, /// 3396 fantasy, /// a style based on a popular fantasy video game 3397 rounded, /// a rounded rectangle 3398 } 3399 3400 version(custom_widgets) 3401 deprecated 3402 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 3403 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 3404 } 3405 3406 version(custom_widgets) 3407 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 3408 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 3409 } 3410 3411 version(custom_widgets) 3412 deprecated 3413 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 3414 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 3415 } 3416 3417 int getBorderWidth(FrameStyle style) { 3418 final switch(style) { 3419 case FrameStyle.sunk, FrameStyle.risen: 3420 return 2; 3421 case FrameStyle.none: 3422 return 0; 3423 case FrameStyle.solid: 3424 return 1; 3425 case FrameStyle.dotted: 3426 return 1; 3427 case FrameStyle.fantasy: 3428 return 3; 3429 case FrameStyle.rounded: 3430 return 2; 3431 } 3432 } 3433 3434 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 3435 int borderWidth = getBorderWidth(style); 3436 final switch(style) { 3437 case FrameStyle.sunk, FrameStyle.risen: 3438 // outer layer 3439 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 3440 break; 3441 case FrameStyle.none: 3442 painter.outlineColor = background; 3443 break; 3444 case FrameStyle.solid: 3445 case FrameStyle.rounded: 3446 painter.pen = Pen(border, 1); 3447 break; 3448 case FrameStyle.dotted: 3449 painter.pen = Pen(border, 1, Pen.Style.Dotted); 3450 break; 3451 case FrameStyle.fantasy: 3452 painter.pen = Pen(border, 3); 3453 break; 3454 } 3455 3456 painter.fillColor = background; 3457 3458 if(style == FrameStyle.rounded) { 3459 painter.drawRectangleRounded(Point(x, y), Size(width, height), 6); 3460 } else { 3461 painter.drawRectangle(Point(x + 0, y + 0), width, height); 3462 3463 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 3464 // 3d effect 3465 auto vt = WidgetPainter.visualTheme; 3466 3467 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 3468 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 3469 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 3470 3471 // inner layer 3472 //right, bottom 3473 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 3474 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 3475 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 3476 // left, top 3477 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 3478 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 3479 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 3480 } else if(style == FrameStyle.fantasy) { 3481 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 3482 painter.fillColor = Color.transparent; 3483 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 3484 } 3485 } 3486 3487 return borderWidth; 3488 } 3489 3490 /++ 3491 An `Action` represents some kind of user action they can trigger through menu options, toolbars, hotkeys, and similar mechanisms. The text label, icon, and handlers are centrally held here instead of repeated in each UI element. 3492 3493 See_Also: 3494 [MenuItem] 3495 [ToolButton] 3496 [Menu.addItem] 3497 +/ 3498 class Action { 3499 version(win32_widgets) { 3500 private int id; 3501 private static int lastId = 9000; 3502 private static Action[int] mapping; 3503 } 3504 3505 KeyEvent accelerator; 3506 3507 // FIXME: disable message 3508 // and toggle thing? 3509 // ??? and trigger arguments too ??? 3510 3511 /++ 3512 Params: 3513 label = the textual label 3514 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 3515 triggered = initial handler, more can be added via the [triggered] member. 3516 +/ 3517 this(string label, ushort icon = 0, void delegate() triggered = null) { 3518 this.label = label; 3519 this.iconId = icon; 3520 if(triggered !is null) 3521 this.triggered ~= triggered; 3522 version(win32_widgets) { 3523 id = ++lastId; 3524 mapping[id] = this; 3525 } 3526 } 3527 3528 private string label; 3529 private ushort iconId; 3530 // icon 3531 3532 // when it is triggered, the triggered event is fired on the window 3533 /// The list of handlers when it is triggered. 3534 void delegate()[] triggered; 3535 } 3536 3537 /* 3538 plan: 3539 keyboard accelerators 3540 3541 * menus (and popups and tooltips) 3542 * status bar 3543 * toolbars and buttons 3544 3545 sortable table view 3546 3547 maybe notification area icons 3548 basic clipboard 3549 3550 * radio box 3551 splitter 3552 toggle buttons (optionally mutually exclusive, like in Paint) 3553 label, rich text display, multi line plain text (selectable) 3554 * fieldset 3555 * nestable grid layout 3556 single line text input 3557 * multi line text input 3558 slider 3559 spinner 3560 list box 3561 drop down 3562 combo box 3563 auto complete box 3564 * progress bar 3565 3566 terminal window/widget (on unix it might even be a pty but really idk) 3567 3568 ok button 3569 cancel button 3570 3571 keyboard hotkeys 3572 3573 scroll widget 3574 3575 event redirections and network transparency 3576 script integration 3577 */ 3578 3579 3580 /* 3581 MENUS 3582 3583 auto bar = new MenuBar(window); 3584 window.menuBar = bar; 3585 3586 auto fileMenu = bar.addItem(new Menu("&File")); 3587 fileMenu.addItem(new MenuItem("&Exit")); 3588 3589 3590 EVENTS 3591 3592 For controls, you should usually use "triggered" rather than "click", etc., because 3593 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 3594 This is the case on menus and pushbuttons. 3595 3596 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 3597 */ 3598 3599 3600 /* 3601 enum LinePreference { 3602 AlwaysOnOwnLine, // always on its own line 3603 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 3604 PreferToShareLine, // does not force new line, and if the next child likes to share too, they will div it up evenly. otherwise, it will expand as much as it can 3605 } 3606 */ 3607 3608 /++ 3609 Convenience mixin for overriding all four sides of margin or padding in a [Widget] with the same code. It mixes in the given string as the return value of the four overridden methods. 3610 3611 --- 3612 class MyWidget : Widget { 3613 this(Widget parent) { super(parent); } 3614 3615 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 3616 mixin Padding!q{4}; 3617 3618 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 3619 mixin Margin!q{8}; 3620 3621 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 3622 // while Top/Bottom/Right remain 8 from the mixin above. 3623 override int marginLeft() { return 2; } 3624 } 3625 --- 3626 3627 3628 The minigui layout model is based on the web's CSS box model. The layout engine* arranges widgets based on their margin for separation and assigns them a size based on thier preferences (e.g. [Widget.minHeight]) and the available space. Widgets are assigned a size by the layout engine. Inside this size, they have a border (see [Widget.Style.borderWidth]), then padding space, and then their content. Their content box may also have an outline drawn on top of it (see [Widget.Style.outlineStyle]). 3629 3630 Padding is the area inside a widget where its background is drawn, but the content avoids. 3631 3632 Margin is the area between widgets. The algorithm is the spacing between any two widgets is the max of their adjacent margins (not the sum!). 3633 3634 * Some widgets do not participate in placement, e.g. [StaticPosition], and some layout systems do their own separate thing too; ultimately, these properties are just hints to the layout function and you can always implement your own to do whatever you want. But this statement is still mostly true. 3635 +/ 3636 mixin template Padding(string code) { 3637 override int paddingLeft() { return mixin(code);} 3638 override int paddingRight() { return mixin(code);} 3639 override int paddingTop() { return mixin(code);} 3640 override int paddingBottom() { return mixin(code);} 3641 } 3642 3643 /// ditto 3644 mixin template Margin(string code) { 3645 override int marginLeft() { return mixin(code);} 3646 override int marginRight() { return mixin(code);} 3647 override int marginTop() { return mixin(code);} 3648 override int marginBottom() { return mixin(code);} 3649 } 3650 3651 private 3652 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3653 enum calcingV = relevantMeasure == "height"; 3654 3655 parent.registerMovement(); 3656 3657 if(parent.children.length == 0) 3658 return; 3659 3660 auto parentStyle = parent.getComputedStyle(); 3661 3662 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3663 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3664 3665 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3666 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3667 3668 // my own width and height should already be set by the caller of this function... 3669 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3670 mixin("parentStyle.padding"~firstThingy~"()") - 3671 mixin("parentStyle.padding"~secondThingy~"()"); 3672 3673 int stretchinessSum; 3674 int stretchyChildSum; 3675 int lastMargin = 0; 3676 3677 int shrinkinessSum; 3678 int shrinkyChildSum; 3679 3680 // set initial size 3681 foreach(child; parent.children) { 3682 3683 auto childStyle = child.getComputedStyle(); 3684 3685 if(cast(StaticPosition) child) 3686 continue; 3687 if(child.hidden) 3688 continue; 3689 3690 const iw = child.flexBasisWidth(); 3691 const ih = child.flexBasisHeight(); 3692 3693 static if(calcingV) { 3694 child.width = parent.width - 3695 mixin("childStyle.margin"~otherFirstThingy~"()") - 3696 mixin("childStyle.margin"~otherSecondThingy~"()") - 3697 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3698 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3699 3700 if(child.width < 0) 3701 child.width = 0; 3702 if(child.width > childStyle.maxWidth()) 3703 child.width = childStyle.maxWidth(); 3704 3705 if(iw > 0) { 3706 auto totalPossible = child.width; 3707 if(child.width > iw && child.widthStretchiness() == 0) 3708 child.width = iw; 3709 } 3710 3711 child.height = mymax(childStyle.minHeight(), ih); 3712 } else { 3713 // set to take all the space 3714 child.height = parent.height - 3715 mixin("childStyle.margin"~firstThingy~"()") - 3716 mixin("childStyle.margin"~secondThingy~"()") - 3717 mixin("parentStyle.padding"~firstThingy~"()") - 3718 mixin("parentStyle.padding"~secondThingy~"()"); 3719 3720 // then clamp it 3721 if(child.height < 0) 3722 child.height = 0; 3723 if(child.height > childStyle.maxHeight()) 3724 child.height = childStyle.maxHeight(); 3725 3726 // and if possible, respect the ideal target 3727 if(ih > 0) { 3728 auto totalPossible = child.height; 3729 if(child.height > ih && child.heightStretchiness() == 0) 3730 child.height = ih; 3731 } 3732 3733 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3734 child.width = mymax(childStyle.minWidth(), iw); 3735 } 3736 3737 spaceRemaining -= mixin("child." ~ relevantMeasure); 3738 3739 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3740 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3741 lastMargin = margin; 3742 spaceRemaining -= thisMargin + margin; 3743 3744 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3745 stretchinessSum += s; 3746 if(s > 0) 3747 stretchyChildSum++; 3748 3749 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3750 shrinkinessSum += s2; 3751 if(s2 > 0) 3752 shrinkyChildSum++; 3753 } 3754 3755 if(spaceRemaining < 0 && shrinkyChildSum) { 3756 // shrink to get into the space if it is possible 3757 auto toRemove = -spaceRemaining; 3758 auto removalPerItem = toRemove / shrinkinessSum; 3759 auto remainder = toRemove % shrinkinessSum; 3760 3761 // FIXME: wtf why am i shrinking things with no shrinkiness? 3762 3763 foreach(child; parent.children) { 3764 auto childStyle = child.getComputedStyle(); 3765 if(cast(StaticPosition) child) 3766 continue; 3767 if(child.hidden) 3768 continue; 3769 static if(calcingV) { 3770 auto minimum = childStyle.minHeight(); 3771 auto stretch = childStyle.heightShrinkiness(); 3772 } else { 3773 auto minimum = childStyle.minWidth(); 3774 auto stretch = childStyle.widthShrinkiness(); 3775 } 3776 3777 if(mixin("child._" ~ relevantMeasure) <= minimum) 3778 continue; 3779 // import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch); 3780 3781 mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh. 3782 3783 spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum; 3784 } 3785 } 3786 3787 // stretch to fill space 3788 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3789 auto spacePerChild = spaceRemaining / stretchinessSum; 3790 bool spreadEvenly; 3791 bool giveToBiggest; 3792 if(spacePerChild <= 0) { 3793 spacePerChild = spaceRemaining / stretchyChildSum; 3794 spreadEvenly = true; 3795 } 3796 if(spacePerChild <= 0) { 3797 giveToBiggest = true; 3798 } 3799 int previousSpaceRemaining = spaceRemaining; 3800 stretchinessSum = 0; 3801 Widget mostStretchy; 3802 int mostStretchyS; 3803 foreach(child; parent.children) { 3804 auto childStyle = child.getComputedStyle(); 3805 if(cast(StaticPosition) child) 3806 continue; 3807 if(child.hidden) 3808 continue; 3809 static if(calcingV) { 3810 auto maximum = childStyle.maxHeight(); 3811 } else { 3812 auto maximum = childStyle.maxWidth(); 3813 } 3814 3815 if(mixin("child." ~ relevantMeasure) >= maximum) { 3816 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3817 mixin("child._" ~ relevantMeasure) -= adj; 3818 spaceRemaining += adj; 3819 continue; 3820 } 3821 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3822 if(s <= 0) 3823 continue; 3824 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3825 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3826 spaceRemaining -= spaceAdjustment; 3827 if(mixin("child." ~ relevantMeasure) > maximum) { 3828 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3829 mixin("child._" ~ relevantMeasure) -= diff; 3830 spaceRemaining += diff; 3831 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3832 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3833 if(mostStretchy is null || s >= mostStretchyS) { 3834 mostStretchy = child; 3835 mostStretchyS = s; 3836 } 3837 } 3838 } 3839 3840 if(giveToBiggest && mostStretchy !is null) { 3841 auto child = mostStretchy; 3842 auto childStyle = child.getComputedStyle(); 3843 int spaceAdjustment = spaceRemaining; 3844 3845 static if(calcingV) 3846 auto maximum = childStyle.maxHeight(); 3847 else 3848 auto maximum = childStyle.maxWidth(); 3849 3850 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3851 spaceRemaining -= spaceAdjustment; 3852 if(mixin("child._" ~ relevantMeasure) > maximum) { 3853 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3854 mixin("child._" ~ relevantMeasure) -= diff; 3855 spaceRemaining += diff; 3856 } 3857 } 3858 3859 if(spaceRemaining == previousSpaceRemaining) { 3860 if(mostStretchy !is null) { 3861 static if(calcingV) 3862 auto maximum = mostStretchy.maxHeight(); 3863 else 3864 auto maximum = mostStretchy.maxWidth(); 3865 3866 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3867 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3868 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3869 } 3870 break; // apparently nothing more we can do 3871 } 3872 } 3873 3874 foreach(child; parent.children) { 3875 auto childStyle = child.getComputedStyle(); 3876 if(cast(StaticPosition) child) 3877 continue; 3878 if(child.hidden) 3879 continue; 3880 3881 static if(calcingV) 3882 auto maximum = childStyle.maxHeight(); 3883 else 3884 auto maximum = childStyle.maxWidth(); 3885 if(mixin("child._" ~ relevantMeasure) > maximum) 3886 mixin("child._" ~ relevantMeasure) = maximum; 3887 } 3888 3889 // position 3890 lastMargin = 0; 3891 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3892 foreach(child; parent.children) { 3893 auto childStyle = child.getComputedStyle(); 3894 if(cast(StaticPosition) child) { 3895 child.recomputeChildLayout(); 3896 continue; 3897 } 3898 if(child.hidden) 3899 continue; 3900 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3901 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3902 currentPos += thisMargin; 3903 static if(calcingV) { 3904 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3905 child.y = currentPos; 3906 } else { 3907 child.x = currentPos; 3908 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3909 3910 } 3911 currentPos += mixin("child." ~ relevantMeasure); 3912 currentPos += margin; 3913 lastMargin = margin; 3914 3915 child.recomputeChildLayout(); 3916 } 3917 } 3918 3919 int mymax(int a, int b) { return a > b ? a : b; } 3920 int mymax(int a, int b, int c) { 3921 auto d = mymax(a, b); 3922 return c > d ? c : d; 3923 } 3924 3925 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3926 // and here, it must be integrable with the layout, the event system, and not be painted over. 3927 version(win32_widgets) { 3928 3929 // this function just does stuff that a parent window needs for redirection 3930 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3931 this_.hookedWndProc(msg, wParam, lParam); 3932 3933 switch(msg) { 3934 3935 case WM_VSCROLL, WM_HSCROLL: 3936 auto pos = HIWORD(wParam); 3937 auto m = LOWORD(wParam); 3938 3939 auto scrollbarHwnd = cast(HWND) lParam; 3940 3941 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3942 3943 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3944 3945 switch(m) { 3946 /+ 3947 // I don't think those messages are ever actually sent normally by the widget itself, 3948 // they are more used for the keyboard interface. methinks. 3949 case SB_BOTTOM: 3950 // writeln("end"); 3951 auto event = new Event("scrolltoend", *widgetp); 3952 event.dispatch(); 3953 //if(!event.defaultPrevented) 3954 break; 3955 case SB_TOP: 3956 // writeln("top"); 3957 auto event = new Event("scrolltobeginning", *widgetp); 3958 event.dispatch(); 3959 break; 3960 case SB_ENDSCROLL: 3961 // idk 3962 break; 3963 +/ 3964 case SB_LINEDOWN: 3965 (*widgetp).emitCommand!"scrolltonextline"(); 3966 return 0; 3967 case SB_LINEUP: 3968 (*widgetp).emitCommand!"scrolltopreviousline"(); 3969 return 0; 3970 case SB_PAGEDOWN: 3971 (*widgetp).emitCommand!"scrolltonextpage"(); 3972 return 0; 3973 case SB_PAGEUP: 3974 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3975 return 0; 3976 case SB_THUMBPOSITION: 3977 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3978 ev.dispatch(); 3979 return 0; 3980 case SB_THUMBTRACK: 3981 // eh kinda lying but i like the real time update display 3982 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3983 ev.dispatch(); 3984 3985 // the event loop doesn't seem to carry on with a requested redraw.. 3986 // so we request it to get our dirty bit set... 3987 // then we need to immediately actually redraw it too for instant feedback to user 3988 SimpleWindow.processAllCustomEvents(); 3989 SimpleWindow.processAllCustomEvents(); 3990 //if(this_.parentWindow) 3991 //this_.parentWindow.actualRedraw(); 3992 3993 // and this ensures the WM_PAINT message is sent fairly quickly 3994 // still seems to lag a little in large windows but meh it basically works. 3995 if(this_.parentWindow) { 3996 // FIXME: if painting is slow, this does still lag 3997 // we probably will want to expose some user hook to ScrollWindowEx 3998 // or something. 3999 UpdateWindow(this_.parentWindow.hwnd); 4000 } 4001 return 0; 4002 default: 4003 } 4004 } 4005 break; 4006 4007 case WM_CONTEXTMENU: 4008 auto hwndFrom = cast(HWND) wParam; 4009 4010 auto xPos = cast(short) LOWORD(lParam); 4011 auto yPos = cast(short) HIWORD(lParam); 4012 4013 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 4014 POINT p; 4015 p.x = xPos; 4016 p.y = yPos; 4017 ScreenToClient(hwnd, &p); 4018 auto clientX = cast(ushort) p.x; 4019 auto clientY = cast(ushort) p.y; 4020 4021 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 4022 4023 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 4024 return 0; 4025 } 4026 } 4027 break; 4028 4029 case WM_DRAWITEM: 4030 auto dis = cast(DRAWITEMSTRUCT*) lParam; 4031 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 4032 return (*widgetp).handleWmDrawItem(dis); 4033 } 4034 break; 4035 4036 case WM_NOTIFY: 4037 auto hdr = cast(NMHDR*) lParam; 4038 auto hwndFrom = hdr.hwndFrom; 4039 auto code = hdr.code; 4040 4041 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 4042 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 4043 } 4044 break; 4045 case WM_COMMAND: 4046 auto handle = cast(HWND) lParam; 4047 auto cmd = HIWORD(wParam); 4048 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 4049 4050 default: 4051 // pass it on 4052 } 4053 return 0; 4054 } 4055 4056 4057 4058 extern(Windows) 4059 private 4060 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 4061 // but can i merge them?! 4062 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 4063 // try { writeln(iMessage); } catch(Exception e) {}; 4064 4065 if(auto te = hWnd in Widget.nativeMapping) { 4066 try { 4067 4068 te.hookedWndProc(iMessage, wParam, lParam); 4069 4070 int mustReturn; 4071 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 4072 if(mustReturn) 4073 return ret; 4074 4075 if(iMessage == WM_SETFOCUS) { 4076 auto lol = *te; 4077 while(lol !is null && lol.implicitlyCreated) 4078 lol = lol.parent; 4079 lol.focus(); 4080 //(*te).parentWindow.focusedWidget = lol; 4081 } 4082 4083 4084 if(iMessage == WM_CTLCOLOREDIT) { 4085 4086 } 4087 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 4088 SetBkMode(cast(HDC) wParam, TRANSPARENT); 4089 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 4090 //GetStockObject(NULL_BRUSH); 4091 } 4092 4093 auto pos = getChildPositionRelativeToParentOrigin(*te); 4094 lastDefaultPrevented = false; 4095 // try { writeln(typeid(*te)); } catch(Exception e) {} 4096 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 4097 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 4098 else { 4099 // it was something we recognized, should only call the window procedure if the default was not prevented 4100 } 4101 } catch(Exception e) { 4102 assert(0, e.toString()); 4103 } 4104 return 0; 4105 } 4106 assert(0, "shouldn't be receiving messages for this window...."); 4107 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 4108 } 4109 4110 extern(Windows) 4111 private 4112 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 4113 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 4114 if(iMessage == WM_ERASEBKGND) { 4115 auto dc = GetDC(hWnd); 4116 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 4117 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 4118 RECT r; 4119 GetWindowRect(hWnd, &r); 4120 // since the pen is null, to fill the whole space, we need the +1 on both. 4121 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 4122 SelectObject(dc, p); 4123 SelectObject(dc, b); 4124 ReleaseDC(hWnd, dc); 4125 InvalidateRect(hWnd, null, false); // redraw the border 4126 return 1; 4127 } 4128 return HookedWndProc(hWnd, iMessage, wParam, lParam); 4129 } 4130 4131 /++ 4132 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 4133 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 4134 of minigui's expectations. 4135 4136 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 4137 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 4138 4139 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 4140 4141 To check if you can use this, use `static if(UsingWin32Widgets)`. 4142 +/ 4143 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 4144 assert(p.parentWindow !is null); 4145 assert(p.parentWindow.win.impl.hwnd !is null); 4146 4147 auto bsgroupbox = style == BS_GROUPBOX; 4148 4149 HWND phwnd; 4150 4151 auto wtf = p.parent; 4152 while(wtf) { 4153 if(wtf.hwnd !is null) { 4154 phwnd = wtf.hwnd; 4155 break; 4156 } 4157 wtf = wtf.parent; 4158 } 4159 4160 if(phwnd is null) 4161 phwnd = p.parentWindow.win.impl.hwnd; 4162 4163 assert(phwnd !is null); 4164 4165 WCharzBuffer wt = WCharzBuffer(windowText); 4166 4167 style |= WS_VISIBLE | WS_CHILD; 4168 //if(className != WC_TABCONTROL) 4169 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 4170 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 4171 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 4172 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 4173 4174 assert(p.hwnd !is null); 4175 4176 4177 static HFONT font; 4178 if(font is null) { 4179 NONCLIENTMETRICS params; 4180 params.cbSize = params.sizeof; 4181 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 4182 font = CreateFontIndirect(¶ms.lfMessageFont); 4183 } 4184 } 4185 4186 if(font) 4187 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 4188 4189 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 4190 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 4191 Widget.nativeMapping[p.hwnd] = p; 4192 4193 if(bsgroupbox) 4194 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 4195 else 4196 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4197 4198 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 4199 4200 p.registerMovement(); 4201 } 4202 } 4203 4204 version(win32_widgets) 4205 private 4206 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 4207 if(hwnd is null || hwnd in Widget.nativeMapping) 4208 return true; 4209 auto parent = cast(Widget) cast(void*) lparam; 4210 Widget p = new Widget(null); 4211 p._parent = parent; 4212 p.parentWindow = parent.parentWindow; 4213 p.hwnd = hwnd; 4214 p.implicitlyCreated = true; 4215 Widget.nativeMapping[p.hwnd] = p; 4216 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4217 return true; 4218 } 4219 4220 /++ 4221 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 4222 +/ 4223 struct WidgetPainter { 4224 this(ScreenPainter screenPainter, Widget drawingUpon) { 4225 this.drawingUpon = drawingUpon; 4226 this.screenPainter = screenPainter; 4227 if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 4228 this.screenPainter.setFont(font); 4229 } 4230 4231 /++ 4232 EXPERIMENTAL. subject to change. 4233 4234 When you draw a cursor, you can draw this to notify your window of where it is, 4235 for IME systems to use. 4236 +/ 4237 void notifyCursorPosition(int x, int y, int width, int height) { 4238 if(auto a = drawingUpon.parentWindow) 4239 if(auto w = a.inputProxy) { 4240 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 4241 } 4242 } 4243 4244 4245 /// 4246 ScreenPainter screenPainter; 4247 /// Forward to the screen painter for other methods 4248 alias screenPainter this; 4249 4250 private Widget drawingUpon; 4251 4252 /++ 4253 This is the list of rectangles that actually need to be redrawn. 4254 4255 Not actually implemented yet. 4256 +/ 4257 Rectangle[] invalidatedRectangles; 4258 4259 private static BaseVisualTheme _visualTheme; 4260 4261 /++ 4262 Functions to access the visual theme and helpers to easily use it. 4263 4264 These are aware of the current widget's computed style out of the theme. 4265 +/ 4266 static @property BaseVisualTheme visualTheme() { 4267 if(_visualTheme is null) 4268 _visualTheme = new DefaultVisualTheme(); 4269 return _visualTheme; 4270 } 4271 4272 /// ditto 4273 static @property void visualTheme(BaseVisualTheme theme) { 4274 _visualTheme = theme; 4275 4276 // FIXME: notify all windows about the new theme, they should recompute layout and redraw. 4277 } 4278 4279 /// ditto 4280 Color themeForeground() { 4281 return drawingUpon.getComputedStyle().foregroundColor(); 4282 } 4283 4284 /// ditto 4285 Color themeBackground() { 4286 return drawingUpon.getComputedStyle().background.color; 4287 } 4288 4289 int isDarkTheme() { 4290 return 0; // unspecified, yes, no as enum. FIXME 4291 } 4292 4293 /++ 4294 Draws the general pattern of a widget if you don't need anything particularly special and/or control the other details through your widget's style theme hints. 4295 4296 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 4297 4298 If you change teh clip rectangle, you should change it back before you return. 4299 4300 4301 The sequence it uses is: 4302 background 4303 content (delegated to you) 4304 border 4305 focused outline 4306 selected overlay 4307 4308 Example code: 4309 4310 --- 4311 void paint(WidgetPainter painter) { 4312 painter.drawThemed((bounds) { 4313 return bounds; // if the selection overlay should be contained, you can return it here. 4314 }); 4315 } 4316 --- 4317 +/ 4318 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 4319 drawThemed((WidgetPainter painter, const Rectangle bounds) { 4320 return drawBody(bounds); 4321 }); 4322 } 4323 // this overload is actually mroe for setting the delegate to a virtual function 4324 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 4325 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 4326 4327 auto cs = drawingUpon.getComputedStyle(); 4328 4329 auto bg = cs.background.color; 4330 4331 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 4332 4333 rect.left += borderWidth; 4334 rect.right -= borderWidth; 4335 rect.top += borderWidth; 4336 rect.bottom -= borderWidth; 4337 4338 auto insideBorderRect = rect; 4339 4340 rect.left += cs.paddingLeft; 4341 rect.right -= cs.paddingRight; 4342 rect.top += cs.paddingTop; 4343 rect.bottom -= cs.paddingBottom; 4344 4345 this.outlineColor = this.themeForeground; 4346 this.fillColor = bg; 4347 4348 auto widgetFont = cs.fontCached; 4349 if(widgetFont !is null) 4350 this.setFont(widgetFont); 4351 4352 rect = drawBody(this, rect); 4353 4354 if(widgetFont !is null) { 4355 if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 4356 this.setFont(vtFont); 4357 else 4358 this.setFont(null); 4359 } 4360 4361 if(auto os = cs.outlineStyle()) { 4362 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 4363 this.fillColor = Color.transparent; 4364 this.drawRectangle(insideBorderRect); 4365 } 4366 } 4367 4368 /++ 4369 First, draw the background. 4370 Then draw your content. 4371 Next, draw the border. 4372 And the focused indicator. 4373 And the is-selected box. 4374 4375 If it is focused i can draw the outline too... 4376 4377 If selected i can even do the xor action but that's at the end. 4378 +/ 4379 void drawThemeBackground() { 4380 4381 } 4382 4383 void drawThemeBorder() { 4384 4385 } 4386 4387 // all this stuff is a dangerous experiment.... 4388 static class ScriptableVersion { 4389 ScreenPainterImplementation* p; 4390 int originX, originY; 4391 4392 @scriptable: 4393 void drawRectangle(int x, int y, int width, int height) { 4394 p.drawRectangle(x + originX, y + originY, width, height); 4395 } 4396 void drawLine(int x1, int y1, int x2, int y2) { 4397 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 4398 } 4399 void drawText(int x, int y, string text) { 4400 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 4401 } 4402 void setOutlineColor(int r, int g, int b) { 4403 p.pen = Pen(Color(r,g,b), 1); 4404 } 4405 void setFillColor(int r, int g, int b) { 4406 p.fillColor = Color(r,g,b); 4407 } 4408 } 4409 4410 ScriptableVersion toArsdJsvar() { 4411 auto sv = new ScriptableVersion; 4412 sv.p = this.screenPainter.impl; 4413 sv.originX = this.screenPainter.originX; 4414 sv.originY = this.screenPainter.originY; 4415 return sv; 4416 } 4417 4418 static WidgetPainter fromJsVar(T)(T t) { 4419 return WidgetPainter.init; 4420 } 4421 // done.......... 4422 } 4423 4424 4425 struct Style { 4426 static struct helper(string m, T) { 4427 enum method = m; 4428 T v; 4429 4430 mixin template MethodOverride(typeof(this) v) { 4431 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 4432 } 4433 } 4434 4435 static auto opDispatch(string method, T)(T value) { 4436 return helper!(method, T)(value); 4437 } 4438 } 4439 4440 /++ 4441 Implementation detail of the [ControlledBy] UDA. 4442 4443 History: 4444 Added Oct 28, 2020 4445 +/ 4446 struct ControlledBy_(T, Args...) { 4447 Args args; 4448 4449 static if(Args.length) 4450 this(Args args) { 4451 this.args = args; 4452 } 4453 4454 private T construct(Widget parent) { 4455 return new T(args, parent); 4456 } 4457 } 4458 4459 /++ 4460 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 4461 4462 History: 4463 Added Oct 28, 2020 4464 +/ 4465 auto ControlledBy(T, Args...)(Args args) { 4466 return ControlledBy_!(T, Args)(args); 4467 } 4468 4469 struct ContainerMeta { 4470 string name; 4471 ContainerMeta[] children; 4472 Widget function(Widget parent) factory; 4473 4474 Widget instantiate(Widget parent) { 4475 auto n = factory(parent); 4476 n.name = name; 4477 foreach(child; children) 4478 child.instantiate(n); 4479 return n; 4480 } 4481 } 4482 4483 /++ 4484 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 4485 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 4486 4487 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 4488 structures. It works fine on structs declared inside functions though. 4489 4490 See: https://issues.dlang.org/show_bug.cgi?id=21984 4491 +/ 4492 template Container(CArgs...) { 4493 static if(CArgs.length && is(CArgs[0] : Widget)) { 4494 private alias Super = CArgs[0]; 4495 private alias CArgs2 = CArgs[1 .. $]; 4496 } else { 4497 private alias Super = Layout; 4498 private alias CArgs2 = CArgs; 4499 } 4500 4501 class Container : Super { 4502 this(Widget parent) { super(parent); } 4503 4504 // just to partially support old gdc versions 4505 version(GNU) { 4506 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 4507 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 4508 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 4509 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 4510 } else mixin(q{ 4511 static foreach(Arg; CArgs2) { 4512 mixin Arg.MethodOverride!(Arg); 4513 } 4514 }); 4515 4516 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 4517 return ContainerMeta( 4518 name, 4519 children.dup, 4520 function (Widget parent) { return new typeof(this)(parent); } 4521 ); 4522 } 4523 4524 static ContainerMeta opCall(ContainerMeta[] children...) { 4525 return opCall(null, children); 4526 } 4527 } 4528 } 4529 4530 /++ 4531 The data controller widget is created by reflecting over the given 4532 data type. You can use [ControlledBy] as a UDA on a struct or 4533 just let it create things automatically. 4534 4535 Unlike [dialog], this uses real-time updating of the data and 4536 you add it to another window yourself. 4537 4538 --- 4539 struct Test { 4540 int x; 4541 int y; 4542 } 4543 4544 auto window = new Window(); 4545 auto dcw = new DataControllerWidget!Test(new Test, window); 4546 --- 4547 4548 The way it works is any public members are given a widget based 4549 on their data type, and public methods trigger an action button 4550 if no relevant parameters or a dialog action if it does have 4551 parameters, similar to the [menu] facility. 4552 4553 If you change data programmatically, without going through the 4554 DataControllerWidget methods, you will have to tell it something 4555 has changed and it needs to redraw. This is done with the `invalidate` 4556 method. 4557 4558 History: 4559 Added Oct 28, 2020 4560 +/ 4561 /// Group: generating_from_code 4562 class DataControllerWidget(T) : WidgetContainer { 4563 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 4564 private alias Tref = T; 4565 else 4566 private alias Tref = T*; 4567 4568 Tref datum; 4569 4570 /++ 4571 See_also: [addDataControllerWidget] 4572 +/ 4573 this(Tref datum, Widget parent) { 4574 this.datum = datum; 4575 4576 Widget cp = this; 4577 4578 super(parent); 4579 4580 foreach(attr; __traits(getAttributes, T)) 4581 static if(is(typeof(attr) == ContainerMeta)) { 4582 cp = attr.instantiate(this); 4583 } 4584 4585 auto def = this.getByName("default"); 4586 if(def !is null) 4587 cp = def; 4588 4589 Widget helper(string name) { 4590 auto maybe = this.getByName(name); 4591 if(maybe is null) 4592 return cp; 4593 return maybe; 4594 4595 } 4596 4597 foreach(member; __traits(allMembers, T)) 4598 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 4599 static if(is(typeof(__traits(getMember, this.datum, member)))) 4600 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 4601 void delegate() update; 4602 4603 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 4604 4605 if(update) 4606 updaters ~= update; 4607 4608 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 4609 w.addEventListener("triggered", delegate() { 4610 makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))(); 4611 notifyDataUpdated(); 4612 }); 4613 } else static if(is(typeof(w.isChecked) == bool)) { 4614 w.addEventListener(EventType.change, (Event ev) { 4615 __traits(getMember, this.datum, member) = w.isChecked; 4616 }); 4617 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 4618 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 4619 } else static if(is(typeof(w.value) == int)) { 4620 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4621 } else static if(is(typeof(w) == DropDownSelection)) { 4622 // special case for this to kinda support enums and such. coudl be better though 4623 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4624 } else { 4625 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 4626 } 4627 } 4628 } 4629 4630 /++ 4631 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 4632 4633 History: 4634 Added May 28, 2021 4635 +/ 4636 void notifyDataUpdated() { 4637 foreach(updater; updaters) 4638 updater(); 4639 4640 this.emit!(ChangeEvent!void)(delegate{}); 4641 } 4642 4643 private Widget[string] memberWidgets; 4644 private void delegate()[] updaters; 4645 4646 mixin Emits!(ChangeEvent!void); 4647 } 4648 4649 private int saturatedSum(int[] values...) { 4650 int sum; 4651 foreach(value; values) { 4652 if(value == int.max) 4653 return int.max; 4654 sum += value; 4655 } 4656 return sum; 4657 } 4658 4659 void genericSetValue(T, W)(T* where, W what) { 4660 import std.conv; 4661 *where = to!T(what); 4662 //*where = cast(T) stringToLong(what); 4663 } 4664 4665 /++ 4666 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4667 4668 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4669 4670 Note that this creates the widget but does not attach any event handlers to it. 4671 +/ 4672 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 4673 4674 string displayName = __traits(identifier, tt).beautify; 4675 4676 static if(controlledByCount!tt == 1) { 4677 foreach(i, attr; __traits(getAttributes, tt)) { 4678 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4679 auto w = attr.construct(parent); 4680 static if(__traits(compiles, w.setPosition(*valptr))) 4681 update = () { w.setPosition(*valptr); }; 4682 else static if(__traits(compiles, w.setValue(*valptr))) 4683 update = () { w.setValue(*valptr); }; 4684 4685 if(update) 4686 update(); 4687 return w; 4688 } 4689 } 4690 } else static if(controlledByCount!tt == 0) { 4691 static if(is(typeof(tt) == enum)) { 4692 // FIXME: update 4693 auto dds = new DropDownSelection(parent); 4694 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4695 dds.addOption(option); 4696 if(__traits(getMember, typeof(tt), option) == *valptr) 4697 dds.setSelection(cast(int) idx); 4698 } 4699 return dds; 4700 } else static if(is(typeof(tt) == bool)) { 4701 auto box = new Checkbox(displayName, parent); 4702 update = () { box.isChecked = *valptr; }; 4703 update(); 4704 return box; 4705 } else static if(is(typeof(tt) : const long)) { 4706 auto le = new LabeledLineEdit(displayName, parent); 4707 update = () { le.content = toInternal!string(*valptr); }; 4708 update(); 4709 return le; 4710 } else static if(is(typeof(tt) : const double)) { 4711 auto le = new LabeledLineEdit(displayName, parent); 4712 import std.conv; 4713 update = () { le.content = to!string(*valptr); }; 4714 update(); 4715 return le; 4716 } else static if(is(typeof(tt) : const string)) { 4717 auto le = new LabeledLineEdit(displayName, parent); 4718 update = () { le.content = *valptr; }; 4719 update(); 4720 return le; 4721 } else static if(is(typeof(tt) == function)) { 4722 auto w = new Button(displayName, parent); 4723 return w; 4724 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4725 return parent.addDataControllerWidget(tt); 4726 } else static assert(0, typeof(tt).stringof); 4727 } else static assert(0, "multiple controllers not yet supported"); 4728 } 4729 4730 private template controlledByCount(alias tt) { 4731 static int helper() { 4732 int count; 4733 foreach(i, attr; __traits(getAttributes, tt)) 4734 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 4735 count++; 4736 return count; 4737 } 4738 4739 enum controlledByCount = helper; 4740 } 4741 4742 /++ 4743 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 4744 4745 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 4746 4747 History: 4748 The `redrawOnChange` parameter was added on May 28, 2021. 4749 +/ 4750 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 4751 auto dcw = new DataControllerWidget!T(t, parent); 4752 initializeDataControllerWidget(dcw, redrawOnChange); 4753 return dcw; 4754 } 4755 4756 /// ditto 4757 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 4758 auto dcw = new DataControllerWidget!T(t, parent); 4759 initializeDataControllerWidget(dcw, redrawOnChange); 4760 return dcw; 4761 } 4762 4763 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 4764 if(redrawOnChange !is null) 4765 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 4766 } 4767 4768 /++ 4769 Get this through [Widget.getComputedStyle]. It provides access to the [Widget.Style] style hints and [Widget] layout hints, possibly modified through the [VisualTheme], through a unifed interface. 4770 4771 History: 4772 Finalized on June 3, 2021 for the dub v10.0 release 4773 +/ 4774 struct StyleInformation { 4775 private Widget w; 4776 private BaseVisualTheme visualTheme; 4777 4778 private this(Widget w) { 4779 this.w = w; 4780 this.visualTheme = WidgetPainter.visualTheme; 4781 } 4782 4783 /++ 4784 Forwards to [Widget.Style] 4785 4786 Bugs: 4787 It is supposed to fall back to the [VisualTheme] if 4788 the style doesn't override the default, but that is 4789 not generally implemented. Many of them may end up 4790 being explicit overloads instead of the generic 4791 opDispatch fallback, like [font] is now. 4792 +/ 4793 public @property opDispatch(string name)() { 4794 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4795 w.useStyleProperties((scope Widget.Style props) { 4796 //visualTheme.useStyleProperties(w, (props) { 4797 prop = __traits(getMember, props, name); 4798 }); 4799 return prop; 4800 } 4801 4802 /++ 4803 Returns the cached font object associated with the widget, 4804 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 4805 4806 History: 4807 Prior to March 21, 2022 (dub v10.7), `font` went through 4808 [opDispatch], which did not use the cache. You can now call it 4809 repeatedly without guilt. 4810 +/ 4811 public @property OperatingSystemFont font() { 4812 OperatingSystemFont prop; 4813 w.useStyleProperties((scope Widget.Style props) { 4814 prop = props.fontCached; 4815 }); 4816 if(prop is null) { 4817 prop = visualTheme.defaultFontCached(w.currentDpi); 4818 } 4819 return prop; 4820 } 4821 4822 @property { 4823 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 4824 /** */ int paddingLeft() { return w.paddingLeft(); } 4825 /** */ int paddingRight() { return w.paddingRight(); } 4826 /** */ int paddingTop() { return w.paddingTop(); } 4827 /** */ int paddingBottom() { return w.paddingBottom(); } 4828 4829 /** */ int marginLeft() { return w.marginLeft(); } 4830 /** */ int marginRight() { return w.marginRight(); } 4831 /** */ int marginTop() { return w.marginTop(); } 4832 /** */ int marginBottom() { return w.marginBottom(); } 4833 4834 /** */ int maxHeight() { return w.maxHeight(); } 4835 /** */ int minHeight() { return w.minHeight(); } 4836 4837 /** */ int maxWidth() { return w.maxWidth(); } 4838 /** */ int minWidth() { return w.minWidth(); } 4839 4840 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 4841 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 4842 4843 /** */ int heightStretchiness() { return w.heightStretchiness(); } 4844 /** */ int widthStretchiness() { return w.widthStretchiness(); } 4845 4846 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 4847 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 4848 4849 // Global helpers some of these are unstable. 4850 static: 4851 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4852 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4853 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4854 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4855 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 4856 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4857 4858 /** */ Color activeTabColor() { return lightAccentColor; } 4859 /** */ Color buttonColor() { return windowBackgroundColor; } 4860 /** */ Color depressedButtonColor() { return darkAccentColor; } 4861 /** the background color of the widget when mouse hovering over it, if it responds to mouse hovers */ Color hoveringColor() { return lightAccentColor; } 4862 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 4863 auto c = WidgetPainter.visualTheme.selectionColor(); 4864 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4865 } 4866 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4867 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4868 } 4869 4870 4871 4872 /+ 4873 4874 private static auto extractStyleProperty(string name)(Widget w) { 4875 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4876 w.useStyleProperties((props) { 4877 prop = __traits(getMember, props, name); 4878 }); 4879 return prop; 4880 } 4881 4882 // FIXME: clear this upon a X server disconnect 4883 private static OperatingSystemFont[string] fontCache; 4884 4885 T getProperty(T)(string name, lazy T default_) { 4886 if(visualTheme !is null) { 4887 auto str = visualTheme.getPropertyString(w, name); 4888 if(str is null) 4889 return default_; 4890 static if(is(T == Color)) 4891 return Color.fromString(str); 4892 else static if(is(T == Measurement)) 4893 return Measurement(cast(int) toInternal!int(str)); 4894 else static if(is(T == WidgetBackground)) 4895 return WidgetBackground.fromString(str); 4896 else static if(is(T == OperatingSystemFont)) { 4897 if(auto f = str in fontCache) 4898 return *f; 4899 else 4900 return fontCache[str] = new OperatingSystemFont(str); 4901 } else static if(is(T == FrameStyle)) { 4902 switch(str) { 4903 default: 4904 return FrameStyle.none; 4905 foreach(style; __traits(allMembers, FrameStyle)) 4906 case style: 4907 return __traits(getMember, FrameStyle, style); 4908 } 4909 } else static assert(0); 4910 } else 4911 return default_; 4912 } 4913 4914 static struct Measurement { 4915 int value; 4916 alias value this; 4917 } 4918 4919 @property: 4920 4921 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 4922 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 4923 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 4924 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 4925 4926 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 4927 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 4928 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 4929 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 4930 4931 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 4932 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 4933 4934 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 4935 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 4936 4937 4938 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 4939 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 4940 4941 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 4942 4943 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 4944 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 4945 4946 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 4947 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 4948 4949 4950 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4951 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4952 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4953 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4954 4955 Color activeTabColor() { return lightAccentColor; } 4956 Color buttonColor() { return windowBackgroundColor; } 4957 Color depressedButtonColor() { return darkAccentColor; } 4958 Color hoveringColor() { return Color(228, 228, 228); } 4959 Color activeListXorColor() { 4960 auto c = WidgetPainter.visualTheme.selectionColor(); 4961 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4962 } 4963 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4964 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4965 +/ 4966 } 4967 4968 4969 4970 // pragma(msg, __traits(classInstanceSize, Widget)); 4971 4972 /*private*/ template EventString(E) { 4973 static if(is(typeof(E.EventString))) 4974 enum EventString = E.EventString; 4975 else 4976 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 4977 } 4978 4979 /*private*/ template EventStringIdentifier(E) { 4980 string helper() { 4981 auto es = EventString!E; 4982 char[] id = new char[](es.length * 2); 4983 size_t idx; 4984 foreach(char ch; es) { 4985 id[idx++] = cast(char)('a' + (ch >> 4)); 4986 id[idx++] = cast(char)('a' + (ch & 0x0f)); 4987 } 4988 return cast(string) id; 4989 } 4990 4991 enum EventStringIdentifier = helper(); 4992 } 4993 4994 4995 template classStaticallyEmits(This, EventType) { 4996 static if(is(This Base == super)) 4997 static if(is(Base : Widget)) 4998 enum baseEmits = classStaticallyEmits!(Base, EventType); 4999 else 5000 enum baseEmits = false; 5001 else 5002 enum baseEmits = false; 5003 5004 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 5005 5006 enum classStaticallyEmits = thisEmits || baseEmits; 5007 } 5008 5009 /++ 5010 A helper to make widgets out of other native windows. 5011 5012 History: 5013 Factored out of OpenGlWidget on November 5, 2021 5014 +/ 5015 class NestedChildWindowWidget : Widget { 5016 SimpleWindow win; 5017 5018 /++ 5019 Used on X to send focus to the appropriate child window when requested by the window manager. 5020 5021 Normally returns its own nested window. Can also return another child or null to revert to the parent 5022 if you override it in a child class. 5023 5024 History: 5025 Added April 2, 2022 (dub v10.8) 5026 +/ 5027 SimpleWindow focusableWindow() { 5028 return win; 5029 } 5030 5031 /// 5032 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 5033 this(SimpleWindow win, Widget parent) { 5034 this.parentWindow = parent.parentWindow; 5035 this.win = win; 5036 5037 super(parent); 5038 windowsetup(win); 5039 } 5040 5041 static protected SimpleWindow getParentWindow(Widget parent) { 5042 assert(parent !is null); 5043 SimpleWindow pwin = parent.parentWindow.win; 5044 5045 version(win32_widgets) { 5046 HWND phwnd; 5047 auto wtf = parent; 5048 while(wtf) { 5049 if(wtf.hwnd) { 5050 phwnd = wtf.hwnd; 5051 break; 5052 } 5053 wtf = wtf.parent; 5054 } 5055 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 5056 if(phwnd) 5057 pwin = new SimpleWindow(phwnd); 5058 } 5059 5060 return pwin; 5061 } 5062 5063 /++ 5064 Called upon the nested window being destroyed. 5065 Remember the window has already been destroyed at 5066 this point, so don't use the native handle for anything. 5067 5068 History: 5069 Added April 3, 2022 (dub v10.8) 5070 +/ 5071 protected void dispose() { 5072 5073 } 5074 5075 protected void windowsetup(SimpleWindow w) { 5076 /* 5077 win.onFocusChange = (bool getting) { 5078 if(getting) 5079 this.focus(); 5080 }; 5081 */ 5082 5083 /+ 5084 win.onFocusChange = (bool getting) { 5085 if(getting) { 5086 this.parentWindow.focusedWidget = this; 5087 this.emit!FocusEvent(); 5088 this.emit!FocusInEvent(); 5089 } else { 5090 this.emit!BlurEvent(); 5091 this.emit!FocusOutEvent(); 5092 } 5093 }; 5094 +/ 5095 5096 win.onDestroyed = () { 5097 this.dispose(); 5098 }; 5099 5100 version(win32_widgets) { 5101 Widget.nativeMapping[win.hwnd] = this; 5102 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 5103 } else { 5104 win.setEventHandlers( 5105 (MouseEvent e) { 5106 Widget p = this; 5107 while(p ! is parentWindow) { 5108 e.x += p.x; 5109 e.y += p.y; 5110 p = p.parent; 5111 } 5112 parentWindow.dispatchMouseEvent(e); 5113 }, 5114 (KeyEvent e) { 5115 //writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 5116 parentWindow.dispatchKeyEvent(e); 5117 }, 5118 (dchar e) { 5119 parentWindow.dispatchCharEvent(e); 5120 }, 5121 ); 5122 } 5123 5124 } 5125 5126 override bool showOrHideIfNativeWindow(bool shouldShow) { 5127 auto cur = hidden; 5128 win.hidden = !shouldShow; 5129 if(cur != shouldShow && shouldShow) 5130 redraw(); 5131 return true; 5132 } 5133 5134 /// OpenGL widgets cannot have child widgets. Do not call this. 5135 /* @disable */ final override void addChild(Widget, int) { 5136 throw new Error("cannot add children to OpenGL widgets"); 5137 } 5138 5139 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 5140 /// Keep in mind that events like mouse coordinates are still relative to your size. 5141 override void registerMovement() { 5142 // writefln("%d %d %d %d", x,y,width,height); 5143 version(win32_widgets) 5144 auto pos = getChildPositionRelativeToParentHwnd(this); 5145 else 5146 auto pos = getChildPositionRelativeToParentOrigin(this); 5147 win.moveResize(pos[0], pos[1], width, height); 5148 5149 registerMovementAdditionalWork(); 5150 sendResizeEvent(); 5151 } 5152 5153 abstract void registerMovementAdditionalWork(); 5154 } 5155 5156 /++ 5157 Nests an opengl capable window inside this window as a widget. 5158 5159 You may also just want to create an additional [SimpleWindow] with 5160 [OpenGlOptions.yes] yourself. 5161 5162 An OpenGL widget cannot have child widgets. It will throw if you try. 5163 +/ 5164 static if(OpenGlEnabled) 5165 class OpenGlWidget : NestedChildWindowWidget { 5166 5167 override void registerMovementAdditionalWork() { 5168 win.setAsCurrentOpenGlContext(); 5169 } 5170 5171 /// 5172 this(Widget parent) { 5173 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 5174 super(win, parent); 5175 } 5176 5177 override void paint(WidgetPainter painter) { 5178 win.setAsCurrentOpenGlContext(); 5179 glViewport(0, 0, this.width, this.height); 5180 win.redrawOpenGlSceneNow(); 5181 } 5182 5183 void redrawOpenGlScene(void delegate() dg) { 5184 win.redrawOpenGlScene = dg; 5185 } 5186 } 5187 5188 /++ 5189 This demo shows how to draw text in an opengl scene. 5190 +/ 5191 unittest { 5192 import arsd.minigui; 5193 import arsd.ttf; 5194 5195 void main() { 5196 auto window = new Window(); 5197 5198 auto widget = new OpenGlWidget(window); 5199 5200 // old means non-shader code so compatible with glBegin etc. 5201 // tbh I haven't implemented new one in font yet... 5202 // anyway, declaring here, will construct soon. 5203 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 5204 5205 // this is a little bit awkward, calling some methods through 5206 // the underlying SimpleWindow `win` method, and you can't do this 5207 // on a nanovega widget due to conflicts so I should probably fix 5208 // the api to be a bit easier. But here it will work. 5209 // 5210 // Alternatively, you could load the font on the first draw, inside 5211 // the redrawOpenGlScene, and keep a flag so you don't do it every 5212 // time. That'd be a bit easier since the lib sets up the context 5213 // by then guaranteed. 5214 // 5215 // But still, I wanna show this. 5216 widget.win.visibleForTheFirstTime = delegate { 5217 // must set the opengl context 5218 widget.win.setAsCurrentOpenGlContext(); 5219 5220 // if you were doing a OpenGL 3+ shader, this 5221 // gets especially important to do in order. With 5222 // old-style opengl, I think you can even do it 5223 // in main(), but meh, let's show it more correctly. 5224 5225 // Anyway, now it is time to load the font from the 5226 // OS (you can alternatively load one from a .ttf file 5227 // you bundle with the application), then load the 5228 // font into texture for drawing. 5229 5230 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 5231 5232 assert(!osfont.isNull()); // make sure it actually loaded 5233 5234 // using typeof to avoid repeating the long name lol 5235 glfont = new typeof(glfont)( 5236 // get the raw data from the font for loading in here 5237 // since it doesn't use the OS function to draw the 5238 // text, we gotta treat it more as a file than as 5239 // a drawing api. 5240 osfont.getTtfBytes(), 5241 18, // need to respecify size since opengl world is different coordinate system 5242 5243 // these last two numbers are why it is called 5244 // "Limited" font. It only loads the characters 5245 // in the given range, since the texture atlas 5246 // it references is all a big image generated ahead 5247 // of time. You could maybe do the whole thing but 5248 // idk how much memory that is. 5249 // 5250 // But here, 0-128 represents the ASCII range, so 5251 // good enough for most English things, numeric labels, 5252 // etc. 5253 0, 5254 128 5255 ); 5256 }; 5257 5258 widget.redrawOpenGlScene = () { 5259 // now we can use the glfont's drawString function 5260 5261 // first some opengl setup. You can do this in one place 5262 // on window first visible too in many cases, just showing 5263 // here cuz it is easier for me. 5264 5265 // gonna need some alpha blending or it just looks awful 5266 glEnable(GL_BLEND); 5267 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 5268 glClearColor(0,0,0,0); 5269 glDepthFunc(GL_LEQUAL); 5270 5271 // Also need to enable 2d textures, since it draws the 5272 // font characters as images baked in 5273 glMatrixMode(GL_MODELVIEW); 5274 glLoadIdentity(); 5275 glDisable(GL_DEPTH_TEST); 5276 glEnable(GL_TEXTURE_2D); 5277 5278 // the orthographic matrix is best for 2d things like text 5279 // so let's set that up. This matrix makes the coordinates 5280 // in the opengl scene be one-to-one with the actual pixels 5281 // on screen. (Not necessarily best, you may wish to scale 5282 // things, but it does help keep fonts looking normal.) 5283 glMatrixMode(GL_PROJECTION); 5284 glLoadIdentity(); 5285 glOrtho(0, widget.width, widget.height, 0, 0, 1); 5286 5287 // you can do other glScale, glRotate, glTranslate, etc 5288 // to the matrix here of course if you want. 5289 5290 // note the x,y coordinates here are for the text baseline 5291 // NOT the upper-left corner. The baseline is like the line 5292 // in the notebook you write on. Most the letters are actually 5293 // above it, but some, like p and q, dip a bit below it. 5294 // 5295 // So if you're used to the upper left coordinate like the 5296 // rest of simpledisplay/minigui usually do, do the 5297 // y + glfont.ascent to bring it down a little. So this 5298 // example puts the string in the upper left of the window. 5299 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 5300 5301 // re color btw: the function sets a solid color internally, 5302 // but you actually COULD do your own thing for rainbow effects 5303 // and the sort if you wanted too, by pulling its guts out. 5304 // Just view its source for an idea of how it actually draws: 5305 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 5306 5307 // it gets a bit complicated with the character positioning, 5308 // but the opengl parts are fairly simple: bind a texture, 5309 // set the color, draw a quad for each letter. 5310 5311 5312 // the last optional argument there btw is a bounding box 5313 // it will/ use to word wrap and return an object you can 5314 // use to implement scrolling or pagination; it tells how 5315 // much of the string didn't fit in the box. But for simple 5316 // labels we can just ignore that. 5317 5318 5319 // I'd suggest drawing text as the last step, after you 5320 // do your other drawing. You might use the push/pop matrix 5321 // stuff to keep your place. You, in theory, should be able 5322 // to do text in a 3d space but I've never actually tried 5323 // that.... 5324 }; 5325 5326 window.loop(); 5327 } 5328 } 5329 5330 version(custom_widgets) 5331 private class TextListViewWidget : GenericListViewWidget { 5332 static class TextListViewItem : GenericListViewItem { 5333 ListWidget controller; 5334 this(ListWidget controller, Widget parent) { 5335 this.controller = controller; 5336 this.tabStop = false; 5337 super(parent); 5338 } 5339 5340 ListWidget.Option* showing; 5341 5342 override void showItem(int idx) { 5343 showing = idx < controller.options.length ? &controller.options[idx] : null; 5344 redraw(); // is this necessary? the generic thing might call it... 5345 } 5346 5347 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 5348 if(showing is null) 5349 return bounds; 5350 painter.drawText(bounds.upperLeft, showing.label); 5351 return bounds; 5352 } 5353 5354 static class Style : Widget.Style { 5355 override WidgetBackground background() { 5356 // FIXME: change it if it is focused or not 5357 // needs to reliably detect if focused (noting the actual focus may be on a parent or child... or even sibling for FreeEntrySelection. maybe i just need a better way to proxy focus in widgets generically). also will need to redraw correctly without defaultEventHandler_focusin hacks like EditableTextWidget uses 5358 auto tlvi = cast(TextListViewItem) widget; 5359 if(tlvi && tlvi.showing && tlvi && tlvi.showing.selected) 5360 return WidgetBackground(true /*widget.parent.isFocused*/ ? WidgetPainter.visualTheme.selectionBackgroundColor : Color(128, 128, 128)); // FIXME: don't hardcode 5361 return super.background(); 5362 } 5363 5364 override Color foregroundColor() { 5365 auto tlvi = cast(TextListViewItem) widget; 5366 return tlvi && tlvi.showing && tlvi && tlvi.showing.selected ? WidgetPainter.visualTheme.selectionForegroundColor : super.foregroundColor(); 5367 } 5368 5369 override FrameStyle outlineStyle() { 5370 // FIXME: change it if it is focused or not 5371 auto tlvi = cast(TextListViewItem) widget; 5372 return (tlvi && tlvi.currentIndexLoaded() == tlvi.controller.focusOn) ? FrameStyle.dotted : super.outlineStyle(); 5373 } 5374 } 5375 mixin OverrideStyle!Style; 5376 5377 mixin Padding!q{2}; 5378 5379 override void defaultEventHandler_click(ClickEvent event) { 5380 if(event.button == MouseButton.left) { 5381 controller.setSelection(currentIndexLoaded()); 5382 controller.focusOn = currentIndexLoaded(); 5383 } 5384 } 5385 5386 } 5387 5388 ListWidget controller; 5389 5390 this(ListWidget parent) { 5391 this.controller = parent; 5392 this.tabStop = false; // this is only used as a child of the ListWidget 5393 super(parent); 5394 5395 smw.movementPerButtonClick(1, itemSize().height); 5396 } 5397 5398 override Size itemSize() { 5399 return Size(0, defaultLineHeight + scaleWithDpi(4 /* the top and bottom padding */)); 5400 } 5401 5402 override GenericListViewItem itemFactory(Widget parent) { 5403 return new TextListViewItem(controller, parent); 5404 } 5405 5406 static class Style : Widget.Style { 5407 override FrameStyle borderStyle() { 5408 return FrameStyle.sunk; 5409 } 5410 5411 override WidgetBackground background() { 5412 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 5413 } 5414 } 5415 mixin OverrideStyle!Style; 5416 } 5417 5418 /++ 5419 A list widget contains a list of strings that the user can examine and select. 5420 5421 5422 In the future, items in the list may be possible to be more than just strings. 5423 5424 See_Also: 5425 [TableView] 5426 +/ 5427 class ListWidget : Widget { 5428 /// Sends a change event when the selection changes, but the data is not attached to the event. You must instead loop the options to see if they are selected. 5429 mixin Emits!(ChangeEvent!void); 5430 5431 version(custom_widgets) 5432 TextListViewWidget glvw; 5433 5434 static struct Option { 5435 string label; 5436 bool selected; 5437 void* tag; 5438 } 5439 private Option[] options; 5440 5441 /++ 5442 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 5443 +/ 5444 void setSelection(int y) { 5445 if(!multiSelect) 5446 foreach(ref opt; options) 5447 opt.selected = false; 5448 if(y >= 0 && y < options.length) 5449 options[y].selected = !options[y].selected; 5450 5451 version(custom_widgets) 5452 focusOn = y; 5453 5454 this.emit!(ChangeEvent!void)(delegate {}); 5455 5456 version(custom_widgets) 5457 redraw(); 5458 } 5459 5460 /++ 5461 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 5462 Returns -1 if nothing is selected. 5463 +/ 5464 int getSelection() 5465 { 5466 foreach(i, opt; options) { 5467 if (opt.selected) 5468 return cast(int) i; 5469 } 5470 return -1; 5471 } 5472 5473 version(custom_widgets) 5474 private int focusOn; 5475 5476 this(Widget parent) { 5477 super(parent); 5478 5479 version(custom_widgets) 5480 glvw = new TextListViewWidget(this); 5481 5482 version(win32_widgets) 5483 createWin32Window(this, WC_LISTBOX, "", 5484 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 5485 } 5486 5487 version(win32_widgets) 5488 override void handleWmCommand(ushort code, ushort id) { 5489 switch(code) { 5490 case LBN_SELCHANGE: 5491 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 5492 setSelection(cast(int) sel); 5493 break; 5494 default: 5495 } 5496 } 5497 5498 5499 void addOption(string text, void* tag = null) { 5500 options ~= Option(text, false, tag); 5501 version(win32_widgets) { 5502 WCharzBuffer buffer = WCharzBuffer(text); 5503 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 5504 } 5505 version(custom_widgets) { 5506 glvw.setItemCount(cast(int) options.length); 5507 //setContentSize(width, cast(int) (options.length * defaultLineHeight)); 5508 redraw(); 5509 } 5510 } 5511 5512 void clear() { 5513 options = null; 5514 version(win32_widgets) { 5515 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 5516 {} 5517 5518 } else version(custom_widgets) { 5519 focusOn = -1; 5520 glvw.setItemCount(0); 5521 redraw(); 5522 } 5523 } 5524 5525 version(custom_widgets) 5526 override void defaultEventHandler_keydown(KeyDownEvent kde) { 5527 void changedFocusOn() { 5528 scrollFocusIntoView(); 5529 if(multiSelect) 5530 redraw(); 5531 else 5532 setSelection(focusOn); 5533 } 5534 switch(kde.key) { 5535 case Key.Up: 5536 if(focusOn) { 5537 focusOn--; 5538 changedFocusOn(); 5539 } 5540 break; 5541 case Key.Down: 5542 if(focusOn + 1 < options.length) { 5543 focusOn++; 5544 changedFocusOn(); 5545 } 5546 break; 5547 case Key.Home: 5548 if(focusOn) { 5549 focusOn = 0; 5550 changedFocusOn(); 5551 } 5552 break; 5553 case Key.End: 5554 if(options.length && focusOn + 1 != options.length) { 5555 focusOn = cast(int) options.length - 1; 5556 changedFocusOn(); 5557 } 5558 break; 5559 case Key.PageUp: 5560 auto n = glvw.numberOfCurrentlyFullyVisibleItems; 5561 focusOn -= n; 5562 if(focusOn < 0) 5563 focusOn = 0; 5564 changedFocusOn(); 5565 break; 5566 case Key.PageDown: 5567 if(options.length == 0) 5568 break; 5569 auto n = glvw.numberOfCurrentlyFullyVisibleItems; 5570 focusOn += n; 5571 if(focusOn >= options.length) 5572 focusOn = cast(int) options.length - 1; 5573 changedFocusOn(); 5574 break; 5575 5576 default: 5577 } 5578 } 5579 5580 version(custom_widgets) 5581 override void defaultEventHandler_char(CharEvent ce) { 5582 if(ce.character == '\n' || ce.character == ' ') { 5583 setSelection(focusOn); 5584 } else { 5585 // search for the item that best matches and jump to it 5586 // FIXME this sucks in tons of ways. the normal thing toolkits 5587 // do here is to search for a substring on a timer, but i'd kinda 5588 // rather make an actual little dialog with some options. still meh for now. 5589 dchar search = ce.character; 5590 if(search >= 'A' && search <= 'Z') 5591 search += 32; 5592 foreach(idx, option; options) { 5593 auto ch = option.label.length ? option.label[0] : 0; 5594 if(ch >= 'A' && ch <= 'Z') 5595 ch += 32; 5596 if(ch == search) { 5597 setSelection(cast(int) idx); 5598 scrollSelectionIntoView(); 5599 break; 5600 } 5601 } 5602 5603 } 5604 } 5605 5606 version(win32_widgets) 5607 enum multiSelect = false; /// not implemented yet 5608 else 5609 bool multiSelect; 5610 5611 override int heightStretchiness() { return 6; } 5612 5613 version(custom_widgets) 5614 void scrollFocusIntoView() { 5615 glvw.ensureItemVisibleInScroll(focusOn); 5616 } 5617 5618 void scrollSelectionIntoView() { 5619 // FIXME: implement on Windows 5620 5621 version(custom_widgets) 5622 glvw.ensureItemVisibleInScroll(getSelection()); 5623 } 5624 5625 /* 5626 version(custom_widgets) 5627 override void defaultEventHandler_focusout(Event foe) { 5628 glvw.redraw(); 5629 } 5630 5631 version(custom_widgets) 5632 override void defaultEventHandler_focusin(Event foe) { 5633 glvw.redraw(); 5634 } 5635 */ 5636 5637 } 5638 5639 5640 5641 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 5642 /// NEVER USED 5643 enum ScrollBarShowPolicy { 5644 automatic, /// automatically show the scroll bar if it is necessary 5645 never, /// never show the scroll bar (scrolling must be done programmatically) 5646 always /// always show the scroll bar, even if it is disabled 5647 } 5648 5649 /++ 5650 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 5651 5652 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 5653 +/ 5654 // FIXME ScrollBarShowPolicy 5655 // FIXME: use the ScrollMessageWidget in here now that it exists 5656 // deprecated("Use ScrollMessageWidget or ScrollableContainerWidget instead") // ugh compiler won't let me do it 5657 class ScrollableWidget : Widget { 5658 // FIXME: make line size configurable 5659 // FIXME: add keyboard controls 5660 version(win32_widgets) { 5661 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 5662 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 5663 auto pos = HIWORD(wParam); 5664 auto m = LOWORD(wParam); 5665 5666 // FIXME: I can reintroduce the 5667 // scroll bars now by using this 5668 // in the top-level window handler 5669 // to forward comamnds 5670 auto scrollbarHwnd = lParam; 5671 switch(m) { 5672 case SB_BOTTOM: 5673 if(msg == WM_HSCROLL) 5674 horizontalScrollTo(contentWidth_); 5675 else 5676 verticalScrollTo(contentHeight_); 5677 break; 5678 case SB_TOP: 5679 if(msg == WM_HSCROLL) 5680 horizontalScrollTo(0); 5681 else 5682 verticalScrollTo(0); 5683 break; 5684 case SB_ENDSCROLL: 5685 // idk 5686 break; 5687 case SB_LINEDOWN: 5688 if(msg == WM_HSCROLL) 5689 horizontalScroll(scaleWithDpi(16)); 5690 else 5691 verticalScroll(scaleWithDpi(16)); 5692 break; 5693 case SB_LINEUP: 5694 if(msg == WM_HSCROLL) 5695 horizontalScroll(scaleWithDpi(-16)); 5696 else 5697 verticalScroll(scaleWithDpi(-16)); 5698 break; 5699 case SB_PAGEDOWN: 5700 if(msg == WM_HSCROLL) 5701 horizontalScroll(scaleWithDpi(100)); 5702 else 5703 verticalScroll(scaleWithDpi(100)); 5704 break; 5705 case SB_PAGEUP: 5706 if(msg == WM_HSCROLL) 5707 horizontalScroll(scaleWithDpi(-100)); 5708 else 5709 verticalScroll(scaleWithDpi(-100)); 5710 break; 5711 case SB_THUMBPOSITION: 5712 case SB_THUMBTRACK: 5713 if(msg == WM_HSCROLL) 5714 horizontalScrollTo(pos); 5715 else 5716 verticalScrollTo(pos); 5717 5718 if(m == SB_THUMBTRACK) { 5719 // the event loop doesn't seem to carry on with a requested redraw.. 5720 // so we request it to get our dirty bit set... 5721 redraw(); 5722 5723 // then we need to immediately actually redraw it too for instant feedback to user 5724 5725 SimpleWindow.processAllCustomEvents(); 5726 //if(parentWindow) 5727 //parentWindow.actualRedraw(); 5728 } 5729 break; 5730 default: 5731 } 5732 } 5733 return super.hookedWndProc(msg, wParam, lParam); 5734 } 5735 } 5736 /// 5737 this(Widget parent) { 5738 this.parentWindow = parent.parentWindow; 5739 5740 version(win32_widgets) { 5741 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 5742 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 5743 super(parent); 5744 } else version(custom_widgets) { 5745 outerContainer = new InternalScrollableContainerWidget(this, parent); 5746 super(outerContainer); 5747 } else static assert(0); 5748 } 5749 5750 version(custom_widgets) 5751 InternalScrollableContainerWidget outerContainer; 5752 5753 override void defaultEventHandler_click(ClickEvent event) { 5754 if(event.button == MouseButton.wheelUp) 5755 verticalScroll(scaleWithDpi(-16)); 5756 if(event.button == MouseButton.wheelDown) 5757 verticalScroll(scaleWithDpi(16)); 5758 super.defaultEventHandler_click(event); 5759 } 5760 5761 override void defaultEventHandler_keydown(KeyDownEvent event) { 5762 switch(event.key) { 5763 case Key.Left: 5764 horizontalScroll(scaleWithDpi(-16)); 5765 break; 5766 case Key.Right: 5767 horizontalScroll(scaleWithDpi(16)); 5768 break; 5769 case Key.Up: 5770 verticalScroll(scaleWithDpi(-16)); 5771 break; 5772 case Key.Down: 5773 verticalScroll(scaleWithDpi(16)); 5774 break; 5775 case Key.Home: 5776 verticalScrollTo(0); 5777 break; 5778 case Key.End: 5779 verticalScrollTo(contentHeight); 5780 break; 5781 case Key.PageUp: 5782 verticalScroll(scaleWithDpi(-160)); 5783 break; 5784 case Key.PageDown: 5785 verticalScroll(scaleWithDpi(160)); 5786 break; 5787 default: 5788 } 5789 super.defaultEventHandler_keydown(event); 5790 } 5791 5792 5793 version(win32_widgets) 5794 override void recomputeChildLayout() { 5795 super.recomputeChildLayout(); 5796 SCROLLINFO info; 5797 info.cbSize = info.sizeof; 5798 info.nPage = viewportHeight; 5799 info.fMask = SIF_PAGE | SIF_RANGE; 5800 info.nMin = 0; 5801 info.nMax = contentHeight_; 5802 SetScrollInfo(hwnd, SB_VERT, &info, true); 5803 5804 info.cbSize = info.sizeof; 5805 info.nPage = viewportWidth; 5806 info.fMask = SIF_PAGE | SIF_RANGE; 5807 info.nMin = 0; 5808 info.nMax = contentWidth_; 5809 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5810 } 5811 5812 /* 5813 Scrolling 5814 ------------ 5815 5816 You are assigned a width and a height by the layout engine, which 5817 is your viewport box. However, you may draw more than that by setting 5818 a contentWidth and contentHeight. 5819 5820 If these can be contained by the viewport, no scrollbar is displayed. 5821 If they cannot fit though, it will automatically show scroll as necessary. 5822 5823 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 5824 is zero, no vertical scrolling is performed. 5825 5826 If scrolling is necessary, the lib will automatically work with the bars. 5827 When you redraw, the origin and clipping info in the painter is set so if 5828 you just draw everything, it will work, but you can be more efficient by checking 5829 the viewportWidth, viewportHeight, and scrollOrigin members. 5830 */ 5831 5832 /// 5833 final @property int viewportWidth() { 5834 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 5835 } 5836 /// 5837 final @property int viewportHeight() { 5838 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 5839 } 5840 5841 // FIXME property 5842 Point scrollOrigin_; 5843 5844 /// 5845 final const(Point) scrollOrigin() { 5846 return scrollOrigin_; 5847 } 5848 5849 // the user sets these two 5850 private int contentWidth_ = 0; 5851 private int contentHeight_ = 0; 5852 5853 /// 5854 int contentWidth() { return contentWidth_; } 5855 /// 5856 int contentHeight() { return contentHeight_; } 5857 5858 /// 5859 void setContentSize(int width, int height) { 5860 contentWidth_ = width; 5861 contentHeight_ = height; 5862 5863 version(custom_widgets) { 5864 if(showingVerticalScroll || showingHorizontalScroll) { 5865 outerContainer.queueRecomputeChildLayout(); 5866 } 5867 5868 if(showingVerticalScroll()) 5869 outerContainer.verticalScrollBar.redraw(); 5870 if(showingHorizontalScroll()) 5871 outerContainer.horizontalScrollBar.redraw(); 5872 } else version(win32_widgets) { 5873 queueRecomputeChildLayout(); 5874 } else static assert(0); 5875 } 5876 5877 /// 5878 void verticalScroll(int delta) { 5879 verticalScrollTo(scrollOrigin.y + delta); 5880 } 5881 /// 5882 void verticalScrollTo(int pos) { 5883 scrollOrigin_.y = pos; 5884 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 5885 scrollOrigin_.y = contentHeight - viewportHeight; 5886 5887 if(scrollOrigin_.y < 0) 5888 scrollOrigin_.y = 0; 5889 5890 version(win32_widgets) { 5891 SCROLLINFO info; 5892 info.cbSize = info.sizeof; 5893 info.fMask = SIF_POS; 5894 info.nPos = scrollOrigin_.y; 5895 SetScrollInfo(hwnd, SB_VERT, &info, true); 5896 } else version(custom_widgets) { 5897 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 5898 } else static assert(0); 5899 5900 redraw(); 5901 } 5902 5903 /// 5904 void horizontalScroll(int delta) { 5905 horizontalScrollTo(scrollOrigin.x + delta); 5906 } 5907 /// 5908 void horizontalScrollTo(int pos) { 5909 scrollOrigin_.x = pos; 5910 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 5911 scrollOrigin_.x = contentWidth - viewportWidth; 5912 5913 if(scrollOrigin_.x < 0) 5914 scrollOrigin_.x = 0; 5915 5916 version(win32_widgets) { 5917 SCROLLINFO info; 5918 info.cbSize = info.sizeof; 5919 info.fMask = SIF_POS; 5920 info.nPos = scrollOrigin_.x; 5921 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5922 } else version(custom_widgets) { 5923 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 5924 } else static assert(0); 5925 5926 redraw(); 5927 } 5928 /// 5929 void scrollTo(Point p) { 5930 verticalScrollTo(p.y); 5931 horizontalScrollTo(p.x); 5932 } 5933 5934 /// 5935 void ensureVisibleInScroll(Point p) { 5936 auto rect = viewportRectangle(); 5937 if(rect.contains(p)) 5938 return; 5939 if(p.x < rect.left) 5940 horizontalScroll(p.x - rect.left); 5941 else if(p.x > rect.right) 5942 horizontalScroll(p.x - rect.right); 5943 5944 if(p.y < rect.top) 5945 verticalScroll(p.y - rect.top); 5946 else if(p.y > rect.bottom) 5947 verticalScroll(p.y - rect.bottom); 5948 } 5949 5950 /// 5951 void ensureVisibleInScroll(Rectangle rect) { 5952 ensureVisibleInScroll(rect.upperLeft); 5953 ensureVisibleInScroll(rect.lowerRight); 5954 } 5955 5956 /// 5957 Rectangle viewportRectangle() { 5958 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 5959 } 5960 5961 /// 5962 bool showingHorizontalScroll() { 5963 return contentWidth > width; 5964 } 5965 /// 5966 bool showingVerticalScroll() { 5967 return contentHeight > height; 5968 } 5969 5970 /// This is called before the ordinary paint delegate, 5971 /// giving you a chance to draw the window frame, etc, 5972 /// before the scroll clip takes effect 5973 void paintFrameAndBackground(WidgetPainter painter) { 5974 version(win32_widgets) { 5975 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5976 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5977 // since the pen is null, to fill the whole space, we need the +1 on both. 5978 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5979 SelectObject(painter.impl.hdc, p); 5980 SelectObject(painter.impl.hdc, b); 5981 } 5982 5983 } 5984 5985 // make space for the scroll bar, and that's it. 5986 final override int paddingRight() { return scaleWithDpi(16); } 5987 final override int paddingBottom() { return scaleWithDpi(16); } 5988 5989 /* 5990 END SCROLLING 5991 */ 5992 5993 override WidgetPainter draw() { 5994 int x = this.x, y = this.y; 5995 auto parent = this.parent; 5996 while(parent) { 5997 x += parent.x; 5998 y += parent.y; 5999 parent = parent.parent; 6000 } 6001 6002 //version(win32_widgets) { 6003 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 6004 //} else { 6005 auto painter = parentWindow.win.draw(true); 6006 //} 6007 painter.originX = x; 6008 painter.originY = y; 6009 6010 painter.originX = painter.originX - scrollOrigin.x; 6011 painter.originY = painter.originY - scrollOrigin.y; 6012 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 6013 6014 return WidgetPainter(painter, this); 6015 } 6016 6017 mixin ScrollableChildren; 6018 } 6019 6020 // you need to have a Point scrollOrigin in the class somewhere 6021 // and a paintFrameAndBackground 6022 private mixin template ScrollableChildren() { 6023 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 6024 if(hidden) 6025 return; 6026 6027 //version(win32_widgets) 6028 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 6029 6030 painter.originX = lox + x; 6031 painter.originY = loy + y; 6032 6033 bool actuallyPainted = false; 6034 6035 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 6036 if(clip == Rectangle.init) 6037 return; 6038 6039 if(force || redrawRequested) { 6040 //painter.setClipRectangle(scrollOrigin, width, height); 6041 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 6042 paintFrameAndBackground(painter); 6043 } 6044 6045 /+ 6046 version(win32_widgets) { 6047 if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW); 6048 } 6049 +/ 6050 6051 painter.originX = painter.originX - scrollOrigin.x; 6052 painter.originY = painter.originY - scrollOrigin.y; 6053 if(force || redrawRequested) { 6054 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 6055 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 6056 6057 //erase(painter); // we paintFrameAndBackground above so no need 6058 if(painter.visualTheme) 6059 painter.visualTheme.doPaint(this, painter); 6060 else 6061 paint(painter); 6062 6063 if(invalidate) { 6064 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 6065 // children are contained inside this, so no need to do extra work 6066 invalidate = false; 6067 } 6068 6069 6070 actuallyPainted = true; 6071 redrawRequested = false; 6072 } 6073 6074 foreach(child; children) { 6075 if(cast(FixedPosition) child) 6076 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 6077 else 6078 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 6079 } 6080 } 6081 } 6082 6083 private class InternalScrollableContainerInsideWidget : ContainerWidget { 6084 ScrollableContainerWidget scw; 6085 6086 this(ScrollableContainerWidget parent) { 6087 scw = parent; 6088 super(parent); 6089 } 6090 6091 version(custom_widgets) 6092 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 6093 if(hidden) 6094 return; 6095 6096 bool actuallyPainted = false; 6097 6098 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 6099 6100 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 6101 if(clip == Rectangle.init) 6102 return; 6103 6104 painter.originX = lox + x - scrollOrigin.x; 6105 painter.originY = loy + y - scrollOrigin.y; 6106 if(force || redrawRequested) { 6107 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 6108 6109 erase(painter); 6110 if(painter.visualTheme) 6111 painter.visualTheme.doPaint(this, painter); 6112 else 6113 paint(painter); 6114 6115 if(invalidate) { 6116 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 6117 // children are contained inside this, so no need to do extra work 6118 invalidate = false; 6119 } 6120 6121 actuallyPainted = true; 6122 redrawRequested = false; 6123 } 6124 foreach(child; children) { 6125 if(cast(FixedPosition) child) 6126 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 6127 else 6128 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 6129 } 6130 } 6131 6132 version(custom_widgets) 6133 override protected void addScrollPosition(ref int x, ref int y) { 6134 x += scw.scrollX_; 6135 y += scw.scrollY_; 6136 } 6137 } 6138 6139 /++ 6140 A widget meant to contain other widgets that may need to scroll. 6141 6142 Currently buggy. 6143 6144 History: 6145 Added July 1, 2021 (dub v10.2) 6146 6147 On January 3, 2022, I tried to use it in a few other cases 6148 and found it only worked well in the original test case. Since 6149 it still sucks, I think I'm going to rewrite it again. 6150 +/ 6151 class ScrollableContainerWidget : ContainerWidget { 6152 /// 6153 this(Widget parent) { 6154 super(parent); 6155 6156 container = new InternalScrollableContainerInsideWidget(this); 6157 hsb = new HorizontalScrollbar(this); 6158 vsb = new VerticalScrollbar(this); 6159 6160 tabStop = false; 6161 container.tabStop = false; 6162 magic = true; 6163 6164 6165 vsb.addEventListener("scrolltonextline", () { 6166 scrollBy(0, scaleWithDpi(16)); 6167 }); 6168 vsb.addEventListener("scrolltopreviousline", () { 6169 scrollBy(0,scaleWithDpi( -16)); 6170 }); 6171 vsb.addEventListener("scrolltonextpage", () { 6172 scrollBy(0, container.height); 6173 }); 6174 vsb.addEventListener("scrolltopreviouspage", () { 6175 scrollBy(0, -container.height); 6176 }); 6177 vsb.addEventListener((scope ScrollToPositionEvent spe) { 6178 scrollTo(scrollX_, spe.value); 6179 }); 6180 6181 this.addEventListener(delegate (scope ClickEvent e) { 6182 if(e.button == MouseButton.wheelUp) { 6183 if(!e.defaultPrevented) 6184 scrollBy(0, scaleWithDpi(-16)); 6185 e.stopPropagation(); 6186 } else if(e.button == MouseButton.wheelDown) { 6187 if(!e.defaultPrevented) 6188 scrollBy(0, scaleWithDpi(16)); 6189 e.stopPropagation(); 6190 } 6191 }); 6192 } 6193 6194 /+ 6195 override void defaultEventHandler_click(ClickEvent e) { 6196 } 6197 +/ 6198 6199 override void removeAllChildren() { 6200 container.removeAllChildren(); 6201 } 6202 6203 void scrollTo(int x, int y) { 6204 scrollBy(x - scrollX_, y - scrollY_); 6205 } 6206 6207 void scrollBy(int x, int y) { 6208 auto ox = scrollX_; 6209 auto oy = scrollY_; 6210 6211 auto nx = ox + x; 6212 auto ny = oy + y; 6213 6214 if(nx < 0) 6215 nx = 0; 6216 if(ny < 0) 6217 ny = 0; 6218 6219 auto maxX = hsb.max - container.width; 6220 if(maxX < 0) maxX = 0; 6221 auto maxY = vsb.max - container.height; 6222 if(maxY < 0) maxY = 0; 6223 6224 if(nx > maxX) 6225 nx = maxX; 6226 if(ny > maxY) 6227 ny = maxY; 6228 6229 auto dx = nx - ox; 6230 auto dy = ny - oy; 6231 6232 if(dx || dy) { 6233 version(win32_widgets) 6234 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 6235 else { 6236 redraw(); 6237 } 6238 6239 hsb.setPosition = nx; 6240 vsb.setPosition = ny; 6241 6242 scrollX_ = nx; 6243 scrollY_ = ny; 6244 } 6245 } 6246 6247 private int scrollX_; 6248 private int scrollY_; 6249 6250 void setTotalArea(int width, int height) { 6251 hsb.setMax(width); 6252 vsb.setMax(height); 6253 } 6254 6255 /// 6256 void setViewableArea(int width, int height) { 6257 hsb.setViewableArea(width); 6258 vsb.setViewableArea(height); 6259 } 6260 6261 private bool magic; 6262 override void addChild(Widget w, int position = int.max) { 6263 if(magic) 6264 container.addChild(w, position); 6265 else 6266 super.addChild(w, position); 6267 } 6268 6269 override void recomputeChildLayout() { 6270 if(hsb is null || vsb is null || container is null) return; 6271 6272 /+ 6273 writeln(x, " ", y , " ", width, " ", height); 6274 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 6275 +/ 6276 6277 registerMovement(); 6278 6279 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 6280 hsb.x = 0; 6281 hsb.y = this.height - hsb.height; 6282 hsb.width = this.width - scaleWithDpi(16); 6283 hsb.recomputeChildLayout(); 6284 6285 vsb.width = scaleWithDpi(16); // FIXME? 6286 vsb.x = this.width - vsb.width; 6287 vsb.y = 0; 6288 vsb.height = this.height - scaleWithDpi(16); 6289 vsb.recomputeChildLayout(); 6290 6291 container.x = 0; 6292 container.y = 0; 6293 container.width = this.width - vsb.width; 6294 container.height = this.height - hsb.height; 6295 container.recomputeChildLayout(); 6296 6297 scrollX_ = 0; 6298 scrollY_ = 0; 6299 6300 hsb.setPosition(0); 6301 vsb.setPosition(0); 6302 6303 int mw, mh; 6304 Widget c = container; 6305 // FIXME: hack here to handle a layout inside... 6306 if(c.children.length == 1 && cast(Layout) c.children[0]) 6307 c = c.children[0]; 6308 foreach(child; c.children) { 6309 auto w = child.x + child.width; 6310 auto h = child.y + child.height; 6311 6312 if(w > mw) mw = w; 6313 if(h > mh) mh = h; 6314 } 6315 6316 setTotalArea(mw, mh); 6317 setViewableArea(width, height); 6318 } 6319 6320 override int minHeight() { return scaleWithDpi(64); } 6321 6322 HorizontalScrollbar hsb; 6323 VerticalScrollbar vsb; 6324 ContainerWidget container; 6325 } 6326 6327 6328 version(custom_widgets) 6329 // deprecated // i can't deprecate it w/o stupid messages ugh 6330 private class InternalScrollableContainerWidget : Widget { 6331 6332 ScrollableWidget sw; 6333 6334 VerticalScrollbar verticalScrollBar; 6335 HorizontalScrollbar horizontalScrollBar; 6336 6337 this(ScrollableWidget sw, Widget parent) { 6338 this.sw = sw; 6339 6340 this.tabStop = false; 6341 6342 super(parent); 6343 6344 horizontalScrollBar = new HorizontalScrollbar(this); 6345 verticalScrollBar = new VerticalScrollbar(this); 6346 6347 horizontalScrollBar.showing_ = false; 6348 verticalScrollBar.showing_ = false; 6349 6350 horizontalScrollBar.addEventListener("scrolltonextline", { 6351 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 6352 sw.horizontalScrollTo(horizontalScrollBar.position); 6353 }); 6354 horizontalScrollBar.addEventListener("scrolltopreviousline", { 6355 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 6356 sw.horizontalScrollTo(horizontalScrollBar.position); 6357 }); 6358 verticalScrollBar.addEventListener("scrolltonextline", { 6359 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 6360 sw.verticalScrollTo(verticalScrollBar.position); 6361 }); 6362 verticalScrollBar.addEventListener("scrolltopreviousline", { 6363 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 6364 sw.verticalScrollTo(verticalScrollBar.position); 6365 }); 6366 horizontalScrollBar.addEventListener("scrolltonextpage", { 6367 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 6368 sw.horizontalScrollTo(horizontalScrollBar.position); 6369 }); 6370 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 6371 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 6372 sw.horizontalScrollTo(horizontalScrollBar.position); 6373 }); 6374 verticalScrollBar.addEventListener("scrolltonextpage", { 6375 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 6376 sw.verticalScrollTo(verticalScrollBar.position); 6377 }); 6378 verticalScrollBar.addEventListener("scrolltopreviouspage", { 6379 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 6380 sw.verticalScrollTo(verticalScrollBar.position); 6381 }); 6382 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 6383 horizontalScrollBar.setPosition(event.intValue); 6384 sw.horizontalScrollTo(horizontalScrollBar.position); 6385 }); 6386 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 6387 verticalScrollBar.setPosition(event.intValue); 6388 sw.verticalScrollTo(verticalScrollBar.position); 6389 }); 6390 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 6391 horizontalScrollBar.setPosition(event.intValue); 6392 sw.horizontalScrollTo(horizontalScrollBar.position); 6393 }); 6394 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 6395 verticalScrollBar.setPosition(event.intValue); 6396 }); 6397 } 6398 6399 // this is supposed to be basically invisible... 6400 override int minWidth() { return sw.minWidth; } 6401 override int minHeight() { return sw.minHeight; } 6402 override int maxWidth() { return sw.maxWidth; } 6403 override int maxHeight() { return sw.maxHeight; } 6404 override int widthStretchiness() { return sw.widthStretchiness; } 6405 override int heightStretchiness() { return sw.heightStretchiness; } 6406 override int marginLeft() { return sw.marginLeft; } 6407 override int marginRight() { return sw.marginRight; } 6408 override int marginTop() { return sw.marginTop; } 6409 override int marginBottom() { return sw.marginBottom; } 6410 override int paddingLeft() { return sw.paddingLeft; } 6411 override int paddingRight() { return sw.paddingRight; } 6412 override int paddingTop() { return sw.paddingTop; } 6413 override int paddingBottom() { return sw.paddingBottom; } 6414 override void focus() { sw.focus(); } 6415 6416 6417 override void recomputeChildLayout() { 6418 // The stupid thing needs to calculate if a scroll bar is needed... 6419 recomputeChildLayoutHelper(); 6420 // then running it again will position things correctly if the bar is NOT needed 6421 recomputeChildLayoutHelper(); 6422 6423 // this sucks but meh it barely works 6424 } 6425 6426 private void recomputeChildLayoutHelper() { 6427 if(sw is null) return; 6428 6429 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 6430 if(horizontalScrollBar && verticalScrollBar) { 6431 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 6432 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 6433 horizontalScrollBar.x = 0; 6434 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 6435 6436 verticalScrollBar.width = verticalScrollBar.minWidth(); 6437 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 6438 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 6439 verticalScrollBar.y = 0 + 2; 6440 6441 sw.x = 0; 6442 sw.y = 0; 6443 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 6444 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 6445 6446 if(sw.contentWidth_ <= this.width) 6447 sw.scrollOrigin_.x = 0; 6448 if(sw.contentHeight_ <= this.height) 6449 sw.scrollOrigin_.y = 0; 6450 6451 horizontalScrollBar.recomputeChildLayout(); 6452 verticalScrollBar.recomputeChildLayout(); 6453 sw.recomputeChildLayout(); 6454 } 6455 6456 if(sw.contentWidth_ <= this.width) 6457 sw.scrollOrigin_.x = 0; 6458 if(sw.contentHeight_ <= this.height) 6459 sw.scrollOrigin_.y = 0; 6460 6461 if(sw.showingHorizontalScroll()) 6462 horizontalScrollBar.showing(true, false); 6463 else 6464 horizontalScrollBar.showing(false, false); 6465 if(sw.showingVerticalScroll()) 6466 verticalScrollBar.showing(true, false); 6467 else 6468 verticalScrollBar.showing(false, false); 6469 6470 verticalScrollBar.setViewableArea(sw.viewportHeight()); 6471 verticalScrollBar.setMax(sw.contentHeight); 6472 verticalScrollBar.setPosition(sw.scrollOrigin.y); 6473 6474 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 6475 horizontalScrollBar.setMax(sw.contentWidth); 6476 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 6477 } 6478 } 6479 6480 /* 6481 class ScrollableClientWidget : Widget { 6482 this(Widget parent) { 6483 super(parent); 6484 } 6485 override void paint(WidgetPainter p) { 6486 parent.paint(p); 6487 } 6488 } 6489 */ 6490 6491 /++ 6492 A slider, also known as a trackbar control, is commonly used in applications like volume controls where you want the user to select a value between a min and a max without needing a specific value or otherwise precise input. 6493 +/ 6494 abstract class Slider : Widget { 6495 this(int min, int max, int step, Widget parent) { 6496 min_ = min; 6497 max_ = max; 6498 step_ = step; 6499 page_ = step; 6500 super(parent); 6501 } 6502 6503 private int min_; 6504 private int max_; 6505 private int step_; 6506 private int position_; 6507 private int page_; 6508 6509 // selection start and selection end 6510 // tics 6511 // tooltip? 6512 // some way to see and just type the value 6513 // win32 buddy controls are labels 6514 6515 /// 6516 void setMin(int a) { 6517 min_ = a; 6518 version(custom_widgets) 6519 redraw(); 6520 version(win32_widgets) 6521 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 6522 } 6523 /// 6524 int min() { 6525 return min_; 6526 } 6527 /// 6528 void setMax(int a) { 6529 max_ = a; 6530 version(custom_widgets) 6531 redraw(); 6532 version(win32_widgets) 6533 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 6534 } 6535 /// 6536 int max() { 6537 return max_; 6538 } 6539 /// 6540 void setPosition(int a) { 6541 if(a > max) 6542 a = max; 6543 if(a < min) 6544 a = min; 6545 position_ = a; 6546 version(custom_widgets) 6547 setPositionCustom(a); 6548 6549 version(win32_widgets) 6550 setPositionWindows(a); 6551 } 6552 version(win32_widgets) { 6553 protected abstract void setPositionWindows(int a); 6554 } 6555 6556 protected abstract int win32direction(); 6557 6558 /++ 6559 Alias for [position] for better compatibility with generic code. 6560 6561 History: 6562 Added October 5, 2021 6563 +/ 6564 @property int value() { 6565 return position; 6566 } 6567 6568 /// 6569 int position() { 6570 return position_; 6571 } 6572 /// 6573 void setStep(int a) { 6574 step_ = a; 6575 version(win32_widgets) 6576 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 6577 } 6578 /// 6579 int step() { 6580 return step_; 6581 } 6582 /// 6583 void setPageSize(int a) { 6584 page_ = a; 6585 version(win32_widgets) 6586 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 6587 } 6588 /// 6589 int pageSize() { 6590 return page_; 6591 } 6592 6593 private void notify() { 6594 auto event = new ChangeEvent!int(this, &this.position); 6595 event.dispatch(); 6596 } 6597 6598 version(win32_widgets) 6599 void win32Setup(int style) { 6600 createWin32Window(this, TRACKBAR_CLASS, "", 6601 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 6602 6603 // the trackbar sends the same messages as scroll, which 6604 // our other layer sends as these... just gonna translate 6605 // here 6606 this.addDirectEventListener("scrolltoposition", (Event event) { 6607 event.stopPropagation(); 6608 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 6609 notify(); 6610 }); 6611 this.addDirectEventListener("scrolltonextline", (Event event) { 6612 event.stopPropagation(); 6613 this.setPosition(this.position + this.step_ * this.win32direction); 6614 notify(); 6615 }); 6616 this.addDirectEventListener("scrolltopreviousline", (Event event) { 6617 event.stopPropagation(); 6618 this.setPosition(this.position - this.step_ * this.win32direction); 6619 notify(); 6620 }); 6621 this.addDirectEventListener("scrolltonextpage", (Event event) { 6622 event.stopPropagation(); 6623 this.setPosition(this.position + this.page_ * this.win32direction); 6624 notify(); 6625 }); 6626 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 6627 event.stopPropagation(); 6628 this.setPosition(this.position - this.page_ * this.win32direction); 6629 notify(); 6630 }); 6631 6632 setMin(min_); 6633 setMax(max_); 6634 setStep(step_); 6635 setPageSize(page_); 6636 } 6637 6638 version(custom_widgets) { 6639 protected MouseTrackingWidget thumb; 6640 6641 protected abstract void setPositionCustom(int a); 6642 6643 override void defaultEventHandler_keydown(KeyDownEvent event) { 6644 switch(event.key) { 6645 case Key.Up: 6646 case Key.Right: 6647 setPosition(position() - step() * win32direction); 6648 changed(); 6649 break; 6650 case Key.Down: 6651 case Key.Left: 6652 setPosition(position() + step() * win32direction); 6653 changed(); 6654 break; 6655 case Key.Home: 6656 setPosition(win32direction > 0 ? min() : max()); 6657 changed(); 6658 break; 6659 case Key.End: 6660 setPosition(win32direction > 0 ? max() : min()); 6661 changed(); 6662 break; 6663 case Key.PageUp: 6664 setPosition(position() - pageSize() * win32direction); 6665 changed(); 6666 break; 6667 case Key.PageDown: 6668 setPosition(position() + pageSize() * win32direction); 6669 changed(); 6670 break; 6671 default: 6672 } 6673 super.defaultEventHandler_keydown(event); 6674 } 6675 6676 protected void changed() { 6677 auto ev = new ChangeEvent!int(this, &position); 6678 ev.dispatch(); 6679 } 6680 } 6681 } 6682 6683 /++ 6684 6685 +/ 6686 class VerticalSlider : Slider { 6687 this(int min, int max, int step, Widget parent) { 6688 version(custom_widgets) 6689 initialize(); 6690 6691 super(min, max, step, parent); 6692 6693 version(win32_widgets) 6694 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 6695 } 6696 6697 protected override int win32direction() { 6698 return -1; 6699 } 6700 6701 version(win32_widgets) 6702 protected override void setPositionWindows(int a) { 6703 // the windows thing makes the top 0 and i don't like that. 6704 SendMessage(hwnd, TBM_SETPOS, true, max - a); 6705 } 6706 6707 version(custom_widgets) 6708 private void initialize() { 6709 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 6710 6711 thumb.tabStop = false; 6712 6713 thumb.thumbWidth = width; 6714 thumb.thumbHeight = scaleWithDpi(16); 6715 6716 thumb.addEventListener(EventType.change, () { 6717 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 6718 sx = max - sx; 6719 //informProgramThatUserChangedPosition(sx); 6720 6721 position_ = sx; 6722 6723 changed(); 6724 }); 6725 } 6726 6727 version(custom_widgets) 6728 override void recomputeChildLayout() { 6729 thumb.thumbWidth = this.width; 6730 super.recomputeChildLayout(); 6731 setPositionCustom(position_); 6732 } 6733 6734 version(custom_widgets) 6735 protected override void setPositionCustom(int a) { 6736 if(max()) 6737 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 6738 redraw(); 6739 } 6740 } 6741 6742 /++ 6743 6744 +/ 6745 class HorizontalSlider : Slider { 6746 this(int min, int max, int step, Widget parent) { 6747 version(custom_widgets) 6748 initialize(); 6749 6750 super(min, max, step, parent); 6751 6752 version(win32_widgets) 6753 win32Setup(TBS_HORZ); 6754 } 6755 6756 version(win32_widgets) 6757 protected override void setPositionWindows(int a) { 6758 SendMessage(hwnd, TBM_SETPOS, true, a); 6759 } 6760 6761 protected override int win32direction() { 6762 return 1; 6763 } 6764 6765 version(custom_widgets) 6766 private void initialize() { 6767 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 6768 6769 thumb.tabStop = false; 6770 6771 thumb.thumbWidth = scaleWithDpi(16); 6772 thumb.thumbHeight = height; 6773 6774 thumb.addEventListener(EventType.change, () { 6775 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 6776 //informProgramThatUserChangedPosition(sx); 6777 6778 position_ = sx; 6779 6780 changed(); 6781 }); 6782 } 6783 6784 version(custom_widgets) 6785 override void recomputeChildLayout() { 6786 thumb.thumbHeight = this.height; 6787 super.recomputeChildLayout(); 6788 setPositionCustom(position_); 6789 } 6790 6791 version(custom_widgets) 6792 protected override void setPositionCustom(int a) { 6793 if(max()) 6794 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 6795 redraw(); 6796 } 6797 } 6798 6799 6800 /// 6801 abstract class ScrollbarBase : Widget { 6802 /// 6803 this(Widget parent) { 6804 super(parent); 6805 tabStop = false; 6806 step_ = scaleWithDpi(16); 6807 } 6808 6809 private int viewableArea_; 6810 private int max_; 6811 private int step_;// = 16; 6812 private int position_; 6813 6814 /// 6815 bool atEnd() { 6816 return position_ + viewableArea_ >= max_; 6817 } 6818 6819 /// 6820 bool atStart() { 6821 return position_ == 0; 6822 } 6823 6824 /// 6825 void setViewableArea(int a) { 6826 viewableArea_ = a; 6827 version(custom_widgets) 6828 redraw(); 6829 } 6830 /// 6831 void setMax(int a) { 6832 max_ = a; 6833 version(custom_widgets) 6834 redraw(); 6835 } 6836 /// 6837 int max() { 6838 return max_; 6839 } 6840 /// 6841 void setPosition(int a) { 6842 auto logicalMax = max_ - viewableArea_; 6843 if(a == int.max) 6844 a = logicalMax; 6845 6846 if(a > logicalMax) 6847 a = logicalMax; 6848 if(a < 0) 6849 a = 0; 6850 6851 position_ = a; 6852 6853 version(custom_widgets) 6854 redraw(); 6855 } 6856 /// 6857 int position() { 6858 return position_; 6859 } 6860 /// 6861 void setStep(int a) { 6862 step_ = a; 6863 } 6864 /// 6865 int step() { 6866 return step_; 6867 } 6868 6869 // FIXME: remove this.... maybe 6870 /+ 6871 protected void informProgramThatUserChangedPosition(int n) { 6872 position_ = n; 6873 auto evt = new Event(EventType.change, this); 6874 evt.intValue = n; 6875 evt.dispatch(); 6876 } 6877 +/ 6878 6879 version(custom_widgets) { 6880 enum MIN_THUMB_SIZE = 8; 6881 6882 abstract protected int getBarDim(); 6883 int thumbSize() { 6884 if(viewableArea_ >= max_ || max_ == 0) 6885 return getBarDim(); 6886 6887 int res = viewableArea_ * getBarDim() / max_; 6888 6889 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 6890 res = scaleWithDpi(MIN_THUMB_SIZE); 6891 6892 return res; 6893 } 6894 6895 int thumbPosition() { 6896 /* 6897 viewableArea_ is the viewport height/width 6898 position_ is where we are 6899 */ 6900 //if(position_ + viewableArea_ >= max_) 6901 //return getBarDim - thumbSize; 6902 6903 auto maximumPossibleValue = getBarDim() - thumbSize; 6904 auto maximiumLogicalValue = max_ - viewableArea_; 6905 6906 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 6907 6908 return p; 6909 } 6910 } 6911 } 6912 6913 //public import mgt; 6914 6915 /++ 6916 A mouse tracking widget is one that follows the mouse when dragged inside it. 6917 6918 Concrete subclasses may include a scrollbar thumb and a volume control. 6919 +/ 6920 //version(custom_widgets) 6921 class MouseTrackingWidget : Widget { 6922 6923 /// 6924 int positionX() { return positionX_; } 6925 /// 6926 int positionY() { return positionY_; } 6927 6928 /// 6929 void positionX(int p) { positionX_ = p; } 6930 /// 6931 void positionY(int p) { positionY_ = p; } 6932 6933 private int positionX_; 6934 private int positionY_; 6935 6936 /// 6937 enum Orientation { 6938 horizontal, /// 6939 vertical, /// 6940 twoDimensional, /// 6941 } 6942 6943 private int thumbWidth_; 6944 private int thumbHeight_; 6945 6946 /// 6947 int thumbWidth() { return thumbWidth_; } 6948 /// 6949 int thumbHeight() { return thumbHeight_; } 6950 /// 6951 int thumbWidth(int a) { return thumbWidth_ = a; } 6952 /// 6953 int thumbHeight(int a) { return thumbHeight_ = a; } 6954 6955 private bool dragging; 6956 private bool hovering; 6957 private int startMouseX, startMouseY; 6958 6959 /// 6960 this(Orientation orientation, Widget parent) { 6961 super(parent); 6962 6963 //assert(parentWindow !is null); 6964 6965 addEventListener((MouseDownEvent event) { 6966 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6967 dragging = true; 6968 startMouseX = event.clientX - positionX; 6969 startMouseY = event.clientY - positionY; 6970 parentWindow.captureMouse(this); 6971 } else { 6972 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6973 positionX = event.clientX - thumbWidth / 2; 6974 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6975 positionY = event.clientY - thumbHeight / 2; 6976 6977 if(positionX + thumbWidth > this.width) 6978 positionX = this.width - thumbWidth; 6979 if(positionY + thumbHeight > this.height) 6980 positionY = this.height - thumbHeight; 6981 6982 if(positionX < 0) 6983 positionX = 0; 6984 if(positionY < 0) 6985 positionY = 0; 6986 6987 6988 // this.emit!(ChangeEvent!void)(); 6989 auto evt = new Event(EventType.change, this); 6990 evt.sendDirectly(); 6991 6992 redraw(); 6993 6994 } 6995 }); 6996 6997 addEventListener(EventType.mouseup, (Event event) { 6998 dragging = false; 6999 parentWindow.releaseMouseCapture(); 7000 }); 7001 7002 addEventListener(EventType.mouseout, (Event event) { 7003 if(!hovering) 7004 return; 7005 hovering = false; 7006 redraw(); 7007 }); 7008 7009 int lpx, lpy; 7010 7011 addEventListener((MouseMoveEvent event) { 7012 auto oh = hovering; 7013 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 7014 hovering = true; 7015 } else { 7016 hovering = false; 7017 } 7018 if(!dragging) { 7019 if(hovering != oh) 7020 redraw(); 7021 return; 7022 } 7023 7024 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 7025 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 7026 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 7027 positionY = event.clientY - startMouseY; 7028 7029 if(positionX + thumbWidth > this.width) 7030 positionX = this.width - thumbWidth; 7031 if(positionY + thumbHeight > this.height) 7032 positionY = this.height - thumbHeight; 7033 7034 if(positionX < 0) 7035 positionX = 0; 7036 if(positionY < 0) 7037 positionY = 0; 7038 7039 if(positionX != lpx || positionY != lpy) { 7040 lpx = positionX; 7041 lpy = positionY; 7042 7043 auto evt = new Event(EventType.change, this); 7044 evt.sendDirectly(); 7045 } 7046 7047 redraw(); 7048 }); 7049 } 7050 7051 version(custom_widgets) 7052 override void paint(WidgetPainter painter) { 7053 auto cs = getComputedStyle(); 7054 auto c = darken(cs.windowBackgroundColor, 0.2); 7055 painter.outlineColor = c; 7056 painter.fillColor = c; 7057 painter.drawRectangle(Point(0, 0), this.width, this.height); 7058 7059 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 7060 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 7061 } 7062 } 7063 7064 //version(custom_widgets) 7065 //private 7066 class HorizontalScrollbar : ScrollbarBase { 7067 7068 version(custom_widgets) { 7069 private MouseTrackingWidget thumb; 7070 7071 override int getBarDim() { 7072 return thumb.width; 7073 } 7074 } 7075 7076 override void setViewableArea(int a) { 7077 super.setViewableArea(a); 7078 7079 version(win32_widgets) { 7080 SCROLLINFO info; 7081 info.cbSize = info.sizeof; 7082 info.nPage = a + 1; 7083 info.fMask = SIF_PAGE; 7084 SetScrollInfo(hwnd, SB_CTL, &info, true); 7085 } else version(custom_widgets) { 7086 thumb.positionX = thumbPosition; 7087 thumb.thumbWidth = thumbSize; 7088 thumb.redraw(); 7089 } else static assert(0); 7090 7091 } 7092 7093 override void setMax(int a) { 7094 super.setMax(a); 7095 version(win32_widgets) { 7096 SCROLLINFO info; 7097 info.cbSize = info.sizeof; 7098 info.nMin = 0; 7099 info.nMax = max; 7100 info.fMask = SIF_RANGE; 7101 SetScrollInfo(hwnd, SB_CTL, &info, true); 7102 } else version(custom_widgets) { 7103 thumb.positionX = thumbPosition; 7104 thumb.thumbWidth = thumbSize; 7105 thumb.redraw(); 7106 } 7107 } 7108 7109 override void setPosition(int a) { 7110 super.setPosition(a); 7111 version(win32_widgets) { 7112 SCROLLINFO info; 7113 info.cbSize = info.sizeof; 7114 info.fMask = SIF_POS; 7115 info.nPos = position; 7116 SetScrollInfo(hwnd, SB_CTL, &info, true); 7117 } else version(custom_widgets) { 7118 thumb.positionX = thumbPosition(); 7119 thumb.thumbWidth = thumbSize; 7120 thumb.redraw(); 7121 } else static assert(0); 7122 } 7123 7124 this(Widget parent) { 7125 super(parent); 7126 7127 version(win32_widgets) { 7128 createWin32Window(this, "Scrollbar"w, "", 7129 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 7130 } else version(custom_widgets) { 7131 auto vl = new HorizontalLayout(this); 7132 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 7133 leftButton.setClickRepeat(scrollClickRepeatInterval); 7134 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 7135 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 7136 rightButton.setClickRepeat(scrollClickRepeatInterval); 7137 7138 leftButton.tabStop = false; 7139 rightButton.tabStop = false; 7140 thumb.tabStop = false; 7141 7142 leftButton.addEventListener(EventType.triggered, () { 7143 this.emitCommand!"scrolltopreviousline"(); 7144 //informProgramThatUserChangedPosition(position - step()); 7145 }); 7146 rightButton.addEventListener(EventType.triggered, () { 7147 this.emitCommand!"scrolltonextline"(); 7148 //informProgramThatUserChangedPosition(position + step()); 7149 }); 7150 7151 thumb.thumbWidth = this.minWidth; 7152 thumb.thumbHeight = scaleWithDpi(16); 7153 7154 thumb.addEventListener(EventType.change, () { 7155 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 7156 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 7157 7158 //informProgramThatUserChangedPosition(sx); 7159 7160 auto ev = new ScrollToPositionEvent(this, sx); 7161 ev.dispatch(); 7162 }); 7163 } 7164 } 7165 7166 override int minHeight() { return scaleWithDpi(16); } 7167 override int maxHeight() { return scaleWithDpi(16); } 7168 override int minWidth() { return scaleWithDpi(48); } 7169 } 7170 7171 final class ScrollToPositionEvent : Event { 7172 enum EventString = "scrolltoposition"; 7173 7174 this(Widget target, int value) { 7175 this.value = value; 7176 super(EventString, target); 7177 } 7178 7179 immutable int value; 7180 7181 override @property int intValue() { 7182 return value; 7183 } 7184 } 7185 7186 //version(custom_widgets) 7187 //private 7188 class VerticalScrollbar : ScrollbarBase { 7189 7190 version(custom_widgets) { 7191 override int getBarDim() { 7192 return thumb.height; 7193 } 7194 7195 private MouseTrackingWidget thumb; 7196 } 7197 7198 override void setViewableArea(int a) { 7199 super.setViewableArea(a); 7200 7201 version(win32_widgets) { 7202 SCROLLINFO info; 7203 info.cbSize = info.sizeof; 7204 info.nPage = a + 1; 7205 info.fMask = SIF_PAGE; 7206 SetScrollInfo(hwnd, SB_CTL, &info, true); 7207 } else version(custom_widgets) { 7208 thumb.positionY = thumbPosition; 7209 thumb.thumbHeight = thumbSize; 7210 thumb.redraw(); 7211 } else static assert(0); 7212 7213 } 7214 7215 override void setMax(int a) { 7216 super.setMax(a); 7217 version(win32_widgets) { 7218 SCROLLINFO info; 7219 info.cbSize = info.sizeof; 7220 info.nMin = 0; 7221 info.nMax = max; 7222 info.fMask = SIF_RANGE; 7223 SetScrollInfo(hwnd, SB_CTL, &info, true); 7224 } else version(custom_widgets) { 7225 thumb.positionY = thumbPosition; 7226 thumb.thumbHeight = thumbSize; 7227 thumb.redraw(); 7228 } 7229 } 7230 7231 override void setPosition(int a) { 7232 super.setPosition(a); 7233 version(win32_widgets) { 7234 SCROLLINFO info; 7235 info.cbSize = info.sizeof; 7236 info.fMask = SIF_POS; 7237 info.nPos = position; 7238 SetScrollInfo(hwnd, SB_CTL, &info, true); 7239 } else version(custom_widgets) { 7240 thumb.positionY = thumbPosition; 7241 thumb.thumbHeight = thumbSize; 7242 thumb.redraw(); 7243 } else static assert(0); 7244 } 7245 7246 this(Widget parent) { 7247 super(parent); 7248 7249 version(win32_widgets) { 7250 createWin32Window(this, "Scrollbar"w, "", 7251 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 7252 } else version(custom_widgets) { 7253 auto vl = new VerticalLayout(this); 7254 auto upButton = new ArrowButton(ArrowDirection.up, vl); 7255 upButton.setClickRepeat(scrollClickRepeatInterval); 7256 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 7257 auto downButton = new ArrowButton(ArrowDirection.down, vl); 7258 downButton.setClickRepeat(scrollClickRepeatInterval); 7259 7260 upButton.addEventListener(EventType.triggered, () { 7261 this.emitCommand!"scrolltopreviousline"(); 7262 //informProgramThatUserChangedPosition(position - step()); 7263 }); 7264 downButton.addEventListener(EventType.triggered, () { 7265 this.emitCommand!"scrolltonextline"(); 7266 //informProgramThatUserChangedPosition(position + step()); 7267 }); 7268 7269 thumb.thumbWidth = this.minWidth; 7270 thumb.thumbHeight = scaleWithDpi(16); 7271 7272 thumb.addEventListener(EventType.change, () { 7273 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 7274 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 7275 7276 auto ev = new ScrollToPositionEvent(this, sy); 7277 ev.dispatch(); 7278 7279 //informProgramThatUserChangedPosition(sy); 7280 }); 7281 7282 upButton.tabStop = false; 7283 downButton.tabStop = false; 7284 thumb.tabStop = false; 7285 } 7286 } 7287 7288 override int minWidth() { return scaleWithDpi(16); } 7289 override int maxWidth() { return scaleWithDpi(16); } 7290 override int minHeight() { return scaleWithDpi(48); } 7291 } 7292 7293 7294 /++ 7295 EXPERIMENTAL 7296 7297 A widget specialized for being a container for other widgets. 7298 7299 History: 7300 Added May 29, 2021. Not stabilized at this time. 7301 +/ 7302 class WidgetContainer : Widget { 7303 this(Widget parent) { 7304 tabStop = false; 7305 super(parent); 7306 } 7307 7308 override int maxHeight() { 7309 if(this.children.length == 1) { 7310 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 7311 } else { 7312 return int.max; 7313 } 7314 } 7315 7316 override int maxWidth() { 7317 if(this.children.length == 1) { 7318 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 7319 } else { 7320 return int.max; 7321 } 7322 } 7323 7324 /+ 7325 7326 override int minHeight() { 7327 int largest = 0; 7328 int margins = 0; 7329 int lastMargin = 0; 7330 foreach(child; children) { 7331 auto mh = child.minHeight(); 7332 if(mh > largest) 7333 largest = mh; 7334 margins += mymax(lastMargin, child.marginTop()); 7335 lastMargin = child.marginBottom(); 7336 } 7337 return largest + margins; 7338 } 7339 7340 override int maxHeight() { 7341 int largest = 0; 7342 int margins = 0; 7343 int lastMargin = 0; 7344 foreach(child; children) { 7345 auto mh = child.maxHeight(); 7346 if(mh == int.max) 7347 return int.max; 7348 if(mh > largest) 7349 largest = mh; 7350 margins += mymax(lastMargin, child.marginTop()); 7351 lastMargin = child.marginBottom(); 7352 } 7353 return largest + margins; 7354 } 7355 7356 override int minWidth() { 7357 int min; 7358 foreach(child; children) { 7359 auto cm = child.minWidth; 7360 if(cm > min) 7361 min = cm; 7362 } 7363 return min + paddingLeft + paddingRight; 7364 } 7365 7366 override int minHeight() { 7367 int min; 7368 foreach(child; children) { 7369 auto cm = child.minHeight; 7370 if(cm > min) 7371 min = cm; 7372 } 7373 return min + paddingTop + paddingBottom; 7374 } 7375 7376 override int maxHeight() { 7377 int largest = 0; 7378 int margins = 0; 7379 int lastMargin = 0; 7380 foreach(child; children) { 7381 auto mh = child.maxHeight(); 7382 if(mh == int.max) 7383 return int.max; 7384 if(mh > largest) 7385 largest = mh; 7386 margins += mymax(lastMargin, child.marginTop()); 7387 lastMargin = child.marginBottom(); 7388 } 7389 return largest + margins; 7390 } 7391 7392 override int heightStretchiness() { 7393 int max; 7394 foreach(child; children) { 7395 auto c = child.heightStretchiness; 7396 if(c > max) 7397 max = c; 7398 } 7399 return max; 7400 } 7401 7402 override int marginTop() { 7403 if(this.children.length) 7404 return this.children[0].marginTop; 7405 return 0; 7406 } 7407 +/ 7408 } 7409 7410 /// 7411 abstract class Layout : Widget { 7412 this(Widget parent) { 7413 tabStop = false; 7414 super(parent); 7415 } 7416 } 7417 7418 /++ 7419 Makes all children minimum width and height, placing them down 7420 left to right, top to bottom. 7421 7422 Useful if you want to make a list of buttons that automatically 7423 wrap to a new line when necessary. 7424 +/ 7425 class InlineBlockLayout : Layout { 7426 /// 7427 this(Widget parent) { super(parent); } 7428 7429 override void recomputeChildLayout() { 7430 registerMovement(); 7431 7432 int x = this.paddingLeft, y = this.paddingTop; 7433 7434 int lineHeight; 7435 int previousMargin = 0; 7436 int previousMarginBottom = 0; 7437 7438 foreach(child; children) { 7439 if(child.hidden) 7440 continue; 7441 if(cast(FixedPosition) child) { 7442 child.recomputeChildLayout(); 7443 continue; 7444 } 7445 child.width = child.flexBasisWidth(); 7446 if(child.width == 0) 7447 child.width = child.minWidth(); 7448 if(child.width == 0) 7449 child.width = 32; 7450 7451 child.height = child.flexBasisHeight(); 7452 if(child.height == 0) 7453 child.height = child.minHeight(); 7454 if(child.height == 0) 7455 child.height = 32; 7456 7457 if(x + child.width + paddingRight > this.width) { 7458 x = this.paddingLeft; 7459 y += lineHeight; 7460 lineHeight = 0; 7461 previousMargin = 0; 7462 previousMarginBottom = 0; 7463 } 7464 7465 auto margin = child.marginLeft; 7466 if(previousMargin > margin) 7467 margin = previousMargin; 7468 7469 x += margin; 7470 7471 child.x = x; 7472 child.y = y; 7473 7474 int marginTopApplied; 7475 if(child.marginTop > previousMarginBottom) { 7476 child.y += child.marginTop; 7477 marginTopApplied = child.marginTop; 7478 } 7479 7480 x += child.width; 7481 previousMargin = child.marginRight; 7482 7483 if(child.marginBottom > previousMarginBottom) 7484 previousMarginBottom = child.marginBottom; 7485 7486 auto h = child.height + previousMarginBottom + marginTopApplied; 7487 if(h > lineHeight) 7488 lineHeight = h; 7489 7490 child.recomputeChildLayout(); 7491 } 7492 7493 } 7494 7495 override int minWidth() { 7496 int min; 7497 foreach(child; children) { 7498 auto cm = child.minWidth; 7499 if(cm > min) 7500 min = cm; 7501 } 7502 return min + paddingLeft + paddingRight; 7503 } 7504 7505 override int minHeight() { 7506 int min; 7507 foreach(child; children) { 7508 auto cm = child.minHeight; 7509 if(cm > min) 7510 min = cm; 7511 } 7512 return min + paddingTop + paddingBottom; 7513 } 7514 } 7515 7516 /++ 7517 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 7518 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 7519 the [TabWidget] will automatically change pages of child widgets. 7520 7521 This allows you to react to it however you see fit rather than having to 7522 be tied to just the new sets of child widgets. 7523 7524 It sends the message in the form of `this.emitCommand!"changetab"();`. 7525 7526 History: 7527 Added December 24, 2021 (dub v10.5) 7528 +/ 7529 class TabMessageWidget : Widget { 7530 7531 protected void tabIndexClicked(int item) { 7532 this.emitCommand!"changetab"(); 7533 } 7534 7535 /++ 7536 Adds the a new tab to the control with the given title. 7537 7538 Returns: 7539 The index of the newly added tab. You will need to know 7540 this index to refer to it later and to know which tab to 7541 change to when you get a changetab message. 7542 +/ 7543 int addTab(string title, int pos = int.max) { 7544 version(win32_widgets) { 7545 TCITEM item; 7546 item.mask = TCIF_TEXT; 7547 WCharzBuffer buf = WCharzBuffer(title); 7548 item.pszText = buf.ptr; 7549 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 7550 } else version(custom_widgets) { 7551 if(pos >= tabs.length) { 7552 tabs ~= title; 7553 redraw(); 7554 return cast(int) tabs.length - 1; 7555 } else if(pos <= 0) { 7556 tabs = title ~ tabs; 7557 redraw(); 7558 return 0; 7559 } else { 7560 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 7561 redraw(); 7562 return pos; 7563 } 7564 } 7565 } 7566 7567 override void addChild(Widget child, int pos = int.max) { 7568 if(container) 7569 container.addChild(child, pos); 7570 else 7571 super.addChild(child, pos); 7572 } 7573 7574 protected Widget makeContainer() { 7575 return new Widget(this); 7576 } 7577 7578 private Widget container; 7579 7580 override void recomputeChildLayout() { 7581 version(win32_widgets) { 7582 this.registerMovement(); 7583 7584 RECT rect; 7585 GetWindowRect(hwnd, &rect); 7586 7587 auto left = rect.left; 7588 auto top = rect.top; 7589 7590 TabCtrl_AdjustRect(hwnd, false, &rect); 7591 foreach(child; children) { 7592 if(!child.showing) continue; 7593 child.x = rect.left - left; 7594 child.y = rect.top - top; 7595 child.width = rect.right - rect.left; 7596 child.height = rect.bottom - rect.top; 7597 child.recomputeChildLayout(); 7598 } 7599 } else version(custom_widgets) { 7600 this.registerMovement(); 7601 foreach(child; children) { 7602 if(!child.showing) continue; 7603 child.x = 2; 7604 child.y = tabBarHeight + 2; // for the border 7605 child.width = width - 4; // for the border 7606 child.height = height - tabBarHeight - 2 - 2; // for the border 7607 child.recomputeChildLayout(); 7608 } 7609 } else static assert(0); 7610 } 7611 7612 version(custom_widgets) 7613 string[] tabs; 7614 7615 this(Widget parent) { 7616 super(parent); 7617 7618 tabStop = false; 7619 7620 version(win32_widgets) { 7621 createWin32Window(this, WC_TABCONTROL, "", 0); 7622 } else version(custom_widgets) { 7623 addEventListener((ClickEvent event) { 7624 if(event.target !is this) 7625 return; 7626 if(event.clientY >= 0 && event.clientY < tabBarHeight) { 7627 auto t = (event.clientX / tabWidth); 7628 if(t >= 0 && t < tabs.length) { 7629 currentTab_ = t; 7630 tabIndexClicked(t); 7631 redraw(); 7632 } 7633 } 7634 }); 7635 } else static assert(0); 7636 7637 this.container = makeContainer(); 7638 } 7639 7640 override int marginTop() { return 4; } 7641 override int paddingBottom() { return 4; } 7642 7643 override int minHeight() { 7644 int max = 0; 7645 foreach(child; children) 7646 max = mymax(child.minHeight, max); 7647 7648 7649 version(win32_widgets) { 7650 RECT rect; 7651 rect.right = this.width; 7652 rect.bottom = max; 7653 TabCtrl_AdjustRect(hwnd, true, &rect); 7654 7655 max = rect.bottom; 7656 } else { 7657 max += defaultLineHeight + 4; 7658 } 7659 7660 7661 return max; 7662 } 7663 7664 version(win32_widgets) 7665 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 7666 switch(code) { 7667 case TCN_SELCHANGE: 7668 auto sel = TabCtrl_GetCurSel(hwnd); 7669 tabIndexClicked(sel); 7670 break; 7671 default: 7672 } 7673 return 0; 7674 } 7675 7676 version(custom_widgets) { 7677 private int currentTab_; 7678 private int tabBarHeight() { return defaultLineHeight; } 7679 int tabWidth() { return scaleWithDpi(80); } 7680 } 7681 7682 version(win32_widgets) 7683 override void paint(WidgetPainter painter) {} 7684 7685 version(custom_widgets) 7686 override void paint(WidgetPainter painter) { 7687 auto cs = getComputedStyle(); 7688 7689 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 7690 7691 int posX = 0; 7692 foreach(idx, title; tabs) { 7693 auto isCurrent = idx == getCurrentTab(); 7694 7695 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 7696 7697 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 7698 painter.outlineColor = cs.foregroundColor; 7699 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 7700 7701 if(isCurrent) { 7702 painter.outlineColor = cs.windowBackgroundColor; 7703 painter.fillColor = Color.transparent; 7704 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 7705 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 7706 7707 painter.outlineColor = Color.white; 7708 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 7709 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 7710 painter.outlineColor = cs.activeTabColor; 7711 painter.drawPixel(Point(posX, tabBarHeight - 1)); 7712 } 7713 7714 posX += tabWidth - 2; 7715 } 7716 } 7717 7718 /// 7719 @scriptable 7720 void setCurrentTab(int item) { 7721 version(win32_widgets) 7722 TabCtrl_SetCurSel(hwnd, item); 7723 else version(custom_widgets) 7724 currentTab_ = item; 7725 else static assert(0); 7726 7727 tabIndexClicked(item); 7728 } 7729 7730 /// 7731 @scriptable 7732 int getCurrentTab() { 7733 version(win32_widgets) 7734 return TabCtrl_GetCurSel(hwnd); 7735 else version(custom_widgets) 7736 return currentTab_; // FIXME 7737 else static assert(0); 7738 } 7739 7740 /// 7741 @scriptable 7742 void removeTab(int item) { 7743 if(item && item == getCurrentTab()) 7744 setCurrentTab(item - 1); 7745 7746 version(win32_widgets) { 7747 TabCtrl_DeleteItem(hwnd, item); 7748 } 7749 7750 for(int a = item; a < children.length - 1; a++) 7751 this._children[a] = this._children[a + 1]; 7752 this._children = this._children[0 .. $-1]; 7753 } 7754 7755 } 7756 7757 7758 /++ 7759 A tab widget is a set of clickable tab buttons followed by a content area. 7760 7761 7762 Tabs can change existing content or can be new pages. 7763 7764 When the user picks a different tab, a `change` message is generated. 7765 +/ 7766 class TabWidget : TabMessageWidget { 7767 this(Widget parent) { 7768 super(parent); 7769 } 7770 7771 override protected Widget makeContainer() { 7772 return null; 7773 } 7774 7775 override void addChild(Widget child, int pos = int.max) { 7776 if(auto twp = cast(TabWidgetPage) child) { 7777 Widget.addChild(child, pos); 7778 if(pos == int.max) 7779 pos = cast(int) this.children.length - 1; 7780 7781 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 7782 7783 if(pos != getCurrentTab) { 7784 child.showing = false; 7785 } 7786 } else { 7787 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 7788 } 7789 } 7790 7791 // FIXME: add tab icons at some point, Windows supports them 7792 /++ 7793 Adds a page and its associated tab with the given label to the widget. 7794 7795 Returns: 7796 The added page object, to which you can add other widgets. 7797 +/ 7798 @scriptable 7799 TabWidgetPage addPage(string title) { 7800 return new TabWidgetPage(title, this); 7801 } 7802 7803 /++ 7804 Gets the page at the given tab index, or `null` if the index is bad. 7805 7806 History: 7807 Added December 24, 2021. 7808 +/ 7809 TabWidgetPage getPage(int index) { 7810 if(index < this.children.length) 7811 return null; 7812 return cast(TabWidgetPage) this.children[index]; 7813 } 7814 7815 /++ 7816 While you can still use the addTab from the parent class, 7817 *strongly* recommend you use [addPage] insteaad. 7818 7819 History: 7820 Added December 24, 2021 to fulful the interface 7821 requirement that came from adding [TabMessageWidget]. 7822 7823 You should not use it though since the [addPage] function 7824 is much easier to use here. 7825 +/ 7826 override int addTab(string title, int pos = int.max) { 7827 auto p = addPage(title); 7828 foreach(idx, child; this.children) 7829 if(child is p) 7830 return cast(int) idx; 7831 return -1; 7832 } 7833 7834 protected override void tabIndexClicked(int item) { 7835 foreach(idx, child; children) { 7836 child.showing(false, false); // batch the recalculates for the end 7837 } 7838 7839 foreach(idx, child; children) { 7840 if(idx == item) { 7841 child.showing(true, false); 7842 if(parentWindow) { 7843 auto f = parentWindow.getFirstFocusable(child); 7844 if(f) 7845 f.focus(); 7846 } 7847 recomputeChildLayout(); 7848 } 7849 } 7850 7851 version(win32_widgets) { 7852 InvalidateRect(hwnd, null, true); 7853 } else version(custom_widgets) { 7854 this.redraw(); 7855 } 7856 } 7857 7858 } 7859 7860 /++ 7861 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 7862 7863 You add [TabWidgetPage]s to it. 7864 +/ 7865 class PageWidget : Widget { 7866 this(Widget parent) { 7867 super(parent); 7868 } 7869 7870 override int minHeight() { 7871 int max = 0; 7872 foreach(child; children) 7873 max = mymax(child.minHeight, max); 7874 7875 return max; 7876 } 7877 7878 7879 override void addChild(Widget child, int pos = int.max) { 7880 if(auto twp = cast(TabWidgetPage) child) { 7881 super.addChild(child, pos); 7882 if(pos == int.max) 7883 pos = cast(int) this.children.length - 1; 7884 7885 if(pos != getCurrentTab) { 7886 child.showing = false; 7887 } 7888 } else { 7889 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 7890 } 7891 } 7892 7893 override void recomputeChildLayout() { 7894 this.registerMovement(); 7895 foreach(child; children) { 7896 child.x = 0; 7897 child.y = 0; 7898 child.width = width; 7899 child.height = height; 7900 child.recomputeChildLayout(); 7901 } 7902 } 7903 7904 private int currentTab_; 7905 7906 /// 7907 @scriptable 7908 void setCurrentTab(int item) { 7909 currentTab_ = item; 7910 7911 showOnly(item); 7912 } 7913 7914 /// 7915 @scriptable 7916 int getCurrentTab() { 7917 return currentTab_; 7918 } 7919 7920 /// 7921 @scriptable 7922 void removeTab(int item) { 7923 if(item && item == getCurrentTab()) 7924 setCurrentTab(item - 1); 7925 7926 for(int a = item; a < children.length - 1; a++) 7927 this._children[a] = this._children[a + 1]; 7928 this._children = this._children[0 .. $-1]; 7929 } 7930 7931 /// 7932 @scriptable 7933 TabWidgetPage addPage(string title) { 7934 return new TabWidgetPage(title, this); 7935 } 7936 7937 private void showOnly(int item) { 7938 foreach(idx, child; children) 7939 if(idx == item) { 7940 child.show(); 7941 child.queueRecomputeChildLayout(); 7942 } else { 7943 child.hide(); 7944 } 7945 } 7946 } 7947 7948 /++ 7949 7950 +/ 7951 class TabWidgetPage : Widget { 7952 string title; 7953 this(string title, Widget parent) { 7954 this.title = title; 7955 this.tabStop = false; 7956 super(parent); 7957 7958 ///* 7959 version(win32_widgets) { 7960 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 7961 } 7962 //*/ 7963 } 7964 7965 override int minHeight() { 7966 int sum = 0; 7967 foreach(child; children) 7968 sum += child.minHeight(); 7969 return sum; 7970 } 7971 } 7972 7973 version(none) 7974 /++ 7975 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 7976 7977 I think I need to modify the layout algorithms to support this. 7978 +/ 7979 class CollapsableSidebar : Widget { 7980 7981 } 7982 7983 /// Stacks the widgets vertically, taking all the available width for each child. 7984 class VerticalLayout : Layout { 7985 // most of this is intentionally blank - widget's default is vertical layout right now 7986 /// 7987 this(Widget parent) { super(parent); } 7988 7989 /++ 7990 Sets a max width for the layout so you don't have to subclass. The max width 7991 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7992 7993 History: 7994 Added November 29, 2021 (dub v10.5) 7995 +/ 7996 this(int maxWidth, Widget parent) { 7997 this.mw = maxWidth; 7998 super(parent); 7999 } 8000 8001 private int mw = int.max; 8002 8003 override int maxWidth() { return scaleWithDpi(mw); } 8004 } 8005 8006 /// Stacks the widgets horizontally, taking all the available height for each child. 8007 class HorizontalLayout : Layout { 8008 /// 8009 this(Widget parent) { super(parent); } 8010 8011 /++ 8012 Sets a max height for the layout so you don't have to subclass. The max height 8013 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 8014 8015 History: 8016 Added November 29, 2021 (dub v10.5) 8017 +/ 8018 this(int maxHeight, Widget parent) { 8019 this.mh = maxHeight; 8020 super(parent); 8021 } 8022 8023 private int mh = 0; 8024 8025 8026 8027 override void recomputeChildLayout() { 8028 .recomputeChildLayout!"width"(this); 8029 } 8030 8031 override int minHeight() { 8032 int largest = 0; 8033 int margins = 0; 8034 int lastMargin = 0; 8035 foreach(child; children) { 8036 auto mh = child.minHeight(); 8037 if(mh > largest) 8038 largest = mh; 8039 margins += mymax(lastMargin, child.marginTop()); 8040 lastMargin = child.marginBottom(); 8041 } 8042 return largest + margins; 8043 } 8044 8045 override int maxHeight() { 8046 if(mh != 0) 8047 return mymax(minHeight, scaleWithDpi(mh)); 8048 8049 int largest = 0; 8050 int margins = 0; 8051 int lastMargin = 0; 8052 foreach(child; children) { 8053 auto mh = child.maxHeight(); 8054 if(mh == int.max) 8055 return int.max; 8056 if(mh > largest) 8057 largest = mh; 8058 margins += mymax(lastMargin, child.marginTop()); 8059 lastMargin = child.marginBottom(); 8060 } 8061 return largest + margins; 8062 } 8063 8064 override int heightStretchiness() { 8065 int max; 8066 foreach(child; children) { 8067 auto c = child.heightStretchiness; 8068 if(c > max) 8069 max = c; 8070 } 8071 return max; 8072 } 8073 } 8074 8075 version(win32_widgets) 8076 private 8077 extern(Windows) 8078 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 8079 Widget* pwin = hwnd in Widget.nativeMapping; 8080 if(pwin is null) 8081 return DefWindowProc(hwnd, message, wparam, lparam); 8082 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 8083 if(win is null) 8084 return DefWindowProc(hwnd, message, wparam, lparam); 8085 8086 switch(message) { 8087 case WM_SIZE: 8088 auto width = LOWORD(lparam); 8089 auto height = HIWORD(lparam); 8090 8091 auto hdc = GetDC(hwnd); 8092 auto hdcBmp = CreateCompatibleDC(hdc); 8093 8094 // FIXME: could this be more efficient? it never relinquishes a large bitmap 8095 if(width > win.bmpWidth || height > win.bmpHeight) { 8096 auto oldBuffer = win.buffer; 8097 win.buffer = CreateCompatibleBitmap(hdc, width, height); 8098 8099 if(oldBuffer) 8100 DeleteObject(oldBuffer); 8101 8102 win.bmpWidth = width; 8103 win.bmpHeight = height; 8104 } 8105 8106 // just always erase it upon resizing so minigui can draw over with a clean slate 8107 auto oldBmp = SelectObject(hdcBmp, win.buffer); 8108 8109 auto brush = GetSysColorBrush(COLOR_3DFACE); 8110 RECT r; 8111 r.left = 0; 8112 r.top = 0; 8113 r.right = width; 8114 r.bottom = height; 8115 FillRect(hdcBmp, &r, brush); 8116 8117 SelectObject(hdcBmp, oldBmp); 8118 DeleteDC(hdcBmp); 8119 ReleaseDC(hwnd, hdc); 8120 break; 8121 case WM_PAINT: 8122 if(win.buffer is null) 8123 goto default; 8124 8125 BITMAP bm; 8126 PAINTSTRUCT ps; 8127 8128 HDC hdc = BeginPaint(hwnd, &ps); 8129 8130 HDC hdcMem = CreateCompatibleDC(hdc); 8131 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 8132 8133 GetObject(win.buffer, bm.sizeof, &bm); 8134 8135 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 8136 8137 SelectObject(hdcMem, hbmOld); 8138 DeleteDC(hdcMem); 8139 EndPaint(hwnd, &ps); 8140 break; 8141 default: 8142 return DefWindowProc(hwnd, message, wparam, lparam); 8143 } 8144 8145 return 0; 8146 } 8147 8148 private wstring Win32Class(wstring name)() { 8149 static bool classRegistered; 8150 if(!classRegistered) { 8151 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 8152 WNDCLASSEX wc; 8153 wc.cbSize = wc.sizeof; 8154 wc.hInstance = hInstance; 8155 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 8156 wc.lpfnWndProc = &DoubleBufferWndProc; 8157 wc.lpszClassName = name.ptr; 8158 if(!RegisterClassExW(&wc)) 8159 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 8160 classRegistered = true; 8161 } 8162 8163 return name; 8164 } 8165 8166 /+ 8167 version(win32_widgets) 8168 extern(Windows) 8169 private 8170 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 8171 switch(iMessage) { 8172 case WM_PAINT: 8173 if(auto te = hWnd in Widget.nativeMapping) { 8174 try { 8175 //te.redraw(); 8176 writeln(te, " drawing"); 8177 } catch(Exception) {} 8178 } 8179 return DefWindowProc(hWnd, iMessage, wParam, lParam); 8180 default: 8181 return DefWindowProc(hWnd, iMessage, wParam, lParam); 8182 } 8183 } 8184 +/ 8185 8186 8187 /++ 8188 A widget specifically designed to hold other widgets. 8189 8190 History: 8191 Added July 1, 2021 8192 +/ 8193 class ContainerWidget : Widget { 8194 this(Widget parent) { 8195 super(parent); 8196 this.tabStop = false; 8197 8198 version(win32_widgets) { 8199 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 8200 } 8201 } 8202 } 8203 8204 /++ 8205 A widget that takes your widget, puts scroll bars around it, and sends 8206 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 8207 no effort to automatically scroll or clip its child widgets - it just sends 8208 the messages. 8209 8210 8211 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 8212 The scroll coordinates are all given in a unit you interpret as you wish. One 8213 of these units is moved on each press of the arrow buttons and represents the 8214 smallest amount the user can scroll. The intention is for this to be one line, 8215 one item in a list, one row in a table, etc. Whatever makes sense for your widget 8216 in each direction that the user might be interested in. 8217 8218 You can set a "page size" with the [step] property. (Yes, I regret the name...) 8219 This is the amount it jumps when the user pressed page up and page down, or clicks 8220 in the exposed part of the scroll bar. 8221 8222 You should add child content to the ScrollMessageWidget. However, it is important to 8223 note that the coordinates are always independent of the scroll position! It is YOUR 8224 responsibility to do any necessary transforms, clipping, etc., while drawing the 8225 content and interpreting mouse events if they are supposed to change with the scroll. 8226 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 8227 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 8228 you more control (which can be considerably more efficient and adapted to your actual data) 8229 at the expense of you also needing to be aware of its reality. 8230 8231 Please note that it does NOT react to mouse wheel events or various keyboard events as of 8232 version 10.3. Maybe this will change in the future.... but for now you must call 8233 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 8234 +/ 8235 class ScrollMessageWidget : Widget { 8236 this(Widget parent) { 8237 super(parent); 8238 8239 container = new Widget(this); 8240 hsb = new HorizontalScrollbar(this); 8241 vsb = new VerticalScrollbar(this); 8242 8243 hsb.addEventListener("scrolltonextline", { 8244 hsb.setPosition(hsb.position + movementPerButtonClickH_); 8245 notify(); 8246 }); 8247 hsb.addEventListener("scrolltopreviousline", { 8248 hsb.setPosition(hsb.position - movementPerButtonClickH_); 8249 notify(); 8250 }); 8251 vsb.addEventListener("scrolltonextline", { 8252 vsb.setPosition(vsb.position + movementPerButtonClickV_); 8253 notify(); 8254 }); 8255 vsb.addEventListener("scrolltopreviousline", { 8256 vsb.setPosition(vsb.position - movementPerButtonClickV_); 8257 notify(); 8258 }); 8259 hsb.addEventListener("scrolltonextpage", { 8260 hsb.setPosition(hsb.position + hsb.step_); 8261 notify(); 8262 }); 8263 hsb.addEventListener("scrolltopreviouspage", { 8264 hsb.setPosition(hsb.position - hsb.step_); 8265 notify(); 8266 }); 8267 vsb.addEventListener("scrolltonextpage", { 8268 vsb.setPosition(vsb.position + vsb.step_); 8269 notify(); 8270 }); 8271 vsb.addEventListener("scrolltopreviouspage", { 8272 vsb.setPosition(vsb.position - vsb.step_); 8273 notify(); 8274 }); 8275 hsb.addEventListener("scrolltoposition", (Event event) { 8276 hsb.setPosition(event.intValue); 8277 notify(); 8278 }); 8279 vsb.addEventListener("scrolltoposition", (Event event) { 8280 vsb.setPosition(event.intValue); 8281 notify(); 8282 }); 8283 8284 8285 tabStop = false; 8286 container.tabStop = false; 8287 magic = true; 8288 } 8289 8290 private int movementPerButtonClickH_ = 1; 8291 private int movementPerButtonClickV_ = 1; 8292 public void movementPerButtonClick(int h, int v) { 8293 movementPerButtonClickH_ = h; 8294 movementPerButtonClickV_ = v; 8295 } 8296 8297 /++ 8298 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 8299 8300 8301 The defaults for [addDefaultWheelListeners] are: 8302 8303 $(LIST 8304 * Mouse wheel scrolls vertically 8305 * Alt key + mouse wheel scrolls horiontally 8306 * Shift + mouse wheel scrolls faster. 8307 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 8308 ) 8309 8310 The defaults for [addDefaultKeyboardListeners] are: 8311 8312 $(LIST 8313 * Arrow keys scroll by the given amounts 8314 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 8315 * Page up and down scroll by the vertical viewable area 8316 * Home and end scroll to the start and end of the verticle viewable area. 8317 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 8318 ) 8319 8320 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 8321 8322 Params: 8323 horizontalArrowScrollAmount = 8324 verticalArrowScrollAmount = 8325 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 8326 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 8327 shiftMultiplier = multiplies the scroll amount by this when shift is held 8328 +/ 8329 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 8330 defaultKeyboardListener_verticalArrowScrollAmount = verticalArrowScrollAmount; 8331 defaultKeyboardListener_horizontalArrowScrollAmount = horizontalArrowScrollAmount; 8332 defaultKeyboardListener_shiftMultiplier = shiftMultiplier; 8333 8334 container.addEventListener(&defaultKeyboardListener); 8335 } 8336 8337 /// ditto 8338 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 8339 auto _this = this; 8340 container.addEventListener((scope ClickEvent ce) { 8341 8342 //if(ce.target && ce.target.tabStop) 8343 //ce.target.focus(); 8344 8345 // ctrl is reserved for the application 8346 if(ce.ctrlKey) 8347 return; 8348 8349 if(horizontalWheelScrollAmount == 0 && ce.altKey) 8350 return; 8351 8352 if(shiftMultiplier == 0 && ce.shiftKey) 8353 return; 8354 8355 if(ce.button == MouseButton.wheelDown) { 8356 if(ce.altKey) 8357 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8358 else 8359 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8360 } else if(ce.button == MouseButton.wheelUp) { 8361 if(ce.altKey) 8362 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8363 else 8364 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8365 } 8366 }); 8367 } 8368 8369 int defaultKeyboardListener_verticalArrowScrollAmount = 1; 8370 int defaultKeyboardListener_horizontalArrowScrollAmount = 1; 8371 int defaultKeyboardListener_shiftMultiplier = 3; 8372 8373 void defaultKeyboardListener(scope KeyDownEvent ke) { 8374 switch(ke.key) { 8375 case Key.Left: 8376 this.scrollLeft(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8377 break; 8378 case Key.Right: 8379 this.scrollRight(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8380 break; 8381 case Key.Up: 8382 this.scrollUp(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8383 break; 8384 case Key.Down: 8385 this.scrollDown(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8386 break; 8387 case Key.PageUp: 8388 if(ke.altKey) 8389 this.scrollLeft(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8390 else 8391 this.scrollUp(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8392 break; 8393 case Key.PageDown: 8394 if(ke.altKey) 8395 this.scrollRight(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8396 else 8397 this.scrollDown(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8398 break; 8399 case Key.Home: 8400 if(ke.altKey) 8401 this.scrollLeft(short.max * 16); 8402 else 8403 this.scrollUp(short.max * 16); 8404 break; 8405 case Key.End: 8406 if(ke.altKey) 8407 this.scrollRight(short.max * 16); 8408 else 8409 this.scrollDown(short.max * 16); 8410 break; 8411 8412 default: 8413 // ignore, not for us. 8414 } 8415 } 8416 8417 /++ 8418 Scrolls the given amount. 8419 8420 History: 8421 The scroll up and down functions was here in the initial release of the class, but the `amount` parameter and left/right functions were added on September 28, 2021. 8422 +/ 8423 void scrollUp(int amount = 1) { 8424 vsb.setPosition(vsb.position.NonOverflowingInt - amount); 8425 notify(); 8426 } 8427 /// ditto 8428 void scrollDown(int amount = 1) { 8429 vsb.setPosition(vsb.position.NonOverflowingInt + amount); 8430 notify(); 8431 } 8432 /// ditto 8433 void scrollLeft(int amount = 1) { 8434 hsb.setPosition(hsb.position.NonOverflowingInt - amount); 8435 notify(); 8436 } 8437 /// ditto 8438 void scrollRight(int amount = 1) { 8439 hsb.setPosition(hsb.position.NonOverflowingInt + amount); 8440 notify(); 8441 } 8442 8443 /// 8444 VerticalScrollbar verticalScrollBar() { return vsb; } 8445 /// 8446 HorizontalScrollbar horizontalScrollBar() { return hsb; } 8447 8448 void notify() { 8449 static bool insideNotify; 8450 8451 if(insideNotify) 8452 return; // avoid the recursive call, even if it isn't strictly correct 8453 8454 insideNotify = true; 8455 scope(exit) insideNotify = false; 8456 8457 this.emit!ScrollEvent(); 8458 } 8459 8460 mixin Emits!ScrollEvent; 8461 8462 /// 8463 Point position() { 8464 return Point(hsb.position, vsb.position); 8465 } 8466 8467 /// 8468 void setPosition(int x, int y) { 8469 hsb.setPosition(x); 8470 vsb.setPosition(y); 8471 } 8472 8473 /// 8474 void setPageSize(int unitsX, int unitsY) { 8475 hsb.setStep(unitsX); 8476 vsb.setStep(unitsY); 8477 } 8478 8479 /// Always call this BEFORE setViewableArea 8480 void setTotalArea(int width, int height) { 8481 hsb.setMax(width); 8482 vsb.setMax(height); 8483 } 8484 8485 /++ 8486 Always set the viewable area AFTER setitng the total area if you are going to change both. 8487 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 8488 If you need to do that, use [queueRecomputeChildLayout]. 8489 +/ 8490 void setViewableArea(int width, int height) { 8491 8492 // actually there IS A need to dothis cuz the max might have changed since then 8493 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 8494 //return; // no need to do what is already done 8495 hsb.setViewableArea(width); 8496 vsb.setViewableArea(height); 8497 8498 bool needsNotify = false; 8499 8500 // FIXME: if at any point the rhs is outside the scrollbar, we need 8501 // to reset to 0. but it should remember the old position in case the 8502 // window resizes again, so it can kinda return ot where it was. 8503 // 8504 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 8505 if(width >= hsb.max) { 8506 // there's plenty of room to display it all so we need to reset to zero 8507 // FIXME: adjust so it matches the note above 8508 hsb.setPosition(0); 8509 needsNotify = true; 8510 } 8511 if(height >= vsb.max) { 8512 // there's plenty of room to display it all so we need to reset to zero 8513 // FIXME: adjust so it matches the note above 8514 vsb.setPosition(0); 8515 needsNotify = true; 8516 } 8517 if(needsNotify) 8518 notify(); 8519 } 8520 8521 private bool magic; 8522 override void addChild(Widget w, int position = int.max) { 8523 if(magic) 8524 container.addChild(w, position); 8525 else 8526 super.addChild(w, position); 8527 } 8528 8529 override void recomputeChildLayout() { 8530 if(hsb is null || vsb is null || container is null) return; 8531 8532 registerMovement(); 8533 8534 enum BUTTON_SIZE = 16; 8535 8536 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 8537 hsb.x = 0; 8538 hsb.y = this.height - hsb.height; 8539 8540 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 8541 vsb.x = this.width - vsb.width; 8542 vsb.y = 0; 8543 8544 auto vsb_width = vsb.showing ? vsb.width : 0; 8545 auto hsb_height = hsb.showing ? hsb.height : 0; 8546 8547 hsb.width = this.width - vsb_width; 8548 vsb.height = this.height - hsb_height; 8549 8550 hsb.recomputeChildLayout(); 8551 vsb.recomputeChildLayout(); 8552 8553 if(this.header is null) { 8554 container.x = 0; 8555 container.y = 0; 8556 container.width = this.width - vsb_width; 8557 container.height = this.height - hsb_height; 8558 container.recomputeChildLayout(); 8559 } else { 8560 header.x = 0; 8561 header.y = 0; 8562 header.width = this.width - vsb_width; 8563 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 8564 header.recomputeChildLayout(); 8565 8566 container.x = 0; 8567 container.y = scaleWithDpi(BUTTON_SIZE); 8568 container.width = this.width - vsb_width; 8569 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 8570 container.recomputeChildLayout(); 8571 } 8572 } 8573 8574 private HorizontalScrollbar hsb; 8575 private VerticalScrollbar vsb; 8576 Widget container; 8577 private Widget header; 8578 8579 /++ 8580 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 8581 8582 History: 8583 Added September 27, 2021 (dub v10.3) 8584 +/ 8585 Widget getHeader() { 8586 if(this.header is null) { 8587 magic = false; 8588 scope(exit) magic = true; 8589 this.header = new Widget(this); 8590 queueRecomputeChildLayout(); 8591 } 8592 return this.header; 8593 } 8594 8595 /++ 8596 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 8597 8598 History: 8599 Added January 3, 2023 (dub v11.0) 8600 +/ 8601 void scrollIntoView(Rectangle rect) { 8602 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 8603 8604 // import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1))); 8605 8606 // the lower right is exclusive normally 8607 auto test = rect.lowerRight; 8608 if(test.x > 0) test.x--; 8609 if(test.y > 0) test.y--; 8610 8611 if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) { 8612 // try to scroll only one dimension at a time if we can 8613 if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y))) 8614 setPosition(rect.upperLeft.x, position.y); 8615 if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y))) 8616 setPosition(position.x, rect.upperLeft.y); 8617 } 8618 8619 } 8620 8621 override int minHeight() { 8622 int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0)); 8623 if(header !is null) 8624 min += header.minHeight; 8625 if(horizontalScrollBar.showing) 8626 min += horizontalScrollBar.minHeight; 8627 return min; 8628 } 8629 8630 override int maxHeight() { 8631 int max = container ? container.maxHeight : int.max; 8632 if(max == int.max) 8633 return max; 8634 if(horizontalScrollBar.showing) 8635 max += horizontalScrollBar.minHeight; 8636 return max; 8637 } 8638 8639 static class Style : Widget.Style { 8640 override WidgetBackground background() { 8641 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 8642 } 8643 } 8644 mixin OverrideStyle!Style; 8645 } 8646 8647 /++ 8648 $(IMG //arsdnet.net/minigui-screenshots/windows/ScrollMessageWidget.png, A box saying "baby will" with three round buttons inside it for the options of "eat", "cry", and "sleep") 8649 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 8650 +/ 8651 version(minigui_screenshots) 8652 @Screenshot("ScrollMessageWidget") 8653 unittest { 8654 auto window = new Window("ScrollMessageWidget"); 8655 8656 auto smw = new ScrollMessageWidget(window); 8657 smw.addDefaultKeyboardListeners(); 8658 smw.addDefaultWheelListeners(); 8659 8660 window.loop(); 8661 } 8662 8663 /++ 8664 Bypasses automatic layout for its children, using manual positioning and sizing only. 8665 While you need to manually position them, you must ensure they are inside the StaticLayout's 8666 bounding box to avoid undefined behavior. 8667 8668 You should almost never use this. 8669 +/ 8670 class StaticLayout : Layout { 8671 /// 8672 this(Widget parent) { super(parent); } 8673 override void recomputeChildLayout() { 8674 registerMovement(); 8675 foreach(child; children) 8676 child.recomputeChildLayout(); 8677 } 8678 } 8679 8680 /++ 8681 Bypasses automatic positioning when being laid out. It is your responsibility to make 8682 room for this widget in the parent layout. 8683 8684 Its children are laid out normally, unless there is exactly one, in which case it takes 8685 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 8686 can do that with `padding`). 8687 +/ 8688 class StaticPosition : Layout { 8689 /// 8690 this(Widget parent) { super(parent); } 8691 8692 override void recomputeChildLayout() { 8693 registerMovement(); 8694 if(this.children.length == 1) { 8695 auto child = children[0]; 8696 child.x = 0; 8697 child.y = 0; 8698 child.width = this.width; 8699 child.height = this.height; 8700 child.recomputeChildLayout(); 8701 } else 8702 foreach(child; children) 8703 child.recomputeChildLayout(); 8704 } 8705 8706 alias width = typeof(super).width; 8707 alias height = typeof(super).height; 8708 8709 @property int width(int w) @nogc pure @safe nothrow { 8710 return this._width = w; 8711 } 8712 8713 @property int height(int w) @nogc pure @safe nothrow { 8714 return this._height = w; 8715 } 8716 8717 } 8718 8719 /++ 8720 FixedPosition is like [StaticPosition], but its coordinates 8721 are always relative to the viewport, meaning they do not scroll with 8722 the parent content. 8723 +/ 8724 class FixedPosition : StaticPosition { 8725 /// 8726 this(Widget parent) { super(parent); } 8727 } 8728 8729 version(win32_widgets) 8730 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 8731 if(true) { 8732 // cmd == 0 = menu, cmd == 1 = accelerator 8733 if(auto item = idm in Action.mapping) { 8734 foreach(handler; (*item).triggered) 8735 handler(); 8736 /* 8737 auto event = new Event("triggered", *item); 8738 event.button = idm; 8739 event.dispatch(); 8740 */ 8741 return 0; 8742 } 8743 } 8744 if(handle) 8745 if(auto widgetp = handle in Widget.nativeMapping) { 8746 (*widgetp).handleWmCommand(cmd, idm); 8747 return 0; 8748 } 8749 return 1; 8750 } 8751 8752 8753 /// 8754 class Window : Widget { 8755 Widget[] mouseCapturedBy; 8756 void captureMouse(Widget byWhom) { 8757 assert(byWhom !is null); 8758 if(mouseCapturedBy.length > 0) { 8759 auto cc = mouseCapturedBy[$-1]; 8760 if(cc is byWhom) 8761 return; // or should it throw? 8762 auto par = byWhom; 8763 while(par) { 8764 if(cc is par) 8765 goto allowed; 8766 par = par.parent; 8767 } 8768 8769 throw new Exception("mouse is already captured by other widget"); 8770 } 8771 allowed: 8772 mouseCapturedBy ~= byWhom; 8773 if(mouseCapturedBy.length == 1) 8774 win.grabInput(false, true, false); 8775 //void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) { 8776 } 8777 void releaseMouseCapture() { 8778 if(mouseCapturedBy.length == 0) 8779 return; // or should it throw? 8780 mouseCapturedBy = mouseCapturedBy[0 .. $-1]; 8781 mouseCapturedBy.assumeSafeAppend(); 8782 if(mouseCapturedBy.length == 0) 8783 win.releaseInputGrab(); 8784 } 8785 8786 8787 /++ 8788 8789 +/ 8790 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 8791 return .messageBox(this, title, message, style, icon); 8792 } 8793 8794 /// ditto 8795 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 8796 return messageBox(null, message, style, icon); 8797 } 8798 8799 8800 /++ 8801 Sets the window icon which is often seen in title bars and taskbars. 8802 8803 A future plan is to offer an overload that takes an array too for multiple sizes, but right now you should probably set 16x16 or 32x32 images here. 8804 8805 History: 8806 Added April 5, 2022 (dub v10.8) 8807 +/ 8808 @property void icon(MemoryImage icon) { 8809 if(win && icon) 8810 win.icon = icon; 8811 } 8812 8813 // forwarder to the top-level icon thing so this doesn't conflict too much with the UDAs seen inside the class ins ome older examples 8814 // this does NOT change the icon on the window! That's what the other overload is for 8815 static @property .icon icon(GenericIcons i) { 8816 return .icon(i); 8817 } 8818 8819 /// 8820 @scriptable 8821 @property bool focused() { 8822 return win.focused; 8823 } 8824 8825 static class Style : Widget.Style { 8826 override WidgetBackground background() { 8827 version(custom_widgets) 8828 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 8829 else version(win32_widgets) 8830 return WidgetBackground(Color.transparent); 8831 else static assert(0); 8832 } 8833 } 8834 mixin OverrideStyle!Style; 8835 8836 /++ 8837 Gives the height of a line according to the default font. You should try to use your computed font instead of this, but until May 8, 2021, this was the only real option. 8838 +/ 8839 deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() { 8840 return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback(); 8841 } 8842 8843 private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() { 8844 OperatingSystemFont font; 8845 if(auto vt = WidgetPainter.visualTheme) { 8846 font = vt.defaultFontCached(96); // FIXME 8847 } 8848 8849 if(font is null) { 8850 static int defaultHeightCache; 8851 if(defaultHeightCache == 0) { 8852 font = new OperatingSystemFont; 8853 font.loadDefault; 8854 defaultHeightCache = font.height();// * 5 / 4; 8855 } 8856 return defaultHeightCache; 8857 } 8858 8859 return font.height();// * 5 / 4; 8860 } 8861 8862 Widget focusedWidget; 8863 8864 private SimpleWindow win_; 8865 8866 @property { 8867 /++ 8868 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 8869 8870 History: 8871 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 8872 +/ 8873 public SimpleWindow win() { 8874 return win_; 8875 } 8876 /// 8877 protected void win(SimpleWindow w) { 8878 win_ = w; 8879 } 8880 } 8881 8882 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 8883 this(Widget p) { 8884 tabStop = false; 8885 super(p); 8886 } 8887 8888 private void actualRedraw() { 8889 if(recomputeChildLayoutRequired) 8890 recomputeChildLayoutEntry(); 8891 if(!showing) return; 8892 8893 assert(parentWindow !is null); 8894 8895 auto w = drawableWindow; 8896 if(w is null) 8897 w = parentWindow.win; 8898 8899 if(w.closed()) 8900 return; 8901 8902 auto ugh = this.parent; 8903 int lox, loy; 8904 while(ugh) { 8905 lox += ugh.x; 8906 loy += ugh.y; 8907 ugh = ugh.parent; 8908 } 8909 auto painter = w.draw(true); 8910 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 8911 } 8912 8913 8914 private bool skipNextChar = false; 8915 8916 /++ 8917 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 8918 8919 This constructor is intended primarily for internal use and may be changed to `protected` later. 8920 +/ 8921 this(SimpleWindow win) { 8922 8923 static if(UsingSimpledisplayX11) { 8924 win.discardAdditionalConnectionState = &discardXConnectionState; 8925 win.recreateAdditionalConnectionState = &recreateXConnectionState; 8926 } 8927 8928 tabStop = false; 8929 super(null); 8930 this.win = win; 8931 8932 win.addEventListener((Widget.RedrawEvent) { 8933 if(win.eventQueued!RecomputeEvent) { 8934 // writeln("skipping"); 8935 return; // let the recompute event do the actual redraw 8936 } 8937 this.actualRedraw(); 8938 }); 8939 8940 win.addEventListener((Widget.RecomputeEvent) { 8941 recomputeChildLayoutEntry(); 8942 if(win.eventQueued!RedrawEvent) 8943 return; // let the queued one do it 8944 else { 8945 // writeln("drawing"); 8946 this.actualRedraw(); // if not queued, it needs to be done now anyway 8947 } 8948 }); 8949 8950 this.width = win.width; 8951 this.height = win.height; 8952 this.parentWindow = this; 8953 8954 win.closeQuery = () { 8955 if(this.emit!ClosingEvent()) 8956 win.close(); 8957 }; 8958 win.onClosing = () { 8959 this.emit!ClosedEvent(); 8960 }; 8961 8962 win.windowResized = (int w, int h) { 8963 this.width = w; 8964 this.height = h; 8965 queueRecomputeChildLayout(); 8966 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 8967 //version(win32_widgets) 8968 //InvalidateRect(hwnd, null, true); 8969 redraw(); 8970 }; 8971 8972 win.onFocusChange = (bool getting) { 8973 // sdpyPrintDebugString("onFocusChange ", getting, " ", this.toString); 8974 if(this.focusedWidget) { 8975 if(getting) { 8976 this.focusedWidget.emit!FocusEvent(); 8977 this.focusedWidget.emit!FocusInEvent(); 8978 } else { 8979 this.focusedWidget.emit!BlurEvent(); 8980 this.focusedWidget.emit!FocusOutEvent(); 8981 } 8982 } 8983 8984 if(getting) { 8985 this.emit!FocusEvent(); 8986 this.emit!FocusInEvent(); 8987 } else { 8988 this.emit!BlurEvent(); 8989 this.emit!FocusOutEvent(); 8990 } 8991 }; 8992 8993 win.onDpiChanged = { 8994 this.queueRecomputeChildLayout(); 8995 auto event = new DpiChangedEvent(this); 8996 event.sendDirectly(); 8997 8998 privateDpiChanged(); 8999 }; 9000 9001 win.setEventHandlers( 9002 (MouseEvent e) { 9003 dispatchMouseEvent(e); 9004 }, 9005 (KeyEvent e) { 9006 //writefln("%x %s", cast(uint) e.key, e.key); 9007 dispatchKeyEvent(e); 9008 }, 9009 (dchar e) { 9010 if(e == 13) e = 10; // hack? 9011 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 9012 dispatchCharEvent(e); 9013 }, 9014 ); 9015 9016 addEventListener("char", (Widget, Event ev) { 9017 if(skipNextChar) { 9018 ev.preventDefault(); 9019 skipNextChar = false; 9020 } 9021 }); 9022 9023 version(win32_widgets) 9024 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 9025 if(hwnd !is this.win.impl.hwnd) 9026 return 1; // we don't care... pass it on 9027 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 9028 if(mustReturn) 9029 return ret; 9030 return 1; // pass it on 9031 }; 9032 9033 if(Window.newWindowCreated) 9034 Window.newWindowCreated(this); 9035 } 9036 9037 version(custom_widgets) 9038 override void defaultEventHandler_click(ClickEvent event) { 9039 if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) { 9040 if(event.target && event.target.tabStop) 9041 event.target.focus(); 9042 } 9043 } 9044 9045 private static void delegate(Window) newWindowCreated; 9046 9047 version(win32_widgets) 9048 override void paint(WidgetPainter painter) { 9049 /* 9050 RECT rect; 9051 rect.right = this.width; 9052 rect.bottom = this.height; 9053 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 9054 */ 9055 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 9056 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 9057 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 9058 // since the pen is null, to fill the whole space, we need the +1 on both. 9059 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 9060 SelectObject(painter.impl.hdc, p); 9061 SelectObject(painter.impl.hdc, b); 9062 } 9063 version(custom_widgets) 9064 override void paint(WidgetPainter painter) { 9065 auto cs = getComputedStyle(); 9066 painter.fillColor = cs.windowBackgroundColor; 9067 painter.outlineColor = cs.windowBackgroundColor; 9068 painter.drawRectangle(Point(0, 0), this.width, this.height); 9069 } 9070 9071 9072 override void defaultEventHandler_keydown(KeyDownEvent event) { 9073 Widget _this = event.target; 9074 9075 if(event.key == Key.Tab) { 9076 /* Window tab ordering is a recursive thingy with each group */ 9077 9078 // FIXME inefficient 9079 Widget[] helper(Widget p) { 9080 if(p.hidden) 9081 return null; 9082 Widget[] childOrdering; 9083 9084 auto children = p.children.dup; 9085 9086 while(true) { 9087 // UIs should be generally small, so gonna brute force it a little 9088 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 9089 9090 Widget smallestTab; 9091 foreach(ref c; children) { 9092 if(c is null) continue; 9093 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 9094 smallestTab = c; 9095 c = null; 9096 } 9097 } 9098 if(smallestTab !is null) { 9099 if(smallestTab.tabStop && !smallestTab.hidden) 9100 childOrdering ~= smallestTab; 9101 if(!smallestTab.hidden) 9102 childOrdering ~= helper(smallestTab); 9103 } else 9104 break; 9105 9106 } 9107 9108 return childOrdering; 9109 } 9110 9111 Widget[] tabOrdering = helper(this); 9112 9113 Widget recipient; 9114 9115 if(tabOrdering.length) { 9116 bool seenThis = false; 9117 Widget previous; 9118 foreach(idx, child; tabOrdering) { 9119 if(child is focusedWidget) { 9120 9121 if(event.shiftKey) { 9122 if(idx == 0) 9123 recipient = tabOrdering[$-1]; 9124 else 9125 recipient = tabOrdering[idx - 1]; 9126 break; 9127 } 9128 9129 seenThis = true; 9130 if(idx + 1 == tabOrdering.length) { 9131 // we're at the end, either move to the next group 9132 // or start back over 9133 recipient = tabOrdering[0]; 9134 } 9135 continue; 9136 } 9137 if(seenThis) { 9138 recipient = child; 9139 break; 9140 } 9141 previous = child; 9142 } 9143 } 9144 9145 if(recipient !is null) { 9146 // writeln(typeid(recipient)); 9147 recipient.focus(); 9148 9149 skipNextChar = true; 9150 } 9151 } 9152 9153 debug if(event.key == Key.F12) { 9154 if(devTools) { 9155 devTools.close(); 9156 devTools = null; 9157 } else { 9158 devTools = new DevToolWindow(this); 9159 devTools.show(); 9160 } 9161 } 9162 } 9163 9164 debug DevToolWindow devTools; 9165 9166 9167 /++ 9168 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 9169 9170 History: 9171 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 9172 9173 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 9174 +/ 9175 this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) { 9176 if(title is null) { 9177 import core.runtime; 9178 if(Runtime.args.length) 9179 title = Runtime.args[0]; 9180 } 9181 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent); 9182 9183 static if(UsingSimpledisplayX11) 9184 if(windowFlags & WindowFlags.managesChildWindowFocus) { 9185 ///+ 9186 // for input proxy 9187 auto display = XDisplayConnection.get; 9188 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 9189 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 9190 XMapWindow(display, inputProxy); 9191 // writefln("input proxy: 0x%0x", inputProxy); 9192 this.inputProxy = new SimpleWindow(inputProxy); 9193 9194 /+ 9195 this.inputProxy.onFocusChange = (bool getting) { 9196 sdpyPrintDebugString("input proxy focus change ", getting); 9197 }; 9198 +/ 9199 9200 XEvent lastEvent; 9201 this.inputProxy.handleNativeEvent = (XEvent ev) { 9202 lastEvent = ev; 9203 return 1; 9204 }; 9205 this.inputProxy.setEventHandlers( 9206 (MouseEvent e) { 9207 dispatchMouseEvent(e); 9208 }, 9209 (KeyEvent e) { 9210 //writefln("%x %s", cast(uint) e.key, e.key); 9211 if(dispatchKeyEvent(e)) { 9212 // FIXME: i should trap error 9213 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 9214 auto thing = nw.focusableWindow(); 9215 if(thing && thing.window) { 9216 lastEvent.xkey.window = thing.window; 9217 // writeln("sending event ", lastEvent.xkey); 9218 trapXErrors( { 9219 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 9220 }); 9221 } 9222 } 9223 } 9224 }, 9225 (dchar e) { 9226 if(e == 13) e = 10; // hack? 9227 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 9228 dispatchCharEvent(e); 9229 }, 9230 ); 9231 9232 this.inputProxy.populateXic(); 9233 // done 9234 //+/ 9235 } 9236 9237 9238 9239 win.setRequestedInputFocus = &this.setRequestedInputFocus; 9240 9241 this(win); 9242 } 9243 9244 SimpleWindow inputProxy; 9245 9246 private SimpleWindow setRequestedInputFocus() { 9247 return inputProxy; 9248 } 9249 9250 /// ditto 9251 this(string title, int width = 500, int height = 500) { 9252 this(width, height, title); 9253 } 9254 9255 /// 9256 @property string title() { return parentWindow.win.title; } 9257 /// 9258 @property void title(string title) { parentWindow.win.title = title; } 9259 9260 /// 9261 @scriptable 9262 void close() { 9263 win.close(); 9264 // I synchronize here upon window closing to ensure all child windows 9265 // get updated too before the event loop. This avoids some random X errors. 9266 static if(UsingSimpledisplayX11) { 9267 runInGuiThread( { 9268 XSync(XDisplayConnection.get, false); 9269 }); 9270 } 9271 } 9272 9273 bool dispatchKeyEvent(KeyEvent ev) { 9274 auto wid = focusedWidget; 9275 if(wid is null) 9276 wid = this; 9277 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 9278 event.originalKeyEvent = ev; 9279 event.key = ev.key; 9280 event.state = ev.modifierState; 9281 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 9282 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 9283 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 9284 event.dispatch(); 9285 9286 return !event.propagationStopped; 9287 } 9288 9289 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 9290 bool dispatchCharEvent(dchar ch) { 9291 if(focusedWidget) { 9292 auto event = new CharEvent(focusedWidget, ch); 9293 event.dispatch(); 9294 return !event.propagationStopped; 9295 } 9296 return true; 9297 } 9298 9299 Widget mouseLastOver; 9300 Widget mouseLastDownOn; 9301 bool lastWasDoubleClick; 9302 bool dispatchMouseEvent(MouseEvent ev) { 9303 auto eleR = widgetAtPoint(this, ev.x, ev.y); 9304 auto ele = eleR.widget; 9305 9306 auto captureEle = ele; 9307 9308 auto mouseCapturedBy = this.mouseCapturedBy.length ? this.mouseCapturedBy[$-1] : null; 9309 if(mouseCapturedBy !is null) { 9310 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 9311 captureEle = mouseCapturedBy; 9312 } 9313 9314 // a hack to get it relative to the widget. 9315 eleR.x = ev.x; 9316 eleR.y = ev.y; 9317 auto pain = captureEle; 9318 while(pain) { 9319 eleR.x -= pain.x; 9320 eleR.y -= pain.y; 9321 pain.addScrollPosition(eleR.x, eleR.y); 9322 pain = pain.parent; 9323 } 9324 9325 void populateMouseEventBase(MouseEventBase event) { 9326 event.button = ev.button; 9327 event.buttonLinear = ev.buttonLinear; 9328 event.state = ev.modifierState; 9329 event.clientX = eleR.x; 9330 event.clientY = eleR.y; 9331 9332 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 9333 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 9334 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 9335 } 9336 9337 if(ev.type == MouseEventType.buttonPressed) { 9338 { 9339 auto event = new MouseDownEvent(captureEle); 9340 populateMouseEventBase(event); 9341 event.dispatch(); 9342 } 9343 9344 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 9345 auto event = new DoubleClickEvent(captureEle); 9346 populateMouseEventBase(event); 9347 event.dispatch(); 9348 lastWasDoubleClick = ev.doubleClick; 9349 } else { 9350 lastWasDoubleClick = false; 9351 } 9352 9353 mouseLastDownOn = ele; 9354 } else if(ev.type == MouseEventType.buttonReleased) { 9355 { 9356 auto event = new MouseUpEvent(captureEle); 9357 populateMouseEventBase(event); 9358 event.dispatch(); 9359 } 9360 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 9361 auto event = new ClickEvent(captureEle); 9362 populateMouseEventBase(event); 9363 event.dispatch(); 9364 } 9365 } else if(ev.type == MouseEventType.motion) { 9366 // motion 9367 { 9368 auto event = new MouseMoveEvent(captureEle); 9369 populateMouseEventBase(event); // fills in button which is meaningless but meh 9370 event.dispatch(); 9371 } 9372 9373 if(mouseLastOver !is ele) { 9374 if(ele !is null) { 9375 if(!isAParentOf(ele, mouseLastOver)) { 9376 ele.setDynamicState(DynamicState.hover, true); 9377 auto event = new MouseEnterEvent(ele); 9378 event.relatedTarget = mouseLastOver; 9379 event.sendDirectly(); 9380 9381 ele.useStyleProperties((scope Widget.Style s) { 9382 ele.parentWindow.win.cursor = s.cursor; 9383 }); 9384 } 9385 } 9386 9387 if(mouseLastOver !is null) { 9388 if(!isAParentOf(mouseLastOver, ele)) { 9389 mouseLastOver.setDynamicState(DynamicState.hover, false); 9390 auto event = new MouseLeaveEvent(mouseLastOver); 9391 event.relatedTarget = ele; 9392 event.sendDirectly(); 9393 } 9394 } 9395 9396 if(ele !is null) { 9397 auto event = new MouseOverEvent(ele); 9398 event.relatedTarget = mouseLastOver; 9399 event.dispatch(); 9400 } 9401 9402 if(mouseLastOver !is null) { 9403 auto event = new MouseOutEvent(mouseLastOver); 9404 event.relatedTarget = ele; 9405 event.dispatch(); 9406 } 9407 9408 mouseLastOver = ele; 9409 } 9410 } 9411 9412 return true; // FIXME: the event default prevented? 9413 } 9414 9415 /++ 9416 Shows the window and runs the application event loop. 9417 9418 Blocks until this window is closed. 9419 9420 Bugs: 9421 9422 $(PITFALL 9423 You should always have one event loop live for your application. 9424 If you make two windows in sequence, the second call to loop (or 9425 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 9426 might fail: 9427 9428 --- 9429 // don't do this! 9430 auto window = new Window(); 9431 window.loop(); 9432 9433 // or new Window or new MainWindow, all the same 9434 auto window2 = new SimpleWindow(); 9435 window2.eventLoop(0); // problematic! might crash 9436 --- 9437 9438 simpledisplay's current implementation assumes that final cleanup is 9439 done when the event loop refcount reaches zero. So after the first 9440 eventLoop returns, when there isn't already another one active, it assumes 9441 the program will exit soon and cleans up. 9442 9443 This is arguably a bug that it doesn't reinitialize, and I'll probably change 9444 it eventually, but in the mean time, there's an easy solution: 9445 9446 --- 9447 // do this 9448 EventLoop mainEventLoop = EventLoop.get; // just add this line 9449 9450 auto window = new Window(); 9451 window.loop(); 9452 9453 // or any other type of Window etc. 9454 auto window2 = new Window(); 9455 window2.loop(); // perfectly fine since mainEventLoop still alive 9456 --- 9457 9458 By adding a top-level reference to the event loop, it ensures the final cleanup 9459 is not performed until it goes out of scope too, letting the individual window loops 9460 work without trouble despite the bug. 9461 ) 9462 9463 History: 9464 The [BlockingMode] parameter was added on December 8, 2021. 9465 The default behavior is to block until the application quits 9466 (so all windows have been closed), unless another minigui or 9467 simpledisplay event loop is already running, in which case it 9468 will block until this window closes specifically. 9469 +/ 9470 @scriptable 9471 void loop(BlockingMode bm = BlockingMode.automatic) { 9472 if(win.closed) 9473 return; // otherwise show will throw 9474 show(); 9475 win.eventLoopWithBlockingMode(bm, 0); 9476 } 9477 9478 private bool firstShow = true; 9479 9480 @scriptable 9481 override void show() { 9482 bool rd = false; 9483 if(firstShow) { 9484 firstShow = false; 9485 queueRecomputeChildLayout(); 9486 // unless the programmer already called focus on something, pick something ourselves 9487 auto f = focusedWidget is null ? getFirstFocusable(this) : focusedWidget; // FIXME: autofocus? 9488 if(f) 9489 f.focus(); 9490 redraw(); 9491 } 9492 win.show(); 9493 super.show(); 9494 } 9495 @scriptable 9496 override void hide() { 9497 win.hide(); 9498 super.hide(); 9499 } 9500 9501 static Widget getFirstFocusable(Widget start) { 9502 if(start is null) 9503 return null; 9504 9505 foreach(widget; &start.focusableWidgets) { 9506 return widget; 9507 } 9508 9509 return null; 9510 } 9511 9512 static Widget getLastFocusable(Widget start) { 9513 if(start is null) 9514 return null; 9515 9516 Widget last; 9517 foreach(widget; &start.focusableWidgets) { 9518 last = widget; 9519 } 9520 9521 return last; 9522 } 9523 9524 9525 mixin Emits!ClosingEvent; 9526 mixin Emits!ClosedEvent; 9527 } 9528 9529 /++ 9530 History: 9531 Added January 12, 2022 9532 9533 Made `final` on January 3, 2025 9534 +/ 9535 final class DpiChangedEvent : Event { 9536 enum EventString = "dpichanged"; 9537 9538 this(Widget target) { 9539 super(EventString, target); 9540 } 9541 } 9542 9543 debug private class DevToolWindow : Window { 9544 Window p; 9545 9546 TextEdit parentList; 9547 TextEdit logWindow; 9548 TextLabel clickX, clickY; 9549 9550 this(Window p) { 9551 this.p = p; 9552 super(400, 300, "Developer Toolbox"); 9553 9554 logWindow = new TextEdit(this); 9555 parentList = new TextEdit(this); 9556 9557 auto hl = new HorizontalLayout(this); 9558 clickX = new TextLabel("", TextAlignment.Right, hl); 9559 clickY = new TextLabel("", TextAlignment.Right, hl); 9560 9561 parentListeners ~= p.addEventListener("*", (Event ev) { 9562 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 9563 }); 9564 9565 parentListeners ~= p.addEventListener((ClickEvent ev) { 9566 auto s = ev.srcElement; 9567 9568 string list; 9569 9570 void addInfo(Widget s) { 9571 list ~= s.toString(); 9572 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 9573 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 9574 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 9575 list ~= "\n\theight: " ~ toInternal!string(s.height); 9576 list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth); 9577 list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth); 9578 list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness); 9579 list ~= "\n\twidth: " ~ toInternal!string(s.width); 9580 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 9581 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 9582 } 9583 9584 addInfo(s); 9585 9586 s = s.parent; 9587 while(s) { 9588 list ~= "\n"; 9589 addInfo(s); 9590 s = s.parent; 9591 } 9592 parentList.content = list; 9593 9594 clickX.label = toInternal!string(ev.clientX); 9595 clickY.label = toInternal!string(ev.clientY); 9596 }); 9597 } 9598 9599 EventListener[] parentListeners; 9600 9601 override void close() { 9602 assert(p !is null); 9603 foreach(p; parentListeners) 9604 p.disconnect(); 9605 parentListeners = null; 9606 p.devTools = null; 9607 p = null; 9608 super.close(); 9609 } 9610 9611 override void defaultEventHandler_keydown(KeyDownEvent ev) { 9612 if(ev.key == Key.F12) { 9613 this.close(); 9614 if(p) 9615 p.devTools = null; 9616 } else { 9617 super.defaultEventHandler_keydown(ev); 9618 } 9619 } 9620 9621 void log(T...)(T t) { 9622 string str; 9623 import std.conv; 9624 foreach(i; t) 9625 str ~= to!string(i); 9626 str ~= "\n"; 9627 logWindow.addText(str); 9628 logWindow.scrollToBottom(); 9629 9630 //version(custom_widgets) 9631 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 9632 } 9633 } 9634 9635 /++ 9636 A dialog is a transient window that intends to get information from 9637 the user before being dismissed. 9638 +/ 9639 class Dialog : Window { 9640 /// 9641 this(Window parent, int width, int height, string title = null) { 9642 super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win); 9643 9644 // this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) { 9645 } 9646 9647 /// 9648 this(Window parent, string title, int width, int height) { 9649 this(parent, width, height, title); 9650 } 9651 9652 deprecated("Pass an explicit parent window, even if it is `null`") 9653 this(int width, int height, string title = null) { 9654 this(null, width, height, title); 9655 } 9656 9657 /// 9658 void OK() { 9659 9660 } 9661 9662 /// 9663 void Cancel() { 9664 this.close(); 9665 } 9666 } 9667 9668 /++ 9669 A custom widget similar to the HTML5 <details> tag. 9670 +/ 9671 version(none) 9672 class DetailsView : Widget { 9673 9674 } 9675 9676 // FIXME: maybe i should expose the other list views Windows offers too 9677 9678 /++ 9679 A TableView is a widget made to display a table of data strings. 9680 9681 9682 Future_Directions: 9683 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 9684 9685 I will add a selection changed event at some point, as well as item clicked events. 9686 History: 9687 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 9688 See_Also: 9689 [ListWidget] which displays a list of strings without additional columns. 9690 +/ 9691 class TableView : Widget { 9692 /++ 9693 9694 +/ 9695 this(Widget parent) { 9696 super(parent); 9697 9698 version(win32_widgets) { 9699 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 9700 } else version(custom_widgets) { 9701 auto smw = new ScrollMessageWidget(this); 9702 smw.addDefaultKeyboardListeners(); 9703 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 9704 tvwi = new TableViewWidgetInner(this, smw); 9705 } 9706 } 9707 9708 // FIXME: auto-size columns on double click of header thing like in Windows 9709 // it need only make the currently displayed things fit well. 9710 9711 9712 private ColumnInfo[] columns; 9713 private int itemCount; 9714 9715 version(custom_widgets) private { 9716 TableViewWidgetInner tvwi; 9717 } 9718 9719 /// Passed to [setColumnInfo] 9720 static struct ColumnInfo { 9721 const(char)[] name; /// the name displayed in the header 9722 /++ 9723 The default width, in pixels. As a special case, you can set this to -1 9724 if you want the system to try to automatically size the width to fit visible 9725 content. If it can't, it will try to pick a sensible default size. 9726 9727 Any other negative value is not allowed and may lead to unpredictable results. 9728 9729 History: 9730 The -1 behavior was specified on December 3, 2021. It actually worked before 9731 anyway on Win32 but now it is a formal feature with partial Linux support. 9732 9733 Bugs: 9734 It doesn't actually attempt to calculate a best-fit width on Linux as of 9735 December 3, 2021. I do plan to fix this in the future, but Windows is the 9736 priority right now. At least it doesn't break things when you use it now. 9737 +/ 9738 int width; 9739 9740 /++ 9741 Alignment of the text in the cell. Applies to the header as well as all data in this 9742 column. 9743 9744 Bugs: 9745 On Windows, the first column ignores this member and is always left aligned. 9746 You can work around this by inserting a dummy first column with width = 0 9747 then putting your actual data in the second column, which does respect the 9748 alignment. 9749 9750 This is a quirk of the operating system's implementation going back a very 9751 long time and is unlikely to ever be fixed. 9752 +/ 9753 TextAlignment alignment; 9754 9755 /++ 9756 After all the pixel widths have been assigned, any left over 9757 space is divided up among all columns and distributed to according 9758 to the widthPercent field. 9759 9760 9761 For example, if you have two fields, both with width 50 and one with 9762 widthPercent of 25 and the other with widthPercent of 75, and the 9763 container is 200 pixels wide, first both get their width of 50. 9764 then the 100 remaining pixels are split up, so the one gets a total 9765 of 75 pixels and the other gets a total of 125. 9766 9767 This is automatically applied as the window is resized. 9768 9769 If there is not enough space - that is, when a horizontal scrollbar 9770 needs to appear - there are 0 pixels divided up, and thus everyone 9771 gets 0. This can cause a column to shrink out of proportion when 9772 passing the scroll threshold. 9773 9774 It is important to still set a fixed width (that is, to populate the 9775 `width` field) even if you use the percents because that will be the 9776 default minimum in the event of a scroll bar appearing. 9777 9778 The percents total in the column can never exceed 100 or be less than 0. 9779 Doing this will trigger an assert error. 9780 9781 Implementation note: 9782 9783 Please note that percentages are only recalculated 1) upon original 9784 construction and 2) upon resizing the control. If the user adjusts the 9785 width of a column, the percentage items will not be updated. 9786 9787 On the other hand, if the user adjusts the width of a percentage column 9788 then resizes the window, it is recalculated, meaning their hand adjustment 9789 is discarded. This specific behavior may change in the future as it is 9790 arguably a bug, but I'm not certain yet. 9791 9792 History: 9793 Added November 10, 2021 (dub v10.4) 9794 +/ 9795 int widthPercent; 9796 9797 9798 private int calculatedWidth; 9799 } 9800 /++ 9801 Sets the number of columns along with information about the headers. 9802 9803 Please note: on Windows, the first column ignores your alignment preference 9804 and is always left aligned. 9805 +/ 9806 void setColumnInfo(ColumnInfo[] columns...) { 9807 9808 foreach(ref c; columns) { 9809 c.name = c.name.idup; 9810 } 9811 this.columns = columns.dup; 9812 9813 updateCalculatedWidth(false); 9814 9815 version(custom_widgets) { 9816 tvwi.header.updateHeaders(); 9817 tvwi.updateScrolls(); 9818 } else version(win32_widgets) 9819 foreach(i, column; this.columns) { 9820 LVCOLUMN lvColumn; 9821 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 9822 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 9823 9824 auto bfr = WCharzBuffer(column.name); 9825 lvColumn.pszText = bfr.ptr; 9826 9827 if(column.alignment & TextAlignment.Center) 9828 lvColumn.fmt = LVCFMT_CENTER; 9829 else if(column.alignment & TextAlignment.Right) 9830 lvColumn.fmt = LVCFMT_RIGHT; 9831 else 9832 lvColumn.fmt = LVCFMT_LEFT; 9833 9834 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 9835 throw new WindowsApiException("Insert Column Fail", GetLastError()); 9836 } 9837 } 9838 9839 private int getActualSetSize(size_t i, bool askWindows) { 9840 version(win32_widgets) 9841 if(askWindows) 9842 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 9843 auto w = columns[i].width; 9844 if(w == -1) 9845 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 9846 return w; 9847 } 9848 9849 private void updateCalculatedWidth(bool informWindows) { 9850 int padding; 9851 version(win32_widgets) 9852 padding = 4; 9853 int remaining = this.width; 9854 foreach(i, column; columns) 9855 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 9856 remaining -= padding; 9857 if(remaining < 0) 9858 remaining = 0; 9859 9860 int percentTotal; 9861 foreach(i, ref column; columns) { 9862 percentTotal += column.widthPercent; 9863 9864 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 9865 9866 column.calculatedWidth = c; 9867 9868 version(win32_widgets) 9869 if(informWindows) 9870 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 9871 } 9872 9873 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 9874 assert(percentTotal <= 100, "The total percents in your column definitions exceeded 100. They must add up to no more than 100 (can be less though)."); 9875 9876 9877 } 9878 9879 override void registerMovement() { 9880 super.registerMovement(); 9881 9882 updateCalculatedWidth(true); 9883 } 9884 9885 /++ 9886 Tells the view how many items are in it. It uses this to set the scroll bar, but the items are not added per se; it calls [getData] as-needed. 9887 +/ 9888 void setItemCount(int count) { 9889 this.itemCount = count; 9890 version(custom_widgets) { 9891 tvwi.updateScrolls(); 9892 redraw(); 9893 } else version(win32_widgets) { 9894 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 9895 } 9896 } 9897 9898 /++ 9899 Clears all items; 9900 +/ 9901 void clear() { 9902 this.itemCount = 0; 9903 this.columns = null; 9904 version(custom_widgets) { 9905 tvwi.header.updateHeaders(); 9906 tvwi.updateScrolls(); 9907 redraw(); 9908 } else version(win32_widgets) { 9909 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 9910 } 9911 } 9912 9913 /+ 9914 version(win32_widgets) 9915 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 9916 auto itemId = dis.itemID; 9917 auto hdc = dis.hDC; 9918 auto rect = dis.rcItem; 9919 switch(dis.itemAction) { 9920 case ODA_DRAWENTIRE: 9921 9922 // FIXME: do other items 9923 // FIXME: do the focus rectangle i guess 9924 // FIXME: alignment 9925 // FIXME: column width 9926 // FIXME: padding left 9927 // FIXME: check dpi scaling 9928 // FIXME: don't owner draw unless it is necessary. 9929 9930 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 9931 RECT itemRect; 9932 itemRect.top = 1; // subitem idx, 1-based 9933 itemRect.left = LVIR_BOUNDS; 9934 9935 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 9936 itemRect.left += padding; 9937 9938 getData(itemId, 0, (in char[] data) { 9939 auto wdata = WCharzBuffer(data); 9940 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 9941 9942 }); 9943 goto case; 9944 case ODA_FOCUS: 9945 if(dis.itemState & ODS_FOCUS) 9946 DrawFocusRect(hdc, &rect); 9947 break; 9948 case ODA_SELECT: 9949 // itemState & ODS_SELECTED 9950 break; 9951 default: 9952 } 9953 return 1; 9954 } 9955 +/ 9956 9957 version(win32_widgets) { 9958 CellStyle last; 9959 COLORREF defaultColor; 9960 COLORREF defaultBackground; 9961 } 9962 9963 version(win32_widgets) 9964 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 9965 switch(code) { 9966 case NM_CUSTOMDRAW: 9967 auto s = cast(NMLVCUSTOMDRAW*) hdr; 9968 switch(s.nmcd.dwDrawStage) { 9969 case CDDS_PREPAINT: 9970 if(getCellStyle is null) 9971 return 0; 9972 9973 mustReturn = true; 9974 return CDRF_NOTIFYITEMDRAW; 9975 case CDDS_ITEMPREPAINT: 9976 mustReturn = true; 9977 return CDRF_NOTIFYSUBITEMDRAW; 9978 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 9979 mustReturn = true; 9980 9981 if(getCellStyle is null) // this SHOULD never happen... 9982 return 0; 9983 9984 if(s.iSubItem == 0) { 9985 // Windows resets it per row so we'll use item 0 as a chance 9986 // to capture these for later 9987 defaultColor = s.clrText; 9988 defaultBackground = s.clrTextBk; 9989 } 9990 9991 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 9992 // if no special style and no reset needed... 9993 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 9994 return 0; // allow default processing to continue 9995 9996 last = style; 9997 9998 // might still need to reset or use the preference. 9999 10000 if(style.flags & CellStyle.Flags.textColorSet) 10001 s.clrText = style.textColor.asWindowsColorRef; 10002 else 10003 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 10004 if(style.flags & CellStyle.Flags.backgroundColorSet) 10005 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 10006 else 10007 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 10008 10009 return CDRF_NEWFONT; 10010 default: 10011 return 0; 10012 10013 } 10014 case NM_RETURN: // no need since i subclass keydown 10015 break; 10016 case LVN_COLUMNCLICK: 10017 auto info = cast(LPNMLISTVIEW) hdr; 10018 this.emit!HeaderClickedEvent(info.iSubItem); 10019 break; 10020 case NM_CLICK: 10021 case NM_DBLCLK: 10022 case NM_RCLICK: 10023 case NM_RDBLCLK: 10024 // the item/subitem is set here and that can be a useful notification 10025 // even beyond the normal click notification 10026 break; 10027 case LVN_GETDISPINFO: 10028 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 10029 if(info.item.mask & LVIF_TEXT) { 10030 if(getData) { 10031 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 10032 auto bfr = WCharzBuffer(dataReceived); 10033 auto len = info.item.cchTextMax; 10034 if(bfr.length < len) 10035 len = cast(typeof(len)) bfr.length; 10036 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 10037 info.item.pszText[len] = 0; 10038 }); 10039 } else { 10040 info.item.pszText[0] = 0; 10041 } 10042 //info.item.iItem 10043 //if(info.item.iSubItem) 10044 } 10045 break; 10046 default: 10047 } 10048 return 0; 10049 } 10050 10051 override bool encapsulatedChildren() { 10052 return true; 10053 } 10054 10055 /++ 10056 Informs the control that content has changed. 10057 10058 History: 10059 Added November 10, 2021 (dub v10.4) 10060 +/ 10061 void update() { 10062 version(custom_widgets) 10063 redraw(); 10064 else { 10065 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 10066 UpdateWindow(hwnd); 10067 } 10068 10069 10070 } 10071 10072 /++ 10073 Called by the system to request the text content of an individual cell. You 10074 should pass the text into the provided `sink` delegate. This function will be 10075 called for each visible cell as-needed when drawing. 10076 +/ 10077 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 10078 10079 /++ 10080 Available per-cell style customization options. Use one of the constructors 10081 provided to set the values conveniently, or default construct it and set individual 10082 values yourself. Just remember to set the `flags` so your values are actually used. 10083 If the flag isn't set, the field is ignored and the system default is used instead. 10084 10085 This is returned by the [getCellStyle] delegate. 10086 10087 Examples: 10088 --- 10089 // assumes you have a variables called `my_data` which is an array of arrays of numbers 10090 auto table = new TableView(window); 10091 // snip: you would set up columns here 10092 10093 // this is how you provide data to the table view class 10094 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 10095 import std.conv; 10096 sink(to!string(my_data[row][column])); 10097 }; 10098 10099 // and this is how you customize the colors 10100 table.getCellStyle = delegate(int row, int column) { 10101 return (my_data[row][column] < 0) ? 10102 TableView.CellStyle(Color.red); // make negative numbers red 10103 : TableView.CellStyle.init; // leave the rest alone 10104 }; 10105 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 10106 --- 10107 10108 History: 10109 Added November 27, 2021 (dub v10.4) 10110 +/ 10111 struct CellStyle { 10112 /// Sets just a custom text color, leaving the background as the default. Use caution with certain colors as it may have illeglible contrast on the (unknown to you) background color. 10113 this(Color textColor) { 10114 this.textColor = textColor; 10115 this.flags |= Flags.textColorSet; 10116 } 10117 /// Sets a custom text and background color. 10118 this(Color textColor, Color backgroundColor) { 10119 this.textColor = textColor; 10120 this.backgroundColor = backgroundColor; 10121 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 10122 } 10123 10124 Color textColor; 10125 Color backgroundColor; 10126 int flags; /// bitmask of [Flags] 10127 /// available options to combine into [flags] 10128 enum Flags { 10129 textColorSet = 1 << 0, 10130 backgroundColorSet = 1 << 1, 10131 } 10132 } 10133 /++ 10134 Companion delegate to [getData] that allows you to custom style each 10135 cell of the table. 10136 10137 Returns: 10138 A [CellStyle] structure that describes the desired style for the 10139 given cell. `return CellStyle.init` if you want the default style. 10140 10141 History: 10142 Added November 27, 2021 (dub v10.4) 10143 +/ 10144 CellStyle delegate(int row, int column) getCellStyle; 10145 10146 // i want to be able to do things like draw little colored things to show red for negative numbers 10147 // or background color indicators or even in-cell charts 10148 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 10149 10150 /++ 10151 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 10152 +/ 10153 mixin Emits!HeaderClickedEvent; 10154 } 10155 10156 /++ 10157 This is emitted by the [TableView] when a user clicks on a column header. 10158 10159 Its member `columnIndex` has the zero-based index of the column that was clicked. 10160 10161 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 10162 10163 History: 10164 Added November 27, 2021 (dub v10.4) 10165 10166 Made `final` on January 3, 2025 10167 +/ 10168 final class HeaderClickedEvent : Event { 10169 enum EventString = "HeaderClicked"; 10170 this(Widget target, int columnIndex) { 10171 this.columnIndex = columnIndex; 10172 super(EventString, target); 10173 } 10174 10175 /// The index of the column 10176 int columnIndex; 10177 10178 /// 10179 override @property int intValue() { 10180 return columnIndex; 10181 } 10182 } 10183 10184 version(custom_widgets) 10185 private class TableViewWidgetInner : Widget { 10186 10187 // wrap this thing in a ScrollMessageWidget 10188 10189 TableView tvw; 10190 ScrollMessageWidget smw; 10191 HeaderWidget header; 10192 10193 this(TableView tvw, ScrollMessageWidget smw) { 10194 this.tvw = tvw; 10195 this.smw = smw; 10196 super(smw); 10197 10198 this.tabStop = true; 10199 10200 header = new HeaderWidget(this, smw.getHeader()); 10201 10202 smw.addEventListener("scroll", () { 10203 this.redraw(); 10204 header.redraw(); 10205 }); 10206 10207 10208 // I need headers outside the scroll area but rendered on the same line as the up arrow 10209 // FIXME: add a fixed header to the SMW 10210 } 10211 10212 enum padding = 3; 10213 10214 void updateScrolls() { 10215 int w; 10216 foreach(idx, column; tvw.columns) { 10217 if(column.width == 0) continue; 10218 w += tvw.getActualSetSize(idx, false);// + padding; 10219 } 10220 smw.setTotalArea(w, tvw.itemCount); 10221 columnsWidth = w; 10222 } 10223 10224 private int columnsWidth; 10225 10226 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 10227 10228 override void registerMovement() { 10229 super.registerMovement(); 10230 // FIXME: actual column width. it might need to be done per-pixel instead of per-column 10231 smw.setViewableArea(this.width, this.height / lh); 10232 } 10233 10234 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 10235 int x; 10236 int y; 10237 10238 int row = smw.position.y; 10239 10240 foreach(lol; 0 .. this.height / lh) { 10241 if(row >= tvw.itemCount) 10242 break; 10243 x = 0; 10244 foreach(columnNumber, column; tvw.columns) { 10245 auto x2 = x + column.calculatedWidth; 10246 auto smwx = smw.position.x; 10247 10248 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 10249 auto startX = x; 10250 auto endX = x + column.calculatedWidth; 10251 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 10252 case TextAlignment.Left: startX += padding; break; 10253 case TextAlignment.Center: startX += padding; endX -= padding; break; 10254 case TextAlignment.Right: endX -= padding; break; 10255 default: /* broken */ break; 10256 } 10257 if(column.width != 0) // no point drawing an invisible column 10258 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 10259 auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endX - smw.position.x, y + lh))); 10260 10261 void dotext(WidgetPainter painter) { 10262 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 10263 } 10264 10265 if(tvw.getCellStyle !is null) { 10266 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 10267 10268 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 10269 auto tempPainter = painter; 10270 tempPainter.fillColor = style.backgroundColor; 10271 tempPainter.outlineColor = style.backgroundColor; 10272 10273 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 10274 Point(endX - smw.position.x, y + lh)); 10275 } 10276 auto tempPainter = painter; 10277 if(style.flags & TableView.CellStyle.Flags.textColorSet) 10278 tempPainter.outlineColor = style.textColor; 10279 10280 dotext(tempPainter); 10281 } else { 10282 dotext(painter); 10283 } 10284 }); 10285 } 10286 10287 x += column.calculatedWidth; 10288 } 10289 row++; 10290 y += lh; 10291 } 10292 return bounds; 10293 } 10294 10295 static class Style : Widget.Style { 10296 override WidgetBackground background() { 10297 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 10298 } 10299 } 10300 mixin OverrideStyle!Style; 10301 10302 private static class HeaderWidget : Widget { 10303 /+ 10304 maybe i should do a splitter thing on top of the other widgets 10305 so the splitter itself isn't really drawn but still replies to mouse events? 10306 +/ 10307 this(TableViewWidgetInner tvw, Widget parent) { 10308 super(parent); 10309 this.tvw = tvw; 10310 10311 this.remainder = new Button("", this); 10312 10313 this.addEventListener((scope ClickEvent ev) { 10314 int header = -1; 10315 foreach(idx, child; this.children[1 .. $]) { 10316 if(child is ev.target) { 10317 header = cast(int) idx; 10318 break; 10319 } 10320 } 10321 10322 if(header != -1) { 10323 auto hce = new HeaderClickedEvent(tvw.tvw, header); 10324 hce.dispatch(); 10325 } 10326 10327 }); 10328 } 10329 10330 void updateHeaders() { 10331 foreach(child; children[1 .. $]) 10332 child.removeWidget(); 10333 10334 foreach(column; tvw.tvw.columns) { 10335 // the cast is ok because I dup it above, just the type is never changed. 10336 // all this is private so it should never get messed up. 10337 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 10338 } 10339 } 10340 10341 Button remainder; 10342 TableViewWidgetInner tvw; 10343 10344 override void recomputeChildLayout() { 10345 registerMovement(); 10346 int pos; 10347 foreach(idx, child; children[1 .. $]) { 10348 if(idx >= tvw.tvw.columns.length) 10349 continue; 10350 child.x = pos; 10351 child.y = 0; 10352 child.width = tvw.tvw.columns[idx].calculatedWidth; 10353 child.height = scaleWithDpi(16);// this.height; 10354 pos += child.width; 10355 10356 child.recomputeChildLayout(); 10357 } 10358 10359 if(remainder is null) 10360 return; 10361 10362 remainder.x = pos; 10363 remainder.y = 0; 10364 if(pos < this.width) 10365 remainder.width = this.width - pos;// + 4; 10366 else 10367 remainder.width = 0; 10368 remainder.height = scaleWithDpi(16); 10369 10370 remainder.recomputeChildLayout(); 10371 } 10372 10373 // for the scrollable children mixin 10374 Point scrollOrigin() { 10375 return Point(tvw.smw.position.x, 0); 10376 } 10377 void paintFrameAndBackground(WidgetPainter painter) { } 10378 10379 mixin ScrollableChildren; 10380 } 10381 } 10382 10383 /+ 10384 10385 // given struct / array / number / string / etc, make it viewable and editable 10386 class DataViewerWidget : Widget { 10387 10388 } 10389 +/ 10390 10391 /++ 10392 A line edit box with an associated label. 10393 10394 History: 10395 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 10396 10397 ``` 10398 Old: ________ 10399 10400 New: 10401 ____________ 10402 ``` 10403 10404 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 10405 10406 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 10407 horizontal label but left aligned. You may also consider a [GridLayout]. 10408 +/ 10409 alias LabeledLineEdit = Labeled!LineEdit; 10410 10411 private int widthThatWouldFitChildLabels(Widget w) { 10412 if(w is null) 10413 return 0; 10414 10415 int max; 10416 10417 if(auto label = cast(TextLabel) w) { 10418 return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight(); 10419 } else { 10420 foreach(child; w.children) { 10421 max = mymax(max, widthThatWouldFitChildLabels(child)); 10422 } 10423 } 10424 10425 return max; 10426 } 10427 10428 /++ 10429 History: 10430 Added May 19, 2021 10431 +/ 10432 class Labeled(T) : Widget { 10433 /// 10434 this(string label, Widget parent) { 10435 super(parent); 10436 initialize!VerticalLayout(label, TextAlignment.Left, parent); 10437 } 10438 10439 /++ 10440 History: 10441 The alignment parameter was added May 17, 2021 10442 +/ 10443 this(string label, TextAlignment alignment, Widget parent) { 10444 super(parent); 10445 initialize!HorizontalLayout(label, alignment, parent); 10446 } 10447 10448 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 10449 tabStop = false; 10450 horizontal = is(L == HorizontalLayout); 10451 auto hl = new L(this); 10452 if(horizontal) { 10453 static class SpecialTextLabel : TextLabel { 10454 Widget outerParent; 10455 10456 this(string label, TextAlignment alignment, Widget outerParent, Widget parent) { 10457 this.outerParent = outerParent; 10458 super(label, alignment, parent); 10459 } 10460 10461 override int flexBasisWidth() { 10462 return widthThatWouldFitChildLabels(outerParent); 10463 } 10464 /+ 10465 override int widthShrinkiness() { return 0; } 10466 override int widthStretchiness() { return 1; } 10467 +/ 10468 10469 override int paddingRight() { return 6; } 10470 override int paddingLeft() { return 9; } 10471 10472 override int paddingTop() { return 3; } 10473 } 10474 this.label = new SpecialTextLabel(label, alignment, parent, hl); 10475 } else 10476 this.label = new TextLabel(label, alignment, hl); 10477 this.lineEdit = new T(hl); 10478 10479 this.label.labelFor = this.lineEdit; 10480 } 10481 10482 private bool horizontal; 10483 10484 TextLabel label; /// 10485 T lineEdit; /// 10486 10487 override int flexBasisWidth() { return 250; } 10488 override int widthShrinkiness() { return 1; } 10489 10490 override int minHeight() { 10491 return this.children[0].minHeight; 10492 } 10493 override int maxHeight() { return minHeight(); } 10494 override int marginTop() { return 4; } 10495 override int marginBottom() { return 4; } 10496 10497 // FIXME: i should prolly call it value as well as content tbh 10498 10499 /// 10500 @property string content() { 10501 return lineEdit.content; 10502 } 10503 /// 10504 @property void content(string c) { 10505 return lineEdit.content(c); 10506 } 10507 10508 /// 10509 void selectAll() { 10510 lineEdit.selectAll(); 10511 } 10512 10513 override void focus() { 10514 lineEdit.focus(); 10515 } 10516 } 10517 10518 /++ 10519 A labeled password edit. 10520 10521 History: 10522 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 10523 10524 The default parameters for the constructors were also removed on May 19, 2021 10525 +/ 10526 alias LabeledPasswordEdit = Labeled!PasswordEdit; 10527 10528 private string toMenuLabel(string s) { 10529 string n; 10530 n.reserve(s.length); 10531 foreach(c; s) 10532 if(c == '_') 10533 n ~= ' '; 10534 else 10535 n ~= c; 10536 return n; 10537 } 10538 10539 private void autoExceptionHandler(Exception e) { 10540 messageBox(e.msg); 10541 } 10542 10543 void callAsIfClickedFromMenu(alias fn)(auto ref __traits(parent, fn) _this, Window window) { 10544 makeAutomaticHandler!(fn)(window, &__traits(child, _this, fn))(); 10545 } 10546 10547 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) { 10548 static if(is(T : void delegate())) { 10549 return () { 10550 try 10551 t(); 10552 catch(Exception e) 10553 autoExceptionHandler(e); 10554 }; 10555 } else static if(is(typeof(fn) Params == __parameters)) { 10556 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 10557 return () { 10558 void onOK(string s) { 10559 member = s; 10560 try 10561 t(Params[0](s)); 10562 catch(Exception e) 10563 autoExceptionHandler(e); 10564 } 10565 10566 if( 10567 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 10568 || type == FileDialogType.Save) 10569 { 10570 getSaveFileName(window, &onOK, member, filters, null); 10571 } else 10572 getOpenFileName(window, &onOK, member, filters, null); 10573 }; 10574 } else { 10575 struct S { 10576 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 10577 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 10578 } else mixin(q{ 10579 static foreach(idx, ignore; Params) { 10580 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 10581 } 10582 }); 10583 } 10584 return () { 10585 dialog(window, (S s) { 10586 try { 10587 static if(is(typeof(t) Ret == return)) { 10588 static if(is(Ret == void)) { 10589 t(s.tupleof); 10590 } else { 10591 auto ret = t(s.tupleof); 10592 import std.conv; 10593 messageBox(to!string(ret), "Returned Value"); 10594 } 10595 } 10596 } catch(Exception e) 10597 autoExceptionHandler(e); 10598 }, null, __traits(identifier, fn)); 10599 }; 10600 } 10601 } 10602 } 10603 10604 private template hasAnyRelevantAnnotations(a...) { 10605 bool helper() { 10606 bool any; 10607 foreach(attr; a) { 10608 static if(is(typeof(attr) == .menu)) 10609 any = true; 10610 else static if(is(typeof(attr) == .toolbar)) 10611 any = true; 10612 else static if(is(attr == .separator)) 10613 any = true; 10614 else static if(is(typeof(attr) == .accelerator)) 10615 any = true; 10616 else static if(is(typeof(attr) == .hotkey)) 10617 any = true; 10618 else static if(is(typeof(attr) == .icon)) 10619 any = true; 10620 else static if(is(typeof(attr) == .label)) 10621 any = true; 10622 else static if(is(typeof(attr) == .tip)) 10623 any = true; 10624 } 10625 return any; 10626 } 10627 10628 enum bool hasAnyRelevantAnnotations = helper(); 10629 } 10630 10631 /++ 10632 A `MainWindow` is a window that includes turnkey support for a menu bar, tool bar, and status bar automatically positioned around a client area where you put your widgets. 10633 +/ 10634 class MainWindow : Window { 10635 /// 10636 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 10637 super(initialWidth, initialHeight, title); 10638 10639 _clientArea = new ClientAreaWidget(); 10640 _clientArea.x = 0; 10641 _clientArea.y = 0; 10642 _clientArea.width = this.width; 10643 _clientArea.height = this.height; 10644 _clientArea.tabStop = false; 10645 10646 super.addChild(_clientArea); 10647 10648 statusBar = new StatusBar(this); 10649 } 10650 10651 /++ 10652 Adds a menu and toolbar from annotated functions. It uses the top-level annotations from this module, so it is better to put the commands in a separate struct instad of in your window subclass, to avoid potential conflicts with method names (if you do hit one though, you can use `@(.icon(...))` instead of plain `@icon(...)` to disambiguate, though). 10653 10654 The only required annotation on a function is `@menu("Label")` to make it appear, but there are several optional ones I'd recommend considering, including `@toolbar("group name")`, `@icon()`, `@accelerator("keyboard shortcut string")`, and `@hotkey('char')`. 10655 10656 You can also use `@separator` to put a separating line in the menu before the function. 10657 10658 Functions may have zero or one argument. If they have an argument, an automatic dialog box (see: [dialog]) will be created to request the data from the user before calling your function. Some types have special treatment, like [FileName], will invoke the file dialog, assuming open or save based on the name of your function. 10659 10660 Let's look at a complete example: 10661 10662 --- 10663 import arsd.minigui; 10664 10665 void main() { 10666 auto window = new MainWindow(); 10667 10668 // we can add widgets before or after setting the menu, either way is fine. 10669 // i'll do it before here so the local variables are available to the commands. 10670 10671 auto textEdit = new TextEdit(window); 10672 10673 // Remember, in D, you can define structs inside of functions 10674 // and those structs can access the function's local variables. 10675 // 10676 // Of course, you might also want to do this separately, and if you 10677 // do, make sure you keep a reference to the window as a struct data 10678 // member so you can refer to it in cases like this Exit function. 10679 struct Commands { 10680 // the & in the string indicates that the next letter is the hotkey 10681 // to access it from the keyboard (so here, alt+f will open the 10682 // file menu) 10683 @menu("&File") { 10684 @accelerator("Ctrl+N") 10685 @hotkey('n') 10686 @icon(GenericIcons.New) // add an icon to the action 10687 @toolbar("File") // adds it to a toolbar. 10688 // The toolbar name is never visible to the user, but is used to group icons. 10689 void New() { 10690 previousFileReferenced = null; 10691 textEdit.content = ""; 10692 } 10693 10694 @icon(GenericIcons.Open) 10695 @toolbar("File") 10696 @hotkey('s') 10697 @accelerator("Ctrl+O") 10698 void Open(FileName!() filename) { 10699 import std.file; 10700 textEdit.content = std.file.readText(filename); 10701 } 10702 10703 @icon(GenericIcons.Save) 10704 @toolbar("File") 10705 @accelerator("Ctrl+S") 10706 @hotkey('s') 10707 void Save() { 10708 // these are still functions, so of course you can 10709 // still call them yourself too 10710 Save_As(previousFileReferenced); 10711 } 10712 10713 // underscores translate to spaces in the visible name 10714 @hotkey('a') 10715 void Save_As(FileName!() filename) { 10716 import std.file; 10717 std.file.write(previousFileReferenced, textEdit.content); 10718 } 10719 10720 // you can put the annotations before or after the function name+args and it works the same way 10721 @separator 10722 void Exit() @accelerator("Alt+F4") @hotkey('x') { 10723 window.close(); 10724 } 10725 } 10726 10727 @menu("&Edit") { 10728 // not putting accelerators here because the text edit widget 10729 // does it locally, so no need to duplicate it globally. 10730 10731 @icon(GenericIcons.Undo) 10732 void Undo() @toolbar("Undo") { 10733 textEdit.undo(); 10734 } 10735 10736 @separator 10737 10738 @icon(GenericIcons.Cut) 10739 void Cut() @toolbar("Edit") { 10740 textEdit.cut(); 10741 } 10742 @icon(GenericIcons.Copy) 10743 void Copy() @toolbar("Edit") { 10744 textEdit.copy(); 10745 } 10746 @icon(GenericIcons.Paste) 10747 void Paste() @toolbar("Edit") { 10748 textEdit.paste(); 10749 } 10750 10751 @separator 10752 void Select_All() { 10753 textEdit.selectAll(); 10754 } 10755 } 10756 10757 @menu("Help") { 10758 void About() @accelerator("F1") { 10759 window.messageBox("A minigui sample program."); 10760 } 10761 10762 // @label changes the name in the menu from what is in the code 10763 @label("In Menu Name") 10764 void otherNameInCode() {} 10765 } 10766 } 10767 10768 // declare the object that holds the commands, and set 10769 // and members you want from it 10770 Commands commands; 10771 10772 // and now tell minigui to do its magic and create the ui for it! 10773 window.setMenuAndToolbarFromAnnotatedCode(commands); 10774 10775 // then, loop the window normally; 10776 window.loop(); 10777 10778 // important to note that the `commands` variable must live through the window's whole life cycle, 10779 // or you can have crashes. If you declare the variable and loop in different functions, make sure 10780 // you do `new Commands` so the garbage collector can take over management of it for you. 10781 } 10782 --- 10783 10784 Note that you can call this function multiple times and it will add the items in order to the given items. 10785 10786 +/ 10787 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 10788 setMenuAndToolbarFromAnnotatedCode_internal(t); 10789 } 10790 /// ditto 10791 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 10792 setMenuAndToolbarFromAnnotatedCode_internal(t); 10793 } 10794 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 10795 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 10796 Menu[string] mcs; 10797 10798 alias ToolbarSection = ToolBar.ToolbarSection; 10799 ToolbarSection[] toolbarSections; 10800 10801 foreach(menu; menuBar.subMenus) { 10802 mcs[menu.label] = menu; 10803 } 10804 10805 foreach(memberName; __traits(derivedMembers, T)) { 10806 static if(memberName != "this") 10807 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 10808 .menu menu; 10809 .toolbar toolbar; 10810 bool separator; 10811 .accelerator accelerator; 10812 .hotkey hotkey; 10813 .icon icon; 10814 string label; 10815 string tip; 10816 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 10817 static if(is(typeof(attr) == .menu)) 10818 menu = attr; 10819 else static if(is(typeof(attr) == .toolbar)) 10820 toolbar = attr; 10821 else static if(is(attr == .separator)) 10822 separator = true; 10823 else static if(is(typeof(attr) == .accelerator)) 10824 accelerator = attr; 10825 else static if(is(typeof(attr) == .hotkey)) 10826 hotkey = attr; 10827 else static if(is(typeof(attr) == .icon)) 10828 icon = attr; 10829 else static if(is(typeof(attr) == .label)) 10830 label = attr.label; 10831 else static if(is(typeof(attr) == .tip)) 10832 tip = attr.tip; 10833 } 10834 10835 if(menu !is .menu.init || toolbar !is .toolbar.init) { 10836 ushort correctIcon = icon.id; // FIXME 10837 if(label.length == 0) 10838 label = memberName.toMenuLabel; 10839 10840 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName)); 10841 10842 auto action = new Action(label, correctIcon, handler); 10843 10844 if(accelerator.keyString.length) { 10845 auto ke = KeyEvent.parse(accelerator.keyString); 10846 action.accelerator = ke; 10847 accelerators[ke.toStr] = handler; 10848 } 10849 10850 if(toolbar !is .toolbar.init) { 10851 bool found; 10852 foreach(ref section; toolbarSections) 10853 if(section.name == toolbar.groupName) { 10854 section.actions ~= action; 10855 found = true; 10856 break; 10857 } 10858 if(!found) { 10859 toolbarSections ~= ToolbarSection(toolbar.groupName, [action]); 10860 } 10861 } 10862 if(menu !is .menu.init) { 10863 Menu mc; 10864 if(menu.name in mcs) { 10865 mc = mcs[menu.name]; 10866 } else { 10867 mc = new Menu(menu.name, this); 10868 menuBar.addItem(mc); 10869 mcs[menu.name] = mc; 10870 } 10871 10872 if(separator) 10873 mc.addSeparator(); 10874 auto mi = mc.addItem(new MenuItem(action)); 10875 10876 if(hotkey !is .hotkey.init) 10877 mi.hotkey = hotkey.ch; 10878 } 10879 } 10880 } 10881 } 10882 10883 this.menuBar = menuBar; 10884 10885 if(toolbarSections.length) { 10886 auto tb = new ToolBar(toolbarSections, this); 10887 } 10888 } 10889 10890 void delegate()[string] accelerators; 10891 10892 override void defaultEventHandler_keydown(KeyDownEvent event) { 10893 auto str = event.originalKeyEvent.toStr; 10894 if(auto acl = str in accelerators) 10895 (*acl)(); 10896 10897 // Windows this this automatically so only on custom need we implement it 10898 version(custom_widgets) { 10899 if(event.altKey && this.menuBar) { 10900 foreach(item; this.menuBar.items) { 10901 if(item.hotkey == keyToLetterCharAssumingLotsOfThingsThatYouMightBetterNotAssume(event.key)) { 10902 // FIXME this kinda sucks but meh just pretending to click on it to trigger other existing mediocre code 10903 item.dynamicState = DynamicState.hover | DynamicState.depressed; 10904 item.redraw(); 10905 auto e = new MouseDownEvent(item); 10906 e.dispatch(); 10907 break; 10908 } 10909 } 10910 } 10911 10912 if(event.key == Key.Menu) { 10913 showContextMenu(-1, -1); 10914 } 10915 } 10916 10917 super.defaultEventHandler_keydown(event); 10918 } 10919 10920 override void defaultEventHandler_mouseover(MouseOverEvent event) { 10921 super.defaultEventHandler_mouseover(event); 10922 if(this.statusBar !is null && event.target.statusTip.length) 10923 this.statusBar.parts[0].content = event.target.statusTip; 10924 else if(this.statusBar !is null && this.statusTip.length) 10925 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 10926 } 10927 10928 override void addChild(Widget c, int position = int.max) { 10929 if(auto tb = cast(ToolBar) c) 10930 version(win32_widgets) 10931 super.addChild(c, 0); 10932 else version(custom_widgets) 10933 super.addChild(c, menuBar ? 1 : 0); 10934 else static assert(0); 10935 else 10936 clientArea.addChild(c, position); 10937 } 10938 10939 ToolBar _toolBar; 10940 /// 10941 ToolBar toolBar() { return _toolBar; } 10942 /// 10943 ToolBar toolBar(ToolBar t) { 10944 _toolBar = t; 10945 foreach(child; this.children) 10946 if(child is t) 10947 return t; 10948 version(win32_widgets) 10949 super.addChild(t, 0); 10950 else version(custom_widgets) 10951 super.addChild(t, menuBar ? 1 : 0); 10952 else static assert(0); 10953 return t; 10954 } 10955 10956 MenuBar _menu; 10957 /// 10958 MenuBar menuBar() { return _menu; } 10959 /// 10960 MenuBar menuBar(MenuBar m) { 10961 if(m is _menu) { 10962 version(custom_widgets) 10963 queueRecomputeChildLayout(); 10964 return m; 10965 } 10966 10967 if(_menu !is null) { 10968 // make sure it is sanely removed 10969 // FIXME 10970 } 10971 10972 _menu = m; 10973 10974 version(win32_widgets) { 10975 SetMenu(parentWindow.win.impl.hwnd, m.handle); 10976 } else version(custom_widgets) { 10977 super.addChild(m, 0); 10978 10979 // clientArea.y = menu.height; 10980 // clientArea.height = this.height - menu.height; 10981 10982 queueRecomputeChildLayout(); 10983 } else static assert(false); 10984 10985 return _menu; 10986 } 10987 private Widget _clientArea; 10988 /// 10989 @property Widget clientArea() { return _clientArea; } 10990 protected @property void clientArea(Widget wid) { 10991 _clientArea = wid; 10992 } 10993 10994 private StatusBar _statusBar; 10995 /++ 10996 Returns the window's [StatusBar]. Be warned it may be `null`. 10997 +/ 10998 @property StatusBar statusBar() { return _statusBar; } 10999 /// ditto 11000 @property void statusBar(StatusBar bar) { 11001 if(_statusBar !is null) 11002 _statusBar.removeWidget(); 11003 _statusBar = bar; 11004 if(bar !is null) 11005 super.addChild(_statusBar); 11006 } 11007 } 11008 11009 /+ 11010 This is really an implementation detail of [MainWindow] 11011 +/ 11012 private class ClientAreaWidget : Widget { 11013 this() { 11014 this.tabStop = false; 11015 super(null); 11016 //sa = new ScrollableWidget(this); 11017 } 11018 /* 11019 ScrollableWidget sa; 11020 override void addChild(Widget w, int position) { 11021 if(sa is null) 11022 super.addChild(w, position); 11023 else { 11024 sa.addChild(w, position); 11025 sa.setContentSize(this.minWidth + 1, this.minHeight); 11026 writeln(sa.contentWidth, "x", sa.contentHeight); 11027 } 11028 } 11029 */ 11030 } 11031 11032 /** 11033 Toolbars are lists of buttons (typically icons) that appear under the menu. 11034 Each button ought to correspond to a menu item, represented by [Action] objects. 11035 */ 11036 class ToolBar : Widget { 11037 version(win32_widgets) { 11038 private int idealHeight; 11039 override int minHeight() { return idealHeight; } 11040 override int maxHeight() { return idealHeight; } 11041 } else version(custom_widgets) { 11042 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 11043 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 11044 } else static assert(false); 11045 override int heightStretchiness() { return 0; } 11046 11047 static struct ToolbarSection { 11048 string name; 11049 Action[] actions; 11050 } 11051 11052 version(win32_widgets) { 11053 HIMAGELIST imageListSmall; 11054 HIMAGELIST imageListLarge; 11055 } 11056 11057 this(Widget parent) { 11058 this(cast(ToolbarSection[]) null, parent); 11059 } 11060 11061 version(win32_widgets) 11062 void changeIconSize(bool useLarge) { 11063 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall)); 11064 11065 /+ 11066 SIZE size; 11067 import core.sys.windows.commctrl; 11068 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 11069 idealHeight = size.cy + 4; // the plus 4 is a hack 11070 +/ 11071 11072 idealHeight = useLarge ? 34 : 26; 11073 11074 if(parent) { 11075 parent.queueRecomputeChildLayout(); 11076 parent.redraw(); 11077 } 11078 11079 SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4)); 11080 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 11081 } 11082 11083 /++ 11084 History: 11085 The `ToolbarSection` overload was added December 31, 2024 11086 +/ 11087 this(Action[] actions, Widget parent) { 11088 this([ToolbarSection(null, actions)], parent); 11089 } 11090 11091 /// ditto 11092 this(ToolbarSection[] sections, Widget parent) { 11093 super(parent); 11094 11095 tabStop = false; 11096 11097 version(win32_widgets) { 11098 // so i like how the flat thing looks on windows, but not on wine 11099 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 11100 // leave it commented 11101 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 11102 11103 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 11104 11105 imageListSmall = ImageList_Create( 11106 // width, height 11107 16, 16, 11108 ILC_COLOR16 | ILC_MASK, 11109 16 /*numberOfButtons*/, 0); 11110 11111 imageListLarge = ImageList_Create( 11112 // width, height 11113 24, 24, 11114 ILC_COLOR16 | ILC_MASK, 11115 16 /*numberOfButtons*/, 0); 11116 11117 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall); 11118 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 11119 11120 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge); 11121 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL); 11122 11123 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 11124 11125 TBBUTTON[] buttons; 11126 11127 // FIXME: I_IMAGENONE is if here is no icon 11128 foreach(sidx, section; sections) { 11129 if(sidx) 11130 buttons ~= TBBUTTON( 11131 scaleWithDpi(4), 11132 0, 11133 TBSTATE_ENABLED, // state 11134 TBSTYLE_SEP | BTNS_SEP, // style 11135 0, // reserved array, just zero it out 11136 0, // dwData 11137 -1 11138 ); 11139 11140 foreach(action; section.actions) 11141 buttons ~= TBBUTTON( 11142 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 11143 action.id, 11144 TBSTATE_ENABLED, // state 11145 0, // style 11146 0, // reserved array, just zero it out 11147 0, // dwData 11148 cast(size_t) toWstringzInternal(action.label) // INT_PTR 11149 ); 11150 } 11151 11152 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 11153 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 11154 11155 /* 11156 RECT rect; 11157 GetWindowRect(hwnd, &rect); 11158 idealHeight = rect.bottom - rect.top + 10; // the +10 is a hack since the size right now doesn't look right on a real Windows XP box 11159 */ 11160 11161 dpiChanged(); // to load the things calling changeIconSize the first time 11162 11163 assert(idealHeight); 11164 } else version(custom_widgets) { 11165 foreach(sidx, section; sections) { 11166 if(sidx) 11167 new HorizontalSpacer(4, this); 11168 foreach(action; section.actions) 11169 new ToolButton(action, this); 11170 } 11171 } else static assert(false); 11172 } 11173 11174 override void recomputeChildLayout() { 11175 .recomputeChildLayout!"width"(this); 11176 } 11177 11178 11179 version(win32_widgets) 11180 override protected void dpiChanged() { 11181 auto sz = scaleWithDpi(16); 11182 if(sz >= 20) 11183 changeIconSize(true); 11184 else 11185 changeIconSize(false); 11186 } 11187 } 11188 11189 /// An implementation helper for [ToolBar]. Generally, you shouldn't create these yourself and instead just pass [Action]s to [ToolBar]'s constructor and let it create the buttons for you. 11190 class ToolButton : Button { 11191 /// 11192 this(Action action, Widget parent) { 11193 super(action.label, parent); 11194 tabStop = false; 11195 this.action = action; 11196 } 11197 11198 version(custom_widgets) 11199 override void defaultEventHandler_click(ClickEvent event) { 11200 foreach(handler; action.triggered) 11201 handler(); 11202 } 11203 11204 Action action; 11205 11206 override int maxWidth() { return toolbarIconSize; } 11207 override int minWidth() { return toolbarIconSize; } 11208 override int maxHeight() { return toolbarIconSize; } 11209 override int minHeight() { return toolbarIconSize; } 11210 11211 version(custom_widgets) 11212 override void paint(WidgetPainter painter) { 11213 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 11214 painter.outlineColor = Color.black; 11215 11216 immutable multiplier = toolbarIconSize / 4; 11217 immutable divisor = 16 / 4; 11218 11219 int ScaledNumber(int n) { 11220 // return n * multiplier / divisor; 11221 auto s = n * multiplier; 11222 auto it = s / divisor; 11223 auto rem = s % divisor; 11224 if(rem && n >= 8) // cuz the original used 0 .. 16 and we want to try to stay centered so things in the bottom half tend to be added a it 11225 it++; 11226 return it; 11227 } 11228 11229 arsd.color.Point Point(int x, int y) { 11230 return arsd.color.Point(ScaledNumber(x), ScaledNumber(y)); 11231 } 11232 11233 switch(action.iconId) { 11234 case GenericIcons.New: 11235 painter.fillColor = Color.white; 11236 painter.drawPolygon( 11237 Point(3, 2), Point(3, 13), Point(12, 13), Point(12, 6), 11238 Point(8, 2), Point(8, 6), Point(12, 6), Point(8, 2), 11239 Point(3, 2), Point(3, 13) 11240 ); 11241 break; 11242 case GenericIcons.Save: 11243 painter.fillColor = Color.white; 11244 painter.outlineColor = Color.black; 11245 painter.drawRectangle(Point(2, 2), Point(13, 13)); 11246 11247 // the label 11248 painter.drawRectangle(Point(4, 8), Point(11, 13)); 11249 11250 // the slider 11251 painter.fillColor = Color.black; 11252 painter.outlineColor = Color.black; 11253 painter.drawRectangle(Point(4, 3), Point(10, 6)); 11254 11255 painter.fillColor = Color.white; 11256 painter.outlineColor = Color.white; 11257 // the disc window 11258 painter.drawRectangle(Point(5, 3), Point(6, 5)); 11259 break; 11260 case GenericIcons.Open: 11261 painter.fillColor = Color.white; 11262 painter.drawPolygon( 11263 Point(4, 4), Point(4, 12), Point(13, 12), Point(13, 3), 11264 Point(9, 3), Point(9, 4), Point(4, 4)); 11265 painter.drawPolygon( 11266 Point(2, 6), Point(11, 6), 11267 Point(12, 12), Point(4, 12), 11268 Point(2, 6)); 11269 //painter.drawLine(Point(9, 6), Point(13, 7)); 11270 break; 11271 case GenericIcons.Copy: 11272 painter.fillColor = Color.white; 11273 painter.drawRectangle(Point(3, 2), Point(9, 10)); 11274 painter.drawRectangle(Point(6, 5), Point(12, 13)); 11275 break; 11276 case GenericIcons.Cut: 11277 painter.fillColor = Color.transparent; 11278 painter.outlineColor = getComputedStyle.foregroundColor(); 11279 painter.drawLine(Point(3, 2), Point(10, 9)); 11280 painter.drawLine(Point(4, 9), Point(11, 2)); 11281 painter.drawRectangle(Point(3, 9), Point(5, 13)); 11282 painter.drawRectangle(Point(9, 9), Point(11, 12)); 11283 break; 11284 case GenericIcons.Paste: 11285 painter.fillColor = Color.white; 11286 painter.drawRectangle(Point(2, 3), Point(11, 11)); 11287 painter.drawRectangle(Point(6, 8), Point(13, 13)); 11288 painter.drawLine(Point(6, 2), Point(4, 5)); 11289 painter.drawLine(Point(6, 2), Point(9, 5)); 11290 painter.fillColor = Color.black; 11291 painter.drawRectangle(Point(4, 5), Point(9, 6)); 11292 break; 11293 case GenericIcons.Help: 11294 painter.outlineColor = getComputedStyle.foregroundColor(); 11295 painter.drawText(arsd.color.Point(0, 0), "?", arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 11296 break; 11297 case GenericIcons.Undo: 11298 painter.fillColor = Color.transparent; 11299 painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64); 11300 painter.outlineColor = Color.black; 11301 painter.fillColor = Color.black; 11302 painter.drawPolygon( 11303 Point(4, 4), 11304 Point(8, 2), 11305 Point(8, 6), 11306 Point(4, 4), 11307 ); 11308 break; 11309 case GenericIcons.Redo: 11310 painter.fillColor = Color.transparent; 11311 painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64); 11312 painter.outlineColor = Color.black; 11313 painter.fillColor = Color.black; 11314 painter.drawPolygon( 11315 Point(10, 4), 11316 Point(6, 2), 11317 Point(6, 6), 11318 Point(10, 4), 11319 ); 11320 break; 11321 default: 11322 painter.outlineColor = getComputedStyle.foregroundColor; 11323 painter.drawText(arsd.color.Point(0, 0), action.label, arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 11324 } 11325 return bounds; 11326 }); 11327 } 11328 11329 } 11330 11331 11332 /++ 11333 You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode]. 11334 +/ 11335 class MenuBar : Widget { 11336 MenuItem[] items; 11337 Menu[] subMenus; 11338 11339 version(win32_widgets) { 11340 HMENU handle; 11341 /// 11342 this(Widget parent = null) { 11343 super(parent); 11344 11345 handle = CreateMenu(); 11346 tabStop = false; 11347 } 11348 } else version(custom_widgets) { 11349 /// 11350 this(Widget parent = null) { 11351 tabStop = false; // these are selected some other way 11352 super(parent); 11353 } 11354 11355 mixin Padding!q{2}; 11356 } else static assert(false); 11357 11358 version(custom_widgets) 11359 override void paint(WidgetPainter painter) { 11360 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 11361 } 11362 11363 /// 11364 MenuItem addItem(MenuItem item) { 11365 this.addChild(item); 11366 items ~= item; 11367 version(win32_widgets) { 11368 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 11369 } 11370 return item; 11371 } 11372 11373 11374 /// 11375 Menu addItem(Menu item) { 11376 11377 subMenus ~= item; 11378 11379 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 11380 11381 addChild(mbItem); 11382 items ~= mbItem; 11383 11384 version(win32_widgets) { 11385 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 11386 } else version(custom_widgets) { 11387 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 11388 item.popup(mbItem); 11389 }; 11390 } else static assert(false); 11391 11392 return item; 11393 } 11394 11395 override void recomputeChildLayout() { 11396 .recomputeChildLayout!"width"(this); 11397 } 11398 11399 override int maxHeight() { return defaultLineHeight + 4; } 11400 override int minHeight() { return defaultLineHeight + 4; } 11401 } 11402 11403 11404 /** 11405 Status bars appear at the bottom of a MainWindow. 11406 They are made out of Parts, with a width and content. 11407 11408 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 11409 11410 11411 sb.parts[0].content = "Status bar text!"; 11412 */ 11413 class StatusBar : Widget { 11414 private Part[] partsArray; 11415 /// 11416 struct Parts { 11417 @disable this(); 11418 this(StatusBar owner) { this.owner = owner; } 11419 //@disable this(this); 11420 /// 11421 @property int length() { return cast(int) owner.partsArray.length; } 11422 private StatusBar owner; 11423 private this(StatusBar owner, Part[] parts) { 11424 this.owner.partsArray = parts; 11425 this.owner = owner; 11426 } 11427 /// 11428 Part opIndex(int p) { 11429 if(owner.partsArray.length == 0) 11430 this ~= new StatusBar.Part(0); 11431 return owner.partsArray[p]; 11432 } 11433 11434 /// 11435 Part opOpAssign(string op : "~" )(Part p) { 11436 assert(owner.partsArray.length < 255); 11437 p.owner = this.owner; 11438 p.idx = cast(int) owner.partsArray.length; 11439 owner.partsArray ~= p; 11440 11441 owner.queueRecomputeChildLayout(); 11442 11443 version(win32_widgets) { 11444 int[256] pos; 11445 int cpos; 11446 foreach(idx, part; owner.partsArray) { 11447 if(idx + 1 == owner.partsArray.length) 11448 pos[idx] = -1; 11449 else { 11450 cpos += part.currentlyAssignedWidth; 11451 pos[idx] = cpos; 11452 } 11453 } 11454 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 11455 } else version(custom_widgets) { 11456 owner.redraw(); 11457 } else static assert(false); 11458 11459 return p; 11460 } 11461 11462 /++ 11463 Sets up proportional parts in one function call. You can use negative numbers to indicate device-independent pixels, and positive numbers to indicate proportions. 11464 11465 No given item should be 0. 11466 11467 History: 11468 Added December 31, 2024 11469 +/ 11470 void setSizes(int[] proportions...) { 11471 assert(this.owner); 11472 this.owner.partsArray = null; 11473 11474 foreach(n; proportions) { 11475 assert(n, "do not give 0 to statusBar.parts.set, it would make an invisible part. Try 1 instead."); 11476 11477 this.opOpAssign!"~"(new StatusBar.Part(n > 0 ? n : -n, n > 0 ? StatusBar.Part.WidthUnits.Proportional : StatusBar.Part.WidthUnits.DeviceIndependentPixels)); 11478 } 11479 11480 } 11481 } 11482 11483 private Parts _parts; 11484 /// 11485 final @property Parts parts() { 11486 return _parts; 11487 } 11488 11489 /++ 11490 11491 +/ 11492 static class Part { 11493 /++ 11494 History: 11495 Added September 1, 2023 (dub v11.1) 11496 +/ 11497 enum WidthUnits { 11498 /++ 11499 Unscaled pixels as they appear on screen. 11500 11501 If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui. 11502 +/ 11503 DeviceDependentPixels, 11504 /++ 11505 Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui. 11506 +/ 11507 DeviceIndependentPixels, 11508 /++ 11509 An approximate character count in the currently selected font (at layout time) of the status bar. This will use the x-width (similar to css `ch`). 11510 +/ 11511 ApproximateCharacters, 11512 /++ 11513 These take a proportion of the remaining space in the window after all other parts have been assigned. The sum of all proportional parts is then divided by the current item to get the amount of space it uses. 11514 11515 If you pass 0, it will assume that this item takes an average of all remaining proportional space. This is there primarily to provide compatibility with code written against older versions of minigui. 11516 +/ 11517 Proportional 11518 } 11519 private WidthUnits units; 11520 private int width; 11521 private StatusBar owner; 11522 11523 private int currentlyAssignedWidth; 11524 11525 /++ 11526 History: 11527 Prior to September 1, 2023, this took a default value of 100 and was interpreted as pixels, unless the value was 0 and it was the last item in the list, in which case it would use the remaining space in the window. 11528 11529 It now allows you to provide your own value for [WidthUnits]. 11530 11531 Additionally, the default value used to be an arbitrary value of 100. It is now 0, to take advantage of the automatic proportional calculator in the new version. If you want the old behavior, pass `100, StatusBar.Part.WidthUnits.DeviceIndependentPixels`. 11532 +/ 11533 this(int w, WidthUnits units = WidthUnits.Proportional) { 11534 this.units = units; 11535 this.width = w; 11536 } 11537 11538 /// ditto 11539 this(int w = 0) { 11540 if(w == 0) 11541 this(w, WidthUnits.Proportional); 11542 else 11543 this(w, WidthUnits.DeviceDependentPixels); 11544 } 11545 11546 private int idx; 11547 private string _content; 11548 /// 11549 @property string content() { return _content; } 11550 /// 11551 @property void content(string s) { 11552 version(win32_widgets) { 11553 _content = s; 11554 WCharzBuffer bfr = WCharzBuffer(s); 11555 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 11556 } else version(custom_widgets) { 11557 if(_content != s) { 11558 _content = s; 11559 owner.redraw(); 11560 } 11561 } else static assert(false); 11562 } 11563 } 11564 string simpleModeContent; 11565 bool inSimpleMode; 11566 11567 11568 /// 11569 this(Widget parent) { 11570 super(null); // FIXME 11571 _parts = Parts(this); 11572 tabStop = false; 11573 version(win32_widgets) { 11574 parentWindow = parent.parentWindow; 11575 createWin32Window(this, "msctls_statusbar32"w, "", 0); 11576 11577 RECT rect; 11578 GetWindowRect(hwnd, &rect); 11579 idealHeight = rect.bottom - rect.top; 11580 assert(idealHeight); 11581 } else version(custom_widgets) { 11582 } else static assert(false); 11583 } 11584 11585 override void recomputeChildLayout() { 11586 int remainingLength = this.width; 11587 11588 int proportionalSum; 11589 int proportionalCount; 11590 foreach(idx, part; this.partsArray) { 11591 with(Part.WidthUnits) 11592 final switch(part.units) { 11593 case DeviceDependentPixels: 11594 part.currentlyAssignedWidth = part.width; 11595 remainingLength -= part.currentlyAssignedWidth; 11596 break; 11597 case DeviceIndependentPixels: 11598 part.currentlyAssignedWidth = scaleWithDpi(part.width); 11599 remainingLength -= part.currentlyAssignedWidth; 11600 break; 11601 case ApproximateCharacters: 11602 auto cs = getComputedStyle(); 11603 auto font = cs.font; 11604 11605 part.currentlyAssignedWidth = font.averageWidth * this.width; 11606 remainingLength -= part.currentlyAssignedWidth; 11607 break; 11608 case Proportional: 11609 proportionalSum += part.width; 11610 proportionalCount ++; 11611 break; 11612 } 11613 } 11614 11615 foreach(part; this.partsArray) { 11616 if(part.units == Part.WidthUnits.Proportional) { 11617 auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width; 11618 if(proportion == 0) 11619 proportion = 1; 11620 11621 if(proportionalSum == 0) 11622 proportionalSum = proportionalCount; 11623 11624 part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum; 11625 } 11626 } 11627 11628 super.recomputeChildLayout(); 11629 } 11630 11631 version(win32_widgets) 11632 override protected void dpiChanged() { 11633 RECT rect; 11634 GetWindowRect(hwnd, &rect); 11635 idealHeight = rect.bottom - rect.top; 11636 assert(idealHeight); 11637 } 11638 11639 version(custom_widgets) 11640 override void paint(WidgetPainter painter) { 11641 auto cs = getComputedStyle(); 11642 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 11643 int cpos = 0; 11644 foreach(idx, part; this.partsArray) { 11645 auto partWidth = part.currentlyAssignedWidth; 11646 // part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 11647 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 11648 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 11649 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 11650 11651 painter.outlineColor = cs.foregroundColor(); 11652 painter.fillColor = cs.foregroundColor(); 11653 11654 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 11655 cpos += partWidth; 11656 } 11657 } 11658 11659 11660 version(win32_widgets) { 11661 private int idealHeight; 11662 override int maxHeight() { return idealHeight; } 11663 override int minHeight() { return idealHeight; } 11664 } else version(custom_widgets) { 11665 override int maxHeight() { return defaultLineHeight + 4; } 11666 override int minHeight() { return defaultLineHeight + 4; } 11667 } else static assert(false); 11668 } 11669 11670 /// Displays an in-progress indicator without known values 11671 version(none) 11672 class IndefiniteProgressBar : Widget { 11673 version(win32_widgets) 11674 this(Widget parent) { 11675 super(parent); 11676 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 11677 tabStop = false; 11678 } 11679 override int minHeight() { return 10; } 11680 } 11681 11682 /// A progress bar with a known endpoint and completion amount 11683 class ProgressBar : Widget { 11684 /++ 11685 History: 11686 Added March 16, 2022 (dub v10.7) 11687 +/ 11688 this(int min, int max, Widget parent) { 11689 this(parent); 11690 setRange(cast(ushort) min, cast(ushort) max); // FIXME 11691 } 11692 this(Widget parent) { 11693 version(win32_widgets) { 11694 super(parent); 11695 createWin32Window(this, "msctls_progress32"w, "", 0); 11696 tabStop = false; 11697 } else version(custom_widgets) { 11698 super(parent); 11699 max = 100; 11700 step = 10; 11701 tabStop = false; 11702 } else static assert(0); 11703 } 11704 11705 version(custom_widgets) 11706 override void paint(WidgetPainter painter) { 11707 auto cs = getComputedStyle(); 11708 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 11709 painter.fillColor = cs.progressBarColor; 11710 painter.drawRectangle(Point(0, 0), width * current / max, height); 11711 } 11712 11713 11714 version(custom_widgets) { 11715 int current; 11716 int max; 11717 int step; 11718 } 11719 11720 /// 11721 void advanceOneStep() { 11722 version(win32_widgets) 11723 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 11724 else version(custom_widgets) 11725 addToPosition(step); 11726 else static assert(false); 11727 } 11728 11729 /// 11730 void setStepIncrement(int increment) { 11731 version(win32_widgets) 11732 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 11733 else version(custom_widgets) 11734 step = increment; 11735 else static assert(false); 11736 } 11737 11738 /// 11739 void addToPosition(int amount) { 11740 version(win32_widgets) 11741 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 11742 else version(custom_widgets) 11743 setPosition(current + amount); 11744 else static assert(false); 11745 } 11746 11747 /// 11748 void setPosition(int pos) { 11749 version(win32_widgets) 11750 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 11751 else version(custom_widgets) { 11752 current = pos; 11753 if(current > max) 11754 current = max; 11755 redraw(); 11756 } 11757 else static assert(false); 11758 } 11759 11760 /// 11761 void setRange(ushort min, ushort max) { 11762 version(win32_widgets) 11763 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 11764 else version(custom_widgets) { 11765 this.max = max; 11766 } 11767 else static assert(false); 11768 } 11769 11770 override int minHeight() { return 10; } 11771 } 11772 11773 version(custom_widgets) 11774 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 11775 thisLabel.reserve(label.length); 11776 bool justSawAmpersand; 11777 foreach(ch; label) { 11778 if(justSawAmpersand) { 11779 justSawAmpersand = false; 11780 if(ch == '&') { 11781 goto plain; 11782 } 11783 thisAccelerator = ch; 11784 } else { 11785 if(ch == '&') { 11786 justSawAmpersand = true; 11787 continue; 11788 } 11789 plain: 11790 thisLabel ~= ch; 11791 } 11792 } 11793 } 11794 11795 /++ 11796 Creates the fieldset (also known as a group box) with the given label. A fieldset is generally used a container for mutually exclusive [Radiobox]s. 11797 11798 11799 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 11800 11801 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11802 11803 History: 11804 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 11805 +/ 11806 class Fieldset : Widget { 11807 // FIXME: on Windows,it doesn't draw the background on the label 11808 // on X, it doesn't fix the clipping rectangle for it 11809 version(win32_widgets) 11810 override int paddingTop() { return defaultLineHeight; } 11811 else version(custom_widgets) 11812 override int paddingTop() { return defaultLineHeight + 2; } 11813 else static assert(false); 11814 override int paddingBottom() { return 6; } 11815 override int paddingLeft() { return 6; } 11816 override int paddingRight() { return 6; } 11817 11818 override int marginLeft() { return 6; } 11819 override int marginRight() { return 6; } 11820 override int marginTop() { return 2; } 11821 override int marginBottom() { return 2; } 11822 11823 string legend; 11824 11825 version(custom_widgets) private dchar accelerator; 11826 11827 this(string legend, Widget parent) { 11828 version(win32_widgets) { 11829 super(parent); 11830 this.legend = legend; 11831 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 11832 tabStop = false; 11833 } else version(custom_widgets) { 11834 super(parent); 11835 tabStop = false; 11836 11837 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 11838 } else static assert(0); 11839 } 11840 11841 version(custom_widgets) 11842 override void paint(WidgetPainter painter) { 11843 auto dlh = defaultLineHeight; 11844 11845 painter.fillColor = Color.transparent; 11846 auto cs = getComputedStyle(); 11847 painter.pen = Pen(cs.foregroundColor, 1); 11848 painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2); 11849 11850 auto tx = painter.textSize(legend); 11851 painter.outlineColor = Color.transparent; 11852 11853 version(Windows) { 11854 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 11855 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 11856 SelectObject(painter.impl.hdc, b); 11857 } else static if(UsingSimpledisplayX11) { 11858 painter.fillColor = getComputedStyle().windowBackgroundColor; 11859 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 11860 } 11861 painter.outlineColor = cs.foregroundColor; 11862 painter.drawText(Point(8, 0), legend); 11863 } 11864 11865 override int maxHeight() { 11866 auto m = paddingTop() + paddingBottom(); 11867 foreach(child; children) { 11868 auto mh = child.maxHeight(); 11869 if(mh == int.max) 11870 return int.max; 11871 m += mh; 11872 m += child.marginBottom(); 11873 m += child.marginTop(); 11874 } 11875 m += 6; 11876 if(m < minHeight) 11877 return minHeight; 11878 return m; 11879 } 11880 11881 override int minHeight() { 11882 auto m = paddingTop() + paddingBottom(); 11883 foreach(child; children) { 11884 m += child.minHeight(); 11885 m += child.marginBottom(); 11886 m += child.marginTop(); 11887 } 11888 return m + 6; 11889 } 11890 11891 override int minWidth() { 11892 return 6 + cast(int) this.legend.length * 7; 11893 } 11894 } 11895 11896 /++ 11897 $(IMG //arsdnet.net/minigui-screenshots/windows/Fieldset.png, A box saying "baby will" with three round buttons inside it for the options of "eat", "cry", and "sleep") 11898 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 11899 +/ 11900 version(minigui_screenshots) 11901 @Screenshot("Fieldset") 11902 unittest { 11903 auto window = new Window(200, 100); 11904 auto set = new Fieldset("Baby will", window); 11905 auto option1 = new Radiobox("Eat", set); 11906 auto option2 = new Radiobox("Cry", set); 11907 auto option3 = new Radiobox("Sleep", set); 11908 window.loop(); 11909 } 11910 11911 /// Draws a line 11912 class HorizontalRule : Widget { 11913 mixin Margin!q{ 2 }; 11914 override int minHeight() { return 2; } 11915 override int maxHeight() { return 2; } 11916 11917 /// 11918 this(Widget parent) { 11919 super(parent); 11920 } 11921 11922 override void paint(WidgetPainter painter) { 11923 auto cs = getComputedStyle(); 11924 painter.outlineColor = cs.darkAccentColor; 11925 painter.drawLine(Point(0, 0), Point(width, 0)); 11926 painter.outlineColor = cs.lightAccentColor; 11927 painter.drawLine(Point(0, 1), Point(width, 1)); 11928 } 11929 } 11930 11931 version(minigui_screenshots) 11932 @Screenshot("HorizontalRule") 11933 /++ 11934 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 11935 11936 +/ 11937 unittest { 11938 auto window = new Window(200, 100); 11939 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 11940 new HorizontalRule(window); 11941 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 11942 window.loop(); 11943 } 11944 11945 /// ditto 11946 class VerticalRule : Widget { 11947 mixin Margin!q{ 2 }; 11948 override int minWidth() { return 2; } 11949 override int maxWidth() { return 2; } 11950 11951 /// 11952 this(Widget parent) { 11953 super(parent); 11954 } 11955 11956 override void paint(WidgetPainter painter) { 11957 auto cs = getComputedStyle(); 11958 painter.outlineColor = cs.darkAccentColor; 11959 painter.drawLine(Point(0, 0), Point(0, height)); 11960 painter.outlineColor = cs.lightAccentColor; 11961 painter.drawLine(Point(1, 0), Point(1, height)); 11962 } 11963 } 11964 11965 11966 /// 11967 class Menu : Window { 11968 void remove() { 11969 foreach(i, child; parentWindow.children) 11970 if(child is this) { 11971 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 11972 break; 11973 } 11974 parentWindow.redraw(); 11975 11976 parentWindow.releaseMouseCapture(); 11977 } 11978 11979 /// 11980 void addSeparator() { 11981 version(win32_widgets) 11982 AppendMenu(handle, MF_SEPARATOR, 0, null); 11983 else version(custom_widgets) 11984 auto hr = new HorizontalRule(this); 11985 else static assert(0); 11986 } 11987 11988 override int paddingTop() { return 4; } 11989 override int paddingBottom() { return 4; } 11990 override int paddingLeft() { return 2; } 11991 override int paddingRight() { return 2; } 11992 11993 version(win32_widgets) {} 11994 else version(custom_widgets) { 11995 11996 Widget previouslyFocusedWidget; 11997 Widget* previouslyFocusedWidgetBelongsIn; 11998 11999 SimpleWindow dropDown; 12000 Widget menuParent; 12001 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 12002 this.menuParent = parent; 12003 12004 previouslyFocusedWidget = parent.parentWindow.focusedWidget; 12005 previouslyFocusedWidgetBelongsIn = &parent.parentWindow.focusedWidget; 12006 parent.parentWindow.focusedWidget = this; 12007 12008 int w = 150; 12009 int h = paddingTop + paddingBottom; 12010 if(this.children.length) { 12011 // hacking it to get the ideal height out of recomputeChildLayout 12012 this.width = w; 12013 this.height = h; 12014 this.recomputeChildLayoutEntry(); 12015 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 12016 h += paddingBottom; 12017 12018 h -= 2; // total hack, i just like the way it looks a bit tighter even though technically MenuItem reserves some space to center in normal circumstances 12019 } 12020 12021 if(offsetY == int.min) 12022 offsetY = parent.defaultLineHeight; 12023 12024 auto coord = parent.globalCoordinates(); 12025 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 12026 this.x = 0; 12027 this.y = 0; 12028 this.width = dropDown.width; 12029 this.height = dropDown.height; 12030 this.drawableWindow = dropDown; 12031 this.recomputeChildLayoutEntry(); 12032 12033 static if(UsingSimpledisplayX11) 12034 XSync(XDisplayConnection.get, 0); 12035 12036 dropDown.visibilityChanged = (bool visible) { 12037 if(visible) { 12038 this.redraw(); 12039 dropDown.grabInput(); 12040 } else { 12041 dropDown.releaseInputGrab(); 12042 } 12043 }; 12044 12045 dropDown.show(); 12046 12047 clickListener = this.addEventListener((scope ClickEvent ev) { 12048 unpopup(); 12049 // need to unlock asap just in case other user handlers block... 12050 static if(UsingSimpledisplayX11) 12051 flushGui(); 12052 }, true /* again for asap action */); 12053 } 12054 12055 EventListener clickListener; 12056 } 12057 else static assert(false); 12058 12059 version(custom_widgets) 12060 void unpopup() { 12061 mouseLastOver = mouseLastDownOn = null; 12062 dropDown.hide(); 12063 if(!menuParent.parentWindow.win.closed) { 12064 if(auto maw = cast(MouseActivatedWidget) menuParent) { 12065 maw.setDynamicState(DynamicState.depressed, false); 12066 maw.setDynamicState(DynamicState.hover, false); 12067 maw.redraw(); 12068 } 12069 // menuParent.parentWindow.win.focus(); 12070 } 12071 clickListener.disconnect(); 12072 12073 if(previouslyFocusedWidgetBelongsIn) 12074 *previouslyFocusedWidgetBelongsIn = previouslyFocusedWidget; 12075 } 12076 12077 MenuItem[] items; 12078 12079 /// 12080 MenuItem addItem(MenuItem item) { 12081 addChild(item); 12082 items ~= item; 12083 version(win32_widgets) { 12084 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 12085 } 12086 return item; 12087 } 12088 12089 string label; 12090 12091 version(win32_widgets) { 12092 HMENU handle; 12093 /// 12094 this(string label, Widget parent) { 12095 // not actually passing the parent since it effs up the drawing 12096 super(cast(Widget) null);// parent); 12097 this.label = label; 12098 handle = CreatePopupMenu(); 12099 } 12100 } else version(custom_widgets) { 12101 /// 12102 this(string label, Widget parent) { 12103 12104 if(dropDown) { 12105 dropDown.close(); 12106 } 12107 dropDown = new SimpleWindow( 12108 150, 4, 12109 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 12110 12111 this.label = label; 12112 12113 super(dropDown); 12114 } 12115 } else static assert(false); 12116 12117 override int maxHeight() { return defaultLineHeight; } 12118 override int minHeight() { return defaultLineHeight; } 12119 12120 version(custom_widgets) { 12121 Widget currentPlace; 12122 12123 void changeCurrentPlace(Widget n) { 12124 if(currentPlace) { 12125 currentPlace.dynamicState = 0; 12126 } 12127 12128 if(n) { 12129 n.dynamicState = DynamicState.hover; 12130 } 12131 12132 currentPlace = n; 12133 } 12134 12135 override void paint(WidgetPainter painter) { 12136 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 12137 } 12138 12139 override void defaultEventHandler_keydown(KeyDownEvent ke) { 12140 switch(ke.key) { 12141 case Key.Down: 12142 Widget next; 12143 Widget first; 12144 foreach(w; this.children) { 12145 if((cast(MenuItem) w) is null) 12146 continue; 12147 12148 if(first is null) 12149 first = w; 12150 12151 if(next !is null) { 12152 next = w; 12153 break; 12154 } 12155 12156 if(currentPlace is null) { 12157 next = w; 12158 break; 12159 } 12160 12161 if(w is currentPlace) { 12162 next = w; 12163 } 12164 } 12165 12166 if(next is currentPlace) 12167 next = first; 12168 12169 changeCurrentPlace(next); 12170 break; 12171 case Key.Up: 12172 Widget prev; 12173 foreach(w; this.children) { 12174 if((cast(MenuItem) w) is null) 12175 continue; 12176 if(w is currentPlace) { 12177 if(prev is null) { 12178 foreach_reverse(c; this.children) { 12179 if((cast(MenuItem) c) !is null) { 12180 prev = c; 12181 break; 12182 } 12183 } 12184 } 12185 break; 12186 } 12187 prev = w; 12188 } 12189 changeCurrentPlace(prev); 12190 break; 12191 case Key.Left: 12192 case Key.Right: 12193 if(menuParent) { 12194 Menu first; 12195 Menu last; 12196 Menu prev; 12197 Menu next; 12198 bool found; 12199 12200 size_t prev_idx; 12201 size_t next_idx; 12202 12203 MenuBar mb = cast(MenuBar) menuParent.parent; 12204 12205 if(mb) { 12206 foreach(idx, menu; mb.subMenus) { 12207 if(first is null) 12208 first = menu; 12209 last = menu; 12210 if(found && next is null) { 12211 next = menu; 12212 next_idx = idx; 12213 } 12214 if(menu is this) 12215 found = true; 12216 if(!found) { 12217 prev = menu; 12218 prev_idx = idx; 12219 } 12220 } 12221 12222 Menu nextMenu; 12223 size_t nextMenuIdx; 12224 if(ke.key == Key.Left) { 12225 nextMenu = prev ? prev : last; 12226 nextMenuIdx = prev ? prev_idx : mb.subMenus.length - 1; 12227 } else { 12228 nextMenu = next ? next : first; 12229 nextMenuIdx = next ? next_idx : 0; 12230 } 12231 12232 unpopup(); 12233 12234 auto rent = mb.children[nextMenuIdx]; // FIXME thsi is not necessarily right 12235 rent.dynamicState = DynamicState.depressed | DynamicState.hover; 12236 nextMenu.popup(rent); 12237 } 12238 } 12239 break; 12240 case Key.Enter: 12241 case Key.PadEnter: 12242 // because the key up and char events will go back to the other window after we unpopup! 12243 // we will wait for the char event to come (in the following method) 12244 break; 12245 case Key.Escape: 12246 unpopup(); 12247 break; 12248 default: 12249 } 12250 } 12251 override void defaultEventHandler_char(CharEvent ke) { 12252 // if one is selected, enter activates it 12253 if(currentPlace) { 12254 if(ke.character == '\n') { 12255 // enter selects 12256 auto event = new Event(EventType.triggered, currentPlace); 12257 event.dispatch(); 12258 unpopup(); 12259 return; 12260 } 12261 } 12262 12263 // otherwise search for a hotkey 12264 foreach(item; items) { 12265 if(item.hotkey == ke.character) { 12266 auto event = new Event(EventType.triggered, item); 12267 event.dispatch(); 12268 unpopup(); 12269 return; 12270 } 12271 } 12272 } 12273 override void defaultEventHandler_mouseover(MouseOverEvent moe) { 12274 if(moe.target && moe.target.parent is this) 12275 changeCurrentPlace(moe.target); 12276 } 12277 } 12278 } 12279 12280 /++ 12281 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 12282 +/ 12283 class MenuItem : MouseActivatedWidget { 12284 Menu submenu; 12285 12286 Action action; 12287 string label; 12288 dchar hotkey; 12289 12290 override int paddingLeft() { return 4; } 12291 12292 override int maxHeight() { return defaultLineHeight + 4; } 12293 override int minHeight() { return defaultLineHeight + 4; } 12294 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 12295 override int maxWidth() { 12296 if(cast(MenuBar) parent) { 12297 return minWidth(); 12298 } 12299 return int.max; 12300 } 12301 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 12302 this(string lbl, Widget parent = null) { 12303 super(parent); 12304 //label = lbl; // FIXME 12305 foreach(idx, char ch; lbl) // FIXME 12306 if(ch != '&') { // FIXME 12307 label ~= ch; // FIXME 12308 } else { 12309 if(idx + 1 < lbl.length) { 12310 hotkey = lbl[idx + 1]; 12311 if(hotkey >= 'A' && hotkey <= 'Z') 12312 hotkey += 32; 12313 } 12314 } 12315 tabStop = false; // these are selected some other way 12316 } 12317 12318 /// 12319 this(Action action, Widget parent = null) { 12320 assert(action !is null); 12321 this(action.label, parent); 12322 this.action = action; 12323 tabStop = false; // these are selected some other way 12324 } 12325 12326 version(custom_widgets) 12327 override void paint(WidgetPainter painter) { 12328 auto cs = getComputedStyle(); 12329 if(dynamicState & DynamicState.depressed) 12330 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 12331 else { 12332 if(dynamicState & DynamicState.hover) { 12333 painter.fillColor = cs.hoveringColor; 12334 painter.outlineColor = Color.transparent; 12335 } else { 12336 painter.fillColor = cs.background.color; 12337 painter.outlineColor = Color.transparent; 12338 } 12339 12340 painter.drawRectangle(Point(0, 0), Size(this.width, this.height)); 12341 } 12342 12343 if(dynamicState & DynamicState.hover) 12344 painter.outlineColor = cs.activeMenuItemColor; 12345 else 12346 painter.outlineColor = cs.foregroundColor; 12347 painter.fillColor = Color.transparent; 12348 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 12349 if(action && action.accelerator !is KeyEvent.init) { 12350 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 12351 12352 } 12353 } 12354 12355 static class Style : Widget.Style { 12356 override bool variesWithState(ulong dynamicStateFlags) { 12357 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 12358 } 12359 } 12360 mixin OverrideStyle!Style; 12361 12362 override void defaultEventHandler_triggered(Event event) { 12363 if(action) 12364 foreach(handler; action.triggered) 12365 handler(); 12366 12367 if(auto pmenu = cast(Menu) this.parent) 12368 pmenu.remove(); 12369 12370 super.defaultEventHandler_triggered(event); 12371 } 12372 } 12373 12374 version(win32_widgets) 12375 /// A "mouse activiated widget" is really just an abstract variant of button. 12376 class MouseActivatedWidget : Widget { 12377 @property bool isChecked() { 12378 assert(hwnd); 12379 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 12380 12381 } 12382 @property void isChecked(bool state) { 12383 assert(hwnd); 12384 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 12385 12386 } 12387 12388 override void handleWmCommand(ushort cmd, ushort id) { 12389 if(cmd == 0) { 12390 auto event = new Event(EventType.triggered, this); 12391 event.dispatch(); 12392 } 12393 } 12394 12395 this(Widget parent) { 12396 super(parent); 12397 } 12398 } 12399 else version(custom_widgets) 12400 /// ditto 12401 class MouseActivatedWidget : Widget { 12402 @property bool isChecked() { return isChecked_; } 12403 @property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;} 12404 12405 private bool isChecked_; 12406 12407 this(Widget parent) { 12408 super(parent); 12409 12410 addEventListener((MouseDownEvent ev) { 12411 if(ev.button == MouseButton.left) { 12412 setDynamicState(DynamicState.depressed, true); 12413 setDynamicState(DynamicState.hover, true); 12414 redraw(); 12415 } 12416 }); 12417 12418 addEventListener((MouseUpEvent ev) { 12419 if(ev.button == MouseButton.left) { 12420 setDynamicState(DynamicState.depressed, false); 12421 setDynamicState(DynamicState.hover, false); 12422 redraw(); 12423 } 12424 }); 12425 12426 addEventListener((MouseMoveEvent mme) { 12427 if(!(mme.state & ModifierState.leftButtonDown)) { 12428 if(dynamicState_ & DynamicState.depressed) { 12429 setDynamicState(DynamicState.depressed, false); 12430 redraw(); 12431 } 12432 } 12433 }); 12434 } 12435 12436 override void defaultEventHandler_focus(FocusEvent ev) { 12437 super.defaultEventHandler_focus(ev); 12438 this.redraw(); 12439 } 12440 override void defaultEventHandler_blur(BlurEvent ev) { 12441 super.defaultEventHandler_blur(ev); 12442 setDynamicState(DynamicState.depressed, false); 12443 this.redraw(); 12444 } 12445 override void defaultEventHandler_keydown(KeyDownEvent ev) { 12446 super.defaultEventHandler_keydown(ev); 12447 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 12448 setDynamicState(DynamicState.depressed, true); 12449 setDynamicState(DynamicState.hover, true); 12450 this.redraw(); 12451 } 12452 } 12453 override void defaultEventHandler_keyup(KeyUpEvent ev) { 12454 super.defaultEventHandler_keyup(ev); 12455 if(!(dynamicState & DynamicState.depressed)) 12456 return; 12457 setDynamicState(DynamicState.depressed, false); 12458 setDynamicState(DynamicState.hover, false); 12459 this.redraw(); 12460 12461 auto event = new Event(EventType.triggered, this); 12462 event.sendDirectly(); 12463 } 12464 override void defaultEventHandler_click(ClickEvent ev) { 12465 super.defaultEventHandler_click(ev); 12466 if(ev.button == MouseButton.left) { 12467 auto event = new Event(EventType.triggered, this); 12468 event.sendDirectly(); 12469 } 12470 } 12471 12472 } 12473 else static assert(false); 12474 12475 /* 12476 /++ 12477 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 12478 12479 Basically the same as a checkbox. 12480 +/ 12481 class OnOffSwitch : MouseActivatedWidget { 12482 12483 } 12484 */ 12485 12486 /++ 12487 History: 12488 Added June 15, 2021 (dub v10.1) 12489 +/ 12490 struct ImageLabel { 12491 /++ 12492 Defines a label+image combo used by some widgets. 12493 12494 If you provide just a text label, that is all the widget will try to 12495 display. Or just an image will display just that. If you provide both, 12496 it may display both text and image side by side or display the image 12497 and offer text on an input event depending on the widget. 12498 12499 History: 12500 The `alignment` parameter was added on September 27, 2021 12501 +/ 12502 this(string label, TextAlignment alignment = TextAlignment.Center) { 12503 this.label = label; 12504 this.displayFlags = DisplayFlags.displayText; 12505 this.alignment = alignment; 12506 } 12507 12508 /// ditto 12509 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 12510 this.label = label; 12511 this.image = image; 12512 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 12513 this.alignment = alignment; 12514 } 12515 12516 /// ditto 12517 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 12518 this.image = image; 12519 this.displayFlags = DisplayFlags.displayImage; 12520 this.alignment = alignment; 12521 } 12522 12523 /// ditto 12524 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 12525 this.label = label; 12526 this.image = image; 12527 this.alignment = alignment; 12528 this.displayFlags = displayFlags; 12529 } 12530 12531 string label; 12532 MemoryImage image; 12533 12534 enum DisplayFlags { 12535 displayText = 1 << 0, 12536 displayImage = 1 << 1, 12537 } 12538 12539 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 12540 12541 TextAlignment alignment; 12542 } 12543 12544 /++ 12545 A basic checked or not checked box with an attached label. 12546 12547 12548 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 12549 12550 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 12551 12552 History: 12553 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 12554 +/ 12555 class Checkbox : MouseActivatedWidget { 12556 version(win32_widgets) { 12557 override int maxHeight() { return scaleWithDpi(16); } 12558 override int minHeight() { return scaleWithDpi(16); } 12559 } else version(custom_widgets) { 12560 private enum buttonSize = 16; 12561 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 12562 override int minHeight() { return maxHeight(); } 12563 } else static assert(0); 12564 12565 override int marginLeft() { return 4; } 12566 12567 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 12568 12569 /++ 12570 Just an alias because I keep typing checked out of web habit. 12571 12572 History: 12573 Added May 31, 2021 12574 +/ 12575 alias checked = isChecked; 12576 12577 private string label; 12578 private dchar accelerator; 12579 12580 /++ 12581 +/ 12582 this(string label, Widget parent) { 12583 this(ImageLabel(label), Appearance.checkbox, parent); 12584 } 12585 12586 /// ditto 12587 this(string label, Appearance appearance, Widget parent) { 12588 this(ImageLabel(label), appearance, parent); 12589 } 12590 12591 /++ 12592 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 12593 12594 History: 12595 Added June 29, 2021 (dub v10.2) 12596 +/ 12597 enum Appearance { 12598 checkbox, /// a normal checkbox 12599 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 12600 //sliderswitch, 12601 } 12602 private Appearance appearance; 12603 12604 /// ditto 12605 private this(ImageLabel label, Appearance appearance, Widget parent) { 12606 super(parent); 12607 version(win32_widgets) { 12608 this.label = label.label; 12609 12610 uint extraStyle; 12611 final switch(appearance) { 12612 case Appearance.checkbox: 12613 break; 12614 case Appearance.pushbutton: 12615 extraStyle |= BS_PUSHLIKE; 12616 break; 12617 } 12618 12619 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 12620 } else version(custom_widgets) { 12621 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 12622 } else static assert(0); 12623 } 12624 12625 version(custom_widgets) 12626 override void paint(WidgetPainter painter) { 12627 auto cs = getComputedStyle(); 12628 if(isFocused()) { 12629 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 12630 painter.fillColor = cs.windowBackgroundColor; 12631 painter.drawRectangle(Point(0, 0), width, height); 12632 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 12633 } else { 12634 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 12635 painter.fillColor = cs.windowBackgroundColor; 12636 painter.drawRectangle(Point(0, 0), width, height); 12637 } 12638 12639 12640 painter.outlineColor = Color.black; 12641 painter.fillColor = Color.white; 12642 enum rectOffset = 2; 12643 painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset)); 12644 12645 if(isChecked) { 12646 auto size = scaleWithDpi(2); 12647 painter.pen = Pen(Color.black, size); 12648 // I'm using height so the checkbox is square 12649 enum padding = 3; 12650 painter.drawLine( 12651 scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)), 12652 scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2) 12653 ); 12654 painter.drawLine( 12655 scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0), 12656 scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 - size % 2) 12657 ); 12658 12659 painter.pen = Pen(Color.black, 1); 12660 } 12661 12662 if(label !is null) { 12663 painter.outlineColor = cs.foregroundColor(); 12664 painter.fillColor = cs.foregroundColor(); 12665 12666 // i want the centerline of the text to be aligned with the centerline of the checkbox 12667 /+ 12668 auto font = cs.font(); 12669 auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2; 12670 painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label); 12671 +/ 12672 painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter); 12673 } 12674 } 12675 12676 override void defaultEventHandler_triggered(Event ev) { 12677 isChecked = !isChecked; 12678 12679 this.emit!(ChangeEvent!bool)(&isChecked); 12680 12681 redraw(); 12682 } 12683 12684 /// Emits a change event with the checked state 12685 mixin Emits!(ChangeEvent!bool); 12686 } 12687 12688 /// Adds empty space to a layout. 12689 class VerticalSpacer : Widget { 12690 private int mh; 12691 12692 /++ 12693 History: 12694 The overload with `maxHeight` was added on December 31, 2024 12695 +/ 12696 this(Widget parent) { 12697 this(0, parent); 12698 } 12699 12700 /// ditto 12701 this(int maxHeight, Widget parent) { 12702 this.mh = maxHeight; 12703 super(parent); 12704 this.tabStop = false; 12705 } 12706 12707 override int maxHeight() { 12708 return mh ? scaleWithDpi(mh) : super.maxHeight(); 12709 } 12710 } 12711 12712 12713 /// ditto 12714 class HorizontalSpacer : Widget { 12715 private int mw; 12716 12717 /++ 12718 History: 12719 The overload with `maxWidth` was added on December 31, 2024 12720 +/ 12721 this(Widget parent) { 12722 this(0, parent); 12723 } 12724 12725 /// ditto 12726 this(int maxWidth, Widget parent) { 12727 this.mw = maxWidth; 12728 super(parent); 12729 this.tabStop = false; 12730 } 12731 12732 override int maxWidth() { 12733 return mw ? scaleWithDpi(mw) : super.maxWidth(); 12734 } 12735 } 12736 12737 12738 /++ 12739 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 12740 12741 12742 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 12743 12744 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 12745 12746 History: 12747 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 12748 +/ 12749 class Radiobox : MouseActivatedWidget { 12750 12751 version(win32_widgets) { 12752 override int maxHeight() { return scaleWithDpi(16); } 12753 override int minHeight() { return scaleWithDpi(16); } 12754 } else version(custom_widgets) { 12755 private enum buttonSize = 16; 12756 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 12757 override int minHeight() { return maxHeight(); } 12758 } else static assert(0); 12759 12760 override int marginLeft() { return 4; } 12761 12762 // FIXME: make a label getter 12763 private string label; 12764 private dchar accelerator; 12765 12766 /++ 12767 12768 +/ 12769 this(string label, Widget parent) { 12770 super(parent); 12771 version(win32_widgets) { 12772 this.label = label; 12773 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 12774 } else version(custom_widgets) { 12775 label.extractWindowsStyleLabel(this.label, this.accelerator); 12776 height = 16; 12777 width = height + 4 + cast(int) label.length * 16; 12778 } 12779 } 12780 12781 version(custom_widgets) 12782 override void paint(WidgetPainter painter) { 12783 auto cs = getComputedStyle(); 12784 12785 if(isFocused) { 12786 painter.fillColor = cs.windowBackgroundColor; 12787 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 12788 } else { 12789 painter.fillColor = cs.windowBackgroundColor; 12790 painter.outlineColor = cs.windowBackgroundColor; 12791 } 12792 painter.drawRectangle(Point(0, 0), width, height); 12793 12794 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 12795 12796 painter.outlineColor = Color.black; 12797 painter.fillColor = Color.white; 12798 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 12799 if(isChecked) { 12800 painter.outlineColor = Color.black; 12801 painter.fillColor = Color.black; 12802 // I'm using height so the checkbox is square 12803 auto size = scaleWithDpi(2); 12804 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2)); 12805 } 12806 12807 painter.outlineColor = cs.foregroundColor(); 12808 painter.fillColor = cs.foregroundColor(); 12809 12810 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 12811 } 12812 12813 12814 override void defaultEventHandler_triggered(Event ev) { 12815 isChecked = true; 12816 12817 if(this.parent) { 12818 foreach(child; this.parent.children) { 12819 if(child is this) continue; 12820 if(auto rb = cast(Radiobox) child) { 12821 rb.isChecked = false; 12822 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 12823 rb.redraw(); 12824 } 12825 } 12826 } 12827 12828 this.emit!(ChangeEvent!bool)(&this.isChecked); 12829 12830 redraw(); 12831 } 12832 12833 /// Emits a change event with if it is checked. Note that when you select one in a group, that one will emit changed with value == true, and the previous one will emit changed with value == false right before. A button group may catch this and change the event. 12834 mixin Emits!(ChangeEvent!bool); 12835 } 12836 12837 12838 /++ 12839 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 12840 12841 12842 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 12843 12844 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 12845 12846 History: 12847 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 12848 +/ 12849 class Button : MouseActivatedWidget { 12850 override int heightStretchiness() { return 3; } 12851 override int widthStretchiness() { return 3; } 12852 12853 /++ 12854 If true, this button will emit trigger events on double (and other quick events, if added) click events as well as on normal single click events. 12855 12856 History: 12857 Added July 2, 2021 12858 +/ 12859 public bool triggersOnMultiClick; 12860 12861 private string label_; 12862 private TextAlignment alignment; 12863 private dchar accelerator; 12864 12865 /// 12866 string label() { return label_; } 12867 /// 12868 void label(string l) { 12869 label_ = l; 12870 version(win32_widgets) { 12871 WCharzBuffer bfr = WCharzBuffer(l); 12872 SetWindowTextW(hwnd, bfr.ptr); 12873 } else version(custom_widgets) { 12874 redraw(); 12875 } 12876 } 12877 12878 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 12879 super.defaultEventHandler_dblclick(ev); 12880 if(triggersOnMultiClick) { 12881 if(ev.button == MouseButton.left) { 12882 auto event = new Event(EventType.triggered, this); 12883 event.sendDirectly(); 12884 } 12885 } 12886 } 12887 12888 private Sprite sprite; 12889 private int displayFlags; 12890 12891 protected bool needsOwnerDraw() { 12892 return &this.paint !is &Button.paint || &this.useStyleProperties !is &Button.useStyleProperties || &this.paintContent !is &Button.paintContent; 12893 } 12894 12895 version(win32_widgets) 12896 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) { 12897 auto itemId = dis.itemID; 12898 auto hdc = dis.hDC; 12899 auto rect = dis.rcItem; 12900 switch(dis.itemAction) { 12901 // skipping setDynamicState because i don't want to queue the redraw unnecessarily 12902 case ODA_SELECT: 12903 dynamicState_ &= ~DynamicState.depressed; 12904 if(dis.itemState & ODS_SELECTED) 12905 dynamicState_ |= DynamicState.depressed; 12906 goto case; 12907 case ODA_FOCUS: 12908 dynamicState_ &= ~DynamicState.focus; 12909 if(dis.itemState & ODS_FOCUS) 12910 dynamicState_ |= DynamicState.focus; 12911 goto case; 12912 case ODA_DRAWENTIRE: 12913 auto painter = WidgetPainter(this.simpleWindowWrappingHwnd.draw(true), this); 12914 //painter.impl.hdc = hdc; 12915 paint(painter); 12916 break; 12917 default: 12918 } 12919 return 1; 12920 12921 } 12922 12923 /++ 12924 Creates a push button with the given label, which may be an image or some text. 12925 12926 Bugs: 12927 If the image is bigger than the button, it may not be displayed in the right position on Linux. 12928 12929 History: 12930 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 12931 12932 The button with label and image will respect requests to show both on Windows as 12933 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 12934 +/ 12935 this(string label, Widget parent) { 12936 this(ImageLabel(label), parent); 12937 } 12938 12939 /// ditto 12940 this(ImageLabel label, Widget parent) { 12941 bool needsImage; 12942 version(win32_widgets) { 12943 super(parent); 12944 12945 // BS_BITMAP is set when we want image only, so checking for exactly that combination 12946 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 12947 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 12948 12949 // could also do a virtual method needsOwnerDraw which default returns true and we control it here. typeid(this) == typeid(Button) for override check. 12950 12951 if(needsOwnerDraw) { 12952 extraStyle |= BS_OWNERDRAW; 12953 needsImage = true; 12954 } 12955 12956 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 12957 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 12958 12959 if(label.image) { 12960 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 12961 12962 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 12963 } 12964 12965 this.label = label.label; 12966 } else version(custom_widgets) { 12967 super(parent); 12968 12969 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 12970 needsImage = true; 12971 } 12972 12973 12974 if(needsImage && label.image) { 12975 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 12976 this.displayFlags = label.displayFlags; 12977 } 12978 12979 this.alignment = label.alignment; 12980 } 12981 12982 override int minHeight() { return defaultLineHeight + 4; } 12983 12984 static class Style : Widget.Style { 12985 override WidgetBackground background() { 12986 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 12987 12988 auto pressed = DynamicState.depressed | DynamicState.hover; 12989 if((widget.dynamicState & pressed) == pressed) { 12990 return WidgetBackground(cs.depressedButtonColor()); 12991 } else if(widget.dynamicState & DynamicState.hover) { 12992 return WidgetBackground(cs.hoveringColor()); 12993 } else { 12994 return WidgetBackground(cs.buttonColor()); 12995 } 12996 } 12997 12998 override FrameStyle borderStyle() { 12999 auto pressed = DynamicState.depressed | DynamicState.hover; 13000 if((widget.dynamicState & pressed) == pressed) { 13001 return FrameStyle.sunk; 13002 } else { 13003 return FrameStyle.risen; 13004 } 13005 13006 } 13007 13008 override bool variesWithState(ulong dynamicStateFlags) { 13009 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 13010 } 13011 } 13012 mixin OverrideStyle!Style; 13013 13014 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 13015 if(sprite) { 13016 sprite.drawAt( 13017 painter, 13018 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 13019 Point(0, 0) 13020 ); 13021 } else { 13022 Point pos = bounds.upperLeft; 13023 if(this.height == 16) 13024 pos.y -= 2; // total hack omg 13025 painter.drawText(pos, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 13026 } 13027 return bounds; 13028 } 13029 13030 override int flexBasisWidth() { 13031 version(win32_widgets) { 13032 SIZE size; 13033 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 13034 if(size.cx == 0) 13035 goto fallback; 13036 return size.cx + scaleWithDpi(16); 13037 } 13038 fallback: 13039 return scaleWithDpi(cast(int) label.length * 8 + 16); 13040 } 13041 13042 override int flexBasisHeight() { 13043 version(win32_widgets) { 13044 SIZE size; 13045 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 13046 if(size.cy == 0) 13047 goto fallback; 13048 return size.cy + scaleWithDpi(6); 13049 } 13050 fallback: 13051 return defaultLineHeight + 4; 13052 } 13053 } 13054 13055 /++ 13056 A button with a custom appearance, even on systems where there is a standard button. You can subclass it to override its style, paint, or paintContent functions, or you can modify its members for common changes. 13057 13058 History: 13059 Added January 14, 2024 13060 +/ 13061 class CustomButton : Button { 13062 this(ImageLabel label, Widget parent) { 13063 super(label, parent); 13064 } 13065 13066 this(string label, Widget parent) { 13067 super(label, parent); 13068 } 13069 13070 version(win32_widgets) 13071 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 13072 // paint is driven by handleWmDrawItem instead of minigui's redraw events 13073 if(hwnd) 13074 InvalidateRect(hwnd, null, false); // get Windows to trigger the actual redraw 13075 return; 13076 } 13077 13078 override void paint(WidgetPainter painter) { 13079 // the parent does `if(hwnd) return;` because 13080 // normally we don't want to draw on standard controls, 13081 // but this is an exception if it is an owner drawn button 13082 // (which is determined in the constructor by testing, 13083 // at runtime, for the existence of an overridden paint 13084 // member anyway, so this needed to trigger BS_OWNERDRAW) 13085 // sdpyPrintDebugString("drawing"); 13086 painter.drawThemed(&paintContent); 13087 } 13088 } 13089 13090 /++ 13091 A button with a consistent size, suitable for user commands like OK and CANCEL. 13092 +/ 13093 class CommandButton : Button { 13094 this(string label, Widget parent) { 13095 super(label, parent); 13096 } 13097 13098 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 13099 13100 override int maxHeight() { 13101 return defaultLineHeight + 4; 13102 } 13103 13104 override int maxWidth() { 13105 return defaultLineHeight * 4; 13106 } 13107 13108 override int marginLeft() { return 12; } 13109 override int marginRight() { return 12; } 13110 override int marginTop() { return 12; } 13111 override int marginBottom() { return 12; } 13112 } 13113 13114 /// 13115 enum ArrowDirection { 13116 left, /// 13117 right, /// 13118 up, /// 13119 down /// 13120 } 13121 13122 /// 13123 version(custom_widgets) 13124 class ArrowButton : Button { 13125 /// 13126 this(ArrowDirection direction, Widget parent) { 13127 super("", parent); 13128 this.direction = direction; 13129 triggersOnMultiClick = true; 13130 } 13131 13132 private ArrowDirection direction; 13133 13134 override int minHeight() { return scaleWithDpi(16); } 13135 override int maxHeight() { return scaleWithDpi(16); } 13136 override int minWidth() { return scaleWithDpi(16); } 13137 override int maxWidth() { return scaleWithDpi(16); } 13138 13139 override void paint(WidgetPainter painter) { 13140 super.paint(painter); 13141 13142 auto cs = getComputedStyle(); 13143 13144 painter.outlineColor = cs.foregroundColor; 13145 painter.fillColor = cs.foregroundColor; 13146 13147 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 13148 13149 final switch(direction) { 13150 case ArrowDirection.up: 13151 painter.drawPolygon( 13152 scaleWithDpi(Point(2, 10) + offset), 13153 scaleWithDpi(Point(7, 5) + offset), 13154 scaleWithDpi(Point(12, 10) + offset), 13155 scaleWithDpi(Point(2, 10) + offset) 13156 ); 13157 break; 13158 case ArrowDirection.down: 13159 painter.drawPolygon( 13160 scaleWithDpi(Point(2, 6) + offset), 13161 scaleWithDpi(Point(7, 11) + offset), 13162 scaleWithDpi(Point(12, 6) + offset), 13163 scaleWithDpi(Point(2, 6) + offset) 13164 ); 13165 break; 13166 case ArrowDirection.left: 13167 painter.drawPolygon( 13168 scaleWithDpi(Point(10, 2) + offset), 13169 scaleWithDpi(Point(5, 7) + offset), 13170 scaleWithDpi(Point(10, 12) + offset), 13171 scaleWithDpi(Point(10, 2) + offset) 13172 ); 13173 break; 13174 case ArrowDirection.right: 13175 painter.drawPolygon( 13176 scaleWithDpi(Point(6, 2) + offset), 13177 scaleWithDpi(Point(11, 7) + offset), 13178 scaleWithDpi(Point(6, 12) + offset), 13179 scaleWithDpi(Point(6, 2) + offset) 13180 ); 13181 break; 13182 } 13183 } 13184 } 13185 13186 private 13187 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 13188 int x, y; 13189 Widget par = c; 13190 while(par) { 13191 x += par.x; 13192 y += par.y; 13193 par = par.parent; 13194 } 13195 return [x, y]; 13196 } 13197 13198 version(win32_widgets) 13199 private 13200 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 13201 // MapWindowPoints? 13202 int x, y; 13203 Widget par = c; 13204 while(par) { 13205 x += par.x; 13206 y += par.y; 13207 par = par.parent; 13208 if(par !is null && par.useNativeDrawing()) 13209 break; 13210 } 13211 return [x, y]; 13212 } 13213 13214 /// 13215 class ImageBox : Widget { 13216 private MemoryImage image_; 13217 13218 override int widthStretchiness() { return 1; } 13219 override int heightStretchiness() { return 1; } 13220 override int widthShrinkiness() { return 1; } 13221 override int heightShrinkiness() { return 1; } 13222 13223 override int flexBasisHeight() { 13224 return image_.height; 13225 } 13226 13227 override int flexBasisWidth() { 13228 return image_.width; 13229 } 13230 13231 /// 13232 public void setImage(MemoryImage image){ 13233 this.image_ = image; 13234 if(this.parentWindow && this.parentWindow.win) { 13235 if(sprite) 13236 sprite.dispose(); 13237 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 13238 } 13239 redraw(); 13240 } 13241 13242 /// How to fit the image in the box if they aren't an exact match in size? 13243 enum HowToFit { 13244 center, /// centers the image, cropping around all the edges as needed 13245 crop, /// always draws the image in the upper left, cropping the lower right if needed 13246 // stretch, /// not implemented 13247 } 13248 13249 private Sprite sprite; 13250 private HowToFit howToFit_; 13251 13252 private Color backgroundColor_; 13253 13254 /// 13255 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 13256 this.image_ = image; 13257 this.tabStop = false; 13258 this.howToFit_ = howToFit; 13259 this.backgroundColor_ = backgroundColor; 13260 super(parent); 13261 updateSprite(); 13262 } 13263 13264 /// ditto 13265 this(MemoryImage image, HowToFit howToFit, Widget parent) { 13266 this(image, howToFit, Color.transparent, parent); 13267 } 13268 13269 private void updateSprite() { 13270 if(sprite is null && this.parentWindow && this.parentWindow.win) { 13271 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 13272 } 13273 } 13274 13275 override void paint(WidgetPainter painter) { 13276 updateSprite(); 13277 if(backgroundColor_.a) { 13278 painter.fillColor = backgroundColor_; 13279 painter.drawRectangle(Point(0, 0), width, height); 13280 } 13281 if(howToFit_ == HowToFit.crop) 13282 sprite.drawAt(painter, Point(0, 0)); 13283 else if(howToFit_ == HowToFit.center) { 13284 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 13285 } 13286 } 13287 } 13288 13289 /// 13290 class TextLabel : Widget { 13291 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; } 13292 override int maxHeight() { return minHeight; } 13293 override int minWidth() { return 32; } 13294 13295 override int flexBasisHeight() { return minHeight(); } 13296 override int flexBasisWidth() { return defaultTextWidth(label); } 13297 13298 string label_; 13299 13300 /++ 13301 Indicates which other control this label is here for. Similar to HTML `for` attribute. 13302 13303 In practice this means a click on the label will focus the `labelFor`. In future versions 13304 it will also set screen reader hints but that is not yet implemented. 13305 13306 History: 13307 Added October 3, 2021 (dub v10.4) 13308 +/ 13309 Widget labelFor; 13310 13311 /// 13312 @scriptable 13313 string label() { return label_; } 13314 13315 /// 13316 @scriptable 13317 void label(string l) { 13318 label_ = l; 13319 version(win32_widgets) { 13320 WCharzBuffer bfr = WCharzBuffer(l); 13321 SetWindowTextW(hwnd, bfr.ptr); 13322 } else version(custom_widgets) 13323 redraw(); 13324 } 13325 13326 override void defaultEventHandler_click(scope ClickEvent ce) { 13327 if(this.labelFor !is null) 13328 this.labelFor.focus(); 13329 } 13330 13331 /++ 13332 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 13333 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 13334 +/ 13335 this(string label, TextAlignment alignment, Widget parent) { 13336 this.label_ = label; 13337 this.alignment = alignment; 13338 this.tabStop = false; 13339 super(parent); 13340 13341 version(win32_widgets) 13342 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 13343 } 13344 13345 /// ditto 13346 this(string label, Widget parent) { 13347 this(label, TextAlignment.Right, parent); 13348 } 13349 13350 TextAlignment alignment; 13351 13352 version(custom_widgets) 13353 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 13354 painter.outlineColor = getComputedStyle().foregroundColor; 13355 painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment); 13356 return bounds; 13357 } 13358 } 13359 13360 class TextDisplayHelper : Widget { 13361 protected TextLayouter l; 13362 protected ScrollMessageWidget smw; 13363 13364 private const(TextLayouter.State)*[] undoStack; 13365 private const(TextLayouter.State)*[] redoStack; 13366 13367 private string preservedPrimaryText; 13368 protected void selectionChanged() { 13369 // sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString()); 13370 static if(UsingSimpledisplayX11) 13371 with(l.selection()) { 13372 if(!isEmpty()) { 13373 //sdpyPrintDebugString("!isEmpty"); 13374 13375 getPrimarySelection(parentWindow.win, (in char[] txt) { 13376 // sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")"); 13377 // import std.stdio; writeln("txt: ", txt, " sel: ", getContentString); 13378 if(txt.length) { 13379 preservedPrimaryText = txt.idup; 13380 // writeln(preservedPrimaryText); 13381 } 13382 13383 setPrimarySelection(parentWindow.win, getContentString()); 13384 }); 13385 } 13386 } 13387 } 13388 13389 final TextLayouter layouter() { 13390 return l; 13391 } 13392 13393 bool readonly; 13394 bool caretNavigation; // scroll lock can flip this 13395 bool singleLine; 13396 bool acceptsTabInput; 13397 13398 private Menu ctx; 13399 override Menu contextMenu(int x, int y) { 13400 if(ctx is null) { 13401 ctx = new Menu("Actions", this); 13402 if(!readonly) { 13403 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 13404 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 13405 ctx.addSeparator(); 13406 } 13407 if(!readonly) 13408 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 13409 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 13410 if(!readonly) 13411 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 13412 if(!readonly) 13413 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 13414 ctx.addSeparator(); 13415 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 13416 } 13417 return ctx; 13418 } 13419 13420 override void defaultEventHandler_blur(BlurEvent ev) { 13421 super.defaultEventHandler_blur(ev); 13422 if(l.wasMutated()) { 13423 auto evt = new ChangeEvent!string(this, &this.content); 13424 evt.dispatch(); 13425 l.clearWasMutatedFlag(); 13426 } 13427 } 13428 13429 private string content() { 13430 return l.getTextString(); 13431 } 13432 13433 void undo() { 13434 if(readonly) return; 13435 if(undoStack.length) { 13436 auto state = undoStack[$-1]; 13437 undoStack = undoStack[0 .. $-1]; 13438 undoStack.assumeSafeAppend(); 13439 redoStack ~= l.saveState(); 13440 l.restoreState(state); 13441 adjustScrollbarSizes(); 13442 scrollForCaret(); 13443 redraw(); 13444 stateCheckpoint = true; 13445 } 13446 } 13447 13448 void redo() { 13449 if(readonly) return; 13450 if(redoStack.length) { 13451 doStateCheckpoint(); 13452 auto state = redoStack[$-1]; 13453 redoStack = redoStack[0 .. $-1]; 13454 redoStack.assumeSafeAppend(); 13455 l.restoreState(state); 13456 adjustScrollbarSizes(); 13457 scrollForCaret(); 13458 redraw(); 13459 stateCheckpoint = true; 13460 } 13461 } 13462 13463 void cut() { 13464 if(readonly) return; 13465 with(l.selection()) { 13466 if(!isEmpty()) { 13467 setClipboardText(parentWindow.win, getContentString()); 13468 doStateCheckpoint(); 13469 replaceContent(""); 13470 adjustScrollbarSizes(); 13471 scrollForCaret(); 13472 this.redraw(); 13473 } 13474 } 13475 13476 } 13477 13478 void copy() { 13479 with(l.selection()) { 13480 if(!isEmpty()) { 13481 setClipboardText(parentWindow.win, getContentString()); 13482 this.redraw(); 13483 } 13484 } 13485 } 13486 13487 void paste() { 13488 if(readonly) return; 13489 getClipboardText(parentWindow.win, (txt) { 13490 doStateCheckpoint(); 13491 if(singleLine) 13492 l.selection.replaceContent(txt.stripInternal()); 13493 else 13494 l.selection.replaceContent(txt); 13495 adjustScrollbarSizes(); 13496 scrollForCaret(); 13497 this.redraw(); 13498 }); 13499 } 13500 13501 void deleteContentOfSelection() { 13502 if(readonly) return; 13503 doStateCheckpoint(); 13504 l.selection.replaceContent(""); 13505 l.selection.setUserXCoordinate(); 13506 adjustScrollbarSizes(); 13507 scrollForCaret(); 13508 redraw(); 13509 } 13510 13511 void selectAll() { 13512 with(l.selection) { 13513 moveToStartOfDocument(); 13514 setAnchor(); 13515 moveToEndOfDocument(); 13516 setFocus(); 13517 13518 selectionChanged(); 13519 } 13520 redraw(); 13521 } 13522 13523 protected bool stateCheckpoint = true; 13524 13525 protected void doStateCheckpoint() { 13526 if(stateCheckpoint) { 13527 undoStack ~= l.saveState(); 13528 stateCheckpoint = false; 13529 } 13530 } 13531 13532 protected void adjustScrollbarSizes() { 13533 // FIXME: will want a content area helper function instead of doing all these subtractions myself 13534 auto borderWidth = 2; 13535 this.smw.setTotalArea(l.width, l.height); 13536 this.smw.setViewableArea( 13537 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 13538 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 13539 } 13540 13541 protected void scrollForCaret() { 13542 // writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 13543 smw.scrollIntoView(l.selection.focusBoundingBox()); 13544 } 13545 13546 // FIXME: this should be a theme changed event listener instead 13547 private BaseVisualTheme currentTheme; 13548 override void recomputeChildLayout() { 13549 if(currentTheme is null) 13550 currentTheme = WidgetPainter.visualTheme; 13551 if(WidgetPainter.visualTheme !is currentTheme) { 13552 currentTheme = WidgetPainter.visualTheme; 13553 auto ds = this.l.defaultStyle; 13554 if(auto ms = cast(MyTextStyle) ds) { 13555 auto cs = getComputedStyle(); 13556 auto font = cs.font(); 13557 if(font !is null) 13558 ms.font_ = font; 13559 else { 13560 auto osc = new OperatingSystemFont(); 13561 osc.loadDefault; 13562 ms.font_ = osc; 13563 } 13564 } 13565 } 13566 super.recomputeChildLayout(); 13567 } 13568 13569 private Point adjustForSingleLine(Point p) { 13570 if(singleLine) 13571 return Point(p.x, this.height / 2); 13572 else 13573 return p; 13574 } 13575 13576 private bool wordWrapEnabled_; 13577 13578 this(TextLayouter l, ScrollMessageWidget parent) { 13579 this.smw = parent; 13580 13581 smw.addDefaultWheelListeners(16, 16, 8); 13582 smw.movementPerButtonClick(16, 16); 13583 13584 this.defaultPadding = Rectangle(2, 2, 2, 2); 13585 13586 this.l = l; 13587 super(parent); 13588 13589 smw.addEventListener((scope ScrollEvent se) { 13590 this.redraw(); 13591 }); 13592 13593 this.addEventListener((scope ResizeEvent re) { 13594 // FIXME: I should add a method to give this client area width thing 13595 if(wordWrapEnabled_) 13596 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 13597 13598 adjustScrollbarSizes(); 13599 scrollForCaret(); 13600 13601 this.redraw(); 13602 }); 13603 13604 } 13605 13606 private { 13607 bool mouseDown; 13608 bool mouseActuallyMoved; 13609 13610 Point downAt; 13611 13612 Timer autoscrollTimer; 13613 int autoscrollDirection; 13614 int autoscrollAmount; 13615 13616 void autoscroll() { 13617 switch(autoscrollDirection) { 13618 case 0: smw.scrollUp(autoscrollAmount); break; 13619 case 1: smw.scrollDown(autoscrollAmount); break; 13620 case 2: smw.scrollLeft(autoscrollAmount); break; 13621 case 3: smw.scrollRight(autoscrollAmount); break; 13622 default: assert(0); 13623 } 13624 13625 this.redraw(); 13626 } 13627 13628 void setAutoscrollTimer(int direction, int amount) { 13629 if(autoscrollTimer is null) { 13630 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 13631 } 13632 13633 autoscrollDirection = direction; 13634 autoscrollAmount = amount; 13635 } 13636 13637 void stopAutoscrollTimer() { 13638 if(autoscrollTimer !is null) { 13639 autoscrollTimer.dispose(); 13640 autoscrollTimer = null; 13641 } 13642 autoscrollAmount = 0; 13643 autoscrollDirection = 0; 13644 } 13645 } 13646 13647 override void defaultEventHandler_mousemove(scope MouseMoveEvent ce) { 13648 if(mouseDown) { 13649 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 13650 13651 // FIXME: when scrolling i actually do want a timer. 13652 // i also want a zone near the sides of the window where i can auto scroll 13653 13654 auto scrollMultiplier = scaleWithDpi(16); 13655 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 13656 13657 if(!singleLine && movedTo.y < 4) { 13658 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 13659 } else 13660 if(!singleLine && (movedTo.y + 6) > this.height) { 13661 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 13662 } else 13663 if(movedTo.x < 4) { 13664 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 13665 } else 13666 if((movedTo.x + 6) > this.width) { 13667 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 13668 } else 13669 stopAutoscrollTimer(); 13670 13671 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 13672 l.selection.setFocus(); 13673 mouseActuallyMoved = true; 13674 this.redraw(); 13675 } 13676 13677 super.defaultEventHandler_mousemove(ce); 13678 } 13679 13680 override void defaultEventHandler_mouseup(scope MouseUpEvent ce) { 13681 // FIXME: assert primary selection 13682 if(mouseDown && ce.button == MouseButton.left) { 13683 stateCheckpoint = true; 13684 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 13685 //l.selection.setFocus(); 13686 mouseDown = false; 13687 parentWindow.releaseMouseCapture(); 13688 stopAutoscrollTimer(); 13689 this.redraw(); 13690 13691 if(mouseActuallyMoved) 13692 selectionChanged(); 13693 } 13694 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 13695 13696 super.defaultEventHandler_mouseup(ce); 13697 } 13698 13699 static if(UsingSimpledisplayX11) 13700 override void defaultEventHandler_click(scope ClickEvent ce) { 13701 if(ce.button == MouseButton.middle) { 13702 parentWindow.win.getPrimarySelection((txt) { 13703 doStateCheckpoint(); 13704 13705 // import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText); 13706 13707 if(txt == l.selection.getContentString && preservedPrimaryText.length) 13708 l.selection.replaceContent(preservedPrimaryText); 13709 else 13710 l.selection.replaceContent(txt); 13711 redraw(); 13712 }); 13713 } 13714 13715 super.defaultEventHandler_click(ce); 13716 } 13717 13718 override void defaultEventHandler_dblclick(scope DoubleClickEvent dce) { 13719 if(dce.button == MouseButton.left) { 13720 with(l.selection()) { 13721 // FIXME: for a url or file picker i might wanna use / as a separator intead 13722 scope dg = delegate const(char)[] (scope return const(char)[] ch) { 13723 if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r") 13724 return ch; 13725 return null; 13726 }; 13727 find(dg, 1, true).moveToEnd.setAnchor; 13728 find(dg, 1, false).moveTo.setFocus; 13729 selectionChanged(); 13730 redraw(); 13731 } 13732 } 13733 13734 super.defaultEventHandler_dblclick(dce); 13735 } 13736 13737 override void defaultEventHandler_mousedown(scope MouseDownEvent ce) { 13738 if(ce.button == MouseButton.left) { 13739 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 13740 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 13741 if(ce.shiftKey) 13742 l.selection.setFocus(); 13743 else 13744 l.selection.setAnchor(); 13745 mouseDown = true; 13746 mouseActuallyMoved = false; 13747 parentWindow.captureMouse(this); 13748 this.redraw(); 13749 } 13750 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 13751 13752 super.defaultEventHandler_mousedown(ce); 13753 } 13754 13755 override void defaultEventHandler_char(scope CharEvent ce) { 13756 super.defaultEventHandler_char(ce); 13757 13758 if(readonly) 13759 return; 13760 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 13761 return; // skip the ctrl+x characters we don't care about as plain text 13762 13763 if(singleLine && ce.character == '\n') 13764 return; 13765 if(!acceptsTabInput && ce.character == '\t') 13766 return; 13767 13768 doStateCheckpoint(); 13769 13770 char[4] buffer; 13771 import arsd.core; 13772 auto stride = encodeUtf8(buffer, ce.character); 13773 l.selection.replaceContent(buffer[0 .. stride]); 13774 l.selection.setUserXCoordinate(); 13775 adjustScrollbarSizes(); 13776 scrollForCaret(); 13777 redraw(); 13778 13779 } 13780 13781 override void defaultEventHandler_keydown(scope KeyDownEvent kde) { 13782 switch(kde.key) { 13783 case Key.Up, Key.Down, Key.Left, Key.Right: 13784 case Key.Home, Key.End: 13785 stateCheckpoint = true; 13786 bool setPosition = false; 13787 switch(kde.key) { 13788 case Key.Up: l.selection.moveUp(); break; 13789 case Key.Down: l.selection.moveDown(); break; 13790 case Key.Left: l.selection.moveLeft(); setPosition = true; break; 13791 case Key.Right: l.selection.moveRight(); setPosition = true; break; 13792 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 13793 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 13794 default: assert(0); 13795 } 13796 13797 if(kde.shiftKey) 13798 l.selection.setFocus(); 13799 else 13800 l.selection.setAnchor(); 13801 13802 selectionChanged(); 13803 13804 if(setPosition) 13805 l.selection.setUserXCoordinate(); 13806 scrollForCaret(); 13807 redraw(); 13808 break; 13809 case Key.PageUp, Key.PageDown: 13810 // want to act like the user clicked on the caret again 13811 // after the scroll operation completed, so it would remain at 13812 // about the same place on the viewport 13813 auto oldY = smw.vsb.position; 13814 smw.defaultKeyboardListener(kde); 13815 auto newY = smw.vsb.position; 13816 with(l.selection) { 13817 auto uc = getUserCoordinate(); 13818 uc.y += newY - oldY; 13819 moveTo(uc); 13820 13821 if(kde.shiftKey) 13822 setFocus(); 13823 else 13824 setAnchor(); 13825 } 13826 break; 13827 case Key.Delete: 13828 if(l.selection.isEmpty()) { 13829 l.selection.setAnchor(); 13830 l.selection.moveRight(); 13831 l.selection.setFocus(); 13832 } 13833 deleteContentOfSelection(); 13834 adjustScrollbarSizes(); 13835 scrollForCaret(); 13836 break; 13837 case Key.Insert: 13838 break; 13839 case Key.A: 13840 if(kde.ctrlKey) 13841 selectAll(); 13842 break; 13843 case Key.F: 13844 // find 13845 break; 13846 case Key.Z: 13847 if(kde.ctrlKey) 13848 undo(); 13849 break; 13850 case Key.R: 13851 if(kde.ctrlKey) 13852 redo(); 13853 break; 13854 case Key.X: 13855 if(kde.ctrlKey) 13856 cut(); 13857 break; 13858 case Key.C: 13859 if(kde.ctrlKey) 13860 copy(); 13861 break; 13862 case Key.V: 13863 if(kde.ctrlKey) 13864 paste(); 13865 break; 13866 case Key.F1: 13867 with(l.selection()) { 13868 moveToStartOfLine(); 13869 setAnchor(); 13870 moveToEndOfLine(); 13871 moveToIncludeAdjacentEndOfLineMarker(); 13872 setFocus(); 13873 replaceContent(""); 13874 } 13875 13876 redraw(); 13877 break; 13878 /* 13879 case Key.F2: 13880 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 13881 //(cast(MyTextStyle) old).font, 13882 font2, 13883 Color.red))); 13884 redraw(); 13885 break; 13886 */ 13887 case Key.Tab: 13888 // we process the char event, so don't want to change focus on it, unless the user overrides that with ctrl 13889 if(acceptsTabInput && !kde.ctrlKey) 13890 kde.preventDefault(); 13891 break; 13892 default: 13893 } 13894 13895 if(!kde.defaultPrevented) 13896 super.defaultEventHandler_keydown(kde); 13897 } 13898 13899 // we want to delegate all the Widget.Style stuff up to the other class that the user can see 13900 override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) { 13901 // this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidget Parent 13902 if(parent && parent.parent && parent.parent.parent) 13903 parent.parent.parent.useStyleProperties(dg); 13904 else 13905 super.useStyleProperties(dg); 13906 } 13907 13908 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; } 13909 override int maxHeight() { 13910 if(singleLine) 13911 return minHeight; 13912 else 13913 return super.maxHeight(); 13914 } 13915 13916 void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 13917 painter.setFont(myStyle.font); 13918 painter.drawText(upperLeft, text); 13919 } 13920 13921 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 13922 //painter.setFont(font); 13923 13924 auto cs = getComputedStyle(); 13925 auto defaultColor = cs.foregroundColor; 13926 13927 auto old = painter.setClipRectangle(bounds); 13928 scope(exit) painter.setClipRectangle(old); 13929 13930 l.getDrawableText(delegate bool(txt, style, info, carets...) { 13931 //writeln("Segment: ", txt); 13932 assert(style !is null); 13933 13934 if(info.selections && info.boundingBox.width > 0) { 13935 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 13936 painter.fillColor = color; 13937 painter.outlineColor = color; 13938 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 13939 painter.outlineColor = cs.selectionForegroundColor; 13940 //painter.fillColor = Color.white; 13941 } else { 13942 painter.outlineColor = defaultColor; 13943 } 13944 13945 if(this.isFocused) 13946 foreach(idx, caret; carets) { 13947 if(idx == 0) 13948 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 13949 painter.drawLine( 13950 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 13951 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 13952 ); 13953 } 13954 13955 if(txt.stripInternal.length) { 13956 // defaultColor = myStyle.color; // FIXME: so wrong 13957 if(auto myStyle = cast(MyTextStyle) style) 13958 drawTextSegment(myStyle, painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 13959 else if(auto myStyle = cast(MyImageStyle) style) 13960 myStyle.draw(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 13961 } 13962 13963 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) { 13964 return false; 13965 } else { 13966 return true; 13967 } 13968 }, Rectangle(smw.position(), bounds.size)); 13969 13970 /+ 13971 int place = 0; 13972 int y = 75; 13973 foreach(width; widths) { 13974 painter.fillColor = Color.red; 13975 painter.drawRectangle(Point(place, y), Size(width, 75)); 13976 //y += 15; 13977 place += width; 13978 } 13979 +/ 13980 13981 return bounds; 13982 } 13983 13984 static class MyTextStyle : TextStyle { 13985 OperatingSystemFont font_; 13986 this(OperatingSystemFont font, bool passwordMode = false) { 13987 this.font_ = font; 13988 } 13989 13990 override OperatingSystemFont font() { 13991 return font_; 13992 } 13993 } 13994 13995 static class MyImageStyle : TextStyle, MeasurableFont { 13996 MemoryImage image_; 13997 Image converted; 13998 this(MemoryImage image) { 13999 this.image_ = image; 14000 this.converted = Image.fromMemoryImage(image); 14001 } 14002 14003 bool isMonospace() { return false; } 14004 int averageWidth() { return image_.width; } 14005 int height() { return image_.height; } 14006 int ascent() { return image_.height; } 14007 int descent() { return 0; } 14008 14009 int stringWidth(scope const(char)[] s, SimpleWindow window = null) { 14010 return image_.width; 14011 } 14012 14013 override MeasurableFont font() { 14014 return this; 14015 } 14016 14017 void draw(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 14018 painter.drawImage(upperLeft, converted); 14019 } 14020 } 14021 } 14022 14023 /+ 14024 class TextWidget : Widget { 14025 TextLayouter l; 14026 ScrollMessageWidget smw; 14027 TextDisplayHelper helper; 14028 this(TextLayouter l, Widget parent) { 14029 this.l = l; 14030 super(parent); 14031 14032 smw = new ScrollMessageWidget(this); 14033 //smw.horizontalScrollBar.hide; 14034 //smw.verticalScrollBar.hide; 14035 smw.addDefaultWheelListeners(16, 16, 8); 14036 smw.movementPerButtonClick(16, 16); 14037 helper = new TextDisplayHelper(l, smw); 14038 14039 // no need to do this here since there's gonna be a resize 14040 // event immediately before any drawing 14041 // smw.setTotalArea(l.width, l.height); 14042 smw.setViewableArea( 14043 this.width - this.paddingLeft - this.paddingRight, 14044 this.height - this.paddingTop - this.paddingBottom); 14045 14046 /+ 14047 writeln(l.width, "x", l.height); 14048 +/ 14049 } 14050 } 14051 +/ 14052 14053 14054 14055 14056 /+ 14057 make sure it calls parentWindow.inputProxy.setIMEPopupLocation too 14058 +/ 14059 14060 /++ 14061 Contains the implementation of text editing and shared basic api. You should construct one of the child classes instead, like [TextEdit], [LineEdit], or [PasswordEdit]. 14062 +/ 14063 abstract class EditableTextWidget : Widget { 14064 protected this(Widget parent) { 14065 version(custom_widgets) 14066 this(true, parent); 14067 else 14068 this(false, parent); 14069 } 14070 14071 private bool useCustomWidget; 14072 14073 protected this(bool useCustomWidget, Widget parent) { 14074 this.useCustomWidget = useCustomWidget; 14075 14076 super(parent); 14077 14078 if(useCustomWidget) 14079 setupCustomTextEditing(); 14080 } 14081 14082 private bool wordWrapEnabled_; 14083 /++ 14084 Enables or disables wrapping of long lines on word boundaries. 14085 +/ 14086 void wordWrapEnabled(bool enabled) { 14087 if(useCustomWidget) { 14088 wordWrapEnabled_ = enabled; 14089 if(tdh) 14090 tdh.wordWrapEnabled_ = true; 14091 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 14092 } else version(win32_widgets) { 14093 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 14094 } 14095 } 14096 14097 override int minWidth() { return scaleWithDpi(16); } 14098 override int widthStretchiness() { return 7; } 14099 override int widthShrinkiness() { return 1; } 14100 14101 override int maxHeight() { 14102 if(useCustomWidget) 14103 return tdh.maxHeight; 14104 else 14105 return super.maxHeight(); 14106 } 14107 14108 override void focus() { 14109 if(useCustomWidget && tdh) 14110 tdh.focus(); 14111 else 14112 super.focus(); 14113 } 14114 14115 override void defaultEventHandler_focusout(FocusOutEvent foe) { 14116 if(tdh !is null && foe.target is tdh) 14117 tdh.redraw(); 14118 } 14119 14120 override void defaultEventHandler_focusin(FocusInEvent foe) { 14121 if(tdh !is null && foe.target is tdh) 14122 tdh.redraw(); 14123 } 14124 14125 14126 /++ 14127 Selects all the text in the control, as if the user did it themselves. When the user types in a widget, the selected text is replaced with the new input, so this might be useful for putting in default text that is easy for the user to replace. 14128 +/ 14129 void selectAll() { 14130 if(useCustomWidget) { 14131 tdh.selectAll(); 14132 } else version(win32_widgets) { 14133 SendMessage(hwnd, EM_SETSEL, 0, -1); 14134 } 14135 } 14136 14137 /++ 14138 Basic clipboard operations. 14139 14140 History: 14141 Added December 31, 2024 14142 +/ 14143 void copy() { 14144 if(useCustomWidget) { 14145 tdh.copy(); 14146 } else version(win32_widgets) { 14147 SendMessage(hwnd, WM_COPY, 0, 0); 14148 } 14149 } 14150 14151 /// ditto 14152 void cut() { 14153 if(useCustomWidget) { 14154 tdh.cut(); 14155 } else version(win32_widgets) { 14156 SendMessage(hwnd, WM_CUT, 0, 0); 14157 } 14158 } 14159 14160 /// ditto 14161 void paste() { 14162 if(useCustomWidget) { 14163 tdh.paste(); 14164 } else version(win32_widgets) { 14165 SendMessage(hwnd, WM_PASTE, 0, 0); 14166 } 14167 } 14168 14169 /// 14170 void undo() { 14171 if(useCustomWidget) { 14172 tdh.undo(); 14173 } else version(win32_widgets) { 14174 SendMessage(hwnd, EM_UNDO, 0, 0); 14175 } 14176 } 14177 14178 // note that WM_CLEAR deletes the selection without copying it to the clipboard 14179 // also windows supports margins, modified flag, and much more 14180 14181 // EM_UNDO and EM_CANUNDO. EM_REDO is only supported in rich text boxes here 14182 14183 // EM_GETSEL, EM_REPLACESEL, and EM_SETSEL might be usable for find etc. 14184 14185 14186 14187 /*protected*/ TextDisplayHelper tdh; 14188 /*protected*/ TextLayouter textLayout; 14189 14190 /++ 14191 Gets or sets the current content of the control, as a plain text string. Setting the content will reset the cursor position and overwrite any changes the user made. 14192 +/ 14193 @property string content() { 14194 if(useCustomWidget) { 14195 return textLayout.getTextString(); 14196 } else version(win32_widgets) { 14197 wchar[4096] bufferstack; 14198 wchar[] buffer; 14199 auto len = GetWindowTextLength(hwnd); 14200 if(len < bufferstack.length) 14201 buffer = bufferstack[0 .. len + 1]; 14202 else 14203 buffer = new wchar[](len + 1); 14204 14205 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 14206 if(l >= 0) 14207 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 14208 else 14209 return null; 14210 } 14211 14212 assert(0); 14213 } 14214 /// ditto 14215 @property void content(string s) { 14216 if(useCustomWidget) { 14217 with(textLayout.selection) { 14218 moveToStartOfDocument(); 14219 setAnchor(); 14220 moveToEndOfDocument(); 14221 setFocus(); 14222 replaceContent(s); 14223 } 14224 14225 tdh.adjustScrollbarSizes(); 14226 // these don't seem to help 14227 // tdh.smw.setPosition(0, 0); 14228 // tdh.scrollForCaret(); 14229 14230 redraw(); 14231 } else version(win32_widgets) { 14232 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 14233 SetWindowTextW(hwnd, bfr.ptr); 14234 } 14235 } 14236 14237 /++ 14238 Appends some text to the widget at the end, without affecting the user selection or cursor position. 14239 +/ 14240 void addText(string txt) { 14241 if(useCustomWidget) { 14242 textLayout.appendText(txt); 14243 tdh.adjustScrollbarSizes(); 14244 redraw(); 14245 } else version(win32_widgets) { 14246 // get the current selection 14247 DWORD StartPos, EndPos; 14248 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 14249 14250 // move the caret to the end of the text 14251 int outLength = GetWindowTextLengthW(hwnd); 14252 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 14253 14254 // insert the text at the new caret position 14255 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 14256 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 14257 14258 // restore the previous selection 14259 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 14260 } 14261 } 14262 14263 // EM_SCROLLCARET scrolls the caret into view 14264 14265 void scrollToBottom() { 14266 if(useCustomWidget) { 14267 tdh.smw.scrollDown(int.max); 14268 } else version(win32_widgets) { 14269 SendMessageW( hwnd, EM_LINESCROLL, 0, int.max ); 14270 } 14271 } 14272 14273 protected TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 14274 return new TextDisplayHelper(textLayout, smw); 14275 } 14276 14277 protected TextStyle defaultTextStyle() { 14278 return new TextDisplayHelper.MyTextStyle(getUsedFont()); 14279 } 14280 14281 private OperatingSystemFont getUsedFont() { 14282 auto cs = getComputedStyle(); 14283 auto font = cs.font; 14284 if(font is null) { 14285 font = new OperatingSystemFont; 14286 font.loadDefault(); 14287 } 14288 return font; 14289 } 14290 14291 protected void setupCustomTextEditing() { 14292 textLayout = new TextLayouter(defaultTextStyle()); 14293 14294 auto smw = new ScrollMessageWidget(this); 14295 if(!showingHorizontalScroll) 14296 smw.horizontalScrollBar.hide(); 14297 if(!showingVerticalScroll) 14298 smw.verticalScrollBar.hide(); 14299 this.tabStop = false; 14300 smw.tabStop = false; 14301 tdh = textDisplayHelperFactory(textLayout, smw); 14302 } 14303 14304 override void newParentWindow(Window old, Window n) { 14305 if(n is null) return; 14306 this.parentWindow.addEventListener((scope DpiChangedEvent dce) { 14307 if(textLayout) { 14308 if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) { 14309 // the dpi change can change the font, so this informs the layouter that it has changed too 14310 style.font_ = getUsedFont(); 14311 14312 // arsd.core.writeln(this.parentWindow.win.actualDpi); 14313 } 14314 } 14315 }); 14316 } 14317 14318 static class Style : Widget.Style { 14319 override WidgetBackground background() { 14320 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 14321 } 14322 14323 override Color foregroundColor() { 14324 return WidgetPainter.visualTheme.foregroundColor; 14325 } 14326 14327 override FrameStyle borderStyle() { 14328 return FrameStyle.sunk; 14329 } 14330 14331 override MouseCursor cursor() { 14332 return GenericCursor.Text; 14333 } 14334 } 14335 mixin OverrideStyle!Style; 14336 14337 version(win32_widgets) { 14338 private string lastContentBlur; 14339 14340 override void defaultEventHandler_blur(BlurEvent ev) { 14341 super.defaultEventHandler_blur(ev); 14342 14343 if(!useCustomWidget) 14344 if(this.content != lastContentBlur) { 14345 auto evt = new ChangeEvent!string(this, &this.content); 14346 evt.dispatch(); 14347 lastContentBlur = this.content; 14348 } 14349 } 14350 } 14351 14352 14353 bool showingVerticalScroll() { return true; } 14354 bool showingHorizontalScroll() { return true; } 14355 } 14356 14357 /++ 14358 A `LineEdit` is an editor of a single line of text, comparable to a HTML `<input type="text" />`. 14359 14360 A `CustomLineEdit` always uses the custom implementation, even on operating systems where the native control is implemented in minigui, which may provide more api styling features but at the cost of poorer integration with the OS and potentially worse user experience in other ways. 14361 14362 See_Also: 14363 [PasswordEdit] for a `LineEdit` that obscures its input. 14364 14365 [TextEdit] for a multi-line plain text editor widget. 14366 14367 [TextLabel] for a single line piece of static text. 14368 14369 [TextDisplay] for a read-only display of a larger piece of plain text. 14370 +/ 14371 class LineEdit : EditableTextWidget { 14372 override bool showingVerticalScroll() { return false; } 14373 override bool showingHorizontalScroll() { return false; } 14374 14375 override int flexBasisWidth() { return 250; } 14376 override int widthShrinkiness() { return 10; } 14377 14378 /// 14379 this(Widget parent) { 14380 super(parent); 14381 version(win32_widgets) { 14382 createWin32Window(this, "edit"w, "", 14383 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 14384 } else version(custom_widgets) { 14385 } else static assert(false); 14386 } 14387 14388 private this(bool useCustomWidget, Widget parent) { 14389 if(!useCustomWidget) 14390 this(parent); 14391 else 14392 super(true, parent); 14393 } 14394 14395 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 14396 auto tdh = new TextDisplayHelper(textLayout, smw); 14397 tdh.singleLine = true; 14398 return tdh; 14399 } 14400 14401 version(win32_widgets) { 14402 mixin Padding!q{0}; 14403 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 14404 override int maxHeight() { return minHeight; } 14405 } 14406 14407 /+ 14408 @property void passwordMode(bool p) { 14409 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 14410 } 14411 +/ 14412 } 14413 14414 /// ditto 14415 class CustomLineEdit : LineEdit { 14416 this(Widget parent) { 14417 super(true, parent); 14418 } 14419 } 14420 14421 /++ 14422 A [LineEdit] that displays `*` in place of the actual characters. 14423 14424 Alas, Windows requires the window to be created differently to use this style, 14425 so it had to be a new class instead of a toggle on and off on an existing object. 14426 14427 History: 14428 Added January 24, 2021 14429 14430 Implemented on Linux on January 31, 2023. 14431 +/ 14432 class PasswordEdit : EditableTextWidget { 14433 override bool showingVerticalScroll() { return false; } 14434 override bool showingHorizontalScroll() { return false; } 14435 14436 override int flexBasisWidth() { return 250; } 14437 14438 override TextStyle defaultTextStyle() { 14439 auto cs = getComputedStyle(); 14440 14441 auto osf = new class OperatingSystemFont { 14442 this() { 14443 super(cs.font); 14444 } 14445 override int stringWidth(scope const(char)[] text, SimpleWindow window = null) { 14446 int count = 0; 14447 foreach(dchar ch; text) 14448 count++; 14449 return count * super.stringWidth("*", window); 14450 } 14451 }; 14452 14453 return new TextDisplayHelper.MyTextStyle(osf); 14454 } 14455 14456 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 14457 static class TDH : TextDisplayHelper { 14458 this(TextLayouter textLayout, ScrollMessageWidget smw) { 14459 singleLine = true; 14460 super(textLayout, smw); 14461 } 14462 14463 override void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 14464 char[256] buffer = void; 14465 int bufferLength = 0; 14466 foreach(dchar ch; text) 14467 buffer[bufferLength++] = '*'; 14468 painter.setFont(myStyle.font); 14469 painter.drawText(upperLeft, buffer[0..bufferLength]); 14470 } 14471 } 14472 14473 return new TDH(textLayout, smw); 14474 } 14475 14476 /// 14477 this(Widget parent) { 14478 super(parent); 14479 version(win32_widgets) { 14480 createWin32Window(this, "edit"w, "", 14481 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 14482 } else version(custom_widgets) { 14483 } else static assert(false); 14484 } 14485 14486 private this(bool useCustomWidget, Widget parent) { 14487 if(!useCustomWidget) 14488 this(parent); 14489 else 14490 super(true, parent); 14491 } 14492 14493 version(win32_widgets) { 14494 mixin Padding!q{2}; 14495 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 14496 override int maxHeight() { return minHeight; } 14497 } 14498 } 14499 14500 /// ditto 14501 class CustomPasswordEdit : PasswordEdit { 14502 this(Widget parent) { 14503 super(true, parent); 14504 } 14505 } 14506 14507 14508 /++ 14509 A `TextEdit` is a multi-line plain text editor, comparable to a HTML `<textarea>`. 14510 14511 See_Also: 14512 [TextDisplay] for a read-only text display. 14513 14514 [LineEdit] for a single line text editor. 14515 14516 [PasswordEdit] for a single line text editor that obscures its input. 14517 +/ 14518 class TextEdit : EditableTextWidget { 14519 /// 14520 this(Widget parent) { 14521 super(parent); 14522 version(win32_widgets) { 14523 createWin32Window(this, "edit"w, "", 14524 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 14525 } else version(custom_widgets) { 14526 } else static assert(false); 14527 } 14528 14529 private this(bool useCustomWidget, Widget parent) { 14530 if(!useCustomWidget) 14531 this(parent); 14532 else 14533 super(true, parent); 14534 } 14535 14536 override int maxHeight() { return int.max; } 14537 override int heightStretchiness() { return 7; } 14538 14539 override int flexBasisWidth() { return 250; } 14540 override int flexBasisHeight() { return 25; } 14541 } 14542 14543 /// ditto 14544 class CustomTextEdit : TextEdit { 14545 this(Widget parent) { 14546 super(true, parent); 14547 } 14548 } 14549 14550 /+ 14551 /++ 14552 14553 +/ 14554 version(none) 14555 class RichTextDisplay : Widget { 14556 @property void content(string c) {} 14557 void appendContent(string c) {} 14558 } 14559 +/ 14560 14561 /++ 14562 A read-only text display. It is based on the editable widget base, but does not allow user edits and displays it on the direct background instead of on an editable background. 14563 14564 History: 14565 Added October 31, 2023 (dub v11.3) 14566 +/ 14567 class TextDisplay : EditableTextWidget { 14568 this(string text, Widget parent) { 14569 super(true, parent); 14570 this.content = text; 14571 } 14572 14573 override int maxHeight() { return int.max; } 14574 override int minHeight() { return Window.defaultLineHeight; } 14575 override int heightStretchiness() { return 7; } 14576 override int heightShrinkiness() { return 2; } 14577 14578 override int flexBasisWidth() { 14579 return scaleWithDpi(250); 14580 } 14581 override int flexBasisHeight() { 14582 if(textLayout is null || this.tdh is null) 14583 return Window.defaultLineHeight; 14584 14585 auto textHeight = borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textLayout.height))).height; 14586 return this.tdh.borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textHeight))).height; 14587 } 14588 14589 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 14590 return new MyTextDisplayHelper(textLayout, smw); 14591 } 14592 14593 override void registerMovement() { 14594 super.registerMovement(); 14595 this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally 14596 } 14597 14598 static class MyTextDisplayHelper : TextDisplayHelper { 14599 this(TextLayouter textLayout, ScrollMessageWidget smw) { 14600 smw.verticalScrollBar.hide(); 14601 smw.horizontalScrollBar.hide(); 14602 super(textLayout, smw); 14603 this.readonly = true; 14604 } 14605 14606 override void registerMovement() { 14607 super.registerMovement(); 14608 14609 // FIXME: do the horizontal one too as needed and make sure that it does 14610 // wordwrapping again 14611 if(l.height + smw.horizontalScrollBar.height > this.height) 14612 smw.verticalScrollBar.show(); 14613 else 14614 smw.verticalScrollBar.hide(); 14615 14616 l.wordWrapWidth = this.width; 14617 14618 smw.verticalScrollBar.setPosition = 0; 14619 } 14620 } 14621 14622 class Style : Widget.Style { 14623 // just want the generic look for these 14624 } 14625 14626 mixin OverrideStyle!Style; 14627 } 14628 14629 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active 14630 /++ 14631 A scrollable viewer for an array of widgets. The widgets inside a list item can be whatever you want, and you can have any number of total items you want because only the visible widgets need to actually exist and load their data at a time, giving constantly predictable performance. 14632 14633 14634 When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`. 14635 14636 Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand. 14637 14638 Note that some state in reused widget objects may either be preserved or reset when the user isn't expecting it. It is your responsibility to handle this when you load an item (try to save it when it is unloaded, then set it when reloaded), but my recommendation would be to have minimal extra state. For example, avoid having a scrollable widget inside a list, since the scroll state might change as it goes out and into view. Instead, I'd suggest making the list be a loader for a details pane on the side. 14639 14640 History: 14641 Added August 12, 2024 (dub v11.6) 14642 +/ 14643 abstract class GenericListViewWidget : Widget { 14644 /++ 14645 14646 +/ 14647 this(Widget parent) { 14648 super(parent); 14649 14650 smw = new ScrollMessageWidget(this); 14651 smw.addDefaultKeyboardListeners(itemSize.height, itemSize.width); 14652 smw.addDefaultWheelListeners(itemSize.height, itemSize.width); 14653 smw.hsb.hide(); // FIXME: this might actually be useful but we can't really communicate that yet 14654 14655 inner = new GenericListViewWidgetInner(this, smw, new GenericListViewInnerContainer(smw)); 14656 inner.tabStop = this.tabStop; 14657 this.tabStop = false; 14658 } 14659 14660 private ScrollMessageWidget smw; 14661 private GenericListViewWidgetInner inner; 14662 14663 /++ 14664 14665 +/ 14666 abstract GenericListViewItem itemFactory(Widget parent); 14667 // in device-dependent pixels 14668 /++ 14669 14670 +/ 14671 abstract Size itemSize(); // use 0 to indicate it can stretch? 14672 14673 enum LayoutMode { 14674 rows, 14675 columns, 14676 gridRowsFirst, 14677 gridColumnsFirst 14678 } 14679 LayoutMode layoutMode() { 14680 return LayoutMode.rows; 14681 } 14682 14683 private int itemCount_; 14684 14685 /++ 14686 Sets the count of available items in the list. This will not allocate any items, but it will adjust the scroll bars and try to load items up to this count on-demand as they appear visible. 14687 +/ 14688 void setItemCount(int count) { 14689 smw.setTotalArea(inner.width, count * itemSize().height); 14690 smw.setViewableArea(inner.width, inner.height); 14691 this.itemCount_ = count; 14692 } 14693 14694 /++ 14695 Returns the current count of items expected to available in the list. 14696 +/ 14697 int itemCount() { 14698 return this.itemCount_; 14699 } 14700 14701 /++ 14702 Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data. 14703 14704 Note you must $(I also) call [setItemCount] if the total item count has changed. 14705 +/ 14706 void notifyItemsChanged(int index, int count = 1) { 14707 } 14708 /// ditto 14709 void notifyItemsInserted(int index, int count = 1) { 14710 } 14711 /// ditto 14712 void notifyItemsRemoved(int index, int count = 1) { 14713 } 14714 /// ditto 14715 void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) { 14716 } 14717 14718 /++ 14719 History: 14720 Added January 1, 2025 14721 +/ 14722 void ensureItemVisibleInScroll(int index) { 14723 auto itemPos = index * itemSize().height; 14724 auto vsb = smw.verticalScrollBar; 14725 auto viewable = vsb.viewableArea_; 14726 14727 if(viewable == 0) { 14728 // viewable == 0 isn't actually supposed to happen, this means 14729 // this method is being called before having our size assigned, it should 14730 // probably just queue it up for later. 14731 queuedScroll = index; 14732 return; 14733 } 14734 14735 queuedScroll = int.min; 14736 14737 if(itemPos < vsb.position) { 14738 // scroll up to it 14739 vsb.setPosition(itemPos); 14740 smw.notify(); 14741 } else if(itemPos + itemSize().height > (vsb.position + viewable)) { 14742 // scroll down to it, so it is at the bottom 14743 14744 auto lastViewableItemPosition = (viewable - itemSize.height) / itemSize.height * itemSize.height; 14745 // need the itemPos to be at the lastViewableItemPosition after scrolling, so subtraction does it 14746 14747 vsb.setPosition(itemPos - lastViewableItemPosition); 14748 smw.notify(); 14749 } 14750 } 14751 14752 /++ 14753 History: 14754 Added January 1, 2025; 14755 +/ 14756 int numberOfCurrentlyFullyVisibleItems() { 14757 return smw.verticalScrollBar.viewableArea_ / itemSize.height; 14758 } 14759 14760 private int queuedScroll = int.min; 14761 14762 override void recomputeChildLayout() { 14763 super.recomputeChildLayout(); 14764 if(queuedScroll != int.min) 14765 ensureItemVisibleInScroll(queuedScroll); 14766 } 14767 14768 private GenericListViewItem[] items; 14769 14770 override void paint(WidgetPainter painter) {} 14771 } 14772 14773 /// ditto 14774 abstract class GenericListViewItem : Widget { 14775 /++ 14776 +/ 14777 this(Widget parent) { 14778 super(parent); 14779 } 14780 14781 private int _currentIndex = -1; 14782 14783 private void showItemPrivate(int idx) { 14784 showItem(idx); 14785 _currentIndex = idx; 14786 } 14787 14788 /++ 14789 Implement this to show an item from your data backing to the list. 14790 14791 Note that even if you are showing the requested index already, you should still try to reload it because it is possible the index now points to a different item (e.g. an item was added so all the indexes have changed) or if data has changed in this index and it is requesting you to update it prior to a repaint. 14792 +/ 14793 abstract void showItem(int idx); 14794 14795 /++ 14796 Maintained by the library after calling [showItem] so the object knows which data index it currently has. 14797 14798 It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent). 14799 14800 Inside the call to `showItem`, `currentIndexLoaded` is the old index, and the argument to `showItem` is the new index. You might use that to save state to the right place as needed before you overwrite it with the new item. 14801 +/ 14802 final int currentIndexLoaded() { 14803 return _currentIndex; 14804 } 14805 } 14806 14807 /// 14808 unittest { 14809 import arsd.minigui; 14810 14811 import std.conv; 14812 14813 void main() { 14814 auto mw = new MainWindow(); 14815 14816 static class MyListViewItem : GenericListViewItem { 14817 this(Widget parent) { 14818 super(parent); 14819 14820 label = new TextLabel("unloaded", TextAlignment.Left, this); 14821 button = new Button("Click", this); 14822 14823 button.addEventListener("triggered", (){ 14824 messageBox(text("clicked ", currentIndexLoaded())); 14825 }); 14826 } 14827 override void showItem(int idx) { 14828 label.label = "Item " ~ to!string(idx); 14829 } 14830 14831 TextLabel label; 14832 Button button; 14833 } 14834 14835 auto widget = new class GenericListViewWidget { 14836 this() { 14837 super(mw); 14838 } 14839 override GenericListViewItem itemFactory(Widget parent) { 14840 return new MyListViewItem(parent); 14841 } 14842 override Size itemSize() { 14843 return Size(0, scaleWithDpi(80)); 14844 } 14845 }; 14846 14847 widget.setItemCount(5000); 14848 14849 mw.loop(); 14850 } 14851 } 14852 14853 // this exists just to wrap the actual GenericListViewWidgetInner so borders 14854 // and padding and stuff can work 14855 private class GenericListViewInnerContainer : Widget { 14856 this(Widget parent) { 14857 super(parent); 14858 this.tabStop = false; 14859 } 14860 14861 override void recomputeChildLayout() { 14862 registerMovement(); 14863 14864 auto cs = getComputedStyle(); 14865 auto bw = getBorderWidth(cs.borderStyle); 14866 14867 assert(children.length < 2); 14868 foreach(child; children) { 14869 child.x = bw + paddingLeft(); 14870 child.y = bw + paddingTop(); 14871 child.width = this.width.NonOverflowingUint - bw - bw - paddingLeft() - paddingRight(); 14872 child.height = this.height.NonOverflowingUint - bw - bw - paddingTop() - paddingBottom(); 14873 14874 child.recomputeChildLayout(); 14875 } 14876 } 14877 14878 override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) { 14879 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 14880 return parent.parent.parent.useStyleProperties(dg); 14881 else 14882 return super.useStyleProperties(dg); 14883 } 14884 14885 override int paddingTop() { 14886 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 14887 return parent.parent.parent.paddingTop(); 14888 else 14889 return super.paddingTop(); 14890 } 14891 14892 override int paddingBottom() { 14893 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 14894 return parent.parent.parent.paddingBottom(); 14895 else 14896 return super.paddingBottom(); 14897 } 14898 14899 override int paddingLeft() { 14900 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 14901 return parent.parent.parent.paddingLeft(); 14902 else 14903 return super.paddingLeft(); 14904 } 14905 14906 override int paddingRight() { 14907 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 14908 return parent.parent.parent.paddingRight(); 14909 else 14910 return super.paddingRight(); 14911 } 14912 14913 14914 } 14915 14916 private class GenericListViewWidgetInner : Widget { 14917 this(GenericListViewWidget glvw, ScrollMessageWidget smw, GenericListViewInnerContainer parent) { 14918 super(parent); 14919 this.glvw = glvw; 14920 14921 reloadVisible(); 14922 14923 smw.addEventListener("scroll", () { 14924 reloadVisible(); 14925 }); 14926 } 14927 14928 override void registerMovement() { 14929 super.registerMovement(); 14930 if(glvw && glvw.smw) 14931 glvw.smw.setViewableArea(this.width, this.height); 14932 } 14933 14934 void reloadVisible() { 14935 auto y = glvw.smw.position.y / glvw.itemSize.height; 14936 14937 // idk why i had this here it doesn't seem to be ueful and actually made last items diasppear 14938 //int offset = glvw.smw.position.y % glvw.itemSize.height; 14939 //if(offset || y >= glvw.itemCount()) 14940 //y--; 14941 14942 if(y < 0) 14943 y = 0; 14944 14945 recomputeChildLayout(); 14946 14947 foreach(item; glvw.items) { 14948 if(y < glvw.itemCount()) { 14949 item.showItemPrivate(y); 14950 item.show(); 14951 } else { 14952 item.hide(); 14953 } 14954 y++; 14955 } 14956 14957 this.redraw(); 14958 } 14959 14960 private GenericListViewWidget glvw; 14961 14962 private bool inRcl; 14963 override void recomputeChildLayout() { 14964 if(inRcl) 14965 return; 14966 inRcl = true; 14967 scope(exit) 14968 inRcl = false; 14969 14970 registerMovement(); 14971 14972 auto ih = glvw.itemSize().height; 14973 14974 auto itemCount = this.height / ih + 2; // extra for partial display before and after 14975 bool hadNew; 14976 while(glvw.items.length < itemCount) { 14977 // FIXME: free the old items? maybe just set length 14978 glvw.items ~= glvw.itemFactory(this); 14979 hadNew = true; 14980 } 14981 14982 if(hadNew) 14983 reloadVisible(); 14984 14985 int y = -(glvw.smw.position.y % ih) + this.paddingTop(); 14986 foreach(child; children) { 14987 child.x = this.paddingLeft(); 14988 child.y = y; 14989 y += glvw.itemSize().height; 14990 child.width = this.width.NonOverflowingUint - this.paddingLeft() - this.paddingRight(); 14991 child.height = ih; 14992 14993 child.recomputeChildLayout(); 14994 } 14995 } 14996 } 14997 14998 14999 15000 /++ 15001 History: 15002 It was a child of Window before, but as of September 29, 2024, it is now a child of `Dialog`. 15003 +/ 15004 class MessageBox : Dialog { 15005 private string message; 15006 MessageBoxButton buttonPressed = MessageBoxButton.None; 15007 /++ 15008 15009 History: 15010 The overload that takes `Window originator` was added on September 29, 2024. 15011 +/ 15012 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 15013 this(null, message, buttons, buttonIds); 15014 } 15015 /// ditto 15016 this(Window originator, string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 15017 message = message.stripRightInternal; 15018 int mainWidth; 15019 15020 // estimate longest line 15021 int count; 15022 foreach(ch; message) { 15023 if(ch == '\n') { 15024 if(count > mainWidth) 15025 mainWidth = count; 15026 count = 0; 15027 } else { 15028 count++; 15029 } 15030 } 15031 mainWidth *= 8; 15032 if(mainWidth < 300) 15033 mainWidth = 300; 15034 if(mainWidth > 600) 15035 mainWidth = 600; 15036 15037 super(originator, mainWidth, 100); 15038 15039 assert(buttons.length); 15040 assert(buttons.length == buttonIds.length); 15041 15042 this.message = message; 15043 15044 auto label = new TextDisplay(message, this); 15045 15046 auto hl = new HorizontalLayout(this); 15047 auto spacer = new HorizontalSpacer(hl); // to right align 15048 15049 foreach(idx, buttonText; buttons) { 15050 auto button = new CommandButton(buttonText, hl); 15051 15052 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 15053 this.buttonPressed = buttonIds[idx]; 15054 win.close(); 15055 }; })(idx)); 15056 15057 if(idx == 0) 15058 button.focus(); 15059 } 15060 15061 if(buttons.length == 1) 15062 auto spacer2 = new HorizontalSpacer(hl); // to center it 15063 15064 auto size = label.flexBasisHeight() + hl.minHeight() + this.paddingTop + this.paddingBottom; 15065 auto max = scaleWithDpi(600); // random max height 15066 if(size > max) 15067 size = max; 15068 15069 win.resize(scaleWithDpi(mainWidth), size); 15070 15071 win.show(); 15072 redraw(); 15073 } 15074 15075 override void OK() { 15076 this.win.close(); 15077 } 15078 15079 mixin Padding!q{16}; 15080 } 15081 15082 /// 15083 enum MessageBoxStyle { 15084 OK, /// 15085 OKCancel, /// 15086 RetryCancel, /// 15087 YesNo, /// 15088 YesNoCancel, /// 15089 RetryCancelContinue /// In a multi-part process, if one part fails, ask the user if you should retry that failed step, cancel the entire process, or just continue with the next step, accepting failure on this step. 15090 } 15091 15092 /// 15093 enum MessageBoxIcon { 15094 None, /// 15095 Info, /// 15096 Warning, /// 15097 Error /// 15098 } 15099 15100 /// Identifies the button the user pressed on a message box. 15101 enum MessageBoxButton { 15102 None, /// The user closed the message box without clicking any of the buttons. 15103 OK, /// 15104 Cancel, /// 15105 Retry, /// 15106 Yes, /// 15107 No, /// 15108 Continue /// 15109 } 15110 15111 15112 /++ 15113 Displays a modal message box, blocking until the user dismisses it. These global ones are discouraged in favor of the same methods on [Window], which give better user experience since the message box is tied the parent window instead of acting independently. 15114 15115 Returns: the button pressed. 15116 +/ 15117 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15118 return messageBox(null, title, message, style, icon); 15119 } 15120 15121 /// ditto 15122 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15123 return messageBox(null, null, message, style, icon); 15124 } 15125 15126 /++ 15127 15128 +/ 15129 MessageBoxButton messageBox(Window originator, string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15130 version(win32_widgets) { 15131 WCharzBuffer t = WCharzBuffer(title); 15132 WCharzBuffer m = WCharzBuffer(message); 15133 UINT type; 15134 with(MessageBoxStyle) 15135 final switch(style) { 15136 case OK: type |= MB_OK; break; 15137 case OKCancel: type |= MB_OKCANCEL; break; 15138 case RetryCancel: type |= MB_RETRYCANCEL; break; 15139 case YesNo: type |= MB_YESNO; break; 15140 case YesNoCancel: type |= MB_YESNOCANCEL; break; 15141 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 15142 } 15143 with(MessageBoxIcon) 15144 final switch(icon) { 15145 case None: break; 15146 case Info: type |= MB_ICONINFORMATION; break; 15147 case Warning: type |= MB_ICONWARNING; break; 15148 case Error: type |= MB_ICONERROR; break; 15149 } 15150 switch(MessageBoxW(originator is null ? null : originator.win.hwnd, m.ptr, t.ptr, type)) { 15151 case IDOK: return MessageBoxButton.OK; 15152 case IDCANCEL: return MessageBoxButton.Cancel; 15153 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 15154 case IDYES: return MessageBoxButton.Yes; 15155 case IDNO: return MessageBoxButton.No; 15156 case IDCONTINUE: return MessageBoxButton.Continue; 15157 default: return MessageBoxButton.None; 15158 } 15159 } else { 15160 string[] buttons; 15161 MessageBoxButton[] buttonIds; 15162 with(MessageBoxStyle) 15163 final switch(style) { 15164 case OK: 15165 buttons = ["OK"]; 15166 buttonIds = [MessageBoxButton.OK]; 15167 break; 15168 case OKCancel: 15169 buttons = ["OK", "Cancel"]; 15170 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 15171 break; 15172 case RetryCancel: 15173 buttons = ["Retry", "Cancel"]; 15174 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 15175 break; 15176 case YesNo: 15177 buttons = ["Yes", "No"]; 15178 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 15179 break; 15180 case YesNoCancel: 15181 buttons = ["Yes", "No", "Cancel"]; 15182 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 15183 break; 15184 case RetryCancelContinue: 15185 buttons = ["Try Again", "Cancel", "Continue"]; 15186 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 15187 break; 15188 } 15189 auto mb = new MessageBox(originator, message, buttons, buttonIds); 15190 EventLoop el = EventLoop.get; 15191 el.run(() { return !mb.win.closed; }); 15192 return mb.buttonPressed; 15193 } 15194 15195 } 15196 15197 /// ditto 15198 int messageBox(Window originator, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15199 return messageBox(originator, null, message, style, icon); 15200 } 15201 15202 15203 /// 15204 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 15205 15206 /++ 15207 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 15208 15209 History: 15210 The data members were `public` (albiet undocumented and not intended for use) prior to May 13, 2021. They are now `private`, reflecting the single intended use of this object. 15211 +/ 15212 struct EventListener { 15213 private Widget widget; 15214 private string event; 15215 private EventHandler handler; 15216 private bool useCapture; 15217 15218 /// 15219 void disconnect() { 15220 widget.removeEventListener(this); 15221 } 15222 } 15223 15224 /++ 15225 The purpose of this enum was to give a compile-time checked version of various standard event strings. 15226 15227 Now, I recommend you use a statically typed event object instead. 15228 15229 See_Also: [Event] 15230 +/ 15231 enum EventType : string { 15232 click = "click", /// 15233 15234 mouseenter = "mouseenter", /// 15235 mouseleave = "mouseleave", /// 15236 mousein = "mousein", /// 15237 mouseout = "mouseout", /// 15238 mouseup = "mouseup", /// 15239 mousedown = "mousedown", /// 15240 mousemove = "mousemove", /// 15241 15242 keydown = "keydown", /// 15243 keyup = "keyup", /// 15244 char_ = "char", /// 15245 15246 focus = "focus", /// 15247 blur = "blur", /// 15248 15249 triggered = "triggered", /// 15250 15251 change = "change", /// 15252 } 15253 15254 /++ 15255 Represents an event that is currently being processed. 15256 15257 15258 Minigui's event model is based on the web browser. An event has a name, a target, 15259 and an associated data object. It starts from the window and works its way down through 15260 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 15261 then goes back up again all the way back to the window, triggering bubble phase handlers. At 15262 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 15263 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 15264 was called; that just stops it from calling other handlers in the widget tree, but the default happens 15265 whenever propagation is done, not only if it gets to the end of the chain). 15266 15267 This model has several nice points: 15268 15269 $(LIST 15270 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 15271 with event handlers set, then add/remove children as much as you want without needing 15272 to manage the event handlers on them - the parent alone can manage everything. 15273 15274 * It is easy to create new custom events in your application. 15275 15276 * It is familiar to many web developers. 15277 ) 15278 15279 There's a few downsides though: 15280 15281 $(LIST 15282 * There's not a lot of type safety. 15283 15284 * You don't get a static list of what events a widget can emit. 15285 15286 * Tracing where an event got cancelled along the chain can get difficult; the downside of 15287 the central delegation benefit is it can be lead to debugging of action at a distance. 15288 ) 15289 15290 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 15291 while keeping the benefits - and most compatibility with - the existing model. The main idea is 15292 to simply use a D object type which provides a static interface as well as a built-in event name. 15293 Then, a new static interface allows you to see what an event can emit and attach handlers to it 15294 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 15295 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 15296 to having a little more help from the D compiler and documentation generator. 15297 15298 Your code would change like this: 15299 15300 --- 15301 // old 15302 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 15303 15304 // new 15305 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 15306 --- 15307 15308 The old-style code will still work, but using certain members of the [Event] class will generate deprecation warnings. Changing handlers to the new style will silence all those warnings at once without requiring any other changes to your code. 15309 15310 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 15311 15312 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 15313 15314 Thus the family of functions are: 15315 15316 [Widget.addEventListener] is the fully-flexible base method. It has two main overload families: one with the string and one without. The one with the string takes the Event object, the one without determines the string from the type you pass. The string "*" matches ALL events that pass through. 15317 15318 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 15319 15320 [Widget.setDefaultEventHandler] is what is called if no preventDefault was called. This should be called in the widget's constructor to set default behaivor. Default event handlers are only called on the event target. 15321 15322 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 15323 15324 --- 15325 class MyCheckbox : Widget { 15326 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 15327 /// It is NOT actually required but should be used whenever possible. 15328 mixin Emits!(ChangeEvent!bool); 15329 15330 this(Widget parent) { 15331 super(parent); 15332 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 15333 } 15334 15335 private bool _checked; 15336 @property bool checked() { return _checked; } 15337 @property void checked(bool set) { 15338 _checked = set; 15339 emit!(ChangeEvent!bool)(&checked); 15340 } 15341 } 15342 --- 15343 15344 ## Creating Your Own Events 15345 15346 To avoid clashing in the string namespace, your events should use your module and class name as the event string. The simple code `mixin Register;` in your Event subclass will do this for you. You should mark events `final` unless you specifically plan to use it as a shared base. Only `Widget` and final classes should actually be sent (and preferably, not even `Widget`), with few exceptions. 15347 15348 --- 15349 final class MyEvent : Event { 15350 this(Widget target) { super(EventString, target); } 15351 mixin Register; // adds EventString and other reflection information 15352 } 15353 --- 15354 15355 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 15356 15357 History: 15358 Prior to May 2021, Event had a set of pre-made members with no extensibility (outside of diy casts) and no static checks on field presence. 15359 15360 After that, those old pre-made members are deprecated accessors and the fields are moved to child classes. To transition, change string events to typed events or do a dynamic cast (don't forget the null check!) in your handler. 15361 +/ 15362 /+ 15363 15364 ## General Conventions 15365 15366 Change events should NOT be emitted when a value is changed programmatically. Indeed, methods should usually not send events. The point of an event is to know something changed and when you call a method, you already know about it. 15367 15368 15369 ## Qt-style signals and slots 15370 15371 Some events make sense to use with just name and data type. These are one-way notifications with no propagation nor default behavior and thus separate from the other event system. 15372 15373 The intention is for events to be used when 15374 15375 --- 15376 class Demo : Widget { 15377 this() { 15378 myPropertyChanged = Signal!int(this); 15379 } 15380 @property myProperty(int v) { 15381 myPropertyChanged.emit(v); 15382 } 15383 15384 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 15385 // but it can just genuinely not care about `this` since that's not really passed. 15386 } 15387 15388 class Foo : Widget { 15389 // the slot uda is not necessary, but it helps the script and ui builder find it. 15390 @slot void setValue(int v) { ... } 15391 } 15392 15393 demo.myPropertyChanged.connect(&foo.setValue); 15394 --- 15395 15396 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 15397 15398 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 15399 15400 class StringChangeEvent : ChangeEvent, Signal!string { 15401 mixin SignalImpl 15402 } 15403 15404 +/ 15405 class Event : ReflectableProperties { 15406 /// Creates an event without populating any members and without sending it. See [dispatch] 15407 this(string eventName, Widget emittedBy) { 15408 this.eventName = eventName; 15409 this.srcElement = emittedBy; 15410 } 15411 15412 15413 /// Implementations for the [ReflectableProperties] interface/ 15414 void getPropertiesList(scope void delegate(string name) sink) const {} 15415 /// ditto 15416 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 15417 /// ditto 15418 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 15419 return SetPropertyResult.notPermitted; 15420 } 15421 15422 15423 /+ 15424 /++ 15425 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 15426 15427 It is just protected so the mixin template can see it from user modules. If I made it private, even my own mixin template couldn't see it due to mixin scoping rules. 15428 +/ 15429 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 15430 if(value.length == 0) { 15431 finalSink(memberName, `""`); 15432 return; 15433 } 15434 15435 char[1024] bufferBacking; 15436 char[] buffer = bufferBacking; 15437 int bufferPosition; 15438 15439 void sink(char ch) { 15440 if(bufferPosition >= buffer.length) 15441 buffer.length = buffer.length + 1024; 15442 buffer[bufferPosition++] = ch; 15443 } 15444 15445 sink('"'); 15446 15447 foreach(ch; value) { 15448 switch(ch) { 15449 case '\\': 15450 sink('\\'); sink('\\'); 15451 break; 15452 case '"': 15453 sink('\\'); sink('"'); 15454 break; 15455 case '\n': 15456 sink('\\'); sink('n'); 15457 break; 15458 case '\r': 15459 sink('\\'); sink('r'); 15460 break; 15461 case '\t': 15462 sink('\\'); sink('t'); 15463 break; 15464 default: 15465 sink(ch); 15466 } 15467 } 15468 15469 sink('"'); 15470 15471 finalSink(memberName, buffer[0 .. bufferPosition]); 15472 } 15473 +/ 15474 15475 /+ 15476 enum EventInitiator { 15477 system, 15478 minigui, 15479 user 15480 } 15481 15482 immutable EventInitiator; initiatedBy; 15483 +/ 15484 15485 /++ 15486 Events should generally follow the propagation model, but there's some exceptions 15487 to that rule. If so, they should override this to return false. In that case, only 15488 bubbling event handlers on the target itself and capturing event handlers on the containing 15489 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 15490 capture -> target -> bubble process.) 15491 15492 History: 15493 Added May 12, 2021 15494 +/ 15495 bool propagates() const pure nothrow @nogc @safe { 15496 return true; 15497 } 15498 15499 /++ 15500 hints as to whether preventDefault will actually do anything. not entirely reliable. 15501 15502 History: 15503 Added May 14, 2021 15504 +/ 15505 bool cancelable() const pure nothrow @nogc @safe { 15506 return true; 15507 } 15508 15509 /++ 15510 You can mix this into child class to register some boilerplate. It includes the `EventString` 15511 member, a constructor, and implementations of the dynamic get data interfaces. 15512 15513 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 15514 15515 15516 You can override the default EventString by simply providing your own in the form of 15517 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 15518 which provides some namespace protection against conflicts in other libraries while still being fairly 15519 easy to use. 15520 15521 If you provide your own constructor, it will override the default constructor provided here. A constructor 15522 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 15523 first argument to your constructor. 15524 15525 History: 15526 Added May 13, 2021. 15527 +/ 15528 protected static mixin template Register() { 15529 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 15530 this(Widget target) { super(EventString, target); } 15531 15532 mixin ReflectableProperties.RegisterGetters; 15533 } 15534 15535 /++ 15536 This is the widget that emitted the event. 15537 15538 15539 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 15540 15541 History: 15542 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 15543 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 15544 so I don't intend to remove these aliases. 15545 +/ 15546 Widget source; 15547 /// ditto 15548 alias source target; 15549 /// ditto 15550 alias source srcElement; 15551 15552 Widget relatedTarget; /// Note: likely to be deprecated at some point. 15553 15554 /// Prevents the default event handler (if there is one) from being called 15555 void preventDefault() { 15556 lastDefaultPrevented = true; 15557 defaultPrevented = true; 15558 } 15559 15560 /// Stops the event propagation immediately. 15561 void stopPropagation() { 15562 propagationStopped = true; 15563 } 15564 15565 private bool defaultPrevented; 15566 private bool propagationStopped; 15567 private string eventName; 15568 15569 private bool isBubbling; 15570 15571 /// This is an internal implementation detail you should not use. It would be private if the language allowed it and it may be removed without notice. 15572 protected void adjustScrolling() { } 15573 /// ditto 15574 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 15575 15576 /++ 15577 this sends it only to the target. If you want propagation, use dispatch() instead. 15578 15579 This should be made private!!! 15580 15581 +/ 15582 void sendDirectly() { 15583 if(srcElement is null) 15584 return; 15585 15586 // i capturing on the parent too. The main reason for this is that gives a central place to log all events for the debug window. 15587 15588 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 15589 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 15590 15591 adjustScrolling(); 15592 15593 if(auto e = target.parentWindow) { 15594 if(auto handlers = "*" in e.capturingEventHandlers) 15595 foreach(handler; *handlers) 15596 if(handler) handler(e, this); 15597 if(auto handlers = eventName in e.capturingEventHandlers) 15598 foreach(handler; *handlers) 15599 if(handler) handler(e, this); 15600 } 15601 15602 auto e = srcElement; 15603 15604 if(auto handlers = eventName in e.bubblingEventHandlers) 15605 foreach(handler; *handlers) 15606 if(handler) handler(e, this); 15607 15608 if(auto handlers = "*" in e.bubblingEventHandlers) 15609 foreach(handler; *handlers) 15610 if(handler) handler(e, this); 15611 15612 // there's never a default for a catch-all event 15613 if(!defaultPrevented) 15614 if(eventName in e.defaultEventHandlers) 15615 e.defaultEventHandlers[eventName](e, this); 15616 } 15617 15618 /// this dispatches the element using the capture -> target -> bubble process 15619 void dispatch() { 15620 if(srcElement is null) 15621 return; 15622 15623 if(!propagates) { 15624 sendDirectly; 15625 return; 15626 } 15627 15628 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 15629 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 15630 15631 adjustScrolling(); 15632 // first capture, then bubble 15633 15634 Widget[] chain; 15635 Widget curr = srcElement; 15636 while(curr) { 15637 auto l = curr; 15638 chain ~= l; 15639 curr = curr.parent; 15640 } 15641 15642 isBubbling = false; 15643 15644 foreach_reverse(e; chain) { 15645 if(auto handlers = "*" in e.capturingEventHandlers) 15646 foreach(handler; *handlers) if(handler !is null) handler(e, this); 15647 15648 if(propagationStopped) 15649 break; 15650 15651 if(auto handlers = eventName in e.capturingEventHandlers) 15652 foreach(handler; *handlers) if(handler !is null) handler(e, this); 15653 15654 // the default on capture should really be to always do nothing 15655 15656 //if(!defaultPrevented) 15657 // if(eventName in e.defaultEventHandlers) 15658 // e.defaultEventHandlers[eventName](e.element, this); 15659 15660 if(propagationStopped) 15661 break; 15662 } 15663 15664 int adjustX; 15665 int adjustY; 15666 15667 isBubbling = true; 15668 if(!propagationStopped) 15669 foreach(e; chain) { 15670 if(auto handlers = eventName in e.bubblingEventHandlers) 15671 foreach(handler; *handlers) if(handler !is null) handler(e, this); 15672 15673 if(propagationStopped) 15674 break; 15675 15676 if(auto handlers = "*" in e.bubblingEventHandlers) 15677 foreach(handler; *handlers) if(handler !is null) handler(e, this); 15678 15679 if(propagationStopped) 15680 break; 15681 15682 if(e.encapsulatedChildren()) { 15683 adjustClientCoordinates(adjustX, adjustY); 15684 target = e; 15685 } else { 15686 adjustX += e.x; 15687 adjustY += e.y; 15688 } 15689 } 15690 15691 if(!defaultPrevented) 15692 foreach(e; chain) { 15693 if(eventName in e.defaultEventHandlers) 15694 e.defaultEventHandlers[eventName](e, this); 15695 } 15696 } 15697 15698 15699 /* old compatibility things */ 15700 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 15701 final @property { 15702 Key key() { return (cast(KeyEventBase) this).key; } 15703 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 15704 15705 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 15706 bool altKey() { return (cast(KeyEventBase) this).altKey; } 15707 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 15708 } 15709 15710 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 15711 final @property { 15712 int clientX() { return (cast(MouseEventBase) this).clientX; } 15713 int clientY() { return (cast(MouseEventBase) this).clientY; } 15714 15715 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 15716 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 15717 15718 int button() { return (cast(MouseEventBase) this).button; } 15719 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 15720 } 15721 15722 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 15723 final @property { 15724 int state() { 15725 if(auto meb = cast(MouseEventBase) this) 15726 return meb.state; 15727 if(auto keb = cast(KeyEventBase) this) 15728 return keb.state; 15729 assert(0); 15730 } 15731 } 15732 15733 deprecated("Use a CharEvent instead of Event in your handler going forward") 15734 final @property { 15735 dchar character() { 15736 if(auto ce = cast(CharEvent) this) 15737 return ce.character; 15738 return dchar.init; 15739 } 15740 } 15741 15742 // for change events 15743 @property { 15744 /// 15745 int intValue() { return 0; } 15746 /// 15747 string stringValue() { return null; } 15748 } 15749 } 15750 15751 /++ 15752 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 15753 15754 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 15755 dynamic and custom events, but the static list helps ensure you get them right. 15756 15757 If this is declared, you can use [Widget.emit] to send the event. 15758 15759 All events work the same way though, following the capture->widget->bubble model described under [Event]. 15760 15761 History: 15762 Added May 4, 2021 15763 +/ 15764 mixin template Emits(EventType) { 15765 import arsd.minigui : EventString; 15766 static if(is(EventType : Event) && !is(EventType == Event)) 15767 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 15768 else 15769 static assert(0, "You can only emit subclasses of Event"); 15770 } 15771 15772 /// ditto 15773 mixin template Emits(string eventString) { 15774 mixin("private Event[0] emits_" ~ eventString ~";"); 15775 } 15776 15777 /* 15778 class SignalEvent(string name) : Event { 15779 15780 } 15781 */ 15782 15783 /++ 15784 Command Events are used with a widget wants to issue a higher-level, yet loosely coupled command do its parents and other interested listeners, for example, "scroll up". 15785 15786 15787 Command Events are a bit special in the way they're used. You don't typically refer to them by object, but instead by a name string and a set of arguments. The expectation is that they will be delegated to a parent, which "consumes" the command - it handles it and stops its propagation upward. The [consumesCommand] method will call your handler with the arguments, then stop the command event's propagation for you, meaning you don't have to call [Event.stopPropagation]. A command event should have no default behavior, so calling [Event.preventDefault] is not necessary either. 15788 15789 History: 15790 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 15791 +/ 15792 class CommandEvent : Event { 15793 enum EventString = "command"; 15794 this(Widget source, string CommandString = EventString) { 15795 super(CommandString, source); 15796 } 15797 } 15798 15799 /++ 15800 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 15801 +/ 15802 class CommandEventWithArgs(Args...) : CommandEvent { 15803 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 15804 Args args; 15805 } 15806 15807 /++ 15808 Declares that the given widget consumes a command identified by the `CommandString` AND containing `Args`. Your `handler` is called with the arguments, then the event's propagation is stopped, so it will not be seen by the consumer's parents. 15809 15810 See [CommandEvent] for more information. 15811 15812 Returns: 15813 The [EventListener] you can use to remove the handler. 15814 +/ 15815 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 15816 return w.addEventListener(CommandString, (Event ev) { 15817 if(ev.target is w) 15818 return; // it does not consume its own commands! 15819 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 15820 handler(cev.args); 15821 ev.stopPropagation(); 15822 } 15823 }); 15824 } 15825 15826 /++ 15827 Emits a command to the sender widget's parents with the given `CommandString` and `args`. You have no way of knowing if it was ever actually consumed due to the loose coupling. Instead, the consumer may broadcast a state update back toward you. 15828 +/ 15829 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 15830 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 15831 event.dispatch(); 15832 } 15833 15834 /++ 15835 Widgets emit `ResizeEvent`s any time they are resized. You check [Widget.width] and [Widget.height] upon receiving this event to know the new size. 15836 15837 If you need to know the old size, you need to store it yourself. 15838 15839 History: 15840 Made final on January 3, 2025 (dub v12.0) 15841 +/ 15842 final class ResizeEvent : Event { 15843 enum EventString = "resize"; 15844 15845 this(Widget target) { super(EventString, target); } 15846 15847 override bool propagates() const { return false; } 15848 } 15849 15850 /++ 15851 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 15852 15853 ClosedEvent happens when the window has been closed. It is already gone by the time this event fires, meaning you cannot prevent the close. Use [ClosingEvent] if you want to cancel, use [ClosedEvent] if you simply want to be notified. 15854 15855 History: 15856 Added June 21, 2021 (dub v10.1) 15857 15858 Made final on January 3, 2025 (dub v12.0) 15859 +/ 15860 final class ClosingEvent : Event { 15861 enum EventString = "closing"; 15862 15863 this(Widget target) { super(EventString, target); } 15864 15865 override bool propagates() const { return false; } 15866 override bool cancelable() const { return true; } 15867 } 15868 15869 /// ditto 15870 final class ClosedEvent : Event { 15871 enum EventString = "closed"; 15872 15873 this(Widget target) { super(EventString, target); } 15874 15875 override bool propagates() const { return false; } 15876 override bool cancelable() const { return false; } 15877 } 15878 15879 /// 15880 final class BlurEvent : Event { 15881 enum EventString = "blur"; 15882 15883 // FIXME: related target? 15884 this(Widget target) { super(EventString, target); } 15885 15886 override bool propagates() const { return false; } 15887 } 15888 15889 /// 15890 final class FocusEvent : Event { 15891 enum EventString = "focus"; 15892 15893 // FIXME: related target? 15894 this(Widget target) { super(EventString, target); } 15895 15896 override bool propagates() const { return false; } 15897 } 15898 15899 /++ 15900 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 15901 15902 History: 15903 Added July 3, 2021 15904 +/ 15905 final class FocusInEvent : Event { 15906 enum EventString = "focusin"; 15907 15908 // FIXME: related target? 15909 this(Widget target) { super(EventString, target); } 15910 15911 override bool cancelable() const { return false; } 15912 } 15913 15914 /// ditto 15915 final class FocusOutEvent : Event { 15916 enum EventString = "focusout"; 15917 15918 // FIXME: related target? 15919 this(Widget target) { super(EventString, target); } 15920 15921 override bool cancelable() const { return false; } 15922 } 15923 15924 /// 15925 final class ScrollEvent : Event { 15926 enum EventString = "scroll"; 15927 this(Widget target) { super(EventString, target); } 15928 15929 override bool cancelable() const { return false; } 15930 } 15931 15932 /++ 15933 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 15934 15935 History: 15936 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 15937 +/ 15938 final class CharEvent : Event { 15939 enum EventString = "char"; 15940 this(Widget target, dchar ch) { 15941 character = ch; 15942 super(EventString, target); 15943 } 15944 15945 immutable dchar character; 15946 } 15947 15948 /++ 15949 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 15950 +/ 15951 abstract class ChangeEventBase : Event { 15952 enum EventString = "change"; 15953 this(Widget target) { 15954 super(EventString, target); 15955 } 15956 15957 /+ 15958 // idk where or how exactly i want to do this. 15959 // i might come back to it later. 15960 15961 // If a widget itself broadcasts one of theses itself, it stops propagation going down 15962 // this way the source doesn't get too confused (think of a nested scroll widget) 15963 // 15964 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 15965 // then you consume that command and change you scroll x position to whatever. then you do 15966 // some kind of change event that is broadcast back to the children and any horizontal scroll 15967 // listeners are now able to update, without having an explicit connection between them. 15968 void broadcastToChildren(string fieldName) { 15969 15970 } 15971 +/ 15972 } 15973 15974 /++ 15975 Single-value widgets (that is, ones with a programming interface that just expose a value that the user has control over) should emit this after their value changes. 15976 15977 15978 Generally speaking, if your widget can reasonably have a `@property T value();` or `@property bool checked();` method, it should probably emit this event when that value changes to inform its parents that they can now read a new value. Whether you emit it on each keystroke or other intermediate values or only when a value is committed (e.g. when the user leaves the field) is up to the widget. You might even make that a togglable property depending on your needs (emitting events can get expensive). 15979 15980 The delegate you pass to the constructor ought to be a handle to your getter property. If your widget has `@property string value()` for example, you emit `ChangeEvent!string(&value);` 15981 15982 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 15983 15984 History: 15985 Added May 11, 2021. Prior to that, widgets would more likely just send `new Event("change")`. These typed ChangeEvents are still compatible with listeners subscribed to generic change events. 15986 +/ 15987 final class ChangeEvent(T) : ChangeEventBase { 15988 this(Widget target, T delegate() getNewValue) { 15989 assert(getNewValue !is null); 15990 this.getNewValue = getNewValue; 15991 super(target); 15992 } 15993 15994 private T delegate() getNewValue; 15995 15996 /++ 15997 Gets the new value that just changed. 15998 +/ 15999 @property T value() { 16000 return getNewValue(); 16001 } 16002 16003 /// compatibility method for old generic Events 16004 static if(is(immutable T == immutable int)) 16005 override int intValue() { return value; } 16006 /// ditto 16007 static if(is(immutable T == immutable string)) 16008 override string stringValue() { return value; } 16009 } 16010 16011 /++ 16012 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 16013 16014 16015 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16016 16017 History: 16018 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 16019 +/ 16020 abstract class KeyEventBase : Event { 16021 this(string name, Widget target) { 16022 super(name, target); 16023 } 16024 16025 // for key events 16026 Key key; /// 16027 16028 KeyEvent originalKeyEvent; 16029 16030 /++ 16031 Indicates the current state of the given keyboard modifier keys. 16032 16033 History: 16034 Added to events on April 15, 2020. 16035 +/ 16036 bool ctrlKey; 16037 16038 /// ditto 16039 bool altKey; 16040 16041 /// ditto 16042 bool shiftKey; 16043 16044 /++ 16045 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 16046 16047 See [arsd.simpledisplay.ModifierState] for other possible flags. 16048 +/ 16049 int state; 16050 16051 mixin Register; 16052 } 16053 16054 /++ 16055 Indicates that the user has pressed a key on the keyboard, or if they've been holding it long enough to repeat (key down events are sent both on the initial press then repeated by the OS on its own time.) For available properties, see [KeyEventBase]. 16056 16057 16058 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16059 16060 Please note that a `KeyDownEvent` will also often send a [CharEvent], but there is not necessarily a one-to-one relationship between them. For example, a capital letter may send KeyDownEvent for Key.Shift, then KeyDownEvent for the letter's key (this key may not match the letter due to keyboard mappings), then CharEvent for the letter, then KeyUpEvent for the letter, and finally, KeyUpEvent for shift. 16061 16062 For some characters, there are other key down events as well. A compose key can be pressed and released, followed by several letters pressed and released to generate one character. This is why [CharEvent] is a separate entity. 16063 16064 See_Also: [KeyUpEvent], [CharEvent] 16065 16066 History: 16067 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 16068 +/ 16069 final class KeyDownEvent : KeyEventBase { 16070 enum EventString = "keydown"; 16071 this(Widget target) { super(EventString, target); } 16072 } 16073 16074 /++ 16075 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 16076 16077 16078 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16079 16080 See_Also: [KeyDownEvent], [CharEvent] 16081 16082 History: 16083 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 16084 +/ 16085 final class KeyUpEvent : KeyEventBase { 16086 enum EventString = "keyup"; 16087 this(Widget target) { super(EventString, target); } 16088 } 16089 16090 /++ 16091 Contains shared properties for various mouse events; 16092 16093 16094 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16095 16096 History: 16097 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 16098 +/ 16099 abstract class MouseEventBase : Event { 16100 this(string name, Widget target) { 16101 super(name, target); 16102 } 16103 16104 // for mouse events 16105 int clientX; /// The mouse event location relative to the target widget 16106 int clientY; /// ditto 16107 16108 int viewportX; /// The mouse event location relative to the window origin 16109 int viewportY; /// ditto 16110 16111 int button; /// See: [MouseEvent.button] 16112 int buttonLinear; /// See: [MouseEvent.buttonLinear] 16113 16114 /++ 16115 Indicates the current state of the given keyboard modifier keys. 16116 16117 History: 16118 Added to mouse events on September 28, 2010. 16119 +/ 16120 bool ctrlKey; 16121 16122 /// ditto 16123 bool altKey; 16124 16125 /// ditto 16126 bool shiftKey; 16127 16128 16129 16130 int state; /// 16131 16132 /++ 16133 for consistent names with key event. 16134 16135 History: 16136 Added September 28, 2021 (dub v10.3) 16137 +/ 16138 alias modifierState = state; 16139 16140 /++ 16141 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 16142 16143 History: 16144 Added May 15, 2021 16145 +/ 16146 bool isMouseWheel() { 16147 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 16148 } 16149 16150 // private 16151 override void adjustClientCoordinates(int deltaX, int deltaY) { 16152 clientX += deltaX; 16153 clientY += deltaY; 16154 } 16155 16156 override void adjustScrolling() { 16157 version(custom_widgets) { // TEMP 16158 viewportX = clientX; 16159 viewportY = clientY; 16160 if(auto se = cast(ScrollableWidget) srcElement) { 16161 clientX += se.scrollOrigin.x; 16162 clientY += se.scrollOrigin.y; 16163 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 16164 //clientX += se.scrollX_; 16165 //clientY += se.scrollY_; 16166 } 16167 } 16168 } 16169 16170 mixin Register; 16171 } 16172 16173 /++ 16174 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 16175 16176 16177 $(WARNING 16178 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 16179 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 16180 behavior. 16181 16182 Use [MouseEventBase.isMouseWheel] to filter wheel events while keeping others. 16183 ) 16184 16185 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 16186 16187 [MouseUpEvent] is sent when the user releases a mouse button. 16188 16189 [MouseMoveEvent] is sent when the mouse is moved. Please note you may not receive this in some cases unless a button is also pressed; the system is free to withhold them as an optimization. (In practice, [arsd.simpledisplay] does not request mouse motion event without a held button if it is on a remote X11 link, but does elsewhere at this time.) 16190 16191 [ClickEvent] is sent when the user clicks on the widget. It may also be sent with keyboard control, though minigui prefers to send a "triggered" event in addition to a mouse click and instead of a simulated mouse click in cases like keyboard activation of a button. 16192 16193 [DoubleClickEvent] is sent when the user clicks twice on a thing quickly, immediately after the second MouseDownEvent. The sequence is: MouseDownEvent, MouseUpEvent, ClickEvent, MouseDownEvent, DoubleClickEvent, MouseUpEvent. The second ClickEvent is NOT sent. Note that this is different than Javascript! They would send down,up,click,down,up,click,dblclick. Minigui does it differently because this is the way the Windows OS reports it. 16194 16195 [MouseOverEvent] is sent then the mouse first goes over a widget. Please note that this participates in event propagation of children! Use [MouseEnterEvent] instead if you are only interested in a specific element's whole bounding box instead of the top-most element in any particular location. 16196 16197 [MouseOutEvent] is sent when the mouse exits a target. Please note that this participates in event propagation of children! Use [MouseLeaveEvent] instead if you are only interested in a specific element's whole bounding box instead of the top-most element in any particular location. 16198 16199 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 16200 16201 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 16202 16203 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16204 16205 Rationale: 16206 16207 If you only want to do drag, mousedown/up works just fine being consistently sent. 16208 16209 If you want click, that event does what you expect (if the user mouse downs then moves the mouse off the widget before going up, no click event happens - a click is only down and back up on the same thing). 16210 16211 If you want double click and listen to that specifically, it also just works, and if you only cared about clicks, odds are the double click should do the same thing as a single click anyway - the double was prolly accidental - so only sending the event once is prolly what user intended. 16212 16213 History: 16214 Added May 2, 2021. Previously, it was only seen as the base [Event] class on event listeners. See the member [EventString] to see what the associated string is with these elements. 16215 +/ 16216 final class MouseUpEvent : MouseEventBase { 16217 enum EventString = "mouseup"; /// 16218 this(Widget target) { super(EventString, target); } 16219 } 16220 /// ditto 16221 final class MouseDownEvent : MouseEventBase { 16222 enum EventString = "mousedown"; /// 16223 this(Widget target) { super(EventString, target); } 16224 } 16225 /// ditto 16226 final class MouseMoveEvent : MouseEventBase { 16227 enum EventString = "mousemove"; /// 16228 this(Widget target) { super(EventString, target); } 16229 } 16230 /// ditto 16231 final class ClickEvent : MouseEventBase { 16232 enum EventString = "click"; /// 16233 this(Widget target) { super(EventString, target); } 16234 } 16235 /// ditto 16236 final class DoubleClickEvent : MouseEventBase { 16237 enum EventString = "dblclick"; /// 16238 this(Widget target) { super(EventString, target); } 16239 } 16240 /// ditto 16241 final class MouseOverEvent : Event { 16242 enum EventString = "mouseover"; /// 16243 this(Widget target) { super(EventString, target); } 16244 } 16245 /// ditto 16246 final class MouseOutEvent : Event { 16247 enum EventString = "mouseout"; /// 16248 this(Widget target) { super(EventString, target); } 16249 } 16250 /// ditto 16251 final class MouseEnterEvent : Event { 16252 enum EventString = "mouseenter"; /// 16253 this(Widget target) { super(EventString, target); } 16254 16255 override bool propagates() const { return false; } 16256 } 16257 /// ditto 16258 final class MouseLeaveEvent : Event { 16259 enum EventString = "mouseleave"; /// 16260 this(Widget target) { super(EventString, target); } 16261 16262 override bool propagates() const { return false; } 16263 } 16264 16265 private bool isAParentOf(Widget a, Widget b) { 16266 if(a is null || b is null) 16267 return false; 16268 16269 while(b !is null) { 16270 if(a is b) 16271 return true; 16272 b = b.parent; 16273 } 16274 16275 return false; 16276 } 16277 16278 private struct WidgetAtPointResponse { 16279 Widget widget; 16280 16281 // x, y relative to the widget in the response. 16282 int x; 16283 int y; 16284 } 16285 16286 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 16287 assert(starting !is null); 16288 16289 starting.addScrollPosition(x, y); 16290 16291 auto child = starting.getChildAtPosition(x, y); 16292 while(child) { 16293 if(child.hidden) 16294 continue; 16295 starting = child; 16296 x -= child.x; 16297 y -= child.y; 16298 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 16299 child = r.widget; 16300 if(child is starting) 16301 break; 16302 } 16303 return WidgetAtPointResponse(starting, x, y); 16304 } 16305 16306 version(win32_widgets) { 16307 private: 16308 import core.sys.windows.commctrl; 16309 16310 pragma(lib, "comctl32"); 16311 shared static this() { 16312 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 16313 INITCOMMONCONTROLSEX ic; 16314 ic.dwSize = cast(DWORD) ic.sizeof; 16315 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 16316 if(!InitCommonControlsEx(&ic)) { 16317 //writeln("ICC failed"); 16318 } 16319 } 16320 16321 16322 // everything from here is just win32 headers copy pasta 16323 private: 16324 extern(Windows): 16325 16326 alias HANDLE HMENU; 16327 HMENU CreateMenu(); 16328 bool SetMenu(HWND, HMENU); 16329 HMENU CreatePopupMenu(); 16330 enum MF_POPUP = 0x10; 16331 enum MF_STRING = 0; 16332 16333 16334 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 16335 struct INITCOMMONCONTROLSEX { 16336 DWORD dwSize; 16337 DWORD dwICC; 16338 } 16339 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 16340 enum { 16341 IDB_STD_SMALL_COLOR, 16342 IDB_STD_LARGE_COLOR, 16343 IDB_VIEW_SMALL_COLOR = 4, 16344 IDB_VIEW_LARGE_COLOR = 5 16345 } 16346 enum { 16347 STD_CUT, 16348 STD_COPY, 16349 STD_PASTE, 16350 STD_UNDO, 16351 STD_REDOW, 16352 STD_DELETE, 16353 STD_FILENEW, 16354 STD_FILEOPEN, 16355 STD_FILESAVE, 16356 STD_PRINTPRE, 16357 STD_PROPERTIES, 16358 STD_HELP, 16359 STD_FIND, 16360 STD_REPLACE, 16361 STD_PRINT // = 14 16362 } 16363 16364 alias HANDLE HIMAGELIST; 16365 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 16366 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 16367 BOOL ImageList_Destroy(HIMAGELIST); 16368 16369 uint MAKELONG(ushort a, ushort b) { 16370 return cast(uint) ((b << 16) | a); 16371 } 16372 16373 16374 struct TBBUTTON { 16375 int iBitmap; 16376 int idCommand; 16377 BYTE fsState; 16378 BYTE fsStyle; 16379 version(Win64) 16380 BYTE[6] bReserved; 16381 else 16382 BYTE[2] bReserved; 16383 DWORD dwData; 16384 INT_PTR iString; 16385 } 16386 16387 enum { 16388 TB_ADDBUTTONSA = WM_USER + 20, 16389 TB_INSERTBUTTONA = WM_USER + 21, 16390 TB_GETIDEALSIZE = WM_USER + 99, 16391 } 16392 16393 struct SIZE { 16394 LONG cx; 16395 LONG cy; 16396 } 16397 16398 16399 enum { 16400 TBSTATE_CHECKED = 1, 16401 TBSTATE_PRESSED = 2, 16402 TBSTATE_ENABLED = 4, 16403 TBSTATE_HIDDEN = 8, 16404 TBSTATE_INDETERMINATE = 16, 16405 TBSTATE_WRAP = 32 16406 } 16407 16408 16409 16410 enum { 16411 ILC_COLOR = 0, 16412 ILC_COLOR4 = 4, 16413 ILC_COLOR8 = 8, 16414 ILC_COLOR16 = 16, 16415 ILC_COLOR24 = 24, 16416 ILC_COLOR32 = 32, 16417 ILC_COLORDDB = 254, 16418 ILC_MASK = 1, 16419 ILC_PALETTE = 2048 16420 } 16421 16422 16423 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 16424 16425 16426 enum { 16427 TB_ENABLEBUTTON = WM_USER + 1, 16428 TB_CHECKBUTTON, 16429 TB_PRESSBUTTON, 16430 TB_HIDEBUTTON, 16431 TB_INDETERMINATE, // = WM_USER + 5, 16432 TB_ISBUTTONENABLED = WM_USER + 9, 16433 TB_ISBUTTONCHECKED, 16434 TB_ISBUTTONPRESSED, 16435 TB_ISBUTTONHIDDEN, 16436 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 16437 TB_SETSTATE = WM_USER + 17, 16438 TB_GETSTATE = WM_USER + 18, 16439 TB_ADDBITMAP = WM_USER + 19, 16440 TB_DELETEBUTTON = WM_USER + 22, 16441 TB_GETBUTTON, 16442 TB_BUTTONCOUNT, 16443 TB_COMMANDTOINDEX, 16444 TB_SAVERESTOREA, 16445 TB_CUSTOMIZE, 16446 TB_ADDSTRINGA, 16447 TB_GETITEMRECT, 16448 TB_BUTTONSTRUCTSIZE, 16449 TB_SETBUTTONSIZE, 16450 TB_SETBITMAPSIZE, 16451 TB_AUTOSIZE, // = WM_USER + 33, 16452 TB_GETTOOLTIPS = WM_USER + 35, 16453 TB_SETTOOLTIPS = WM_USER + 36, 16454 TB_SETPARENT = WM_USER + 37, 16455 TB_SETROWS = WM_USER + 39, 16456 TB_GETROWS, 16457 TB_GETBITMAPFLAGS, 16458 TB_SETCMDID, 16459 TB_CHANGEBITMAP, 16460 TB_GETBITMAP, 16461 TB_GETBUTTONTEXTA, 16462 TB_REPLACEBITMAP, // = WM_USER + 46, 16463 TB_GETBUTTONSIZE = WM_USER + 58, 16464 TB_SETBUTTONWIDTH = WM_USER + 59, 16465 TB_GETBUTTONTEXTW = WM_USER + 75, 16466 TB_SAVERESTOREW = WM_USER + 76, 16467 TB_ADDSTRINGW = WM_USER + 77, 16468 } 16469 16470 extern(Windows) 16471 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 16472 16473 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 16474 16475 16476 enum { 16477 TB_SETINDENT = WM_USER + 47, 16478 TB_SETIMAGELIST, 16479 TB_GETIMAGELIST, 16480 TB_LOADIMAGES, 16481 TB_GETRECT, 16482 TB_SETHOTIMAGELIST, 16483 TB_GETHOTIMAGELIST, 16484 TB_SETDISABLEDIMAGELIST, 16485 TB_GETDISABLEDIMAGELIST, 16486 TB_SETSTYLE, 16487 TB_GETSTYLE, 16488 //TB_GETBUTTONSIZE, 16489 //TB_SETBUTTONWIDTH, 16490 TB_SETMAXTEXTROWS, 16491 TB_GETTEXTROWS // = WM_USER + 61 16492 } 16493 16494 enum { 16495 CCM_FIRST = 0x2000, 16496 CCM_LAST = CCM_FIRST + 0x200, 16497 CCM_SETBKCOLOR = 8193, 16498 CCM_SETCOLORSCHEME = 8194, 16499 CCM_GETCOLORSCHEME = 8195, 16500 CCM_GETDROPTARGET = 8196, 16501 CCM_SETUNICODEFORMAT = 8197, 16502 CCM_GETUNICODEFORMAT = 8198, 16503 CCM_SETVERSION = 0x2007, 16504 CCM_GETVERSION = 0x2008, 16505 CCM_SETNOTIFYWINDOW = 0x2009 16506 } 16507 16508 16509 enum { 16510 PBM_SETRANGE = WM_USER + 1, 16511 PBM_SETPOS, 16512 PBM_DELTAPOS, 16513 PBM_SETSTEP, 16514 PBM_STEPIT, // = WM_USER + 5 16515 PBM_SETRANGE32 = 1030, 16516 PBM_GETRANGE, 16517 PBM_GETPOS, 16518 PBM_SETBARCOLOR, // = 1033 16519 PBM_SETBKCOLOR = CCM_SETBKCOLOR 16520 } 16521 16522 enum { 16523 PBS_SMOOTH = 1, 16524 PBS_VERTICAL = 4 16525 } 16526 16527 enum { 16528 ICC_LISTVIEW_CLASSES = 1, 16529 ICC_TREEVIEW_CLASSES = 2, 16530 ICC_BAR_CLASSES = 4, 16531 ICC_TAB_CLASSES = 8, 16532 ICC_UPDOWN_CLASS = 16, 16533 ICC_PROGRESS_CLASS = 32, 16534 ICC_HOTKEY_CLASS = 64, 16535 ICC_ANIMATE_CLASS = 128, 16536 ICC_WIN95_CLASSES = 255, 16537 ICC_DATE_CLASSES = 256, 16538 ICC_USEREX_CLASSES = 512, 16539 ICC_COOL_CLASSES = 1024, 16540 ICC_STANDARD_CLASSES = 0x00004000, 16541 } 16542 16543 enum WM_USER = 1024; 16544 } 16545 16546 version(win32_widgets) 16547 pragma(lib, "comdlg32"); 16548 16549 16550 /// 16551 enum GenericIcons : ushort { 16552 None, /// 16553 // these happen to match the win32 std icons numerically if you just subtract one from the value 16554 Cut, /// 16555 Copy, /// 16556 Paste, /// 16557 Undo, /// 16558 Redo, /// 16559 Delete, /// 16560 New, /// 16561 Open, /// 16562 Save, /// 16563 PrintPreview, /// 16564 Properties, /// 16565 Help, /// 16566 Find, /// 16567 Replace, /// 16568 Print, /// 16569 } 16570 16571 enum FileDialogType { 16572 Automatic, 16573 Open, 16574 Save 16575 } 16576 16577 /++ 16578 The default string [FileName] refers to to store the last file referenced. You can use this if you like, or provide a different variable to `FileName` in your function. 16579 +/ 16580 string previousFileReferenced; 16581 16582 /++ 16583 Used in automatic menu functions to indicate that the user should be able to browse for a file. 16584 16585 Params: 16586 storage = an alias to a `static string` variable that stores the last file referenced. It will 16587 use this to pre-fill the dialog with a suggestion. 16588 16589 Please note that it MUST be `static` or you will get compile errors. 16590 16591 filters = the filters param to [getFileName] 16592 16593 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 16594 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 16595 a save dialog box. Otherwise, it will show an open dialog box. 16596 +/ 16597 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 16598 string name; 16599 alias name this; 16600 16601 @implicit this(string name) { 16602 this.name = name; 16603 } 16604 } 16605 16606 /++ 16607 Gets a file name for an open or save operation, calling your `onOK` function when the user has selected one. This function may or may not block depending on the operating system, you MUST assume it will complete asynchronously. 16608 16609 History: 16610 onCancel was added November 6, 2021. 16611 16612 The dialog itself on Linux was modified on December 2, 2021 to include 16613 a directory picker in addition to the command line completion view. 16614 16615 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 16616 16617 The `owner` argument was added September 29, 2024. The overloads without this argument are likely to be deprecated in the next major version. 16618 Future_directions: 16619 I want to add some kind of custom preview and maybe thumbnail thing in the future, 16620 at least on Linux, maybe on Windows too. 16621 +/ 16622 void getOpenFileName( 16623 Window owner, 16624 void delegate(string) onOK, 16625 string prefilledName = null, 16626 string[] filters = null, 16627 void delegate() onCancel = null, 16628 string initialDirectory = null, 16629 ) 16630 { 16631 return getFileName(owner, true, onOK, prefilledName, filters, onCancel, initialDirectory); 16632 } 16633 16634 /// ditto 16635 void getSaveFileName( 16636 Window owner, 16637 void delegate(string) onOK, 16638 string prefilledName = null, 16639 string[] filters = null, 16640 void delegate() onCancel = null, 16641 string initialDirectory = null, 16642 ) 16643 { 16644 return getFileName(owner, false, onOK, prefilledName, filters, onCancel, initialDirectory); 16645 } 16646 16647 // deprecated("Pass an explicit owner window as the first argument, even if `null`. You can usually pass the `parentWindow` member of the widget that prompted this interaction.") 16648 /// ditto 16649 void getOpenFileName( 16650 void delegate(string) onOK, 16651 string prefilledName = null, 16652 string[] filters = null, 16653 void delegate() onCancel = null, 16654 string initialDirectory = null, 16655 ) 16656 { 16657 return getFileName(null, true, onOK, prefilledName, filters, onCancel, initialDirectory); 16658 } 16659 16660 /// ditto 16661 void getSaveFileName( 16662 void delegate(string) onOK, 16663 string prefilledName = null, 16664 string[] filters = null, 16665 void delegate() onCancel = null, 16666 string initialDirectory = null, 16667 ) 16668 { 16669 return getFileName(null, false, onOK, prefilledName, filters, onCancel, initialDirectory); 16670 } 16671 16672 /++ 16673 It is possible to override or customize the file dialog in some cases. These members provide those hooks: you do `fileDialogDelegate = new YourSubclassOf_FileDialogDelegate;` and you can do your own thing. 16674 16675 This is a customization hook and you should not call methods on this class directly. Use the public functions [getOpenFileName] and [getSaveFileName], or make an automatic dialog with [FileName] instead. 16676 16677 History: 16678 Added January 1, 2025 16679 +/ 16680 class FileDialogDelegate { 16681 16682 /++ 16683 16684 +/ 16685 static abstract class PreviewWidget : Widget { 16686 /// Call this from your subclass' constructor 16687 this(Widget parent) { 16688 super(parent); 16689 } 16690 16691 /// Load the file given to you and show its preview inside the widget here 16692 abstract void previewFile(string filename); 16693 } 16694 16695 /++ 16696 Override this to add preview capabilities to the dialog for certain files. 16697 +/ 16698 protected PreviewWidget makePreviewWidget(Widget parent) { 16699 return null; 16700 } 16701 16702 /++ 16703 Override this to change the dialog entirely. 16704 16705 This function IS allowed to block, but is NOT required to. 16706 +/ 16707 protected void getFileName( 16708 Window owner, 16709 bool openOrSave, // true if open, false if save 16710 void delegate(string) onOK, 16711 string prefilledName, 16712 string[] filters, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 16713 void delegate() onCancel, 16714 string initialDirectory, 16715 ) 16716 { 16717 16718 version(win32_widgets) { 16719 import core.sys.windows.commdlg; 16720 /* 16721 Ofn.lStructSize = sizeof(OPENFILENAME); 16722 Ofn.hwndOwner = hWnd; 16723 Ofn.lpstrFilter = szFilter; 16724 Ofn.lpstrFile= szFile; 16725 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 16726 Ofn.lpstrFileTitle = szFileTitle; 16727 Ofn.nMaxFileTitle = sizeof(szFileTitle); 16728 Ofn.lpstrInitialDir = (LPSTR)NULL; 16729 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 16730 Ofn.lpstrTitle = szTitle; 16731 */ 16732 16733 16734 wchar[1024] file = 0; 16735 wchar[1024] filterBuffer = 0; 16736 makeWindowsString(prefilledName, file[]); 16737 OPENFILENAME ofn; 16738 ofn.lStructSize = ofn.sizeof; 16739 ofn.hwndOwner = owner is null ? null : owner.win.hwnd; 16740 if(filters.length) { 16741 string filter; 16742 foreach(i, f; filters) { 16743 filter ~= f; 16744 filter ~= "\0"; 16745 } 16746 filter ~= "\0"; 16747 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 16748 } 16749 ofn.lpstrFile = file.ptr; 16750 ofn.nMaxFile = file.length; 16751 16752 wchar[1024] initialDir = 0; 16753 if(initialDirectory !is null) { 16754 makeWindowsString(initialDirectory, initialDir[]); 16755 ofn.lpstrInitialDir = file.ptr; 16756 } 16757 16758 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 16759 { 16760 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 16761 if(okString.length && okString[$-1] == '\0') 16762 okString = okString[0..$-1]; 16763 onOK(okString); 16764 } else { 16765 if(onCancel) 16766 onCancel(); 16767 } 16768 } else version(custom_widgets) { 16769 filters ~= ["All Files\0*.*"]; 16770 auto picker = new FilePicker(openOrSave, prefilledName, filters, initialDirectory, owner); 16771 picker.onOK = onOK; 16772 picker.onCancel = onCancel; 16773 picker.show(); 16774 } 16775 } 16776 16777 } 16778 16779 /// ditto 16780 FileDialogDelegate fileDialogDelegate() { 16781 if(fileDialogDelegate_ is null) 16782 fileDialogDelegate_ = new FileDialogDelegate(); 16783 return fileDialogDelegate_; 16784 } 16785 16786 /// ditto 16787 void fileDialogDelegate(FileDialogDelegate replacement) { 16788 fileDialogDelegate_ = replacement; 16789 } 16790 16791 private FileDialogDelegate fileDialogDelegate_; 16792 16793 struct FileNameFilter { 16794 string description; 16795 string[] globPatterns; 16796 16797 string toString() { 16798 string ret; 16799 ret ~= description; 16800 ret ~= " ("; 16801 foreach(idx, pattern; globPatterns) { 16802 if(idx) 16803 ret ~= "; "; 16804 ret ~= pattern; 16805 } 16806 ret ~= ")"; 16807 16808 return ret; 16809 } 16810 16811 static FileNameFilter fromString(string s) { 16812 size_t end = s.length; 16813 size_t start = 0; 16814 foreach_reverse(idx, ch; s) { 16815 if(ch == ')' && end == s.length) 16816 end = idx; 16817 else if(ch == '(' && end != s.length) { 16818 start = idx + 1; 16819 break; 16820 } 16821 } 16822 16823 FileNameFilter fnf; 16824 fnf.description = s[0 .. start ? start - 1 : 0]; 16825 size_t globStart = 0; 16826 s = s[start .. end]; 16827 foreach(idx, ch; s) 16828 if(ch == ';') { 16829 auto ptn = stripInternal(s[globStart .. idx]); 16830 if(ptn.length) 16831 fnf.globPatterns ~= ptn; 16832 globStart = idx + 1; 16833 16834 } 16835 auto ptn = stripInternal(s[globStart .. $]); 16836 if(ptn.length) 16837 fnf.globPatterns ~= ptn; 16838 return fnf; 16839 } 16840 } 16841 16842 struct FileNameFilterSet { 16843 FileNameFilter[] filters; 16844 16845 static FileNameFilterSet fromWindowsFileNameFilterDescription(string[] filters) { 16846 FileNameFilter[] ret; 16847 16848 foreach(filter; filters) { 16849 FileNameFilter fnf; 16850 size_t filterStartPoint; 16851 foreach(idx, ch; filter) { 16852 if(ch == 0) { 16853 fnf.description = filter[0 .. idx]; 16854 filterStartPoint = idx + 1; 16855 } else if(filterStartPoint && ch == ';') { 16856 fnf.globPatterns ~= filter[filterStartPoint .. idx]; 16857 filterStartPoint = idx + 1; 16858 } 16859 } 16860 fnf.globPatterns ~= filter[filterStartPoint .. $]; 16861 16862 ret ~= fnf; 16863 } 16864 16865 return FileNameFilterSet(ret); 16866 } 16867 } 16868 16869 void getFileName( 16870 Window owner, 16871 bool openOrSave, 16872 void delegate(string) onOK, 16873 string prefilledName = null, 16874 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 16875 void delegate() onCancel = null, 16876 string initialDirectory = null, 16877 ) 16878 { 16879 return fileDialogDelegate().getFileName(owner, openOrSave, onOK, prefilledName, filters, onCancel, initialDirectory); 16880 } 16881 16882 version(custom_widgets) 16883 private 16884 class FilePicker : Dialog { 16885 void delegate(string) onOK; 16886 void delegate() onCancel; 16887 LabeledLineEdit lineEdit; 16888 bool isOpenDialogInsteadOfSave; 16889 16890 static struct HistoryItem { 16891 string cwd; 16892 FileNameFilter filters; 16893 } 16894 HistoryItem[] historyStack; 16895 size_t historyStackPosition; 16896 16897 void back() { 16898 if(historyStackPosition) { 16899 historyStackPosition--; 16900 currentDirectory = historyStack[historyStackPosition].cwd; 16901 currentFilter = historyStack[historyStackPosition].filters; 16902 filesOfType.content = currentFilter.toString(); 16903 loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true); 16904 lineEdit.focus(); 16905 } 16906 } 16907 16908 void forward() { 16909 if(historyStackPosition + 1 < historyStack.length) { 16910 historyStackPosition++; 16911 currentDirectory = historyStack[historyStackPosition].cwd; 16912 currentFilter = historyStack[historyStackPosition].filters; 16913 filesOfType.content = currentFilter.toString(); 16914 loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true); 16915 lineEdit.focus(); 16916 } 16917 } 16918 16919 void up() { 16920 currentDirectory = currentDirectory ~ ".."; 16921 loadFiles(currentDirectory, currentFilter); 16922 lineEdit.focus(); 16923 } 16924 16925 void refresh() { 16926 loadFiles(currentDirectory, currentFilter); 16927 lineEdit.focus(); 16928 } 16929 16930 // returns common prefix 16931 static struct CommonPrefixInfo { 16932 string commonPrefix; 16933 int fileCount; 16934 string exactMatch; 16935 } 16936 CommonPrefixInfo loadFiles(string cwd, FileNameFilter filters, bool comingFromHistory = false) { 16937 16938 if(!comingFromHistory) { 16939 if(historyStack.length) { 16940 historyStack = historyStack[0 .. historyStackPosition + 1]; 16941 historyStack.assumeSafeAppend(); 16942 } 16943 historyStack ~= HistoryItem(cwd, filters); 16944 historyStackPosition = historyStack.length - 1; 16945 } 16946 16947 string[] files; 16948 string[] dirs; 16949 16950 dirs ~= "$HOME"; 16951 dirs ~= "$PWD"; 16952 16953 string commonPrefix; 16954 int commonPrefixCount; 16955 string exactMatch; 16956 16957 bool matchesFilter(string name) { 16958 foreach(filter; filters.globPatterns) { 16959 if( 16960 filter.length <= 1 || 16961 filter == "*.*" || // we always treat *.* the same as *, but it is a bit different than .* 16962 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 16963 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 16964 ) 16965 { 16966 if(name.length > 1 && name[0] == '.') 16967 if(filter.length == 0 || filter[0] != '.') 16968 return false; 16969 16970 return true; 16971 } 16972 } 16973 16974 return false; 16975 } 16976 16977 void considerCommonPrefix(string name, bool prefiltered) { 16978 if(!prefiltered && !matchesFilter(name)) 16979 return; 16980 16981 if(commonPrefix is null) { 16982 commonPrefix = name; 16983 commonPrefixCount = 1; 16984 exactMatch = commonPrefix; 16985 } else { 16986 foreach(idx, char i; name) { 16987 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 16988 commonPrefix = commonPrefix[0 .. idx]; 16989 commonPrefixCount ++; 16990 exactMatch = null; 16991 break; 16992 } 16993 } 16994 } 16995 } 16996 16997 bool applyFilterToDirectories = true; 16998 bool showDotFiles = false; 16999 foreach(filter; filters.globPatterns) { 17000 if(filter == ".*") 17001 showDotFiles = true; 17002 else foreach(ch; filter) 17003 if(ch == '.') { 17004 // a filter like *.exe should not apply to the directory 17005 applyFilterToDirectories = false; 17006 break; 17007 } 17008 } 17009 17010 try 17011 getFiles(cwd, (string name, bool isDirectory) { 17012 if(name == ".") 17013 return; // skip this as unnecessary 17014 if(isDirectory) { 17015 if(applyFilterToDirectories) { 17016 if(matchesFilter(name)) { 17017 dirs ~= name; 17018 considerCommonPrefix(name, false); 17019 } 17020 } else if(name != ".." && name.length > 1 && name[0] == '.') { 17021 if(showDotFiles) { 17022 dirs ~= name; 17023 considerCommonPrefix(name, false); 17024 } 17025 } else { 17026 dirs ~= name; 17027 considerCommonPrefix(name, false); 17028 } 17029 } else { 17030 if(matchesFilter(name)) { 17031 files ~= name; 17032 17033 //if(filter.length > 0 && filter[$-1] == '*') { 17034 considerCommonPrefix(name, true); 17035 //} 17036 } 17037 } 17038 }); 17039 catch(ArsdExceptionBase e) { 17040 messageBox("Unable to read requested directory"); 17041 // FIXME: give them a chance to create it? or at least go back? 17042 /+ 17043 comingFromHistory = true; 17044 back(); 17045 return null; 17046 +/ 17047 } 17048 17049 extern(C) static int comparator(scope const void* a, scope const void* b) { 17050 auto sa = *cast(string*) a; 17051 auto sb = *cast(string*) b; 17052 17053 /+ 17054 Goal here: 17055 17056 Dot first. This puts `foo.d` before `foo2.d` 17057 Then numbers , natural sort order (so 9 comes before 10) for positive numbers 17058 Then letters, in order Aa, Bb, Cc 17059 Then other symbols in ascii order 17060 +/ 17061 static int nextPiece(ref string whole) { 17062 if(whole.length == 0) 17063 return -1; 17064 17065 enum specialZoneSize = 1; 17066 17067 char current = whole[0]; 17068 if(current >= '0' && current <= '9') { 17069 int accumulator; 17070 do { 17071 whole = whole[1 .. $]; 17072 accumulator *= 10; 17073 accumulator += current - '0'; 17074 current = whole.length ? whole[0] : 0; 17075 } while (current >= '0' && current <= '9'); 17076 17077 return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols 17078 } else { 17079 whole = whole[1 .. $]; 17080 17081 if(current == '.') 17082 return 0; // the special case to put it before numbers 17083 17084 // anything above should be < specialZoneSize 17085 17086 int letterZoneSize = 26 * 2; 17087 int base = int.max - letterZoneSize - char.max; // leaves space at end for symbols too if we want them after chars 17088 17089 if(current >= 'A' && current <= 'Z') 17090 return base + (current - 'A') * 2; 17091 if(current >= 'a' && current <= 'z') 17092 return base + (current - 'a') * 2 + 1; 17093 // return base + letterZoneSize + current; // would put symbols after numbers and letters 17094 return specialZoneSize + current; // puts symbols before numbers and letters, but after the special zone 17095 } 17096 } 17097 17098 while(sa.length || sb.length) { 17099 auto pa = nextPiece(sa); 17100 auto pb = nextPiece(sb); 17101 17102 auto diff = pa - pb; 17103 if(diff) 17104 return diff; 17105 } 17106 17107 return 0; 17108 } 17109 17110 nonPhobosSort(files, &comparator); 17111 nonPhobosSort(dirs, &comparator); 17112 17113 listWidget.clear(); 17114 dirWidget.clear(); 17115 foreach(name; dirs) 17116 dirWidget.addOption(name); 17117 foreach(name; files) 17118 listWidget.addOption(name); 17119 17120 return CommonPrefixInfo(commonPrefix, commonPrefixCount, exactMatch); 17121 } 17122 17123 ListWidget listWidget; 17124 ListWidget dirWidget; 17125 17126 FreeEntrySelection filesOfType; 17127 LineEdit directoryHolder; 17128 17129 string currentDirectory_; 17130 FileNameFilter currentNonTabFilter; 17131 FileNameFilter currentFilter; 17132 FileNameFilterSet filterOptions; 17133 17134 void currentDirectory(string s) { 17135 currentDirectory_ = FilePath(s).makeAbsolute(getCurrentWorkingDirectory()).toString(); 17136 directoryHolder.content = currentDirectory_; 17137 } 17138 string currentDirectory() { 17139 return currentDirectory_; 17140 } 17141 17142 private string getUserHomeDir() { 17143 import core.stdc.stdlib; 17144 version(Windows) 17145 return (stringz(getenv("HOMEDRIVE")).borrow ~ stringz(getenv("HOMEPATH")).borrow).idup; 17146 else 17147 return (stringz(getenv("HOME")).borrow).idup; 17148 } 17149 17150 private string expandTilde(string s) { 17151 // FIXME: cannot look up other user dirs 17152 if(s.length == 1 && s == "~") 17153 return getUserHomeDir(); 17154 if(s.length > 1 && s[0] == '~' && s[1] == '/') 17155 return getUserHomeDir() ~ s[1 .. $]; 17156 return s; 17157 } 17158 17159 // FIXME: allow many files to be picked too sometimes 17160 17161 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 17162 this(bool isOpenDialogInsteadOfSave, string prefilledName, string[] filtersInWindowsFormat, string initialDirectory, Window owner = null) { 17163 this.filterOptions = FileNameFilterSet.fromWindowsFileNameFilterDescription(filtersInWindowsFormat); 17164 this.isOpenDialogInsteadOfSave = isOpenDialogInsteadOfSave; 17165 super(owner, 500, 400, "Choose File..."); // owner); 17166 17167 { 17168 auto navbar = new HorizontalLayout(24, this); 17169 auto backButton = new ToolButton(new Action("<", 0, &this.back), navbar); 17170 auto forwardButton = new ToolButton(new Action(">", 0, &this.forward), navbar); 17171 auto upButton = new ToolButton(new Action("^", 0, &this.up), navbar); // hmm with .. in the dir list we don't really need an up button 17172 17173 directoryHolder = new LineEdit(navbar); 17174 17175 directoryHolder.addEventListener(delegate(scope KeyDownEvent kde) { 17176 if(kde.key == Key.Enter || kde.key == Key.PadEnter) { 17177 kde.stopPropagation(); 17178 17179 currentDirectory = directoryHolder.content; 17180 loadFiles(currentDirectory, currentFilter); 17181 17182 lineEdit.focus(); 17183 } 17184 }); 17185 17186 auto refreshButton = new ToolButton(new Action("R", 0, &this.refresh), navbar); // can live without refresh since you can cancel and reopen but still nice. it should be automatic when it can maybe. 17187 17188 /+ 17189 auto newDirectoryButton = new ToolButton(new Action("N"), navbar); 17190 17191 // FIXME: make sure putting `.` in the dir filter goes back to the CWD 17192 // and that ~ goes back to the home dir 17193 // and blanking it goes back to the suggested dir 17194 17195 auto homeButton = new ToolButton(new Action("H"), navbar); 17196 auto cwdButton = new ToolButton(new Action("."), navbar); 17197 auto suggestedDirectoryButton = new ToolButton(new Action("*"), navbar); 17198 +/ 17199 17200 filesOfType = new class FreeEntrySelection { 17201 this() { 17202 string[] opt; 17203 foreach(option; filterOptions.filters) 17204 opt ~= option.toString; 17205 super(opt, navbar); 17206 } 17207 override int flexBasisWidth() { 17208 return scaleWithDpi(150); 17209 } 17210 override int widthStretchiness() { 17211 return 1;//super.widthStretchiness() / 2; 17212 } 17213 }; 17214 filesOfType.setSelection(0); 17215 currentFilter = filterOptions.filters[0]; 17216 currentNonTabFilter = currentFilter; 17217 } 17218 17219 { 17220 auto mainGrid = new GridLayout(4, 1, this); 17221 17222 dirWidget = new ListWidget(mainGrid); 17223 listWidget = new ListWidget(mainGrid); 17224 listWidget.tabStop = false; 17225 dirWidget.tabStop = false; 17226 17227 FileDialogDelegate.PreviewWidget previewWidget = fileDialogDelegate.makePreviewWidget(mainGrid); 17228 17229 mainGrid.setChildPosition(dirWidget, 0, 0, 1, 1); 17230 mainGrid.setChildPosition(listWidget, 1, 0, previewWidget !is null ? 2 : 3, 1); 17231 if(previewWidget) 17232 mainGrid.setChildPosition(previewWidget, 2, 0, 1, 1); 17233 17234 // double click events normally trigger something else but 17235 // here user might be clicking kinda fast and we'd rather just 17236 // keep it 17237 dirWidget.addEventListener((scope DoubleClickEvent dev) { 17238 auto ce = new ChangeEvent!void(dirWidget, () {}); 17239 ce.dispatch(); 17240 lineEdit.focus(); 17241 }); 17242 17243 dirWidget.addEventListener((scope ChangeEvent!void sce) { 17244 string v; 17245 foreach(o; dirWidget.options) 17246 if(o.selected) { 17247 v = o.label; 17248 break; 17249 } 17250 if(v.length) { 17251 if(v == "$HOME") 17252 currentDirectory = getUserHomeDir(); 17253 else if(v == "$PWD") 17254 currentDirectory = "."; 17255 else 17256 currentDirectory = currentDirectory ~ "/" ~ v; 17257 loadFiles(currentDirectory, currentFilter); 17258 } 17259 17260 dirWidget.focusOn = -1; 17261 lineEdit.focus(); 17262 }); 17263 17264 // double click here, on the other hand, selects the file 17265 // and moves on 17266 listWidget.addEventListener((scope DoubleClickEvent dev) { 17267 OK(); 17268 }); 17269 } 17270 17271 lineEdit = new LabeledLineEdit("File name:", TextAlignment.Right, this); 17272 lineEdit.focus(); 17273 lineEdit.addEventListener(delegate(CharEvent event) { 17274 if(event.character == '\t' || event.character == '\n') 17275 event.preventDefault(); 17276 }); 17277 17278 listWidget.addEventListener(EventType.change, () { 17279 foreach(o; listWidget.options) 17280 if(o.selected) 17281 lineEdit.content = o.label; 17282 }); 17283 17284 currentDirectory = initialDirectory is null ? "." : initialDirectory; 17285 17286 auto prefilledPath = FilePath(expandTilde(prefilledName)).makeAbsolute(FilePath(currentDirectory)); 17287 currentDirectory = prefilledPath.directoryName; 17288 prefilledName = prefilledPath.filename; 17289 loadFiles(currentDirectory, currentFilter); 17290 17291 filesOfType.addEventListener(delegate (FreeEntrySelection.SelectionChangedEvent ce) { 17292 currentFilter = FileNameFilter.fromString(ce.stringValue); 17293 currentNonTabFilter = currentFilter; 17294 loadFiles(currentDirectory, currentFilter); 17295 // lineEdit.focus(); // this causes a recursive crash..... 17296 }); 17297 17298 filesOfType.addEventListener(delegate(KeyDownEvent event) { 17299 if(event.key == Key.Enter) { 17300 currentFilter = FileNameFilter.fromString(filesOfType.content); 17301 currentNonTabFilter = currentFilter; 17302 loadFiles(currentDirectory, currentFilter); 17303 event.stopPropagation(); 17304 // FIXME: refocus on the line edit 17305 } 17306 }); 17307 17308 lineEdit.addEventListener((KeyDownEvent event) { 17309 if(event.key == Key.Tab && !event.ctrlKey && !event.shiftKey) { 17310 17311 auto path = FilePath(expandTilde(lineEdit.content)).makeAbsolute(FilePath(currentDirectory)); 17312 currentDirectory = path.directoryName; 17313 auto current = path.filename; 17314 17315 auto newFilter = current; 17316 if(current.length && current[0] != '*' && current[$-1] != '*') 17317 newFilter ~= "*"; 17318 else if(newFilter.length == 0) 17319 newFilter = "*"; 17320 17321 auto newFilterObj = FileNameFilter("Custom filter", [newFilter]); 17322 17323 CommonPrefixInfo commonPrefix = loadFiles(currentDirectory, newFilterObj); 17324 if(commonPrefix.fileCount == 1) { 17325 // exactly one file, let's see what it is 17326 auto specificFile = FilePath(commonPrefix.exactMatch).makeAbsolute(FilePath(currentDirectory)); 17327 if(getFileType(specificFile.toString) == FileType.dir) { 17328 // a directory means we should change to it and keep the old filter 17329 currentDirectory = specificFile.toString(); 17330 lineEdit.content = specificFile.toString() ~ "/"; 17331 loadFiles(currentDirectory, currentFilter); 17332 } else { 17333 // any other file should be selected in the list 17334 currentDirectory = specificFile.directoryName; 17335 current = specificFile.filename; 17336 lineEdit.content = current; 17337 loadFiles(currentDirectory, currentFilter); 17338 } 17339 } else if(commonPrefix.fileCount > 1) { 17340 currentFilter = newFilterObj; 17341 filesOfType.content = currentFilter.toString(); 17342 lineEdit.content = commonPrefix.commonPrefix; 17343 } else { 17344 // if there were no files, we don't really want to change the filter.. 17345 //sdpyPrintDebugString("no files"); 17346 } 17347 17348 // FIXME: if that is a directory, add the slash? or even go inside? 17349 17350 event.preventDefault(); 17351 } 17352 else if(event.key == Key.Left && event.altKey) { 17353 this.back(); 17354 event.preventDefault(); 17355 } 17356 else if(event.key == Key.Right && event.altKey) { 17357 this.forward(); 17358 event.preventDefault(); 17359 } 17360 }); 17361 17362 17363 lineEdit.content = prefilledName; 17364 17365 auto hl = new HorizontalLayout(60, this); 17366 auto cancelButton = new Button("Cancel", hl); 17367 auto okButton = new Button(isOpenDialogInsteadOfSave ? "Open" : "Save"/*"OK"*/, hl); 17368 17369 cancelButton.addEventListener(EventType.triggered, &Cancel); 17370 okButton.addEventListener(EventType.triggered, &OK); 17371 17372 this.addEventListener((KeyDownEvent event) { 17373 if(event.key == Key.Enter || event.key == Key.PadEnter) { 17374 event.preventDefault(); 17375 OK(); 17376 } 17377 else if(event.key == Key.Escape) 17378 Cancel(); 17379 else if(event.key == Key.F5) 17380 refresh(); 17381 else if(event.key == Key.Up && event.altKey) 17382 up(); // ditto 17383 else if(event.key == Key.Left && event.altKey) 17384 back(); // FIXME: it sends the key to the line edit too 17385 else if(event.key == Key.Right && event.altKey) 17386 forward(); // ditto 17387 else if(event.key == Key.Up) 17388 listWidget.setSelection(listWidget.getSelection() - 1); 17389 else if(event.key == Key.Down) 17390 listWidget.setSelection(listWidget.getSelection() + 1); 17391 }); 17392 17393 // FIXME: set the list view's focusOn to -1 on most interactions so it doesn't keep a thing highlighted 17394 // FIXME: button to create new directory 17395 // FIXME: show dirs in the files list too? idk. 17396 17397 // FIXME: support ~ as alias for home in the input 17398 // FIXME: tab complete ought to be able to change+complete dir too 17399 } 17400 17401 override void OK() { 17402 if(lineEdit.content.length) { 17403 auto c = expandTilde(lineEdit.content); 17404 17405 FilePath accepted = FilePath(c).makeAbsolute(FilePath(currentDirectory)); 17406 17407 auto ft = getFileType(accepted.toString); 17408 17409 if(ft == FileType.error && isOpenDialogInsteadOfSave) { 17410 // FIXME: tell the user why 17411 messageBox("Cannot open file: " ~ accepted.toString ~ "\nTry another or cancel."); 17412 lineEdit.focus(); 17413 return; 17414 17415 } 17416 17417 // FIXME: symlinks to dirs should prolly also get this behavior 17418 if(ft == FileType.dir) { 17419 currentDirectory = accepted.toString; 17420 17421 currentFilter = currentNonTabFilter; 17422 filesOfType.content = currentFilter.toString(); 17423 17424 loadFiles(currentDirectory, currentFilter); 17425 lineEdit.content = ""; 17426 17427 lineEdit.focus(); 17428 17429 return; 17430 } 17431 17432 if(onOK) 17433 onOK(accepted.toString); 17434 } 17435 close(); 17436 } 17437 17438 override void Cancel() { 17439 if(onCancel) 17440 onCancel(); 17441 close(); 17442 } 17443 } 17444 17445 private enum FileType { 17446 error, 17447 dir, 17448 other 17449 } 17450 17451 private FileType getFileType(string name) { 17452 version(Windows) { 17453 auto ws = WCharzBuffer(name); 17454 auto ret = GetFileAttributesW(ws.ptr); 17455 if(ret == INVALID_FILE_ATTRIBUTES) 17456 return FileType.error; 17457 return ((ret & FILE_ATTRIBUTE_DIRECTORY) != 0) ? FileType.dir : FileType.other; 17458 } else version(Posix) { 17459 import core.sys.posix.sys.stat; 17460 stat_t buf; 17461 auto ret = stat((name ~ '\0').ptr, &buf); 17462 if(ret == -1) 17463 return FileType.error; 17464 return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other; 17465 } else assert(0, "Not implemented"); 17466 } 17467 17468 /* 17469 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 17470 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 17471 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 17472 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 17473 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 17474 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 17475 http://www.sbin.org/doc/Xlib/chapt_03.html 17476 17477 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 17478 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 17479 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 17480 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 17481 */ 17482 17483 17484 // These are all for setMenuAndToolbarFromAnnotatedCode 17485 /// This item in the menu will be preceded by a separator line 17486 /// Group: generating_from_code 17487 struct separator {} 17488 deprecated("It was misspelled, use separator instead") alias seperator = separator; 17489 /// Program-wide keyboard shortcut to trigger the action 17490 /// Group: generating_from_code 17491 struct accelerator { string keyString; } // FIXME: allow multiple aliases here 17492 /// tells which menu the action will be on 17493 /// Group: generating_from_code 17494 struct menu { string name; } 17495 /// Describes which toolbar section the action appears on 17496 /// Group: generating_from_code 17497 struct toolbar { string groupName; } 17498 /// 17499 /// Group: generating_from_code 17500 struct icon { ushort id; } 17501 /// 17502 /// Group: generating_from_code 17503 struct label { string label; } 17504 /// 17505 /// Group: generating_from_code 17506 struct hotkey { dchar ch; } 17507 /// 17508 /// Group: generating_from_code 17509 struct tip { string tip; } 17510 /// 17511 /// Group: generating_from_code 17512 enum context_menu = menu.init; 17513 17514 17515 /++ 17516 Observes and allows inspection of an object via automatic gui 17517 +/ 17518 /// Group: generating_from_code 17519 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 17520 return new ObjectInspectionWindowImpl!(T)(t); 17521 } 17522 17523 class ObjectInspectionWindow : Window { 17524 this(int a, int b, string c) { 17525 super(a, b, c); 17526 } 17527 17528 abstract void readUpdatesFromObject(); 17529 } 17530 17531 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 17532 T t; 17533 this(T t) { 17534 this.t = t; 17535 17536 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 17537 17538 foreach(memberName; __traits(derivedMembers, T)) {{ 17539 alias member = I!(__traits(getMember, t, memberName))[0]; 17540 alias type = typeof(member); 17541 static if(is(type == int)) { 17542 auto le = new LabeledLineEdit(memberName ~ ": ", this); 17543 //le.addEventListener("char", (Event ev) { 17544 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 17545 //ev.preventDefault(); 17546 //}); 17547 le.addEventListener(EventType.change, (Event ev) { 17548 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 17549 }); 17550 17551 updateMemberDelegates[memberName] = () { 17552 le.content = toInternal!string(__traits(getMember, t, memberName)); 17553 }; 17554 } 17555 }} 17556 } 17557 17558 void delegate()[string] updateMemberDelegates; 17559 17560 override void readUpdatesFromObject() { 17561 foreach(k, v; updateMemberDelegates) 17562 v(); 17563 } 17564 } 17565 17566 /++ 17567 Creates a dialog based on a data structure. 17568 17569 --- 17570 dialog(window, (YourStructure value) { 17571 // the user filled in the struct and clicked OK, 17572 // you can check the members now 17573 }); 17574 --- 17575 17576 Params: 17577 initialData = the initial value to show in the dialog. It will not modify this unless 17578 it is a class then it might, no promises. 17579 17580 History: 17581 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 17582 17583 The overloads with `parent` were added September 29, 2024. The ones without it are likely to 17584 be deprecated soon. 17585 +/ 17586 /// Group: generating_from_code 17587 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 17588 dialog(null, T.init, onOK, onCancel, title); 17589 } 17590 /// ditto 17591 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 17592 dialog(null, T.init, onOK, onCancel, title); 17593 } 17594 /// ditto 17595 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 17596 dialog(parent, T.init, onOK, onCancel, title); 17597 } 17598 /// ditto 17599 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 17600 dialog(parent, initialData, onOK, onCancel, title); 17601 } 17602 /// ditto 17603 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 17604 auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title); 17605 dg.show(); 17606 } 17607 17608 private static template I(T...) { alias I = T; } 17609 17610 17611 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 17612 if(name == "id") 17613 return allLowerCase ? name : "ID"; 17614 17615 char[160] buffer; 17616 int bufferIndex = 0; 17617 bool shouldCap = true; 17618 bool shouldSpace; 17619 bool lastWasCap; 17620 foreach(idx, char ch; name) { 17621 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 17622 17623 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 17624 if(lastWasCap) { 17625 // two caps in a row, don't change. Prolly acronym. 17626 } else { 17627 if(idx) 17628 shouldSpace = true; // new word, add space 17629 } 17630 17631 lastWasCap = true; 17632 } else { 17633 lastWasCap = false; 17634 } 17635 17636 if(shouldSpace) { 17637 buffer[bufferIndex++] = space; 17638 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 17639 shouldSpace = false; 17640 } 17641 if(shouldCap) { 17642 if(ch >= 'a' && ch <= 'z') 17643 ch -= 32; 17644 shouldCap = false; 17645 } 17646 if(allLowerCase && ch >= 'A' && ch <= 'Z') 17647 ch += 32; 17648 buffer[bufferIndex++] = ch; 17649 } 17650 return buffer[0 .. bufferIndex].idup; 17651 } 17652 17653 /++ 17654 This is the implementation for [dialog]. None of its details are guaranteed stable and may change at any time; the stable interface is just the [dialog] function at this time. 17655 +/ 17656 class AutomaticDialog(T) : Dialog { 17657 T t; 17658 17659 void delegate(T) onOK; 17660 void delegate() onCancel; 17661 17662 override int paddingTop() { return defaultLineHeight; } 17663 override int paddingBottom() { return defaultLineHeight; } 17664 override int paddingRight() { return defaultLineHeight; } 17665 override int paddingLeft() { return defaultLineHeight; } 17666 17667 this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 17668 assert(onOK !is null); 17669 17670 t = initialData; 17671 17672 static if(is(T == class)) { 17673 if(t is null) 17674 t = new T(); 17675 } 17676 this.onOK = onOK; 17677 this.onCancel = onCancel; 17678 super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title); 17679 17680 static if(is(T == class)) 17681 this.addDataControllerWidget(t); 17682 else 17683 this.addDataControllerWidget(&t); 17684 17685 auto hl = new HorizontalLayout(this); 17686 auto stretch = new HorizontalSpacer(hl); // to right align 17687 auto ok = new CommandButton("OK", hl); 17688 auto cancel = new CommandButton("Cancel", hl); 17689 ok.addEventListener(EventType.triggered, &OK); 17690 cancel.addEventListener(EventType.triggered, &Cancel); 17691 17692 this.addEventListener((KeyDownEvent ev) { 17693 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 17694 ok.focus(); 17695 OK(); 17696 ev.preventDefault(); 17697 } 17698 if(ev.key == Key.Escape) { 17699 Cancel(); 17700 ev.preventDefault(); 17701 } 17702 }); 17703 17704 this.addEventListener((scope ClosedEvent ce) { 17705 if(onCancel) 17706 onCancel(); 17707 }); 17708 17709 //this.children[0].focus(); 17710 } 17711 17712 override void OK() { 17713 onOK(t); 17714 close(); 17715 } 17716 17717 override void Cancel() { 17718 if(onCancel) 17719 onCancel(); 17720 close(); 17721 } 17722 } 17723 17724 private template baseClassCount(Class) { 17725 private int helper() { 17726 int count = 0; 17727 static if(is(Class bases == super)) { 17728 foreach(base; bases) 17729 static if(is(base == class)) 17730 count += 1 + baseClassCount!base; 17731 } 17732 return count; 17733 } 17734 17735 enum int baseClassCount = helper(); 17736 } 17737 17738 private long stringToLong(string s) { 17739 long ret; 17740 if(s.length == 0) 17741 return ret; 17742 bool negative = s[0] == '-'; 17743 if(negative) 17744 s = s[1 .. $]; 17745 foreach(ch; s) { 17746 if(ch >= '0' && ch <= '9') { 17747 ret *= 10; 17748 ret += ch - '0'; 17749 } 17750 } 17751 if(negative) 17752 ret = -ret; 17753 return ret; 17754 } 17755 17756 17757 interface ReflectableProperties { 17758 /++ 17759 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 17760 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 17761 json in the current implementation. 17762 17763 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 17764 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 17765 as of the June 2, 2021 release. 17766 17767 History: 17768 Added June 2, 2021. 17769 17770 See_Also: [getPropertyAsString], [setPropertyFromString] 17771 +/ 17772 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 17773 /++ 17774 Requests a property to be delivered to you as a string, through your `sink` delegate. 17775 17776 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 17777 be interpreted as json, otherwise, it is just a plain string. 17778 17779 The sink should always be called exactly once for each call (it is basically a return value, but it might 17780 use a local buffer it maintains instead of allocating a return value). 17781 17782 History: 17783 Added June 2, 2021. 17784 17785 See_Also: [getPropertiesList], [setPropertyFromString] 17786 +/ 17787 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 17788 /++ 17789 Sets the given property, if it exists, to the given value, if possible. If `strIsJson` is true, it will json decode (if the implementation wants to) then apply the value, otherwise it will treat it as a plain string. 17790 17791 History: 17792 Added June 2, 2021. 17793 17794 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 17795 +/ 17796 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 17797 17798 /// [setPropertyFromString] possible return values 17799 enum SetPropertyResult { 17800 success = 0, /// the property has been successfully set to the request value 17801 notPermitted = -1, /// the property exists but it cannot be changed at this time 17802 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 17803 noSuchProperty = -3, /// there is no property by that name 17804 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 17805 invalidValue = -5, /// the string is in the correct format, but the specific given value could not be used (for example, because it was out of bounds) 17806 } 17807 17808 /++ 17809 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 17810 17811 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 17812 17813 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 17814 rarely need to use these building blocks directly. 17815 +/ 17816 mixin template RegisterSetters() { 17817 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 17818 switch(name) { 17819 foreach(memberName; __traits(derivedMembers, typeof(this))) { 17820 case memberName: 17821 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 17822 if(value != "true" && value != "false") 17823 return SetPropertyResult.wrongFormat; 17824 __traits(getMember, this, memberName) = value == "true" ? true : false; 17825 return SetPropertyResult.success; 17826 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 17827 import core.stdc.stdlib; 17828 char[128] zero = 0; 17829 if(buffer.length + 1 >= zero.length) 17830 return SetPropertyResult.wrongFormat; 17831 zero[0 .. buffer.length] = buffer[]; 17832 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 17833 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 17834 import core.stdc.stdlib; 17835 char[128] zero = 0; 17836 if(buffer.length + 1 >= zero.length) 17837 return SetPropertyResult.wrongFormat; 17838 zero[0 .. buffer.length] = buffer[]; 17839 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 17840 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 17841 __traits(getMember, this, memberName) = value.idup; 17842 } else { 17843 return SetPropertyResult.notImplemented; 17844 } 17845 17846 } 17847 default: 17848 return super.setPropertyFromString(name, value, valueIsJson); 17849 } 17850 } 17851 } 17852 17853 /++ 17854 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 17855 17856 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 17857 17858 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 17859 rarely need to use these building blocks directly. 17860 +/ 17861 mixin template RegisterGetters() { 17862 override void getPropertiesList(scope void delegate(string name) sink) const { 17863 super.getPropertiesList(sink); 17864 17865 foreach(memberName; __traits(derivedMembers, typeof(this))) { 17866 sink(memberName); 17867 } 17868 } 17869 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 17870 switch(name) { 17871 foreach(memberName; __traits(derivedMembers, typeof(this))) { 17872 case memberName: 17873 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 17874 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 17875 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 17876 import core.stdc.stdio; 17877 char[32] buffer; 17878 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 17879 sink(name, buffer[0 .. len], true); 17880 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 17881 import core.stdc.stdio; 17882 char[32] buffer; 17883 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 17884 sink(name, buffer[0 .. len], true); 17885 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 17886 sink(name, __traits(getMember, this, memberName), false); 17887 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 17888 } else { 17889 sink(name, null, true); 17890 } 17891 17892 return; 17893 } 17894 default: 17895 return super.getPropertyAsString(name, sink); 17896 } 17897 } 17898 } 17899 } 17900 17901 private struct Stack(T) { 17902 this(int maxSize) { 17903 internalLength = 0; 17904 arr = initialBuffer[]; 17905 } 17906 17907 ///. 17908 void push(T t) { 17909 if(internalLength >= arr.length) { 17910 auto oldarr = arr; 17911 if(arr.length < 4096) 17912 arr = new T[arr.length * 2]; 17913 else 17914 arr = new T[arr.length + 4096]; 17915 arr[0 .. oldarr.length] = oldarr[]; 17916 } 17917 17918 arr[internalLength] = t; 17919 internalLength++; 17920 } 17921 17922 ///. 17923 T pop() { 17924 assert(internalLength); 17925 internalLength--; 17926 return arr[internalLength]; 17927 } 17928 17929 ///. 17930 T peek() { 17931 assert(internalLength); 17932 return arr[internalLength - 1]; 17933 } 17934 17935 ///. 17936 @property bool empty() { 17937 return internalLength ? false : true; 17938 } 17939 17940 ///. 17941 private T[] arr; 17942 private size_t internalLength; 17943 private T[64] initialBuffer; 17944 // the static array is allocated with this object, so if we have a small stack (which we prolly do; dom trees usually aren't insanely deep), 17945 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 17946 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 17947 } 17948 17949 /// This is the lazy range that walks the tree for you. It tries to go in the lexical order of the source: node, then children from first to last, each recursively. 17950 private struct WidgetStream { 17951 17952 ///. 17953 @property Widget front() { 17954 return current.widget; 17955 } 17956 17957 /// Use Widget.tree instead. 17958 this(Widget start) { 17959 current.widget = start; 17960 current.childPosition = -1; 17961 isEmpty = false; 17962 stack = typeof(stack)(0); 17963 } 17964 17965 /* 17966 Handle it 17967 handle its children 17968 17969 */ 17970 17971 ///. 17972 void popFront() { 17973 more: 17974 if(isEmpty) return; 17975 17976 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 17977 17978 current.childPosition++; 17979 if(current.childPosition >= current.widget.children.length) { 17980 if(stack.empty()) 17981 isEmpty = true; 17982 else { 17983 current = stack.pop(); 17984 goto more; 17985 } 17986 } else { 17987 stack.push(current); 17988 current.widget = current.widget.children[current.childPosition]; 17989 current.childPosition = -1; 17990 } 17991 } 17992 17993 ///. 17994 @property bool empty() { 17995 return isEmpty; 17996 } 17997 17998 private: 17999 18000 struct Current { 18001 Widget widget; 18002 int childPosition; 18003 } 18004 18005 Current current; 18006 18007 Stack!(Current) stack; 18008 18009 bool isEmpty; 18010 } 18011 18012 18013 /+ 18014 18015 I could fix up the hierarchy kinda like this 18016 18017 class Widget { 18018 Widget[] children() { return null; } 18019 } 18020 interface WidgetContainer { 18021 Widget asWidget(); 18022 void addChild(Widget w); 18023 18024 // alias asWidget this; // but meh 18025 } 18026 18027 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 18028 18029 class Layout : Widget, WidgetContainer {} 18030 18031 class Window : WidgetContainer {} 18032 18033 18034 All constructors that previously took Widgets should now take WidgetContainers instead 18035 18036 18037 18038 But I'm kinda meh toward it, im not sure this is a real problem even though there are some addChild things that throw "plz don't". 18039 +/ 18040 18041 /+ 18042 LAYOUTS 2.0 18043 18044 can just be assigned as a function. assigning a new one will cause it to be immediately called. 18045 18046 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 18047 18048 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 18049 18050 and even Paint can just use computedStyle... 18051 18052 background color 18053 font 18054 border color and style 18055 18056 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 18057 please note that many widgets and in some modes will completely ignore properties as they will. 18058 they are just hints you set, not promises. 18059 18060 18061 18062 18063 18064 So generally the existing virtual functions are just the default for the class. But individual objects 18065 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 18066 +/ 18067 18068 /++ 18069 Structure to represent a collection of background hints. New features can be added here, so make sure you use the provided constructors and factories for maximum compatibility. 18070 18071 History: 18072 Added May 24, 2021. 18073 +/ 18074 struct WidgetBackground { 18075 /++ 18076 A background with the given solid color. 18077 +/ 18078 this(Color color) { 18079 this.color = color; 18080 } 18081 18082 this(WidgetBackground bg) { 18083 this = bg; 18084 } 18085 18086 /++ 18087 Creates a widget from the string. 18088 18089 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 18090 +/ 18091 static WidgetBackground fromString(string s) { 18092 return WidgetBackground(Color.fromString(s)); 18093 } 18094 18095 /++ 18096 The background is not necessarily a solid color, but you can always specify a color as a fallback. 18097 18098 History: 18099 Made `public` on December 18, 2022 (dub v10.10). 18100 +/ 18101 Color color; 18102 } 18103 18104 /++ 18105 Interface to a custom visual theme which is able to access and use style hint properties, draw stylistic elements, and even completely override existing class' paint methods (though I'd note that can be a lot harder than it may seem due to the various little details of state you need to reflect visually, so that should be your last result!) 18106 18107 Please note that this is only guaranteed to be used by custom widgets, and custom widgets are generally inferior to system widgets. Layout properties may be used by sytstem widgets though. 18108 18109 You should not inherit from this directly, but instead use [VisualTheme]. 18110 18111 History: 18112 Added May 8, 2021 18113 +/ 18114 abstract class BaseVisualTheme { 18115 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 18116 abstract void doPaint(Widget widget, WidgetPainter painter); 18117 18118 /+ 18119 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 18120 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 18121 +/ 18122 18123 /++ 18124 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 18125 where the interpretation of the string varies for each property and may include things like measurement units. 18126 +/ 18127 abstract string getPropertyString(Widget widget, string propertyName); 18128 18129 /++ 18130 Default background color of the window. Widgets also use this to simulate transparency. 18131 18132 Probably some shade of grey. 18133 +/ 18134 abstract Color windowBackgroundColor(); 18135 abstract Color widgetBackgroundColor(); 18136 abstract Color foregroundColor(); 18137 abstract Color lightAccentColor(); 18138 abstract Color darkAccentColor(); 18139 18140 /++ 18141 Colors used to indicate active selections in lists and text boxes, etc. 18142 +/ 18143 abstract Color selectionForegroundColor(); 18144 /// ditto 18145 abstract Color selectionBackgroundColor(); 18146 18147 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 18148 18149 /++ 18150 If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally. 18151 +/ 18152 abstract OperatingSystemFont defaultFont(int dpi); 18153 18154 private OperatingSystemFont[int] defaultFontCache_; 18155 private OperatingSystemFont defaultFontCached(int dpi) { 18156 if(dpi !in defaultFontCache_) { 18157 // FIXME: set this to false if X disconnect or if visual theme changes 18158 defaultFontCache_[dpi] = defaultFont(dpi); 18159 } 18160 return defaultFontCache_[dpi]; 18161 } 18162 } 18163 18164 /+ 18165 A widget should have: 18166 classList 18167 dataset 18168 attributes 18169 computedStyles 18170 state (persistent) 18171 dynamic state (focused, hover, etc) 18172 +/ 18173 18174 // visualTheme.computedStyle(this).paddingLeft 18175 18176 18177 /++ 18178 This is your entry point to create your own visual theme for custom widgets. 18179 18180 You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods. 18181 18182 Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating. 18183 +/ 18184 abstract class VisualTheme(CRTP) : BaseVisualTheme { 18185 override string getPropertyString(Widget widget, string propertyName) { 18186 return null; 18187 } 18188 18189 /+ 18190 mixin StyleOverride!Widget 18191 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 18192 w.useStyleProperties(dg); 18193 } 18194 +/ 18195 18196 final override void doPaint(Widget widget, WidgetPainter painter) { 18197 auto derived = cast(CRTP) cast(void*) this; 18198 18199 scope void delegate(Widget, WidgetPainter) bestMatch; 18200 int bestMatchScore; 18201 18202 static if(__traits(hasMember, CRTP, "paint")) 18203 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 18204 static if(is(typeof(overload) Params == __parameters)) { 18205 static assert(Params.length == 2); 18206 static assert(is(Params[0] : Widget)); 18207 static assert(is(Params[1] == WidgetPainter)); 18208 static assert(is(typeof(&__traits(child, derived, overload)) == delegate), "Found a paint method that doesn't appear to be a delegate. One cause of this can be your dmd being too old, make sure it is version 2.094 or newer to use this feature."); // , __traits(getLocation, overload).stringof ~ " is not a delegate " ~ typeof(&__traits(child, derived, overload)).stringof); 18209 18210 alias type = Params[0]; 18211 if(cast(type) widget) { 18212 auto score = baseClassCount!type; 18213 18214 if(score > bestMatchScore) { 18215 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 18216 bestMatchScore = score; 18217 } 18218 } 18219 } else static assert(0, "paint should be a method."); 18220 } 18221 18222 if(bestMatch) 18223 bestMatch(widget, painter); 18224 else 18225 widget.paint(painter); 18226 } 18227 18228 deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; } 18229 18230 // I have to put these here even though I kinda don't want to since dmd regressed on detecting unimplemented interface functions through abstract classes 18231 // mixin Beautiful95Theme; 18232 mixin DefaultLightTheme; 18233 18234 private static struct Cached { 18235 // i prolly want to do this 18236 } 18237 } 18238 18239 /// ditto 18240 mixin template Beautiful95Theme() { 18241 override Color windowBackgroundColor() { return Color(212, 212, 212); } 18242 override Color widgetBackgroundColor() { return Color.white; } 18243 override Color foregroundColor() { return Color.black; } 18244 override Color darkAccentColor() { return Color(172, 172, 172); } 18245 override Color lightAccentColor() { return Color(223, 223, 223); } 18246 override Color selectionForegroundColor() { return Color.white; } 18247 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 18248 override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr 18249 } 18250 18251 /// ditto 18252 mixin template DefaultLightTheme() { 18253 override Color windowBackgroundColor() { return Color(232, 232, 232); } 18254 override Color widgetBackgroundColor() { return Color.white; } 18255 override Color foregroundColor() { return Color.black; } 18256 override Color darkAccentColor() { return Color(172, 172, 172); } 18257 override Color lightAccentColor() { return Color(223, 223, 223); } 18258 override Color selectionForegroundColor() { return Color.white; } 18259 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 18260 override OperatingSystemFont defaultFont(int dpi) { 18261 version(Windows) 18262 return new OperatingSystemFont("Segoe UI"); 18263 else static if(UsingSimpledisplayCocoa) { 18264 return (new OperatingSystemFont()).loadDefault; 18265 } else { 18266 // FIXME: undo xft's scaling so we don't end up double scaled 18267 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 18268 } 18269 } 18270 } 18271 18272 /// ditto 18273 mixin template DefaultDarkTheme() { 18274 override Color windowBackgroundColor() { return Color(64, 64, 64); } 18275 override Color widgetBackgroundColor() { return Color.black; } 18276 override Color foregroundColor() { return Color.white; } 18277 override Color darkAccentColor() { return Color(20, 20, 20); } 18278 override Color lightAccentColor() { return Color(80, 80, 80); } 18279 override Color selectionForegroundColor() { return Color.white; } 18280 override Color selectionBackgroundColor() { return Color(128, 0, 128); } 18281 override OperatingSystemFont defaultFont(int dpi) { 18282 version(Windows) 18283 return new OperatingSystemFont("Segoe UI", 12); 18284 else static if(UsingSimpledisplayCocoa) { 18285 return (new OperatingSystemFont()).loadDefault; 18286 } else { 18287 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 18288 } 18289 } 18290 } 18291 18292 /// ditto 18293 alias DefaultTheme = DefaultLightTheme; 18294 18295 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 18296 /+ 18297 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 18298 Color windowBackgroundColor() { return Color(242, 242, 242); } 18299 Color darkAccentColor() { return windowBackgroundColor; } 18300 Color lightAccentColor() { return windowBackgroundColor; } 18301 +/ 18302 } 18303 18304 /++ 18305 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 18306 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 18307 18308 History: 18309 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 18310 18311 Made `final` on January 3, 2025 18312 +/ 18313 final class StateChanged(alias field) : Event { 18314 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 18315 override bool cancelable() const { return false; } 18316 this(Widget target, typeof(field) newValue) { 18317 this.newValue = newValue; 18318 super(EventString, target); 18319 } 18320 18321 typeof(field) newValue; 18322 } 18323 18324 /++ 18325 Convenience function to add a `triggered` event listener. 18326 18327 Its implementation is simply `w.addEventListener("triggered", dg);` 18328 18329 History: 18330 Added November 27, 2021 (dub v10.4) 18331 +/ 18332 void addWhenTriggered(Widget w, void delegate() dg) { 18333 w.addEventListener("triggered", dg); 18334 } 18335 18336 /++ 18337 Observable varables can be added to widgets and when they are changed, it fires 18338 off a [StateChanged] event so you can react to it. 18339 18340 It is implemented as a getter and setter property, along with another helper you 18341 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 18342 event through the usual means. Just give the name of the variable. See [StateChanged] for an 18343 example. 18344 18345 History: 18346 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 18347 +/ 18348 mixin template Observable(T, string name) { 18349 private T backing; 18350 18351 mixin(q{ 18352 void } ~ name ~ q{_changed (void delegate(T) dg) { 18353 this.addEventListener((StateChanged!this_thing ev) { 18354 dg(ev.newValue); 18355 }); 18356 } 18357 18358 @property T } ~ name ~ q{ () { 18359 return backing; 18360 } 18361 18362 @property void } ~ name ~ q{ (T t) { 18363 backing = t; 18364 auto event = new StateChanged!this_thing(this, t); 18365 event.dispatch(); 18366 } 18367 }); 18368 18369 mixin("private alias this_thing = " ~ name ~ ";"); 18370 } 18371 18372 18373 private bool startsWith(string test, string thing) { 18374 if(test.length < thing.length) 18375 return false; 18376 return test[0 .. thing.length] == thing; 18377 } 18378 18379 private bool endsWith(string test, string thing) { 18380 if(test.length < thing.length) 18381 return false; 18382 return test[$ - thing.length .. $] == thing; 18383 } 18384 18385 /++ 18386 Context menus can have `@hotkey`, `@label`, `@tip`, `@separator`, and `@icon` 18387 18388 Note they can NOT have accelerators or toolbars; those annotations will be ignored. 18389 18390 Mark the functions callable from it with `@context_menu { ... }` Presence of other `@menu(...)` annotations will exclude it from the context menu at this time. 18391 18392 See_Also: 18393 [Widget.setMenuAndToolbarFromAnnotatedCode] 18394 +/ 18395 Menu createContextMenuFromAnnotatedCode(TWidget)(TWidget w) if(is(TWidget : Widget)) { 18396 return createContextMenuFromAnnotatedCode(w, w); 18397 } 18398 18399 /// ditto 18400 Menu createContextMenuFromAnnotatedCode(T)(Widget w, ref T t) if(!is(T == class) && !is(T == interface)) { 18401 return createContextMenuFromAnnotatedCode_internal(w, t); 18402 } 18403 /// ditto 18404 Menu createContextMenuFromAnnotatedCode(T)(Widget w, T t) if(is(T == class) || is(T == interface)) { 18405 return createContextMenuFromAnnotatedCode_internal(w, t); 18406 } 18407 Menu createContextMenuFromAnnotatedCode_internal(T)(Widget w, ref T t) { 18408 Menu ret = new Menu("", w); 18409 18410 foreach(memberName; __traits(derivedMembers, T)) { 18411 static if(memberName != "this") 18412 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 18413 .menu menu; 18414 bool separator; 18415 .hotkey hotkey; 18416 .icon icon; 18417 string label; 18418 string tip; 18419 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 18420 static if(is(typeof(attr) == .menu)) 18421 menu = attr; 18422 else static if(is(attr == .separator)) 18423 separator = true; 18424 else static if(is(typeof(attr) == .hotkey)) 18425 hotkey = attr; 18426 else static if(is(typeof(attr) == .icon)) 18427 icon = attr; 18428 else static if(is(typeof(attr) == .label)) 18429 label = attr.label; 18430 else static if(is(typeof(attr) == .tip)) 18431 tip = attr.tip; 18432 } 18433 18434 if(menu is .menu.init) { 18435 ushort correctIcon = icon.id; // FIXME 18436 if(label.length == 0) 18437 label = memberName.toMenuLabel; 18438 18439 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(w.parentWindow, &__traits(getMember, t, memberName)); 18440 18441 auto action = new Action(label, correctIcon, handler); 18442 18443 if(separator) 18444 ret.addSeparator(); 18445 ret.addItem(new MenuItem(action)); 18446 } 18447 } 18448 } 18449 18450 return ret; 18451 } 18452 18453 // still do layout delegation 18454 // and... split off Window from Widget. 18455 18456 version(minigui_screenshots) 18457 struct Screenshot { 18458 string name; 18459 } 18460 18461 version(minigui_screenshots) 18462 static if(__VERSION__ > 2092) 18463 mixin(q{ 18464 shared static this() { 18465 import core.runtime; 18466 18467 static UnitTestResult screenshotMagic() { 18468 string name; 18469 18470 import arsd.png; 18471 18472 auto results = new Window(); 18473 auto button = new Button("do it", results); 18474 18475 Window.newWindowCreated = delegate(Window w) { 18476 Timer timer; 18477 timer = new Timer(250, { 18478 auto img = w.win.takeScreenshot(); 18479 timer.destroy(); 18480 18481 version(Windows) 18482 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 18483 else 18484 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 18485 18486 w.close(); 18487 }); 18488 }; 18489 18490 button.addWhenTriggered( { 18491 18492 foreach(test; __traits(getUnitTests, mixin("arsd.minigui"))) { 18493 name = null; 18494 static foreach(attr; __traits(getAttributes, test)) { 18495 static if(is(typeof(attr) == Screenshot)) 18496 name = attr.name; 18497 } 18498 if(name.length) { 18499 test(); 18500 } 18501 } 18502 18503 }); 18504 18505 results.loop(); 18506 18507 return UnitTestResult(0, 0, false, false); 18508 } 18509 18510 18511 Runtime.extendedModuleUnitTester = &screenshotMagic; 18512 } 18513 }); 18514 version(minigui_screenshots) { 18515 version(unittest) 18516 void main() {} 18517 else static assert(0, "dont forget the -unittest flag to dmd"); 18518 } 18519 18520 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 18521 // FIXME: make multiple accelerators disambiguate based ona rgs 18522 // FIXME: MainWindow ctor should have same arg order as Window 18523 // FIXME: mainwindow ctor w/ client area size instead of total size. 18524 // Push on/off button (basically an alternate display of a checkbox) -- BS_PUSHLIKE and maybe BS_TEXT (BS_TOP moves it). see also BS_FLAT. 18525 // FIXME: tri-state checkbox 18526 // FIXME: subordinate controls grouping...