1 /+ 2 BreakpointSplitter 3 - if not all widgets fit, it collapses to tabs 4 - if they do, you get a splitter 5 - you set priority to display things first and optional breakpoint (otherwise it uses flex basis and min width) 6 +/ 7 8 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx 9 10 // if doing nested menus, make sure the straight line from where it pops up to any destination on the new popup is not going to disappear the menu until at least a delay 11 12 // me@arsd:~/.kde/share/config$ vim kdeglobals 13 14 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too. 15 16 // https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/ 17 18 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever. 19 20 // responsive minigui, menu search, and file open with a preview hook on the side. 21 22 // FIXME: add menu checkbox and menu icon eventually 23 24 // FIXME: checkbox menus and submenus and stuff 25 26 // FOXME: look at Windows rebar control too 27 28 /* 29 30 im tempted to add some css kind of thing to minigui. i've not done in the past cuz i have a lot of virtual functins i use but i think i have an evil plan 31 32 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 33 */ 34 35 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places 36 37 // FIXME: text label must be copyable to the clipboard, at least as a full chunk. 38 39 // FIXME: opt-in file picker widget with image support 40 41 // FIXME: number widget 42 43 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm 44 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles 45 46 // osx style menu search. 47 48 // would be cool for a scroll bar to have marking capabilities 49 // kinda like vim's marks just on clicks etc and visual representation 50 // generically. may be cool to add an up arrow to the bottom too 51 // 52 // leave a shadow of where you last were for going back easily 53 54 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 55 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 56 // the window. 57 58 // so what about context menus? 59 60 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 61 62 // FIXME: make the scroll thing go to bottom when the content changes. 63 64 // add a knob slider view... you click and go up and down so basically same as a vertical slider, just presented as a round image 65 66 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 67 68 69 // FIXME: add a command search thingy built in and implement tip. 70 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 71 72 // On Windows: 73 // FIXME: various labels look broken in high contrast mode 74 // FIXME: changing themes while the program is upen doesn't trigger a redraw 75 76 // add note about manifest to documentation. also icons. 77 78 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 79 // FIXME: clear the corner of scrollbars if they pop up 80 81 // minigui needs to have a stdout redirection for gui mode on windows writeln 82 83 // I kinda wanna do state reacting. sort of. idk tho 84 85 // need a viewer widget that works like a web page - arrows scroll down consistently 86 87 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 88 89 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 90 // and help info about menu items. 91 // and search in menus? 92 93 // FIXME: a scroll area event signaling when a thing comes into view might be good 94 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 95 96 // FIXME: unify Windows style line endings 97 98 /* 99 TODO: 100 101 pie menu 102 103 class Form with submit behavior -- see AutomaticDialog 104 105 disabled widgets and menu items 106 107 event cleanup 108 tooltips. 109 api improvements 110 111 margins are kinda broken, they don't collapse like they should. at least. 112 113 a table form btw would be a horizontal layout of vertical layouts holding each column 114 that would give the same width things 115 */ 116 117 /* 118 119 1(15:19:48) NotSpooky: Menus, text entry, label, notebook, box, frame, file dialogs and layout (this one is very useful because I can draw lines between its child widgets 120 */ 121 122 /++ 123 minigui is a smallish GUI widget library, aiming to be on par with at least 124 HTML4 forms and a few other expected gui components. It uses native controls 125 on Windows and does its own thing on Linux (Mac is not currently supported but 126 I'm slowly working on it). 127 128 129 $(H3 Conceptual Overviews) 130 131 A gui application is made out of widgets laid out in windows that display information and respond to events from the user. They also typically have actions available in menus, and you might also want to customize the appearance. How do we do these things with minigui? Let's break it down into several categories. 132 133 $(H4 Code structure) 134 135 You will typically want to create the ui, prepare event handlers, then run an event loop. The event loop drives the program, calling your methods to respond to user activity. 136 137 --- 138 import arsd.minigui; 139 140 void main() { 141 // first, create a window, the (optional) string here is its title 142 auto window = new MainWindow("Hello, World!"); 143 144 // lay out some widgets inside the window to create the ui 145 auto name = new LabeledLineEdit("What is your name?", window); 146 auto button = new Button("Say Hello", window); 147 148 // prepare event handlers 149 button.addEventListener(EventType.triggered, () { 150 window.messageBox("Hello, " ~ name.content ~ "!"); 151 }); 152 153 // show the window and run the event loop until this window is closed 154 window.loop(); 155 } 156 --- 157 158 To compile, run `opend hello.d`, then run the generated `hello` program. 159 160 While the specifics will change, nearly all minigui applications will roughly follow this pattern. 161 162 $(TIP 163 There are two other ways to run event loops: `arsd.simpledisplay.EventLoop.get.run();` and `arsd.core.getThisThreadEventLoop().run();`. They all call the same underlying functions, but have different exit conditions - the `EventLoop.get.run()` keeps running until all top-level windows are closed, and `getThisThreadEventLoop().run` keeps running until all "tasks are resolved"; it is more abstract, supporting more than just windows. 164 165 You may call this if you don't have a single main window. 166 167 Even a basic minigui window can benefit from these if you don't have a single main window: 168 169 --- 170 import arsd.minigui; 171 172 void main() { 173 // create a struct to hold gathered info 174 struct Hello { string name; } 175 // let minigui create a dialog box to get that 176 // info from the user. If you have a main window, 177 // you'd pass that here, but it is not required 178 dialog((Hello info) { 179 // inline handler of the "OK" button 180 messageBox("Hello, " ~ info.name); 181 }); 182 183 // since there is no main window to loop on, 184 // we instead call the event loop singleton ourselves 185 EventLoop.get.run; 186 } 187 --- 188 189 This is also useful when your programs lives as a notification area (aka systray) icon instead of as a window. But let's not get too far ahead of ourselves! 190 ) 191 192 $(H4 How to lay out widgets) 193 194 To better understand the details of layout algorithms and see more available included classes, see [Layout]. 195 196 $(H5 Default layouts) 197 198 minigui windows default to a flexible vertical layout, where widgets are added, from top to bottom on the window, in the same order of you creating them, then they are sized according to layout hints on the widget itself to fill the available space. This gives a reasonably usable setup but you'll probably want to customize it. 199 200 $(TIP 201 minigui's default [VerticalLayout] and [HorizontalLayout] are roughly based on css flexbox with wrap turned off. 202 ) 203 204 Generally speaking, there are two ways to customize layouts: either subclass the widget and change its hints, or wrap it in another layout widget. You can also create your own layout classes and do it all yourself, but that's fairly complicated. Wrapping existing widgets in other layout widgets is usually the easiest way to make things work. 205 206 $(NOTE 207 minigui widgets are not supposed to overlap, but can contain children, and are always rectangular. Children are laid out as rectangles inside the parent's rectangular area. 208 ) 209 210 For example, to display two widgets side-by-side, you can wrap them in a [HorizontalLayout]: 211 212 --- 213 import arsd.minigui; 214 void main() { 215 auto window = new MainWindow(); 216 217 // make the layout a child of our window 218 auto hl = new HorizontalLayout(window); 219 220 // then make the widgets children of the layout 221 auto leftButton = new Button("Left", hl); 222 auto rightButton = new Button("Right", hl); 223 224 window.loop(); 225 } 226 --- 227 228 A [HorizontalLayout] works just like the default [VerticalLayout], except in the other direction. These two buttons will take up all the available vertical space, then split available horizontal space equally. 229 230 $(H5 Nesting layouts) 231 232 Nesting layouts lets you carve up the rectangle in different ways. 233 234 $(EMBED_UNITTEST layout-example) 235 236 $(H5 Special layouts) 237 238 [TabWidget] can show pages of layouts as tabs. 239 240 See [ScrollableWidget] but be warned that it is weird. You might want to consider something like [GenericListViewWidget] instead. 241 242 $(H5 Other common layout classes) 243 244 [HorizontalLayout], [VerticalLayout], [InlineBlockLayout], [GridLayout] 245 246 $(H4 How to respond to widget events) 247 248 To better understanding the underlying event system, see [Event]. 249 250 Each widget emits its own events, which propagate up through their parents until they reach their top-level window. 251 252 $(H4 How to do overall ui - title, icons, menus, toolbar, hotkeys, statuses, etc.) 253 254 We started this series with a [MainWindow], but only added widgets to it. MainWindows also support menus and toolbars with various keyboard shortcuts. You can construct these menus by constructing classes and calling methods, but minigui also lets you just write functions in a command object and it does the rest! 255 256 See [MainWindow.setMenuAndToolbarFromAnnotatedCode] for an example. 257 258 Note that toggleable menu or toolbar items are not yet implemented, but on the todolist. Submenus and disabled items are also not supported at this time and not currently on the work list (but if you need it, let me know and MAYBE we can work something out. Emphasis on $(I maybe)). 259 260 $(TIP 261 The automatic dialog box logic is also available for you to invoke on demand with [dialog] and the data setting logic can be used with a child widget inside an existing window [addDataControllerWidget], which also has annotation-based layout capabilities. 262 ) 263 264 All windows also have titles. You can change this at any time with the `window.title = "string";` property. 265 266 Windows also have icons, which can be set with the `window.icon` property. It takes a [arsd.color.MemoryImage] object, which is an in-memory bitmap. [arsd.image] can load common file formats into these objects, or you can make one yourself. The default icon on Windows is the icon of your exe, which you can set through a resource file. (FIXME: explain how to do this easily.) 267 268 The `MainWindow` also provides a status bar across the bottom. These aren't so common in new applications, but I love them - on my own computer, I even have a global status bar for my whole desktop! I suggest you use it: a status bar is a consistent place to put information and notifications that will never overlap other content. 269 270 A status bar has parts, and the parts have content. The first part's content is assumed to change frequently; the default mouse over event will set it to [Widget.statusTip], a public `string` you can assign to any widget you want at any time. 271 272 Other parts can be added by you and are under your control. You add them with: 273 274 --- 275 window.statusBar.parts ~= StatusBar.Part(optional_size, optional_units); 276 --- 277 278 The size can be in a variety of units and what you get with mixes can get complicated. The rule is: explicit pixel sizes are used first. Then, proportional sizes are applied to the remaining space. Then, finally, if there is any space left, any items without an explicit size split them equally. 279 280 You may prefer to set them all at once, with: 281 282 --- 283 window.statusBar.parts.setSizes(1, 1, 1); 284 --- 285 286 This makes a three-part status bar, each with the same size - they all take the same proportion of the total size. Negative numbers here will use auto-scaled pixels. 287 288 You should call this right after creating your `MainWindow` as part of your setup code. 289 290 Once you make parts, you can explicitly change their content with `window.statusBar.parts[index].content = "some string";` 291 292 $(NOTE 293 I'm thinking about making the other parts do other things by default too, but if I do change it, I'll try not to break any explicitly set things you do anyway. 294 ) 295 296 If you really don't want a status bar on your main window, you can remove it with `window.statusBar = null;` Make sure you don't try to use it again, or your program will likely crash! 297 298 Status bars, at this time, cannot hold non-text content, but I do want to change that. They also cannot have event listeners at this time, but again, that is likely to change. I have something in mind where they can hold clickable messages with a history and maybe icons, but haven't implemented any of that yet. Right now, they're just a (still very useful!) display area. 299 300 $(H4 How to do custom styles) 301 302 Minigui's custom widgets support styling parameters on the level of individual widgets, or application-wide with [VisualTheme]s. 303 304 $(WARNING 305 These don't apply to non-custom widgets! They will use the operating system's native theme unless the documentation for that specific class says otherwise. 306 307 At this time, custom widgets gain capability in styling, but lose capability in terms of keeping all the right integrated details of the user experience and availability to accessibility and other automation tools. Evaluate if the benefit is worth the costs before making your decision. 308 309 I'd like to erase more and more of these gaps, but no promises as to when - or even if - that will ever actually happen. 310 ) 311 312 See [Widget.Style] for more information. 313 314 $(H4 Selection of categorized widgets) 315 316 $(LIST 317 * Buttons: [Button] 318 * Text display widgets: [TextLabel], [TextDisplay] 319 * Text edit widgets: [LineEdit] (and [LabeledLineEdit]), [PasswordEdit] (and [LabeledPasswordEdit]), [TextEdit] 320 * Selecting multiple on/off options: [Checkbox] 321 * Selecting just one from a list of options: [Fieldset], [Radiobox], [DropDownSelection] 322 * Getting rough numeric input: [HorizontalSlider], [VerticalSlider] 323 * Displaying data: [ImageBox], [ProgressBar], [TableView] 324 * Showing a list of editable items: [GenericListViewWidget] 325 * Helpers for building your own widgets: [OpenGlWidget], [ScrollMessageWidget] 326 ) 327 328 And more. See [#members] until I write up more of this later and also be aware of the package [arsd.minigui_addons]. 329 330 If none of these do what you need, you'll want to write your own. More on that in the following section. 331 332 $(H4 custom widgets - how to write your own) 333 334 See some example programs: https://github.com/adamdruppe/minigui-samples 335 336 When you can't build your application out of existing widgets, you'll want to make your own. The general pattern is to subclass [Widget], write a constructor that takes a `Widget` parent argument you pass to `super`, then set some values, override methods you want to customize, and maybe add child widgets and events as appropriate. You might also be able to subclass an existing other Widget and customize that way. 337 338 To get more specific, let's consider a few illustrative examples, then we'll come back to some principles. 339 340 $(H5 Custom Widget Examples) 341 342 $(H5 More notes) 343 344 See [Widget]. 345 346 If you override [Widget.recomputeChildLayout], don't forget to call `registerMovement()` at the top of it, then call recomputeChildLayout of all its children too! 347 348 If you need a nested OS level window, see [NestedChildWindowWidget]. Use [Widget.scaleWithDpi] to convert logical pixels to physical pixels, as required. 349 350 See [Widget.OverrideStyle], [Widget.paintContent], [Widget.dynamicState] for some useful starting points. 351 352 You may also want to provide layout and style hints by overriding things like [Widget.flexBasisWidth], [Widget.flexBasisHeight], [Widget.minHeight], yada, yada, yada. 353 354 You might make a compound widget out of other widgets. [Widget.encapsulatedChildren] can help hide this from the outside world (though is not necessary and might hurt some debugging!) 355 356 $(TIP 357 Compile your application with the `-debug` switch and press F12 in your window to open a web-browser-inspired debug window. It sucks right now and doesn't do a lot, but is sometimes better than nothing. 358 ) 359 360 $(H5 Timers and animations) 361 362 The [Timer] class is available and you can call `widget.redraw();` to trigger a redraw from a timer handler. 363 364 I generally don't like animations in my programs, so it hasn't been a priority for me to do more than this. I also hate uis that move outside of explicit user action, so minigui kinda supports this but I'd rather you didn't. I kinda wanna do something like `requestAnimationFrame` or something but haven't yet so it is just the `Timer` class. 365 366 $(H5 Clipboard integrations, drag and drop) 367 368 GUI application users tend to expect integration with their system, so clipboard support is basically a must, and drag and drop is nice to offer too. The functions for these are provided in [arsd.simpledisplay], which is public imported from minigui, and thus available to you here too. 369 370 I'd like to think of some better abstractions to make this more automagic, but you must do it yourself when implementing your custom widgets right now. 371 372 See: [draggable], [DropHandler], [setClipboardText], [setClipboardImage], [getClipboardText], [getClipboardImage], [setPrimarySelection], and others from simpledisplay. 373 374 $(H5 Context menus) 375 376 Override [Widget.contextMenu] in your subclass. 377 378 $(H4 Coming later) 379 380 Among the unfinished features: unified selections, translateable strings, external integrations. 381 382 $(H2 Running minigui programs) 383 384 Note the environment variable ARSD_SCALING_FACTOR on Linux can set multi-monitor scaling factors. I should also read it from a root window property so it easier to do with migrations... maybe a default theme selector from there too. 385 386 $(H2 Building minigui programs) 387 388 minigui's only required dependencies are [arsd.simpledisplay], [arsd.color], and 389 [arsd.textlayouter], on which it is built. simpledisplay provides the low-level 390 interfaces and minigui builds the concept of widgets inside the windows on top of it. 391 392 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 393 It isn't hugely concerned with appearance - on Windows, it just uses the native 394 controls and native theme, and on Linux, it keeps it simple and I may change that 395 at any time, though after May 2021, you can customize some things with css-inspired 396 [Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`, 397 you can use the custom implementation there too, but... you shouldn't.) 398 399 The event model is similar to what you use in the browser with Javascript and the 400 layout engine tries to automatically fit things in, similar to a css flexbox. 401 402 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 403 `-L/SUBSYSTEM:WINDOWS` and -L/entry:mainCRTStartup`. If using ldc instead 404 of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w". 405 406 Otherwise you'll get a console and possibly other visual bugs. But if you do use 407 the subsystem:windows, note that Phobos' writeln will crash the program! 408 409 HTML_To_Classes: 410 $(SMALL_TABLE 411 HTML Code | Minigui Class 412 413 `<input type="text">` | [LineEdit] 414 `<input type="password">` | [PasswordEdit] 415 `<textarea>` | [TextEdit] 416 `<select>` | [DropDownSelection] 417 `<input type="checkbox">` | [Checkbox] 418 `<input type="radio">` | [Radiobox] 419 `<button>` | [Button] 420 ) 421 422 423 Stretchiness: 424 The default is 4. You can use larger numbers for things that should 425 consume a lot of space, and lower numbers for ones that are better at 426 smaller sizes. 427 428 Overlapped_input: 429 COMING EVENTUALLY: 430 minigui will include a little bit of I/O functionality that just works 431 with the event loop. If you want to get fancy, I suggest spinning up 432 another thread and posting events back and forth. 433 434 $(H2 Add ons) 435 See the `minigui_addons` directory in the arsd repo for some add on widgets 436 you can import separately too. 437 438 $(H3 XML definitions) 439 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 440 441 $(H3 Scriptability) 442 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 443 in this documentation, it means you can call it from the script language. 444 445 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 446 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 447 448 --- 449 import arsd.minigui_xml; 450 import arsd.script; 451 452 var globals = var.emptyObject; 453 globals.makeWidgetFromString = &makeWidgetFromString; 454 455 // this now works 456 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 457 --- 458 459 More to come. 460 461 Widget_tree_notes: 462 minigui doesn't really formalize these distinctions, but in practice, there are multiple types of widgets: 463 464 $(LIST 465 * Containers - a widget that holds other widgets directly, generally [Layout]s. [WidgetContainer] is an attempt to formalize this but is nothing really special. 466 467 * Reparenting containers - a widget that holds other widgets inside a different one of their parents. [MainWindow] is an example - any time you try to add a child to the main window, it actually goes to a special container one layer deeper. [ScrollMessageWidget] also works this way. 468 469 --- 470 auto child = new Widget(mainWindow); 471 assert(child.parent is mainWindow); // fails, its actual parent is mainWindow's inner container instead. 472 --- 473 474 * Limiting containers - a widget that can only hold children of a particular type. See [TabWidget], which can only hold [TabWidgetPage]s. 475 476 * Simple controls - a widget that cannot have children, but instead does a specific job. 477 478 * Compound controls - a widget that is comprised of children internally to help it do a specific job, but externally acts like a simple control that does not allow any more children. Ideally, this is encapsulated, but in practice, it leaks right now. 479 ) 480 481 In practice, all of these are [Widget]s right now, but this violates the OOP principles of substitutability since some operations are not actually valid on all subclasses. 482 483 Future breaking changes might be related to making this more structured but im not sure it is that important to actually break stuff over. 484 485 My_UI_Guidelines: 486 Note that the Linux custom widgets generally aim to be efficient on remote X network connections. 487 488 In a perfect world, you'd achieve all the following goals: 489 490 $(LIST 491 * All operations are present in the menu 492 * The operations the user wants at the moment are right where they want them 493 * All operations can be scripted 494 * The UI does not move any elements without explicit user action 495 * All numbers can be seen and typed in if wanted, even if the ui usually hides them 496 ) 497 498 $(H2 Future Directions) 499 500 I want to do some newer ideas that might not be easy to keep working fully on Windows, like adding a menu search feature and scrollbar custom marks and typing in numbers. I might make them a default part of the widget with custom, and let you provide them through a menu or something elsewhere. 501 502 History: 503 In January 2025 (dub v12.0), minigui got a few more breaking changes: 504 505 $(LIST 506 * `defaultEventHandler_*` functions take more specific objects. So if you see errors like: 507 508 --- 509 Error: function `void arsd.minigui.EditableTextWidget.defaultEventHandler_focusin(Event foe)` does not override any function, did you mean to override `void arsd.minigui.Widget.defaultEventHandler_focusin(arsd.minigui.FocusInEvent event)`? 510 --- 511 512 Go to the file+line number from the error message and change `Event` to `FocusInEvent` (or whatever one it tells you in the "did you mean" part of the error) and recompile. No other changes should be necessary to be compatible with this change. 513 514 * Most event classes, except those explicitly used as a base class, are now marked `final`. If you depended on this subclassing, let me know and I'll see what I can do, but I expect there's little use of it. I now recommend all event classes the `final` unless you are specifically planning on extending it. 515 ) 516 517 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 518 519 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 520 tag this as version 2.0. 521 522 Among the changes: 523 $(LIST 524 * The event model changed to prefer strongly-typed events, though the Javascript string style ones still work, using properties off them is deprecated. It will still compile and function, but you should change the handler to use the classes in its argument list. I adapted my code to use the new model in just a few minutes, so it shouldn't too hard. 525 526 See [Event] for details. 527 528 * A [DoubleClickEvent] was added. Previously, you'd get two rapidly repeated click events. Now, you get one click event followed by a double click event. If you must recreate the old way exactly, you can listen for a DoubleClickEvent, set a flag upon receiving one, then send yourself a synthetic ClickEvent on the next MouseUpEvent, but your program might be better served just working with [MouseDownEvent]s instead. 529 530 See [DoubleClickEvent] for details. 531 532 * Styling hints were added, and the few that existed before have been moved to a new helper class. Deprecated forwarders exist for the (few) old properties to help you transition. Note that most of these only affect a `custom_events` build, which is the default on Linux, but opt in only on Windows. 533 534 See [Widget.Style] for details. 535 536 * Widgets now draw their keyboard focus by default instead of opt in. You may wish to set `tabStop = false;` if it wasn't supposed to receive it. 537 538 * Most Widget constructors no longer have a default `parent` argument. You must pass the parent to almost all widgets, or in rare cases, an explict `null`, but more often than not, you need the parent so the default argument was not very useful at best and misleading to a crash at worst. 539 540 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 541 542 * Several conversions of public fields to properties, deprecated, or made private. It is unlikely this will affect you, but the compiler will tell you if it does. 543 544 * Various non-breaking additions. 545 ) 546 +/ 547 module arsd.minigui; 548 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 549 550 /++ 551 This hello world sample will have an oversized button, but that's ok, you see your first window! 552 +/ 553 version(Demo) 554 unittest { 555 import arsd.minigui; 556 557 void main() { 558 auto window = new MainWindow(); 559 560 // note the parent widget is almost always passed as the last argument to a constructor 561 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 562 auto button = new Button("Close", window); 563 button.addWhenTriggered({ 564 window.close(); 565 }); 566 567 window.loop(); 568 } 569 570 main(); // exclude from docs 571 } 572 573 /++ 574 $(ID layout-example) 575 576 This example shows one way you can partition your window into a header 577 and sidebar. Here, the header and sidebar have a fixed width, while the 578 rest of the content sizes with the window. 579 580 It might be a new way of thinking about window layout to do things this 581 way - perhaps [GridLayout] more matches your style of thought - but the 582 concept here is to partition the window into sub-boxes with a particular 583 size, then partition those boxes into further boxes. 584 585 $(IMG //arsdnet.net/minigui-screenshots/windows/layout.png, The example window has a header across the top, then below it a sidebar to the left and a content area to the right.) 586 587 So to make the header, start with a child layout that has a max height. 588 It will use that space from the top, then the remaining children will 589 split the remaining area, meaning you can think of is as just being another 590 box you can split again. Keep splitting until you have the look you desire. 591 +/ 592 // https://github.com/adamdruppe/arsd/issues/310 593 version(minigui_screenshots) 594 @Screenshot("layout") 595 unittest { 596 import arsd.minigui; 597 598 // This helper class is just to help make the layout boxes visible. 599 // think of it like a <div style="background-color: whatever;"></div> in HTML. 600 class ColorWidget : Widget { 601 this(Color color, Widget parent) { 602 this.color = color; 603 super(parent); 604 } 605 Color color; 606 class Style : Widget.Style { 607 override WidgetBackground background() { return WidgetBackground(color); } 608 } 609 mixin OverrideStyle!Style; 610 } 611 612 void main() { 613 auto window = new Window; 614 615 // the key is to give it a max height. This is one way to do it: 616 auto header = new class HorizontalLayout { 617 this() { super(window); } 618 override int maxHeight() { return 50; } 619 }; 620 // this next line is a shortcut way of doing it too, but it only works 621 // for HorizontalLayout and VerticalLayout, and is less explicit, so it 622 // is good to know how to make a new class like above anyway. 623 // auto header = new HorizontalLayout(50, window); 624 625 auto bar = new HorizontalLayout(window); 626 627 // or since this is so common, VerticalLayout and HorizontalLayout both 628 // can just take an argument in their constructor for max width/height respectively 629 630 // (could have tone this above too, but I wanted to demo both techniques) 631 auto left = new VerticalLayout(100, bar); 632 633 // and this is the main section's container. A plain Widget instance is good enough here. 634 auto container = new Widget(bar); 635 636 // and these just add color to the containers we made above for the screenshot. 637 // in a real application, you can just add your actual controls instead of these. 638 auto headerColorBox = new ColorWidget(Color.teal, header); 639 auto leftColorBox = new ColorWidget(Color.green, left); 640 auto rightColorBox = new ColorWidget(Color.purple, container); 641 642 window.loop(); 643 } 644 645 main(); // exclude from docs 646 } 647 648 649 import arsd.core; 650 import arsd.textlayouter; 651 652 alias Timer = arsd.simpledisplay.Timer; 653 public import arsd.simpledisplay; 654 /++ 655 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 656 657 History: 658 Was private until May 15, 2021. 659 +/ 660 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 661 662 version(Windows) { 663 import core.sys.windows.winnls; 664 import core.sys.windows.windef; 665 import core.sys.windows.basetyps; 666 import core.sys.windows.winbase; 667 import core.sys.windows.winuser; 668 import core.sys.windows.wingdi; 669 static import gdi = core.sys.windows.wingdi; 670 } 671 672 version(Windows) { 673 // to swap the default 674 // version(minigui_manifest) {} else version=minigui_no_manifest; 675 676 version(minigui_no_manifest) {} else { 677 version(D_OpenD) { 678 // OpenD always supports it 679 version=UseManifestMinigui; 680 } else { 681 static if(__VERSION__ >= 2_083) 682 version(CRuntime_Microsoft) // FIXME: mingw? 683 version=UseManifestMinigui; 684 } 685 686 } 687 688 689 version(UseManifestMinigui) { 690 // assume we want commctrl6 whenever possible since there's really no reason not to 691 // and this avoids some of the manifest hassle 692 pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""); 693 } 694 } 695 696 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 697 private bool lastDefaultPrevented; 698 699 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 700 alias scriptable = arsd_jsvar_compatible; 701 702 version(Windows) { 703 // use native widgets when available unless specifically asked otherwise 704 version(custom_widgets) { 705 enum bool UsingCustomWidgets = true; 706 enum bool UsingWin32Widgets = false; 707 } else { 708 version = win32_widgets; 709 enum bool UsingCustomWidgets = false; 710 enum bool UsingWin32Widgets = true; 711 } 712 // and native theming when needed 713 //version = win32_theming; 714 } else { 715 enum bool UsingCustomWidgets = true; 716 enum bool UsingWin32Widgets = false; 717 version=custom_widgets; 718 } 719 720 721 722 /* 723 724 The main goals of minigui.d are to: 725 1) Provide basic widgets that just work in a lightweight lib. 726 I basically want things comparable to a plain HTML form, 727 plus the easy and obvious things you expect from Windows 728 apps like a menu. 729 2) Use native things when possible for best functionality with 730 least library weight. 731 3) Give building blocks to provide easy extension for your 732 custom widgets, or hooking into additional native widgets 733 I didn't wrap. 734 4) Provide interfaces for easy interaction between third 735 party minigui extensions. (event model, perhaps 736 signals/slots, drop-in ease of use bits.) 737 5) Zero non-system dependencies, including Phobos as much as 738 I reasonably can. It must only import arsd.color and 739 my simpledisplay.d. If you need more, it will have to be 740 an extension module. 741 6) An easy layout system that generally works. 742 743 A stretch goal is to make it easy to make gui forms with code, 744 some kind of resource file (xml?) and even a wysiwyg designer. 745 746 Another stretch goal is to make it easy to hook data into the gui, 747 including from reflection. So like auto-generate a form from a 748 function signature or struct definition, or show a list from an 749 array that automatically updates as the array is changed. Then, 750 your program focuses on the data more than the gui interaction. 751 752 753 754 STILL NEEDED: 755 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 756 * slider 757 * listbox 758 * spinner 759 * label? 760 * rich text 761 */ 762 763 764 /+ 765 enum LayoutMethods { 766 verticalFlex, 767 horizontalFlex, 768 inlineBlock, // left to right, no stretch, goes to next line as needed 769 static, // just set to x, y 770 verticalNoStretch, // browser style default 771 772 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 773 774 grid, // magic 775 } 776 +/ 777 778 /++ 779 The `Widget` is the base class for minigui's functionality, ranging from UI components like checkboxes or text displays to abstract groupings of other widgets like a layout container or a html `<div>`. You will likely want to use pre-made widgets as well as creating your own. 780 781 782 To create your own widget, you must inherit from it and create a constructor that passes a parent to `super`. Everything else after that is optional. 783 784 --- 785 class MinimalWidget : Widget { 786 this(Widget parent) { 787 super(parent); 788 } 789 } 790 --- 791 792 $(SIDEBAR 793 I'm not entirely happy with leaf, container, and windows all coming from the same base Widget class, but I so far haven't thought of a better solution that's good enough to justify the breakage of a transition. It hasn't been a major problem in practice anyway. 794 ) 795 796 Broadly, there's two kinds of widgets: leaf widgets, which are intended to be the direct user-interactive components, and container widgets, which organize, lay out, and aggregate other widgets in the object tree. A special case of a container widget is [Window], which represents a separate top-level window on the screen. Both leaf and container widgets inherit from `Widget`, so this distinction is more conventional than formal. 797 798 Among the things you'll most likely want to change in your custom widget: 799 800 $(LIST 801 * In your constructor, set `tabStop = false;` if the widget is not supposed to receive keyboard focus. (Please note its childen still can, so `tabStop = false;` is appropriate on most container widgets.) 802 803 You may explicitly set `tabStop = true;` to ensure you get it, even against future changes to the library, though that's the default right now. 804 805 Do this $(I after) calling the `super` constructor. 806 807 * Override [paint] if you want full control of the widget's drawing area (except the area obscured by children!), or [paintContent] if you want to participate in the styling engine's system. You'll also possibly want to make a subclass of [Style] and use [OverrideStyle] to change the default hints given to the styling engine for widget. 808 809 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 810 811 * Override default event handlers with your behavior. For example [defaultEventHandler_click] may be overridden to make clicks do something. Again, this is generally a job for leaf widgets rather than containers; most events are dispatched to the lowest leaf on the widget tree, but they also pass through all their parents. See [Event] for more details about the event model. 812 813 * You may also want to override the various layout hints like [minWidth], [maxHeight], etc. In particular [Padding] and [Margin] are often relevant for both container and leaf widgets and the default values of 0 are often not what you want. 814 ) 815 816 On Microsoft Windows, many widgets are also based on native controls. You can also do this if `static if(UsingWin32Widgets)` passes. You should use the helper function [createWin32Window] to create the window and let minigui do what it needs to do to create its bridge structures. This will populate [Widget.hwnd] which you can access later for communcating with the native window. You may also consider overriding [Widget.handleWmCommand] and [Widget.handleWmNotify] for the widget to translate those messages into appropriate minigui [Event]s. 817 818 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 819 820 Your own custom-drawn and native system controls can exist side-by-side. 821 822 Later I'll add more complete examples, but for now [TextLabel] and [LabeledPasswordEdit] are both simple widgets you can view implementation to get some ideas. 823 +/ 824 class Widget : ReflectableProperties { 825 826 private int toolbarIconSize() { 827 return scaleWithDpi(24); 828 } 829 830 831 /++ 832 Returns the current size of the widget. 833 834 History: 835 Added January 3, 2025 836 +/ 837 final Size size() const { 838 return Size(width, height); 839 } 840 841 private bool willDraw() { 842 return true; 843 } 844 845 /+ 846 /++ 847 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 848 849 History: 850 Added September 15, 2021 851 implemented.... ??? 852 +/ 853 void prepareReflection(this This)() { 854 855 } 856 +/ 857 858 private bool _enabled = true; 859 860 /++ 861 Determines whether the control is marked enabled. Disabled controls are generally displayed as greyed out and clicking on them does nothing. It is also possible for a control to be disabled because its parent is disabled, in which case this will still return `true`, but setting `enabled = true` may have no effect. Check [disabledBy] to see which parent caused it to be disabled. 862 863 I also recommend you set a [disabledReason] if you chose to set `enabled = false` to tell the user why the control does not work and what they can do to enable it. 864 865 History: 866 Added November 23, 2021 (dub v10.4) 867 868 Warning: the specific behavior of disabling with parents may change in the future. 869 Bugs: 870 Currently only implemented for widgets backed by native Windows controls. 871 872 See_Also: [disabledReason], [disabledBy] 873 +/ 874 @property bool enabled() { 875 return disabledBy() is null; 876 } 877 878 /// ditto 879 @property void enabled(bool yes) { 880 _enabled = yes; 881 version(win32_widgets) { 882 if(hwnd) 883 EnableWindow(hwnd, yes); 884 } 885 setDynamicState(DynamicState.disabled, yes); 886 } 887 888 private string disabledReason_; 889 890 /++ 891 If the widget is not [enabled] this string may be presented to the user when they try to use it. The exact manner and time it gets displayed is up to the implementation of the control. 892 893 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 894 895 History: 896 Added November 23, 2021 (dub v10.4) 897 See_Also: [enabled], [disabledBy] 898 +/ 899 @property string disabledReason() { 900 auto w = disabledBy(); 901 return (w is null) ? null : w.disabledReason_; 902 } 903 904 /// ditto 905 @property void disabledReason(string reason) { 906 disabledReason_ = reason; 907 } 908 909 /++ 910 Returns the widget that disabled this. It might be this or one of its parents all the way up the chain, or `null` if the widget is not disabled by anything. You can check [disabledReason] on the return value (after the null check!) to get a hint to display to the user. 911 912 History: 913 Added November 25, 2021 (dub v10.4) 914 See_Also: [enabled], [disabledReason] 915 +/ 916 Widget disabledBy() { 917 Widget p = this; 918 while(p) { 919 if(!p._enabled) 920 return p; 921 p = p.parent; 922 } 923 return null; 924 } 925 926 /// Implementations of [ReflectableProperties] interface. See the interface for details. 927 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 928 if(valueIsJson) 929 return SetPropertyResult.wrongFormat; 930 switch(name) { 931 case "name": 932 this.name = value.idup; 933 return SetPropertyResult.success; 934 case "statusTip": 935 this.statusTip = value.idup; 936 return SetPropertyResult.success; 937 default: 938 return SetPropertyResult.noSuchProperty; 939 } 940 } 941 /// ditto 942 void getPropertiesList(scope void delegate(string name) sink) const { 943 sink("name"); 944 sink("statusTip"); 945 } 946 /// ditto 947 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 948 switch(name) { 949 case "name": 950 sink(name, this.name, false); 951 return; 952 case "statusTip": 953 sink(name, this.statusTip, false); 954 return; 955 default: 956 sink(name, null, true); 957 } 958 } 959 960 /++ 961 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 962 963 History: 964 Added November 25, 2021 (dub v10.5) 965 `Point` overload added January 12, 2022 (dub v10.6) 966 +/ 967 int scaleWithDpi(int value, int assumedDpi = 96) { 968 // avoid potential overflow with common special values 969 if(value == int.max) 970 return int.max; 971 if(value == int.min) 972 return int.min; 973 if(value == 0) 974 return 0; 975 return value * currentDpi(assumedDpi) / assumedDpi; 976 } 977 978 /// ditto 979 Point scaleWithDpi(Point value, int assumedDpi = 96) { 980 return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi)); 981 } 982 983 /++ 984 Returns the current scaling factor as a logical dpi value for this widget. Generally speaking, this divided by 96 gives you the user scaling factor. 985 986 Not entirely stable. 987 988 History: 989 Added August 25, 2023 (dub v11.1) 990 +/ 991 final int currentDpi(int assumedDpi = 96) { 992 // assert(parentWindow !is null); 993 // assert(parentWindow.win !is null); 994 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 995 //divide = 138; // to test 1.5x 996 // for lower values it is something i don't really want changed anyway since it is an old monitor and you don't want to scale down. 997 // this also covers the case when actualDpi returns 0. 998 if(divide < 96) 999 divide = 96; 1000 return divide; 1001 } 1002 1003 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 1004 // I'll think up something better eventually 1005 1006 // FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight. 1007 protected final int defaultLineHeight() { 1008 auto cs = getComputedStyle(); 1009 if(cs.font && !cs.font.isNull) 1010 return cs.font.height() * 5 / 4; 1011 else 1012 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4); 1013 } 1014 1015 /++ 1016 1017 History: 1018 Added August 25, 2023 (dub v11.1) 1019 +/ 1020 protected final int defaultTextHeight(int numberOfLines = 1) { 1021 auto cs = getComputedStyle(); 1022 if(cs.font && !cs.font.isNull) 1023 return cs.font.height() * numberOfLines; 1024 else 1025 return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines; 1026 } 1027 1028 protected final int defaultTextWidth(const(char)[] text) { 1029 auto cs = getComputedStyle(); 1030 if(cs.font && !cs.font.isNull) 1031 return cs.font.stringWidth(text); 1032 else 1033 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2); 1034 } 1035 1036 /++ 1037 If `encapsulatedChildren` returns true, it changes the event handling mechanism to act as if events from the child widgets are actually targeted on this widget. 1038 1039 The idea is then you can use child widgets as part of your implementation, but not expose those details through the event system; if someone checks the mouse coordinates and target of the event once it bubbles past you, it will show as it it came from you. 1040 1041 History: 1042 Added May 22, 2021 1043 +/ 1044 protected bool encapsulatedChildren() { 1045 return false; 1046 } 1047 1048 private void privateDpiChanged() { 1049 dpiChanged(); 1050 foreach(child; children) 1051 child.privateDpiChanged(); 1052 } 1053 1054 /++ 1055 Virtual hook to update any caches or fonts you need on the event of a dpi scaling change. 1056 1057 History: 1058 Added January 12, 2022 (dub v10.6) 1059 +/ 1060 protected void dpiChanged() { 1061 1062 } 1063 1064 // Default layout properties { 1065 1066 int minWidth() { return 0; } 1067 int minHeight() { 1068 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 1069 int sum = this.paddingTop + this.paddingBottom; 1070 foreach(child; children) { 1071 if(child.hidden) 1072 continue; 1073 sum += child.minHeight(); 1074 sum += child.marginTop(); 1075 sum += child.marginBottom(); 1076 } 1077 1078 return sum; 1079 } 1080 int maxWidth() { return int.max; } 1081 int maxHeight() { return int.max; } 1082 int widthStretchiness() { return 4; } 1083 int heightStretchiness() { return 4; } 1084 1085 /++ 1086 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 1087 1088 History: 1089 Added June 15, 2021 (dub v10.1) 1090 +/ 1091 int widthShrinkiness() { return 0; } 1092 /// ditto 1093 int heightShrinkiness() { return 0; } 1094 1095 /++ 1096 The initial size of the widget for layout calculations. Default is 0. 1097 1098 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 1099 1100 History: 1101 Added June 15, 2021 (dub v10.1) 1102 +/ 1103 int flexBasisWidth() { return 0; } 1104 /// ditto 1105 int flexBasisHeight() { return 0; } 1106 1107 /++ 1108 Not stable. 1109 1110 Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored. 1111 1112 So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6. 1113 1114 History: 1115 Added January 5, 2023 1116 +/ 1117 Rectangle defaultMargin; 1118 /// ditto 1119 Rectangle defaultPadding; 1120 1121 int marginLeft() { return scaleWithDpi(defaultMargin.left); } 1122 int marginRight() { return scaleWithDpi(defaultMargin.right); } 1123 int marginTop() { return scaleWithDpi(defaultMargin.top); } 1124 int marginBottom() { return scaleWithDpi(defaultMargin.bottom); } 1125 int paddingLeft() { return scaleWithDpi(defaultPadding.left); } 1126 int paddingRight() { return scaleWithDpi(defaultPadding.right); } 1127 int paddingTop() { return scaleWithDpi(defaultPadding.top); } 1128 int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); } 1129 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 1130 1131 private bool recomputeChildLayoutRequired = true; 1132 private static class RecomputeEvent {} 1133 private __gshared rce = new RecomputeEvent(); 1134 protected final void queueRecomputeChildLayout() { 1135 recomputeChildLayoutRequired = true; 1136 1137 if(this.parentWindow) { 1138 auto sw = this.parentWindow.win; 1139 assert(sw !is null); 1140 if(!sw.eventQueued!RecomputeEvent) { 1141 sw.postEvent(rce); 1142 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1143 } 1144 } 1145 1146 } 1147 1148 protected final void recomputeChildLayoutEntry() { 1149 if(recomputeChildLayoutRequired) { 1150 recomputeChildLayout(); 1151 recomputeChildLayoutRequired = false; 1152 redraw(); 1153 } else { 1154 // I still need to check the tree just in case one of them was queued up 1155 // and the event came up here instead of there. 1156 foreach(child; children) 1157 child.recomputeChildLayoutEntry(); 1158 } 1159 } 1160 1161 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 1162 void recomputeChildLayout() { 1163 .recomputeChildLayout!"height"(this); 1164 } 1165 1166 // } 1167 1168 1169 /++ 1170 Returns the style's tag name string this object uses. 1171 1172 The default is to use the typeid() name trimmed down to whatever is after the last dot which is typically the identifier of the class. 1173 1174 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 1175 1176 History: 1177 Added May 10, 2021 1178 +/ 1179 string styleTagName() const { 1180 string n = typeid(this).name; 1181 foreach_reverse(idx, ch; n) 1182 if(ch == '.') { 1183 n = n[idx + 1 .. $]; 1184 break; 1185 } 1186 return n; 1187 } 1188 1189 /// API for the [styleClassList] 1190 static struct ClassList { 1191 private Widget widget; 1192 1193 /// 1194 void add(string s) { 1195 widget.styleClassList_ ~= s; 1196 } 1197 1198 /// 1199 void remove(string s) { 1200 foreach(idx, s1; widget.styleClassList_) 1201 if(s1 == s) { 1202 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 1203 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 1204 widget.styleClassList_.assumeSafeAppend(); 1205 return; 1206 } 1207 } 1208 1209 /// Returns true if it was added, false if it was removed. 1210 bool toggle(string s) { 1211 if(contains(s)) { 1212 remove(s); 1213 return false; 1214 } else { 1215 add(s); 1216 return true; 1217 } 1218 } 1219 1220 /// 1221 bool contains(string s) const { 1222 foreach(s1; widget.styleClassList_) 1223 if(s1 == s) 1224 return true; 1225 return false; 1226 1227 } 1228 } 1229 1230 private string[] styleClassList_; 1231 1232 /++ 1233 Returns a "class list" that can be used by the visual theme's style engine via [VisualTheme.getPropertyString] if it chooses to do something like CSS. 1234 1235 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 1236 1237 History: 1238 Added May 10, 2021 1239 +/ 1240 inout(ClassList) styleClassList() inout { 1241 return cast(inout(ClassList)) ClassList(cast() this); 1242 } 1243 1244 /++ 1245 List of dynamic states made available to the style engine, for cases like CSS pseudo-classes and also used by default paint methods. It is stored in a 64 bit variable attached to the widget that you can update. The style cache is aware of the fact that these can frequently change. 1246 1247 The lower 32 bits are defined here or reserved for future use by the library. You should keep these updated if you reasonably can on custom widgets if they apply to you, but don't use them for a purpose they aren't defined for. 1248 1249 The upper 32 bits are available for your own extensions. 1250 1251 History: 1252 Added May 10, 2021 1253 1254 Examples: 1255 1256 --- 1257 addEventListener((MouseUpEvent ev) { 1258 if(ev.button == MouseButton.left) { 1259 // the first arg is the state to modify, the second arg is what to set it to 1260 setDynamicState(DynamicState.depressed, false); 1261 } 1262 }); 1263 --- 1264 1265 +/ 1266 enum DynamicState : ulong { 1267 focus = (1 << 0), /// the widget currently has the keyboard focus 1268 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 1269 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if no validation has been performed!) 1270 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if no validation has been performed!) 1271 checked = (1 << 4), /// the widget is toggleable and currently toggled on 1272 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 1273 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 1274 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 1275 depressed = (1 << 8), /// the widget is being actively pressed or clicked (compare to css `:active`). Can be combined with hover to visually indicate if a mouse up would result in a click event. 1276 1277 USER_BEGIN = (1UL << 32), 1278 } 1279 1280 // I want to add the primary and cancel styles to buttons at least at some point somehow. 1281 1282 /// ditto 1283 @property ulong dynamicState() { return dynamicState_; } 1284 /// ditto 1285 @property ulong dynamicState(ulong newValue) { 1286 if(dynamicState != newValue) { 1287 auto old = dynamicState_; 1288 dynamicState_ = newValue; 1289 1290 useStyleProperties((scope Widget.Style s) { 1291 if(s.variesWithState(old ^ newValue)) 1292 redraw(); 1293 }); 1294 } 1295 return dynamicState_; 1296 } 1297 1298 /// ditto 1299 void setDynamicState(ulong flags, bool state) { 1300 auto ds = dynamicState_; 1301 if(state) 1302 ds |= flags; 1303 else 1304 ds &= ~flags; 1305 1306 dynamicState = ds; 1307 } 1308 1309 private ulong dynamicState_; 1310 1311 deprecated("Use dynamic styles instead now") { 1312 Color backgroundColor() { return backgroundColor_; } 1313 void backgroundColor(Color c){ this.backgroundColor_ = c; } 1314 1315 MouseCursor cursor() { return GenericCursor.Default; } 1316 } private Color backgroundColor_ = Color.transparent; 1317 1318 1319 /++ 1320 Style properties are defined as an accessory class so they can be referenced and overridden independently, but they are nested so you can refer to them easily by name (e.g. generic `Widget.Style` vs `Button.Style` and such). 1321 1322 It is here so there can be a specificity switch. 1323 1324 See [OverrideStyle] for a helper function to use your own. 1325 1326 History: 1327 Added May 11, 2021 1328 +/ 1329 static class Style/* : StyleProperties*/ { 1330 public Widget widget; // public because the mixin template needs access to it 1331 1332 /++ 1333 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 1334 1335 History: 1336 Added May 11, 2021, but changed on July 2, 2021 to return false by default. You MUST override this if you want declarative hover effects etc to take effect. 1337 +/ 1338 bool variesWithState(ulong dynamicStateFlags) { 1339 version(win32_widgets) { 1340 if(widget.hwnd) 1341 return false; 1342 } 1343 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 1344 } 1345 1346 /// 1347 Color foregroundColor() { 1348 return WidgetPainter.visualTheme.foregroundColor; 1349 } 1350 1351 /// 1352 WidgetBackground background() { 1353 // the default is a "transparent" background, which means 1354 // it goes as far up as it can to get the color 1355 if (widget.backgroundColor_ != Color.transparent) 1356 return WidgetBackground(widget.backgroundColor_); 1357 if (widget.parent) 1358 return widget.parent.getComputedStyle.background; 1359 return WidgetBackground(widget.backgroundColor_); 1360 } 1361 1362 private static OperatingSystemFont fontCached_; 1363 private OperatingSystemFont fontCached() { 1364 if(fontCached_ is null) 1365 fontCached_ = font(); 1366 return fontCached_; 1367 } 1368 1369 /++ 1370 Returns the default font to be used with this widget. The return value will be cached by the library, so you can not expect live updates. 1371 +/ 1372 OperatingSystemFont font() { 1373 return null; 1374 } 1375 1376 /++ 1377 Returns the cursor that should be used over this widget. You may change this and updates will be reflected next time the mouse enters the widget. 1378 1379 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 1380 1381 History: 1382 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 1383 +/ 1384 MouseCursor cursor() { 1385 return GenericCursor.Default; 1386 } 1387 1388 FrameStyle borderStyle() { 1389 return FrameStyle.none; 1390 } 1391 1392 /++ 1393 +/ 1394 Color borderColor() { 1395 return Color.transparent; 1396 } 1397 1398 FrameStyle outlineStyle() { 1399 if(widget.dynamicState & DynamicState.focus) 1400 return FrameStyle.dotted; 1401 else 1402 return FrameStyle.none; 1403 } 1404 1405 Color outlineColor() { 1406 return foregroundColor; 1407 } 1408 } 1409 1410 /++ 1411 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 1412 The basic usage is simple: 1413 1414 --- 1415 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 1416 // override style hints as-needed here 1417 } 1418 OverrideStyle!Style; // add the method 1419 --- 1420 1421 $(TIP 1422 While the class is not forced to be `static`, for best results, it should be. A non-static class 1423 can not be inherited by other objects whereas the static one can. A property on the base class, 1424 called [Widget.Style.widget|widget], is available for you to access its properties. 1425 ) 1426 1427 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 1428 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 1429 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 1430 1431 1432 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 1433 You may also just override `variesWithState` when you use this flag. 1434 1435 --- 1436 mixin OverrideStyle!( 1437 DynamicState.focus, YourFocusedStyle, 1438 DynamicState.hover, YourHoverStyle, 1439 YourDefaultStyle 1440 ) 1441 --- 1442 1443 It checks if `dynamicState` matches the state and if so, returns the object given. 1444 1445 If there is no state mask given, the next one matches everything. The first match given is used. 1446 1447 However, since in most cases you'll want check state inside your individual methods, you probably won't 1448 find much use for this whole-class swap out. 1449 1450 History: 1451 Added May 16, 2021 1452 +/ 1453 static protected mixin template OverrideStyle(S...) { 1454 static import amg = arsd.minigui; 1455 override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) { 1456 ulong mask = 0; 1457 foreach(idx, thing; S) { 1458 static if(is(typeof(thing) : ulong)) { 1459 mask = thing; 1460 } else { 1461 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 1462 //static assert(!__traits(isNested, thing), thing.stringof ~ " is a nested class. For best results, mark it `static`. You can still access the widget through a `widget` variable inside the Style class."); 1463 scope amg.Widget.Style s = new thing(); 1464 s.widget = this; 1465 dg(s); 1466 return; 1467 } 1468 } 1469 } 1470 } 1471 } 1472 /++ 1473 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 1474 +/ 1475 void useStyleProperties(scope void delegate(scope Style props) dg) { 1476 scope Style s = new Style(); 1477 s.widget = this; 1478 dg(s); 1479 } 1480 1481 1482 protected void sendResizeEvent() { 1483 this.emit!ResizeEvent(); 1484 } 1485 1486 /++ 1487 Override this to provide a custom context menu for your widget. (x, y) is where the menu was requested. If x == -1 && y == -1, the menu was triggered by the keyboard instead of the mouse and it should use the current cursor, selection, or whatever would make sense for where a keyboard user's attention would currently be. 1488 1489 It should return an instance of the [Menu] object. You may choose to cache this object. To construct one, either make `new Menu("", this);` (the empty string there is the menu's label, but for a context menu, that is not important), then call the `menu.addItem(new Action("Label Text", 0 /* icon id */, () { on clicked handler }), menu);` and `menu.addSeparator() methods, or use `return createContextMenuFromAnnotatedCode(this, some_command_struct);` 1490 1491 Context menus are automatically triggered by default by the keyboard menu key, mouse right click, and possibly other conventions per platform. You can also invoke one by calling the [showContextMenu] method. 1492 1493 See_Also: 1494 [createContextMenuFromAnnotatedCode] 1495 +/ 1496 Menu contextMenu(int x, int y) { return null; } 1497 1498 /++ 1499 Shows the widget's context menu, as if the user right clicked at the x, y position. You should rarely, if ever, have to call this, since default event handlers will do it for you automatically. To control what menu shows up, you can pass one as `menuToShow`, but if you don't, it will call [contextMenu], which you can override on a per-widget basis. 1500 1501 History: 1502 The `menuToShow` parameter was added on March 19, 2025. 1503 +/ 1504 final bool showContextMenu(int x, int y, Menu menuToShow = null) { 1505 return showContextMenu(x, y, -2, -2, menuToShow); 1506 } 1507 1508 private final bool showContextMenu(int x, int y, int screenX, int screenY, Menu menu = null) { 1509 if(parentWindow is null || parentWindow.win is null) return false; 1510 1511 if(menu is null) 1512 menu = this.contextMenu(x, y); 1513 1514 if(menu is null) 1515 return false; 1516 1517 version(win32_widgets) { 1518 // FIXME: if it is -1, -1, do it at the current selection location instead 1519 // tho the corner of the window, which it does now, isn't the literal worst. 1520 1521 // i see notepad just seems to put it in the center of the window so idk 1522 1523 if(screenX < 0 && screenY < 0) { 1524 auto p = this.globalCoordinates(); 1525 if(screenX == -2) 1526 p.x += x; 1527 if(screenY == -2) 1528 p.y += y; 1529 1530 screenX = p.x; 1531 screenY = p.y; 1532 } 1533 1534 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1535 throw new Exception("TrackContextMenuEx"); 1536 } else version(custom_widgets) { 1537 menu.popup(this, x, y); 1538 } 1539 1540 return true; 1541 } 1542 1543 /++ 1544 Removes this widget from its parent. 1545 1546 History: 1547 `removeWidget` was made `final` on May 11, 2021. 1548 +/ 1549 @scriptable 1550 final void removeWidget() { 1551 auto p = this.parent; 1552 if(p) { 1553 int item; 1554 for(item = 0; item < p._children.length; item++) 1555 if(p._children[item] is this) 1556 break; 1557 auto idx = item; 1558 for(; item < p._children.length - 1; item++) 1559 p._children[item] = p._children[item + 1]; 1560 p._children = p._children[0 .. $-1]; 1561 1562 this.parent.widgetRemoved(idx, this); 1563 //this.parent = null; 1564 1565 p.queueRecomputeChildLayout(); 1566 } 1567 version(win32_widgets) { 1568 removeAllChildren(); 1569 if(hwnd) { 1570 DestroyWindow(hwnd); 1571 hwnd = null; 1572 } 1573 } 1574 } 1575 1576 /++ 1577 Notifies the subclass that a widget was removed. If you keep auxillary data about your children, you can override this to help keep that data in sync. 1578 1579 History: 1580 Added September 19, 2021 1581 +/ 1582 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 1583 1584 /++ 1585 Removes all child widgets from `this`. You should not use the removed widgets again. 1586 1587 Note that on Windows, it also destroys the native handles for the removed children recursively. 1588 1589 History: 1590 Added July 1, 2021 (dub v10.2) 1591 +/ 1592 void removeAllChildren() { 1593 version(win32_widgets) 1594 foreach(child; _children) { 1595 child.removeAllChildren(); 1596 if(child.hwnd) { 1597 DestroyWindow(child.hwnd); 1598 child.hwnd = null; 1599 } 1600 } 1601 auto orig = this._children; 1602 this._children = null; 1603 foreach(idx, w; orig) 1604 this.widgetRemoved(idx, w); 1605 1606 queueRecomputeChildLayout(); 1607 } 1608 1609 /++ 1610 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1611 +/ 1612 @scriptable 1613 Widget getChildByName(string name) { 1614 return getByName(name); 1615 } 1616 /++ 1617 Finds the nearest descendant with the requested type and [name]. May return `this`. 1618 +/ 1619 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1620 if(this.name == name) 1621 if(auto c = cast(WidgetClass) this) 1622 return c; 1623 foreach(child; children) { 1624 auto w = child.getByName(name); 1625 if(auto c = cast(WidgetClass) w) 1626 return c; 1627 } 1628 return null; 1629 } 1630 1631 /++ 1632 The name is a string tag that is used to reference the widget from scripts, gui loaders, declarative ui templates, etc. Similar to a HTML id attribute. 1633 Names should be unique in a window. 1634 1635 See_Also: [getByName], [getChildByName] 1636 +/ 1637 @scriptable string name; 1638 1639 private EventHandler[][string] bubblingEventHandlers; 1640 private EventHandler[][string] capturingEventHandlers; 1641 1642 /++ 1643 Default event handlers. These are called on the appropriate 1644 event unless [Event.preventDefault] is called on the event at 1645 some point through the bubbling process. 1646 1647 1648 If you are implementing your own widget and want to add custom 1649 events, you should follow the same pattern here: create a virtual 1650 function named `defaultEventHandler_eventname` with the implementation, 1651 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1652 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1653 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1654 This ensures virtual dispatch based on the correct subclass. 1655 1656 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1657 overridden version. 1658 1659 You only need to do that on parent classes adding NEW event types. If you 1660 just want to change the default behavior of an existing event type in a subclass, 1661 you override the function (and optionally call `super.method_name`) like normal. 1662 1663 History: 1664 Some of the events changed to take specific subclasses instead of generic `Event` 1665 on January 3, 2025. 1666 1667 +/ 1668 protected EventHandler[string] defaultEventHandlers; 1669 1670 /// ditto 1671 void setupDefaultEventHandlers() { 1672 defaultEventHandlers["click"] = (Widget t, Event event) { if(auto e = cast(ClickEvent) event) t.defaultEventHandler_click(e); }; 1673 defaultEventHandlers["dblclick"] = (Widget t, Event event) { if(auto e = cast(DoubleClickEvent) event) t.defaultEventHandler_dblclick(e); }; 1674 defaultEventHandlers["keydown"] = (Widget t, Event event) { if(auto e = cast(KeyDownEvent) event) t.defaultEventHandler_keydown(e); }; 1675 defaultEventHandlers["keyup"] = (Widget t, Event event) { if(auto e = cast(KeyUpEvent) event) t.defaultEventHandler_keyup(e); }; 1676 defaultEventHandlers["mouseover"] = (Widget t, Event event) { if(auto e = cast(MouseOverEvent) event) t.defaultEventHandler_mouseover(e); }; 1677 defaultEventHandlers["mouseout"] = (Widget t, Event event) { if(auto e = cast(MouseOutEvent) event) t.defaultEventHandler_mouseout(e); }; 1678 defaultEventHandlers["mousedown"] = (Widget t, Event event) { if(auto e = cast(MouseDownEvent) event) t.defaultEventHandler_mousedown(e); }; 1679 defaultEventHandlers["mouseup"] = (Widget t, Event event) { if(auto e = cast(MouseUpEvent) event) t.defaultEventHandler_mouseup(e); }; 1680 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { if(auto e = cast(MouseEnterEvent) event) t.defaultEventHandler_mouseenter(e); }; 1681 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { if(auto e = cast(MouseLeaveEvent) event) t.defaultEventHandler_mouseleave(e); }; 1682 defaultEventHandlers["mousemove"] = (Widget t, Event event) { if(auto e = cast(MouseMoveEvent) event) t.defaultEventHandler_mousemove(e); }; 1683 defaultEventHandlers["char"] = (Widget t, Event event) { if(auto e = cast(CharEvent) event) t.defaultEventHandler_char(e); }; 1684 defaultEventHandlers["triggered"] = (Widget t, Event event) { if(auto e = cast(Event) event) t.defaultEventHandler_triggered(e); }; 1685 defaultEventHandlers["change"] = (Widget t, Event event) { if(auto e = cast(ChangeEventBase) event) t.defaultEventHandler_change(e); }; 1686 defaultEventHandlers["focus"] = (Widget t, Event event) { if(auto e = cast(FocusEvent) event) t.defaultEventHandler_focus(e); }; 1687 defaultEventHandlers["blur"] = (Widget t, Event event) { if(auto e = cast(BlurEvent) event) t.defaultEventHandler_blur(e); }; 1688 defaultEventHandlers["focusin"] = (Widget t, Event event) { if(auto e = cast(FocusInEvent) event) t.defaultEventHandler_focusin(e); }; 1689 defaultEventHandlers["focusout"] = (Widget t, Event event) { if(auto e = cast(FocusOutEvent) event) t.defaultEventHandler_focusout(e); }; 1690 } 1691 1692 /// ditto 1693 void defaultEventHandler_click(ClickEvent event) {} 1694 /// ditto 1695 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1696 /// ditto 1697 void defaultEventHandler_keydown(KeyDownEvent event) {} 1698 /// ditto 1699 void defaultEventHandler_keyup(KeyUpEvent event) {} 1700 /// ditto 1701 void defaultEventHandler_mousedown(MouseDownEvent event) { 1702 if(event.button == MouseButton.left) { 1703 if(this.tabStop) { 1704 this.focus(); 1705 } 1706 } else if(event.button == MouseButton.right) { 1707 showContextMenu(event.clientX, event.clientY); 1708 } 1709 } 1710 /// ditto 1711 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1712 /// ditto 1713 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1714 /// ditto 1715 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1716 /// ditto 1717 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1718 /// ditto 1719 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1720 /// ditto 1721 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1722 /// ditto 1723 void defaultEventHandler_char(CharEvent event) {} 1724 /// ditto 1725 void defaultEventHandler_triggered(Event event) {} 1726 /// ditto 1727 void defaultEventHandler_change(ChangeEventBase event) {} 1728 /// ditto 1729 void defaultEventHandler_focus(FocusEvent event) {} 1730 /// ditto 1731 void defaultEventHandler_blur(BlurEvent event) {} 1732 /// ditto 1733 void defaultEventHandler_focusin(FocusInEvent event) {} 1734 /// ditto 1735 void defaultEventHandler_focusout(FocusOutEvent event) {} 1736 1737 /++ 1738 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1739 1740 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1741 1742 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1743 of participating in handler delegation. 1744 1745 $(TIP 1746 Use `scope` on your handlers when you can. While it currently does nothing, this will future-proof your code against future optimizations I want to do. Instead of copying whole event objects out if you do need to store them, just copy the properties you need. 1747 ) 1748 +/ 1749 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1750 return addEventListener(event, (Widget, scope Event e) { 1751 if(e.srcElement is this) 1752 handler(); 1753 }, useCapture); 1754 } 1755 1756 /// ditto 1757 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1758 return addEventListener(event, (Widget, Event e) { 1759 if(e.srcElement is this) 1760 handler(e); 1761 }, useCapture); 1762 } 1763 1764 /// ditto 1765 EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) { 1766 static if(is(Handler Fn == delegate)) { 1767 static if(is(Fn Params == __parameters)) { 1768 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1769 if(e.srcElement !is this) 1770 return; 1771 auto ty = cast(Params[0]) e; 1772 if(ty !is null) 1773 handler(ty); 1774 }, useCapture); 1775 } else static assert(0); 1776 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1777 } 1778 1779 /// ditto 1780 @scriptable 1781 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1782 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1783 } 1784 1785 /// ditto 1786 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1787 static if(is(Handler Fn == delegate)) { 1788 static if(is(Fn Params == __parameters)) { 1789 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1790 auto ty = cast(Params[0]) e; 1791 if(ty !is null) 1792 handler(ty); 1793 }, useCapture); 1794 } else static assert(0); 1795 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1796 } 1797 1798 /// ditto 1799 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1800 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1801 } 1802 1803 /// ditto 1804 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1805 if(event.length > 2 && event[0..2] == "on") 1806 event = event[2 .. $]; 1807 1808 if(useCapture) 1809 capturingEventHandlers[event] ~= handler; 1810 else 1811 bubblingEventHandlers[event] ~= handler; 1812 1813 return EventListener(this, event, handler, useCapture); 1814 } 1815 1816 /// ditto 1817 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1818 if(event.length > 2 && event[0..2] == "on") 1819 event = event[2 .. $]; 1820 1821 if(useCapture) { 1822 if(event in capturingEventHandlers) 1823 foreach(ref evt; capturingEventHandlers[event]) 1824 if(evt is handler) evt = null; 1825 } else { 1826 if(event in bubblingEventHandlers) 1827 foreach(ref evt; bubblingEventHandlers[event]) 1828 if(evt is handler) evt = null; 1829 } 1830 } 1831 1832 /// ditto 1833 void removeEventListener(EventListener listener) { 1834 removeEventListener(listener.event, listener.handler, listener.useCapture); 1835 } 1836 1837 static if(UsingSimpledisplayX11) { 1838 void discardXConnectionState() { 1839 foreach(child; children) 1840 child.discardXConnectionState(); 1841 } 1842 1843 void recreateXConnectionState() { 1844 foreach(child; children) 1845 child.recreateXConnectionState(); 1846 redraw(); 1847 } 1848 } 1849 1850 /++ 1851 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1852 1853 History: 1854 `globalCoordinates` was made `final` on May 11, 2021. 1855 +/ 1856 Point globalCoordinates() { 1857 int x = this.x; 1858 int y = this.y; 1859 auto p = this.parent; 1860 while(p) { 1861 x += p.x; 1862 y += p.y; 1863 p = p.parent; 1864 } 1865 1866 static if(UsingSimpledisplayX11) { 1867 auto dpy = XDisplayConnection.get; 1868 arsd.simpledisplay.Window dummyw; 1869 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1870 } else version(Windows) { 1871 POINT pt; 1872 pt.x = x; 1873 pt.y = y; 1874 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1875 x = pt.x; 1876 y = pt.y; 1877 } else { 1878 featureNotImplemented(); 1879 } 1880 1881 return Point(x, y); 1882 } 1883 1884 version(win32_widgets) 1885 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1886 1887 version(win32_widgets) 1888 /// Called when a WM_COMMAND is sent to the associated hwnd. 1889 void handleWmCommand(ushort cmd, ushort id) {} 1890 1891 version(win32_widgets) 1892 /++ 1893 Called when a WM_NOTIFY is sent to the associated hwnd. 1894 1895 History: 1896 +/ 1897 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1898 1899 version(win32_widgets) 1900 deprecated("This overload is problematic since it is liable to discard return values. Add the `out int mustReturn` to your override as the last parameter and set it to 1 when you must forward the return value to Windows. Otherwise, you can just add the parameter then ignore it and use the default value of 0 to maintain the status quo.") int handleWmNotify(NMHDR* hdr, int code) { int ignored; return handleWmNotify(hdr, code, ignored); } 1901 1902 /++ 1903 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1904 1905 Updates to this variable will only be made visible on the next mouse enter event. 1906 +/ 1907 @scriptable string statusTip; 1908 // string toolTip; 1909 // string helpText; 1910 1911 /++ 1912 If true, this widget can be focused via keyboard control with the tab key. 1913 1914 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1915 +/ 1916 bool tabStop = true; 1917 /++ 1918 The tab key cycles through widgets by the order of a.tabOrder < b.tabOrder. If they are equal, it does them in child order (which is typically the order they were added to the widget.) 1919 +/ 1920 int tabOrder; 1921 1922 version(win32_widgets) { 1923 static Widget[HWND] nativeMapping; 1924 /// The native handle, if there is one. 1925 HWND hwnd; 1926 WNDPROC originalWindowProcedure; 1927 1928 SimpleWindow simpleWindowWrappingHwnd; 1929 1930 // please note it IGNORES your return value and does NOT forward it to Windows! 1931 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1932 return 0; 1933 } 1934 } 1935 private bool implicitlyCreated; 1936 1937 /// Child's position relative to the parent's origin. only the layout manager should be modifying this and even reading it is of limited utility. It may be made `private` at some point in the future without advance notice. Do NOT depend on it being available unless you are writing a layout manager. 1938 int x; 1939 /// ditto 1940 int y; 1941 private int _width; 1942 private int _height; 1943 private Widget[] _children; 1944 private Widget _parent; 1945 private Window _parentWindow; 1946 1947 /++ 1948 Returns the window to which this widget is attached. 1949 1950 History: 1951 Prior to May 11, 2021, the `Window parentWindow` variable was directly available. Now, only this property getter is available and the actual store is private. 1952 +/ 1953 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1954 private @property void parentWindow(Window parent) { 1955 auto old = _parentWindow; 1956 _parentWindow = parent; 1957 newParentWindow(old, _parentWindow); 1958 foreach(child; children) 1959 child.parentWindow = parent; // please note that this is recursive 1960 } 1961 1962 /++ 1963 Called when the widget has been added to or remove from a parent window. 1964 1965 Note that either oldParent and/or newParent may be null any time this is called. 1966 1967 History: 1968 Added September 13, 2024 1969 +/ 1970 protected void newParentWindow(Window oldParent, Window newParent) {} 1971 1972 /++ 1973 Returns the list of the widget's children. 1974 1975 History: 1976 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1977 1978 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1979 +/ 1980 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1981 1982 /++ 1983 Returns the widget's parent. 1984 1985 History: 1986 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1987 1988 The parent should only be managed by the [addChild] and [removeWidget] method. 1989 +/ 1990 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1991 1992 /// The widget's current size. 1993 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1994 /// ditto 1995 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1996 1997 /// Only the layout manager should be calling these. 1998 final protected @property int width(int a) @safe { return _width = a; } 1999 /// ditto 2000 final protected @property int height(int a) @safe { return _height = a; } 2001 2002 /++ 2003 This function is called by the layout engine after it has updated the position (in variables `x` and `y`) and the size (in properties `width` and `height`) to give you a chance to update the actual position of the native child window (if there is one) or whatever. 2004 2005 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 2006 +/ 2007 protected void registerMovement() { 2008 version(win32_widgets) { 2009 if(hwnd) { 2010 auto pos = getChildPositionRelativeToParentHwnd(this); 2011 MoveWindow(hwnd, pos[0], pos[1], width, height, true); // setting this to false can sometimes speed things up but only if it is actually drawn later and that's kinda iffy to do right here so being slower but safer rn 2012 this.redraw(); 2013 } 2014 } 2015 sendResizeEvent(); 2016 } 2017 2018 /// Creates the widget and adds it to the parent. 2019 this(Widget parent) { 2020 if(parent !is null) 2021 parent.addChild(this); 2022 setupDefaultEventHandlers(); 2023 } 2024 2025 /// Returns true if this is the current focused widget inside the parent window. Please note it may return `true` when the window itself is unfocused. In that case, it indicates this widget will receive focuse again when the window does. 2026 @scriptable 2027 bool isFocused() { 2028 return parentWindow && parentWindow.focusedWidget is this; 2029 } 2030 2031 private bool showing_ = true; 2032 /// 2033 bool showing() const { return showing_; } 2034 /// 2035 bool hidden() const { return !showing_; } 2036 /++ 2037 Shows or hides the window. Meant to be assigned as a property. If `recalculate` is true (the default), it recalculates the layout of the parent widget to use the space this widget being hidden frees up or make space for this widget to appear again. 2038 2039 Note that a widget only ever shows if all its parents are showing too. 2040 +/ 2041 void showing(bool s, bool recalculate = true) { 2042 if(s != showing_) { 2043 showing_ = s; 2044 // writeln(typeid(this).toString, " ", this.parent ? typeid(this.parent).toString : "null", " ", s); 2045 2046 showNativeWindowChildren(s); 2047 2048 if(parent && recalculate) { 2049 parent.queueRecomputeChildLayout(); 2050 parent.redraw(); 2051 } 2052 2053 if(s) { 2054 queueRecomputeChildLayout(); 2055 redraw(); 2056 } 2057 } 2058 } 2059 /// Convenience method for `showing = true` 2060 @scriptable 2061 void show() { 2062 showing = true; 2063 } 2064 /// Convenience method for `showing = false` 2065 @scriptable 2066 void hide() { 2067 showing = false; 2068 } 2069 2070 /++ 2071 If you are a native window, show/hide it based on shouldShow and return `true`. 2072 2073 Otherwise, do nothing and return false. 2074 +/ 2075 protected bool showOrHideIfNativeWindow(bool shouldShow) { 2076 version(win32_widgets) { 2077 if(hwnd) { 2078 ShowWindow(hwnd, shouldShow ? SW_SHOW : SW_HIDE); 2079 return true; 2080 } else { 2081 return false; 2082 } 2083 } else { 2084 return false; 2085 } 2086 } 2087 2088 private void showNativeWindowChildren(bool s) { 2089 if(!showOrHideIfNativeWindow(s && showing)) 2090 foreach(child; children) 2091 child.showNativeWindowChildren(s); 2092 } 2093 2094 /// 2095 @scriptable 2096 void focus() { 2097 assert(parentWindow !is null); 2098 if(isFocused()) 2099 return; 2100 2101 if(parentWindow.focusedWidget) { 2102 // FIXME: more details here? like from and to 2103 auto from = parentWindow.focusedWidget; 2104 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 2105 parentWindow.focusedWidget = null; 2106 from.emit!BlurEvent(); 2107 from.emit!FocusOutEvent(); 2108 } 2109 2110 2111 version(win32_widgets) { 2112 if(this.hwnd !is null) 2113 SetFocus(this.hwnd); 2114 } 2115 //else static if(UsingSimpledisplayX11) 2116 //this.parentWindow.win.focus(); 2117 2118 parentWindow.focusedWidget = this; 2119 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 2120 this.emit!FocusEvent(); 2121 this.emit!FocusInEvent(); 2122 } 2123 2124 /+ 2125 /++ 2126 Unfocuses the widget. This may reset 2127 +/ 2128 @scriptable 2129 void blur() { 2130 2131 } 2132 +/ 2133 2134 2135 /++ 2136 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 2137 2138 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 2139 +/ 2140 void attachedToWindow(Window w) {} 2141 /++ 2142 Callback when the widget is added to another widget. 2143 2144 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 2145 +/ 2146 void addedTo(Widget w) {} 2147 2148 /++ 2149 Adds a child to the given position. This is `protected` because you generally shouldn't be calling this directly. Instead, construct widgets with the parent directly. 2150 2151 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 2152 +/ 2153 protected void addChild(Widget w, int position = int.max) { 2154 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 2155 assert(w !is this, "Child cannot be its own parent!"); 2156 w._parent = this; 2157 if(position == int.max || position == children.length) { 2158 _children ~= w; 2159 } else { 2160 assert(position < _children.length); 2161 _children.length = _children.length + 1; 2162 for(int i = cast(int) _children.length - 1; i > position; i--) 2163 _children[i] = _children[i - 1]; 2164 _children[position] = w; 2165 } 2166 2167 this.parentWindow = this._parentWindow; 2168 2169 w.addedTo(this); 2170 2171 bool parentIsNative; 2172 version(win32_widgets) { 2173 parentIsNative = hwnd !is null; 2174 } 2175 if(!parentIsNative && !showing) 2176 w.showOrHideIfNativeWindow(false); 2177 2178 if(parentWindow !is null) { 2179 w.attachedToWindow(parentWindow); 2180 parentWindow.queueRecomputeChildLayout(); 2181 parentWindow.redraw(); 2182 } 2183 } 2184 2185 /++ 2186 Finds the child at the top of the z-order at the given coordinates (relative to the `this` widget's origin), or null if none are found. 2187 +/ 2188 Widget getChildAtPosition(int x, int y) { 2189 // it goes backward so the last one to show gets picked first 2190 // might use z-index later 2191 foreach_reverse(child; children) { 2192 if(child.hidden) 2193 continue; 2194 if(child.x <= x && child.y <= y 2195 && ((x - child.x) < child.width) 2196 && ((y - child.y) < child.height)) 2197 { 2198 return child; 2199 } 2200 } 2201 2202 return null; 2203 } 2204 2205 /++ 2206 If the widget is a scrollable container, this should add the current scroll position to the given coordinates so the mouse events can be dispatched correctly. 2207 2208 History: 2209 Added July 2, 2021 (v10.2) 2210 +/ 2211 protected void addScrollPosition(ref int x, ref int y) {} 2212 2213 /++ 2214 Responsible for actually painting the widget to the screen. The clip rectangle and coordinate translation in the [WidgetPainter] are pre-configured so you can draw independently. 2215 2216 This function paints the entire widget, including styled borders, backgrounds, etc. You are also responsible for displaying any important active state to the user, including if you hold the active keyboard focus. If you only want to be responsible for the content while letting the style engine draw the rest, override [paintContent] instead. 2217 2218 [paint] is not called for system widgets as the OS library draws them instead. 2219 2220 2221 The default implementation forwards to [WidgetPainter.drawThemed], passing [paintContent] as the delegate. If you override this, you might use those same functions or you can do your own thing. 2222 2223 You should also look at [WidgetPainter.visualTheme] to be theme aware. 2224 2225 History: 2226 Prior to May 15, 2021, the default implementation was empty. Now, it is `painter.drawThemed(&paintContent);`. You may wish to override [paintContent] instead of [paint] to take advantage of the new styling engine. 2227 +/ 2228 void paint(WidgetPainter painter) { 2229 version(win32_widgets) 2230 if(hwnd) { 2231 return; 2232 } 2233 painter.drawThemed(&paintContent); // note this refers to the following overload 2234 } 2235 2236 /++ 2237 Responsible for drawing the content as the theme engine is responsible for other elements. 2238 2239 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 2240 2241 Params: 2242 painter = your painter (forwarded from [paint]) for drawing on the widget. The clip rectangle and coordinate translation are prepared for you ahead of time so you can use widget coordinates. It also has the theme foreground preloaded into the painter outline color, the theme font preloaded as the painter's active font, and the theme background preloaded as the painter's fill color. 2243 2244 bounds = the bounds, inside the widget, where your content should be drawn. This is the rectangle inside the border and padding (if any). The stuff outside is not clipped - it is still part of your widget - but you should respect these bounds for visual consistency and respecting the theme's area. 2245 2246 If you do want to clip it, you can of course call `auto oldClip = painter.setClipRectangle(bounds); scope(exit) painter.setClipRectangle(oldClip);` to modify it and return to the previous setting when you return. 2247 2248 Returns: 2249 The rectangle representing your actual content. Typically, this is simply `return bounds;`. The theme engine uses this return value to determine where the outline and overlay should be. 2250 2251 History: 2252 Added May 15, 2021 2253 +/ 2254 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2255 return bounds; 2256 } 2257 2258 deprecated("Change ScreenPainter to WidgetPainter") 2259 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 2260 2261 /// I don't actually like the name of this 2262 /// this draws a background on it 2263 void erase(WidgetPainter painter) { 2264 version(win32_widgets) 2265 if(hwnd) return; // Windows will do it. I think. 2266 2267 auto c = getComputedStyle().background.color; 2268 painter.fillColor = c; 2269 painter.outlineColor = c; 2270 2271 version(win32_widgets) { 2272 HANDLE b, p; 2273 if(c.a == 0 && parent is parentWindow) { 2274 // I don't remember why I had this really... 2275 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 2276 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 2277 } 2278 } 2279 painter.drawRectangle(Point(0, 0), width, height); 2280 version(win32_widgets) { 2281 if(c.a == 0 && parent is parentWindow) { 2282 SelectObject(painter.impl.hdc, p); 2283 SelectObject(painter.impl.hdc, b); 2284 } 2285 } 2286 } 2287 2288 /// 2289 WidgetPainter draw() { 2290 int x = this.x, y = this.y; 2291 auto parent = this.parent; 2292 while(parent) { 2293 x += parent.x; 2294 y += parent.y; 2295 parent = parent.parent; 2296 } 2297 2298 auto painter = parentWindow.win.draw(true); 2299 painter.originX = x; 2300 painter.originY = y; 2301 painter.setClipRectangle(Point(0, 0), width, height); 2302 return WidgetPainter(painter, this); 2303 } 2304 2305 /// This can be overridden by scroll things. It is responsible for actually calling [paint]. Do not override unless you've studied minigui.d's source code. There are no stability guarantees if you do override this; it can (and likely will) break without notice. 2306 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 2307 if(hidden) 2308 return; 2309 2310 int paintX = x; 2311 int paintY = y; 2312 if(this.useNativeDrawing()) { 2313 paintX = 0; 2314 paintY = 0; 2315 lox = 0; 2316 loy = 0; 2317 containment = Rectangle(0, 0, int.max, int.max); 2318 } 2319 2320 painter.originX = lox + paintX; 2321 painter.originY = loy + paintY; 2322 2323 bool actuallyPainted = false; 2324 2325 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 2326 if(clip == Rectangle.init) { 2327 // writeln(this, " clipped out"); 2328 return; 2329 } 2330 2331 bool invalidateChildren = invalidate; 2332 2333 if(redrawRequested || force) { 2334 painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 2335 2336 painter.drawingUpon = this; 2337 2338 erase(painter); 2339 if(painter.visualTheme) 2340 painter.visualTheme.doPaint(this, painter); 2341 else 2342 paint(painter); 2343 2344 if(invalidate) { 2345 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 2346 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 2347 painter.invalidateRect(region); 2348 // children are contained inside this, so no need to do extra work 2349 invalidateChildren = false; 2350 } 2351 2352 redrawRequested = false; 2353 actuallyPainted = true; 2354 } 2355 2356 foreach(child; children) { 2357 version(win32_widgets) 2358 if(child.useNativeDrawing()) continue; 2359 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 2360 } 2361 2362 version(win32_widgets) 2363 foreach(child; children) { 2364 if(child.useNativeDrawing) { 2365 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 2366 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, true); // have to reset the invalidate flag since these are not necessarily affected the same way, being native children with a clip 2367 } 2368 } 2369 } 2370 2371 protected bool useNativeDrawing() nothrow { 2372 version(win32_widgets) 2373 return hwnd !is null; 2374 else 2375 return false; 2376 } 2377 2378 private static class RedrawEvent {} 2379 private __gshared re = new RedrawEvent(); 2380 2381 private bool redrawRequested; 2382 /// 2383 final void redraw(string file = __FILE__, size_t line = __LINE__) { 2384 redrawRequested = true; 2385 2386 if(this.parentWindow) { 2387 auto sw = this.parentWindow.win; 2388 assert(sw !is null); 2389 if(!sw.eventQueued!RedrawEvent) { 2390 sw.postEvent(re); 2391 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 2392 } 2393 } 2394 } 2395 2396 private SimpleWindow drawableWindow; 2397 2398 /++ 2399 Allows a class to easily dispatch its own statically-declared event (see [Emits]). The main benefit of using this over constructing an event yourself is simply that you ensure you haven't sent something you haven't documented you can send. 2400 2401 Returns: 2402 `true` if you should do your default behavior. 2403 2404 History: 2405 Added May 5, 2021 2406 2407 Bugs: 2408 It does not do the static checks on gdc right now. 2409 +/ 2410 final protected bool emit(EventType, this This, Args...)(Args args) { 2411 version(GNU) {} else 2412 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 2413 auto e = new EventType(this, args); 2414 e.dispatch(); 2415 return !e.defaultPrevented; 2416 } 2417 /// ditto 2418 final protected bool emit(string eventString, this This)() { 2419 auto e = new Event(eventString, this); 2420 e.dispatch(); 2421 return !e.defaultPrevented; 2422 } 2423 2424 /++ 2425 Does the same as [addEventListener]'s delegate overload, but adds an additional check to ensure the event you are subscribing to is actually emitted by the static type you are using. Since it works on static types, if you have a generic [Widget], this can only subscribe to events declared as [Emits] inside [Widget] itself, not any child classes nor any child elements. If this is too restrictive, simply use [addEventListener] instead. 2426 2427 History: 2428 Added May 5, 2021 2429 +/ 2430 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 2431 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 2432 return addEventListener(handler); 2433 } 2434 2435 /++ 2436 Gets the computed style properties from the visual theme. 2437 2438 You should use this in your paint and layout functions instead of the direct properties on the widget if you want to be style aware. (But when setting defaults in your classes, overriding is the right thing to do. Override to set defaults, but then read out of [getComputedStyle].) 2439 2440 History: 2441 Added May 8, 2021 2442 +/ 2443 final StyleInformation getComputedStyle() { 2444 return StyleInformation(this); 2445 } 2446 2447 int focusableWidgets(scope int delegate(Widget) dg) { 2448 foreach(widget; WidgetStream(this)) { 2449 if(widget.tabStop && !widget.hidden) { 2450 int result = dg(widget); 2451 if (result) 2452 return result; 2453 } 2454 } 2455 return 0; 2456 } 2457 2458 /++ 2459 Calculates the border box (that is, the full width/height of the widget, from border edge to border edge) 2460 for the given content box (the area between the padding) 2461 2462 History: 2463 Added January 4, 2023 (dub v11.0) 2464 +/ 2465 Rectangle borderBoxForContentBox(Rectangle contentBox) { 2466 auto cs = getComputedStyle(); 2467 2468 auto borderWidth = getBorderWidth(cs.borderStyle); 2469 2470 auto rect = contentBox; 2471 2472 rect.left -= borderWidth; 2473 rect.right += borderWidth; 2474 rect.top -= borderWidth; 2475 rect.bottom += borderWidth; 2476 2477 auto insideBorderRect = rect; 2478 2479 rect.left -= cs.paddingLeft; 2480 rect.right += cs.paddingRight; 2481 rect.top -= cs.paddingTop; 2482 rect.bottom += cs.paddingBottom; 2483 2484 return rect; 2485 } 2486 2487 2488 // FIXME: I kinda want to hide events from implementation widgets 2489 // so it just catches them all and stops propagation... 2490 // i guess i can do it with a event listener on star. 2491 2492 mixin Emits!KeyDownEvent; /// 2493 mixin Emits!KeyUpEvent; /// 2494 mixin Emits!CharEvent; /// 2495 2496 mixin Emits!MouseDownEvent; /// 2497 mixin Emits!MouseUpEvent; /// 2498 mixin Emits!ClickEvent; /// 2499 mixin Emits!DoubleClickEvent; /// 2500 mixin Emits!MouseMoveEvent; /// 2501 mixin Emits!MouseOverEvent; /// 2502 mixin Emits!MouseOutEvent; /// 2503 mixin Emits!MouseEnterEvent; /// 2504 mixin Emits!MouseLeaveEvent; /// 2505 2506 mixin Emits!ResizeEvent; /// 2507 2508 mixin Emits!BlurEvent; /// 2509 mixin Emits!FocusEvent; /// 2510 2511 mixin Emits!FocusInEvent; /// 2512 mixin Emits!FocusOutEvent; /// 2513 } 2514 2515 /+ 2516 /++ 2517 Interface to indicate that the widget has a simple value property. 2518 2519 History: 2520 Added August 26, 2021 2521 +/ 2522 interface HasValue!T { 2523 /// Getter 2524 @property T value(); 2525 /// Setter 2526 @property void value(T); 2527 } 2528 2529 /++ 2530 Interface to indicate that the widget has a range of possible values for its simple value property. 2531 This would be present on something like a slider or possibly a number picker. 2532 2533 History: 2534 Added September 11, 2021 2535 +/ 2536 interface HasRangeOfValues!T : HasValue!T { 2537 /// The minimum and maximum values in the range, inclusive. 2538 @property T minValue(); 2539 @property void minValue(T); /// ditto 2540 @property T maxValue(); /// ditto 2541 @property void maxValue(T); /// ditto 2542 2543 /// The smallest step the user interface allows. User may still type in values without this limitation. 2544 @property void step(T); 2545 @property T step(); /// ditto 2546 } 2547 2548 /++ 2549 Interface to indicate that the widget has a list of possible values the user can choose from. 2550 This would be present on something like a drop-down selector. 2551 2552 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2553 combobox. 2554 2555 History: 2556 Added September 11, 2021 2557 +/ 2558 interface HasListOfValues!T : HasValue!T { 2559 @property T[] values; 2560 @property void values(T[]); 2561 2562 @property int selectedIndex(); // note it may return -1! 2563 @property void selectedIndex(int); 2564 } 2565 +/ 2566 2567 /++ 2568 History: 2569 Added September 2021 (dub v10.4) 2570 +/ 2571 class GridLayout : Layout { 2572 2573 // FIXME: grid padding around edges and also cell spacing between units. even though you could do that by just specifying some gutter yourself in the layout. 2574 2575 /++ 2576 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2577 +/ 2578 enum Gravity { 2579 Center = 0, 2580 NorthWest = North | West, 2581 North = 0b10_00, 2582 NorthEast = North | East, 2583 West = 0b00_10, 2584 East = 0b00_01, 2585 SouthWest = South | West, 2586 South = 0b01_00, 2587 SouthEast = South | East, 2588 } 2589 2590 /++ 2591 The width and height are in some proportional units and can often just be 12. 2592 +/ 2593 this(int width, int height, Widget parent) { 2594 this.gridWidth = width; 2595 this.gridHeight = height; 2596 super(parent); 2597 } 2598 2599 /++ 2600 Sets the position of the given child. 2601 2602 The units of these arguments are in the proportional grid units you set in the constructor. 2603 +/ 2604 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2605 // ensure it is in bounds 2606 // then ensure no overlaps 2607 2608 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2609 2610 foreach(ref position; positions) { 2611 if(position.widget is child) { 2612 position = p; 2613 goto set; 2614 } 2615 } 2616 2617 positions ~= p; 2618 2619 set: 2620 2621 // FIXME: should this batch? 2622 queueRecomputeChildLayout(); 2623 2624 return child; 2625 } 2626 2627 override void addChild(Widget w, int position = int.max) { 2628 super.addChild(w, position); 2629 //positions ~= ChildPosition(w); 2630 if(position != int.max) { 2631 // FIXME: align it so they actually match. 2632 } 2633 } 2634 2635 override void widgetRemoved(size_t idx, Widget w) { 2636 // FIXME: keep the positions array aligned 2637 // positions[idx].widget = null; 2638 } 2639 2640 override void recomputeChildLayout() { 2641 registerMovement(); 2642 int onGrid = cast(int) positions.length; 2643 c: foreach(child; children) { 2644 // just snap it to the grid 2645 if(onGrid) 2646 foreach(position; positions) 2647 if(position.widget is child) { 2648 child.x = this.width * position.x / this.gridWidth; 2649 child.y = this.height * position.y / this.gridHeight; 2650 child.width = this.width * position.width / this.gridWidth; 2651 child.height = this.height * position.height / this.gridHeight; 2652 2653 auto diff = child.width - child.maxWidth(); 2654 // FIXME: gravity? 2655 if(diff > 0) { 2656 child.width = child.width - diff; 2657 2658 if(position.gravity & Gravity.West) { 2659 // nothing needed, already aligned 2660 } else if(position.gravity & Gravity.East) { 2661 child.x += diff; 2662 } else { 2663 child.x += diff / 2; 2664 } 2665 } 2666 2667 diff = child.height - child.maxHeight(); 2668 // FIXME: gravity? 2669 if(diff > 0) { 2670 child.height = child.height - diff; 2671 2672 if(position.gravity & Gravity.North) { 2673 // nothing needed, already aligned 2674 } else if(position.gravity & Gravity.South) { 2675 child.y += diff; 2676 } else { 2677 child.y += diff / 2; 2678 } 2679 } 2680 child.recomputeChildLayout(); 2681 onGrid--; 2682 continue c; 2683 } 2684 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2685 } 2686 } 2687 2688 private struct ChildPosition { 2689 Widget widget; 2690 int x; 2691 int y; 2692 int width; 2693 int height; 2694 Gravity gravity; 2695 } 2696 private ChildPosition[] positions; 2697 2698 int gridWidth = 12; 2699 int gridHeight = 12; 2700 } 2701 2702 /// 2703 abstract class ComboboxBase : Widget { 2704 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2705 // or to always show the list, we want CBS_SIMPLE == 1 2706 version(win32_widgets) 2707 this(uint style, Widget parent) { 2708 super(parent); 2709 createWin32Window(this, "ComboBox"w, null, style); 2710 } 2711 else version(custom_widgets) 2712 this(Widget parent) { 2713 super(parent); 2714 2715 addEventListener((KeyDownEvent event) { 2716 if(event.key == Key.Up) { 2717 setSelection(selection_-1); 2718 event.preventDefault(); 2719 } 2720 if(event.key == Key.Down) { 2721 setSelection(selection_+1); 2722 event.preventDefault(); 2723 } 2724 2725 }); 2726 2727 } 2728 else static assert(false); 2729 2730 protected void scrollSelectionIntoView() {} 2731 2732 /++ 2733 Returns the current list of options in the selection. 2734 2735 History: 2736 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2737 +/ 2738 final @property string[] options() const { 2739 return cast(string[]) options_; 2740 } 2741 2742 /++ 2743 Replaces the list of options in the box. Note that calling this will also reset the selection. 2744 2745 History: 2746 Added December, 29 2024 2747 +/ 2748 final @property void options(string[] options) { 2749 version(win32_widgets) 2750 SendMessageW(hwnd, 331 /*CB_RESETCONTENT*/, 0, 0); 2751 selection_ = -1; 2752 options_ = null; 2753 foreach(opt; options) 2754 addOption(opt); 2755 2756 version(custom_widgets) 2757 redraw(); 2758 } 2759 2760 private string[] options_; 2761 private int selection_ = -1; 2762 2763 /++ 2764 Adds an option to the end of options array. 2765 +/ 2766 void addOption(string s) { 2767 options_ ~= s; 2768 version(win32_widgets) 2769 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2770 } 2771 2772 /++ 2773 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2774 +/ 2775 int getSelection() { 2776 return selection_; 2777 } 2778 2779 /++ 2780 Returns the current selection as a string. 2781 2782 History: 2783 Added November 17, 2021 2784 +/ 2785 string getSelectionString() { 2786 return selection_ == -1 ? null : options[selection_]; 2787 } 2788 2789 /++ 2790 Sets the current selection to an index in the options array, or to the given option if present. 2791 Please note that the string version may do a linear lookup. 2792 2793 Returns: 2794 the index you passed in 2795 2796 History: 2797 The `string` based overload was added on March 1, 2022 (dub v10.7). 2798 2799 The return value was `void` prior to March 1, 2022. 2800 +/ 2801 int setSelection(int idx) { 2802 if(idx < -1) 2803 idx = -1; 2804 if(idx + 1 > options.length) 2805 idx = cast(int) options.length - 1; 2806 2807 selection_ = idx; 2808 2809 version(win32_widgets) 2810 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2811 2812 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2813 t.dispatch(); 2814 2815 scrollSelectionIntoView(); 2816 2817 return idx; 2818 } 2819 2820 /// ditto 2821 int setSelection(string s) { 2822 if(s !is null) 2823 foreach(idx, item; options) 2824 if(item == s) { 2825 return setSelection(cast(int) idx); 2826 } 2827 return setSelection(-1); 2828 } 2829 2830 /++ 2831 This event is fired when the selection changes. Both [Event.stringValue] and 2832 [Event.intValue] are filled in - `stringValue` is the text in the selection 2833 and `intValue` is the index of the selection. If the combo box allows multiple 2834 selection, these values will include only one of the selected items - for those, 2835 you should loop through the values and check their selected flag instead. 2836 2837 (I know that sucks, but it is how it is right now.) 2838 2839 History: 2840 It originally inherited from `ChangeEvent!String`, but now does from [ChangeEventBase] as of January 3, 2025. 2841 This shouldn't break anything if you used it through either its own name `SelectionChangedEvent` or through the 2842 base `Event`, only if you specifically used `ChangeEvent!string` - those handlers may now get `null` or fail to 2843 be called. If you did do this, just change it to generic `Event`, as `stringValue` and `intValue` are already there. 2844 +/ 2845 static final class SelectionChangedEvent : ChangeEventBase { 2846 this(Widget target, int iv, string sv) { 2847 super(target); 2848 this.iv = iv; 2849 this.sv = sv; 2850 } 2851 immutable int iv; 2852 immutable string sv; 2853 2854 deprecated("Use stringValue or intValue instead") @property string value() { 2855 return sv; 2856 } 2857 2858 override @property string stringValue() { return sv; } 2859 override @property int intValue() { return iv; } 2860 } 2861 2862 version(win32_widgets) 2863 override void handleWmCommand(ushort cmd, ushort id) { 2864 if(cmd == CBN_SELCHANGE) { 2865 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2866 fireChangeEvent(); 2867 } 2868 } 2869 2870 private void fireChangeEvent() { 2871 if(selection_ >= options.length) 2872 selection_ = -1; 2873 2874 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2875 t.dispatch(); 2876 } 2877 2878 override int minWidth() { return scaleWithDpi(32); } 2879 2880 version(win32_widgets) { 2881 override int minHeight() { return defaultLineHeight + 6; } 2882 override int maxHeight() { return defaultLineHeight + 6; } 2883 } else { 2884 override int minHeight() { return defaultLineHeight + 4; } 2885 override int maxHeight() { return defaultLineHeight + 4; } 2886 } 2887 2888 version(custom_widgets) 2889 void popup() { 2890 CustomComboBoxPopup popup = new CustomComboBoxPopup(this); 2891 } 2892 2893 } 2894 2895 private class CustomComboBoxPopup : Window { 2896 private ComboboxBase associatedWidget; 2897 private ListWidget lw; 2898 private bool cancelled; 2899 2900 this(ComboboxBase associatedWidget) { 2901 this.associatedWidget = associatedWidget; 2902 2903 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2904 2905 auto w = associatedWidget.width; 2906 // FIXME: suggestedDropdownHeight see below 2907 auto h = cast(int) associatedWidget.options.length * associatedWidget.defaultLineHeight + associatedWidget.scaleWithDpi(8); 2908 2909 // FIXME: this sux 2910 if(h > associatedWidget.parentWindow.height) 2911 h = associatedWidget.parentWindow.height; 2912 2913 auto mh = associatedWidget.scaleWithDpi(16 + 16 + 32); // to make the scrollbar look ok 2914 if(h < mh) 2915 h = mh; 2916 2917 auto coord = associatedWidget.globalCoordinates(); 2918 auto dropDown = new SimpleWindow( 2919 w, h, 2920 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, associatedWidget.parentWindow ? associatedWidget.parentWindow.win : null); 2921 2922 super(dropDown); 2923 2924 dropDown.move(coord.x, coord.y + associatedWidget.height); 2925 2926 this.lw = new ListWidget(this); 2927 version(custom_widgets) 2928 lw.multiSelect = false; 2929 foreach(option; associatedWidget.options) 2930 lw.addOption(option); 2931 2932 auto originalSelection = associatedWidget.getSelection; 2933 lw.setSelection(originalSelection); 2934 lw.scrollSelectionIntoView(); 2935 2936 /+ 2937 { 2938 auto cs = getComputedStyle(); 2939 auto painter = dropDown.draw(); 2940 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2941 auto p = Point(4, 4); 2942 painter.outlineColor = cs.foregroundColor; 2943 foreach(option; associatedWidget.options) { 2944 painter.drawText(p, option); 2945 p.y += defaultLineHeight; 2946 } 2947 } 2948 2949 dropDown.setEventHandlers( 2950 (MouseEvent event) { 2951 if(event.type == MouseEventType.buttonReleased) { 2952 dropDown.close(); 2953 auto element = (event.y - 4) / defaultLineHeight; 2954 if(element >= 0 && element <= associatedWidget.options.length) { 2955 associatedWidget.selection_ = element; 2956 2957 associatedWidget.fireChangeEvent(); 2958 } 2959 } 2960 } 2961 ); 2962 +/ 2963 2964 Widget previouslyFocusedWidget; 2965 2966 dropDown.visibilityChanged = (bool visible) { 2967 if(visible) { 2968 this.redraw(); 2969 captureMouse(this); 2970 //dropDown.grabInput(); 2971 2972 if(previouslyFocusedWidget is null) 2973 previouslyFocusedWidget = associatedWidget.parentWindow.focusedWidget; 2974 associatedWidget.parentWindow.focusedWidget = lw; 2975 } else { 2976 //dropDown.releaseInputGrab(); 2977 releaseMouseCapture(); 2978 2979 if(!cancelled) 2980 associatedWidget.setSelection(lw.getSelection); 2981 2982 associatedWidget.parentWindow.focusedWidget = previouslyFocusedWidget; 2983 } 2984 }; 2985 2986 dropDown.show(); 2987 } 2988 2989 private bool shouldCloseIfClicked(Widget w) { 2990 if(w is this) 2991 return true; 2992 version(custom_widgets) 2993 if(cast(TextListViewWidget.TextListViewItem) w) 2994 return true; 2995 return false; 2996 } 2997 2998 override void defaultEventHandler_click(ClickEvent ce) { 2999 if(ce.button == MouseButton.left && shouldCloseIfClicked(ce.target)) { 3000 this.win.close(); 3001 } 3002 } 3003 3004 override void defaultEventHandler_char(CharEvent ce) { 3005 if(ce.character == '\n') 3006 this.win.close(); 3007 } 3008 3009 override void defaultEventHandler_keydown(KeyDownEvent kde) { 3010 if(kde.key == Key.Escape) { 3011 cancelled = true; 3012 this.win.close(); 3013 }/+ else if(kde.key == Key.Up || kde.key == Key.Down) 3014 {} // intentionally blank, the list view handles these 3015 // separately from the scroll message widget default handler 3016 else if(lw && lw.glvw && lw.glvw.smw) 3017 lw.glvw.smw.defaultKeyboardListener(kde);+/ 3018 } 3019 } 3020 3021 /++ 3022 A drop-down list where the user must select one of the 3023 given options. Like `<select>` in HTML. 3024 3025 The current selection is given as a string or an index. 3026 It emits a SelectionChangedEvent when it changes. 3027 +/ 3028 class DropDownSelection : ComboboxBase { 3029 /++ 3030 Creates a drop down selection, optionally passing its initial list of options. 3031 3032 History: 3033 The overload with the `options` parameter was added December 29, 2024. 3034 +/ 3035 this(Widget parent) { 3036 version(win32_widgets) 3037 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 3038 else version(custom_widgets) { 3039 super(parent); 3040 3041 addEventListener("focus", () { this.redraw; }); 3042 addEventListener("blur", () { this.redraw; }); 3043 addEventListener(EventType.change, () { this.redraw; }); 3044 addEventListener("mousedown", () { this.focus(); this.popup(); }); 3045 addEventListener((KeyDownEvent event) { 3046 if(event.key == Key.Space) 3047 popup(); 3048 }); 3049 } else static assert(false); 3050 } 3051 3052 /// ditto 3053 this(string[] options, Widget parent) { 3054 this(parent); 3055 this.options = options; 3056 } 3057 3058 mixin Padding!q{2}; 3059 static class Style : Widget.Style { 3060 override FrameStyle borderStyle() { return FrameStyle.risen; } 3061 } 3062 mixin OverrideStyle!Style; 3063 3064 version(custom_widgets) 3065 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 3066 auto cs = getComputedStyle(); 3067 3068 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 3069 3070 painter.outlineColor = cs.foregroundColor; 3071 painter.fillColor = cs.foregroundColor; 3072 3073 /+ 3074 Point[4] triangle; 3075 enum padding = 6; 3076 enum paddingV = 7; 3077 enum triangleWidth = 10; 3078 triangle[0] = Point(width - padding - triangleWidth, paddingV); 3079 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 3080 triangle[2] = Point(width - padding - 0, paddingV); 3081 triangle[3] = triangle[0]; 3082 painter.drawPolygon(triangle[]); 3083 +/ 3084 3085 auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2); 3086 3087 painter.drawPolygon( 3088 scaleWithDpi(Point(2, 6) + offset), 3089 scaleWithDpi(Point(7, 11) + offset), 3090 scaleWithDpi(Point(12, 6) + offset), 3091 scaleWithDpi(Point(2, 6) + offset) 3092 ); 3093 3094 3095 return bounds; 3096 } 3097 3098 version(win32_widgets) 3099 override void registerMovement() { 3100 version(win32_widgets) { 3101 if(hwnd) { 3102 auto pos = getChildPositionRelativeToParentHwnd(this); 3103 // the height given to this from Windows' perspective is supposed 3104 // to include the drop down's height. so I add to it to give some 3105 // room for that. 3106 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 3107 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 3108 } 3109 } 3110 sendResizeEvent(); 3111 } 3112 } 3113 3114 /++ 3115 A text box with a drop down arrow listing selections. 3116 The user can choose from the list, or type their own. 3117 +/ 3118 class FreeEntrySelection : ComboboxBase { 3119 this(Widget parent) { 3120 this(null, parent); 3121 } 3122 3123 this(string[] options, Widget parent) { 3124 version(win32_widgets) 3125 super(2 /* CBS_DROPDOWN */, parent); 3126 else version(custom_widgets) { 3127 super(parent); 3128 auto hl = new HorizontalLayout(this); 3129 lineEdit = new LineEdit(hl); 3130 3131 tabStop = false; 3132 3133 // lineEdit.addEventListener((FocusEvent fe) { lineEdit.selectAll(); } ); 3134 3135 auto btn = new class ArrowButton { 3136 this() { 3137 super(ArrowDirection.down, hl); 3138 } 3139 override int heightStretchiness() { 3140 return 1; 3141 } 3142 override int heightShrinkiness() { 3143 return 1; 3144 } 3145 override int maxHeight() { 3146 return lineEdit.maxHeight; 3147 } 3148 }; 3149 //btn.addDirectEventListener("focus", &lineEdit.focus); 3150 btn.addEventListener("triggered", &this.popup); 3151 addEventListener(EventType.change, (Event event) { 3152 lineEdit.content = event.stringValue; 3153 lineEdit.focus(); 3154 redraw(); 3155 }); 3156 } 3157 else static assert(false); 3158 3159 this.options = options; 3160 } 3161 3162 string content() { 3163 version(win32_widgets) 3164 assert(0, "not implemented"); 3165 else version(custom_widgets) 3166 return lineEdit.content; 3167 else static assert(0); 3168 } 3169 3170 void content(string s) { 3171 version(win32_widgets) 3172 assert(0, "not implemented"); 3173 else version(custom_widgets) 3174 lineEdit.content = s; 3175 else static assert(0); 3176 } 3177 3178 version(custom_widgets) { 3179 LineEdit lineEdit; 3180 3181 override int widthStretchiness() { 3182 return lineEdit ? lineEdit.widthStretchiness : super.widthStretchiness; 3183 } 3184 override int flexBasisWidth() { 3185 return lineEdit ? lineEdit.flexBasisWidth : super.flexBasisWidth; 3186 } 3187 } 3188 } 3189 3190 /++ 3191 A combination of free entry with a list below it. 3192 +/ 3193 class ComboBox : ComboboxBase { 3194 this(Widget parent) { 3195 version(win32_widgets) 3196 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 3197 else version(custom_widgets) { 3198 super(parent); 3199 lineEdit = new LineEdit(this); 3200 listWidget = new ListWidget(this); 3201 listWidget.multiSelect = false; 3202 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 3203 string c = null; 3204 foreach(option; listWidget.options) 3205 if(option.selected) { 3206 c = option.label; 3207 break; 3208 } 3209 lineEdit.content = c; 3210 }); 3211 3212 listWidget.tabStop = false; 3213 this.tabStop = false; 3214 listWidget.addEventListener("focusin", &lineEdit.focus); 3215 this.addEventListener("focusin", &lineEdit.focus); 3216 3217 addDirectEventListener(EventType.change, { 3218 listWidget.setSelection(selection_); 3219 if(selection_ != -1) 3220 lineEdit.content = options[selection_]; 3221 lineEdit.focus(); 3222 redraw(); 3223 }); 3224 3225 lineEdit.addEventListener("focusin", &lineEdit.selectAll); 3226 3227 listWidget.addDirectEventListener(EventType.change, { 3228 int set = -1; 3229 foreach(idx, opt; listWidget.options) 3230 if(opt.selected) { 3231 set = cast(int) idx; 3232 break; 3233 } 3234 if(set != selection_) 3235 this.setSelection(set); 3236 }); 3237 } else static assert(false); 3238 } 3239 3240 override int minHeight() { return defaultLineHeight * 3; } 3241 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 3242 override int heightStretchiness() { return 5; } 3243 3244 version(custom_widgets) { 3245 LineEdit lineEdit; 3246 ListWidget listWidget; 3247 3248 override void addOption(string s) { 3249 listWidget.addOption(s); 3250 ComboboxBase.addOption(s); 3251 } 3252 3253 override void scrollSelectionIntoView() { 3254 listWidget.scrollSelectionIntoView(); 3255 } 3256 } 3257 } 3258 3259 /+ 3260 class Spinner : Widget { 3261 version(win32_widgets) 3262 this(Widget parent) { 3263 super(parent); 3264 parentWindow = parent.parentWindow; 3265 auto hlayout = new HorizontalLayout(this); 3266 lineEdit = new LineEdit(hlayout); 3267 upDownControl = new UpDownControl(hlayout); 3268 } 3269 3270 LineEdit lineEdit; 3271 UpDownControl upDownControl; 3272 } 3273 3274 class UpDownControl : Widget { 3275 version(win32_widgets) 3276 this(Widget parent) { 3277 super(parent); 3278 parentWindow = parent.parentWindow; 3279 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 3280 } 3281 3282 override int minHeight() { return defaultLineHeight; } 3283 override int maxHeight() { return defaultLineHeight * 3/2; } 3284 3285 override int minWidth() { return defaultLineHeight * 3/2; } 3286 override int maxWidth() { return defaultLineHeight * 3/2; } 3287 } 3288 +/ 3289 3290 /+ 3291 class DataView : Widget { 3292 // this is the omnibus data viewer 3293 // the internal data layout is something like: 3294 // string[string][] but also each node can have parents 3295 } 3296 +/ 3297 3298 3299 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 3300 3301 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 3302 3303 // FIXME: menus should prolly capture the mouse. ugh i kno. 3304 /* 3305 TextEdit needs: 3306 3307 * caret manipulation 3308 * selection control 3309 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 3310 3311 For example: 3312 3313 connect(paste, &textEdit.insertTextAtCaret); 3314 3315 would be nice. 3316 3317 3318 3319 I kinda want an omnibus dataview that combines list, tree, 3320 and table - it can be switched dynamically between them. 3321 3322 Flattening policy: only show top level, show recursive, show grouped 3323 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 3324 3325 Single select, multi select, organization, drag+drop 3326 */ 3327 3328 //static if(UsingSimpledisplayX11) 3329 version(win32_widgets) {} 3330 else version(custom_widgets) { 3331 enum scrollClickRepeatInterval = 50; 3332 3333 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 3334 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 3335 enum activeTabColor = lightAccentColor; 3336 enum hoveringColor = Color(228, 228, 228); 3337 enum buttonColor = windowBackgroundColor; 3338 enum depressedButtonColor = darkAccentColor; 3339 enum activeListXorColor = Color(255, 255, 127); 3340 enum progressBarColor = Color(0, 0, 128); 3341 enum activeMenuItemColor = Color(0, 0, 128); 3342 3343 }} 3344 else static assert(false); 3345 deprecated("Get these properties off the `visualTheme` instead.") { 3346 // these are used by horizontal rule so not just custom_widgets. for now at least. 3347 enum darkAccentColor = Color(172, 172, 172); 3348 enum lightAccentColor = Color(223, 223, 223); // used to be 223 3349 } 3350 3351 private const(wchar)* toWstringzInternal(in char[] s) { 3352 wchar[] str; 3353 str.reserve(s.length + 1); 3354 foreach(dchar ch; s) 3355 str ~= ch; 3356 str ~= '\0'; 3357 return str.ptr; 3358 } 3359 3360 static if(SimpledisplayTimerAvailable) 3361 void setClickRepeat(Widget w, int interval, int delay = 250) { 3362 Timer timer; 3363 int delayRemaining = delay / interval; 3364 if(delayRemaining <= 1) 3365 delayRemaining = 2; 3366 3367 immutable originalDelayRemaining = delayRemaining; 3368 3369 w.addDirectEventListener((scope MouseDownEvent ev) { 3370 if(ev.srcElement !is w) 3371 return; 3372 if(timer !is null) { 3373 timer.destroy(); 3374 timer = null; 3375 } 3376 delayRemaining = originalDelayRemaining; 3377 timer = new Timer(interval, () { 3378 if(delayRemaining > 0) 3379 delayRemaining--; 3380 else { 3381 auto ev = new Event("triggered", w); 3382 ev.sendDirectly(); 3383 } 3384 }); 3385 }); 3386 3387 w.addDirectEventListener((scope MouseUpEvent ev) { 3388 if(ev.srcElement !is w) 3389 return; 3390 if(timer !is null) { 3391 timer.destroy(); 3392 timer = null; 3393 } 3394 }); 3395 3396 w.addDirectEventListener((scope MouseLeaveEvent ev) { 3397 if(ev.srcElement !is w) 3398 return; 3399 if(timer !is null) { 3400 timer.destroy(); 3401 timer = null; 3402 } 3403 }); 3404 3405 } 3406 else 3407 void setClickRepeat(Widget w, int interval, int delay = 250) {} 3408 3409 enum FrameStyle { 3410 none, /// 3411 risen, /// a 3d pop-out effect (think Windows 95 button) 3412 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 3413 solid, /// 3414 dotted, /// 3415 fantasy, /// a style based on a popular fantasy video game 3416 rounded, /// a rounded rectangle 3417 } 3418 3419 version(custom_widgets) 3420 deprecated 3421 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 3422 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 3423 } 3424 3425 version(custom_widgets) 3426 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 3427 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 3428 } 3429 3430 version(custom_widgets) 3431 deprecated 3432 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 3433 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 3434 } 3435 3436 int getBorderWidth(FrameStyle style) { 3437 final switch(style) { 3438 case FrameStyle.sunk, FrameStyle.risen: 3439 return 2; 3440 case FrameStyle.none: 3441 return 0; 3442 case FrameStyle.solid: 3443 return 1; 3444 case FrameStyle.dotted: 3445 return 1; 3446 case FrameStyle.fantasy: 3447 return 3; 3448 case FrameStyle.rounded: 3449 return 2; 3450 } 3451 } 3452 3453 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 3454 int borderWidth = getBorderWidth(style); 3455 final switch(style) { 3456 case FrameStyle.sunk, FrameStyle.risen: 3457 // outer layer 3458 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 3459 break; 3460 case FrameStyle.none: 3461 painter.outlineColor = background; 3462 break; 3463 case FrameStyle.solid: 3464 case FrameStyle.rounded: 3465 painter.pen = Pen(border, 1); 3466 break; 3467 case FrameStyle.dotted: 3468 painter.pen = Pen(border, 1, Pen.Style.Dotted); 3469 break; 3470 case FrameStyle.fantasy: 3471 painter.pen = Pen(border, 3); 3472 break; 3473 } 3474 3475 painter.fillColor = background; 3476 3477 if(style == FrameStyle.rounded) { 3478 painter.drawRectangleRounded(Point(x, y), Size(width, height), 6); 3479 } else { 3480 painter.drawRectangle(Point(x + 0, y + 0), width, height); 3481 3482 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 3483 // 3d effect 3484 auto vt = WidgetPainter.visualTheme; 3485 3486 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 3487 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 3488 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 3489 3490 // inner layer 3491 //right, bottom 3492 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 3493 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 3494 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 3495 // left, top 3496 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 3497 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 3498 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 3499 } else if(style == FrameStyle.fantasy) { 3500 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 3501 painter.fillColor = Color.transparent; 3502 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 3503 } 3504 } 3505 3506 return borderWidth; 3507 } 3508 3509 /++ 3510 An `Action` represents some kind of user action they can trigger through menu options, toolbars, hotkeys, and similar mechanisms. The text label, icon, and handlers are centrally held here instead of repeated in each UI element. 3511 3512 See_Also: 3513 [MenuItem] 3514 [ToolButton] 3515 [Menu.addItem] 3516 +/ 3517 class Action { 3518 version(win32_widgets) { 3519 private int id; 3520 private static int lastId = 9000; 3521 private static Action[int] mapping; 3522 } 3523 3524 KeyEvent accelerator; 3525 3526 // FIXME: disable message 3527 // and toggle thing? 3528 // ??? and trigger arguments too ??? 3529 3530 /++ 3531 Params: 3532 label = the textual label 3533 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 3534 triggered = initial handler, more can be added via the [triggered] member. 3535 +/ 3536 this(string label, ushort icon = 0, void delegate() triggered = null) { 3537 this.label = label; 3538 this.iconId = icon; 3539 if(triggered !is null) 3540 this.triggered ~= triggered; 3541 version(win32_widgets) { 3542 id = ++lastId; 3543 mapping[id] = this; 3544 } 3545 } 3546 3547 private string label; 3548 private ushort iconId; 3549 // icon 3550 3551 // when it is triggered, the triggered event is fired on the window 3552 /// The list of handlers when it is triggered. 3553 void delegate()[] triggered; 3554 } 3555 3556 /* 3557 plan: 3558 keyboard accelerators 3559 3560 * menus (and popups and tooltips) 3561 * status bar 3562 * toolbars and buttons 3563 3564 sortable table view 3565 3566 maybe notification area icons 3567 basic clipboard 3568 3569 * radio box 3570 splitter 3571 toggle buttons (optionally mutually exclusive, like in Paint) 3572 label, rich text display, multi line plain text (selectable) 3573 * fieldset 3574 * nestable grid layout 3575 single line text input 3576 * multi line text input 3577 slider 3578 spinner 3579 list box 3580 drop down 3581 combo box 3582 auto complete box 3583 * progress bar 3584 3585 terminal window/widget (on unix it might even be a pty but really idk) 3586 3587 ok button 3588 cancel button 3589 3590 keyboard hotkeys 3591 3592 scroll widget 3593 3594 event redirections and network transparency 3595 script integration 3596 */ 3597 3598 3599 /* 3600 MENUS 3601 3602 auto bar = new MenuBar(window); 3603 window.menuBar = bar; 3604 3605 auto fileMenu = bar.addItem(new Menu("&File")); 3606 fileMenu.addItem(new MenuItem("&Exit")); 3607 3608 3609 EVENTS 3610 3611 For controls, you should usually use "triggered" rather than "click", etc., because 3612 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 3613 This is the case on menus and pushbuttons. 3614 3615 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 3616 */ 3617 3618 3619 /* 3620 enum LinePreference { 3621 AlwaysOnOwnLine, // always on its own line 3622 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 3623 PreferToShareLine, // does not force new line, and if the next child likes to share too, they will div it up evenly. otherwise, it will expand as much as it can 3624 } 3625 */ 3626 3627 /++ 3628 Convenience mixin for overriding all four sides of margin or padding in a [Widget] with the same code. It mixes in the given string as the return value of the four overridden methods. 3629 3630 --- 3631 class MyWidget : Widget { 3632 this(Widget parent) { super(parent); } 3633 3634 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 3635 mixin Padding!q{4}; 3636 3637 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 3638 mixin Margin!q{8}; 3639 3640 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 3641 // while Top/Bottom/Right remain 8 from the mixin above. 3642 override int marginLeft() { return 2; } 3643 } 3644 --- 3645 3646 3647 The minigui layout model is based on the web's CSS box model. The layout engine* arranges widgets based on their margin for separation and assigns them a size based on thier preferences (e.g. [Widget.minHeight]) and the available space. Widgets are assigned a size by the layout engine. Inside this size, they have a border (see [Widget.Style.borderWidth]), then padding space, and then their content. Their content box may also have an outline drawn on top of it (see [Widget.Style.outlineStyle]). 3648 3649 Padding is the area inside a widget where its background is drawn, but the content avoids. 3650 3651 Margin is the area between widgets. The algorithm is the spacing between any two widgets is the max of their adjacent margins (not the sum!). 3652 3653 * Some widgets do not participate in placement, e.g. [StaticPosition], and some layout systems do their own separate thing too; ultimately, these properties are just hints to the layout function and you can always implement your own to do whatever you want. But this statement is still mostly true. 3654 +/ 3655 mixin template Padding(string code) { 3656 override int paddingLeft() { return mixin(code);} 3657 override int paddingRight() { return mixin(code);} 3658 override int paddingTop() { return mixin(code);} 3659 override int paddingBottom() { return mixin(code);} 3660 } 3661 3662 /// ditto 3663 mixin template Margin(string code) { 3664 override int marginLeft() { return mixin(code);} 3665 override int marginRight() { return mixin(code);} 3666 override int marginTop() { return mixin(code);} 3667 override int marginBottom() { return mixin(code);} 3668 } 3669 3670 private 3671 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3672 enum calcingV = relevantMeasure == "height"; 3673 3674 parent.registerMovement(); 3675 3676 if(parent.children.length == 0) 3677 return; 3678 3679 auto parentStyle = parent.getComputedStyle(); 3680 3681 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3682 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3683 3684 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3685 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3686 3687 // my own width and height should already be set by the caller of this function... 3688 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3689 mixin("parentStyle.padding"~firstThingy~"()") - 3690 mixin("parentStyle.padding"~secondThingy~"()"); 3691 3692 int stretchinessSum; 3693 int stretchyChildSum; 3694 int lastMargin = 0; 3695 3696 int shrinkinessSum; 3697 int shrinkyChildSum; 3698 3699 // set initial size 3700 foreach(child; parent.children) { 3701 3702 auto childStyle = child.getComputedStyle(); 3703 3704 if(cast(StaticPosition) child) 3705 continue; 3706 if(child.hidden) 3707 continue; 3708 3709 const iw = child.flexBasisWidth(); 3710 const ih = child.flexBasisHeight(); 3711 3712 static if(calcingV) { 3713 child.width = parent.width - 3714 mixin("childStyle.margin"~otherFirstThingy~"()") - 3715 mixin("childStyle.margin"~otherSecondThingy~"()") - 3716 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3717 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3718 3719 if(child.width < 0) 3720 child.width = 0; 3721 if(child.width > childStyle.maxWidth()) 3722 child.width = childStyle.maxWidth(); 3723 3724 if(iw > 0) { 3725 auto totalPossible = child.width; 3726 if(child.width > iw && child.widthStretchiness() == 0) 3727 child.width = iw; 3728 } 3729 3730 child.height = mymax(childStyle.minHeight(), ih); 3731 } else { 3732 // set to take all the space 3733 child.height = parent.height - 3734 mixin("childStyle.margin"~firstThingy~"()") - 3735 mixin("childStyle.margin"~secondThingy~"()") - 3736 mixin("parentStyle.padding"~firstThingy~"()") - 3737 mixin("parentStyle.padding"~secondThingy~"()"); 3738 3739 // then clamp it 3740 if(child.height < 0) 3741 child.height = 0; 3742 if(child.height > childStyle.maxHeight()) 3743 child.height = childStyle.maxHeight(); 3744 3745 // and if possible, respect the ideal target 3746 if(ih > 0) { 3747 auto totalPossible = child.height; 3748 if(child.height > ih && child.heightStretchiness() == 0) 3749 child.height = ih; 3750 } 3751 3752 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3753 child.width = mymax(childStyle.minWidth(), iw); 3754 } 3755 3756 spaceRemaining -= mixin("child." ~ relevantMeasure); 3757 3758 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3759 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3760 lastMargin = margin; 3761 spaceRemaining -= thisMargin + margin; 3762 3763 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3764 stretchinessSum += s; 3765 if(s > 0) 3766 stretchyChildSum++; 3767 3768 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3769 shrinkinessSum += s2; 3770 if(s2 > 0) 3771 shrinkyChildSum++; 3772 } 3773 3774 if(spaceRemaining < 0 && shrinkyChildSum) { 3775 // shrink to get into the space if it is possible 3776 auto toRemove = -spaceRemaining; 3777 auto removalPerItem = toRemove / shrinkinessSum; 3778 auto remainder = toRemove % shrinkinessSum; 3779 3780 // FIXME: wtf why am i shrinking things with no shrinkiness? 3781 3782 foreach(child; parent.children) { 3783 auto childStyle = child.getComputedStyle(); 3784 if(cast(StaticPosition) child) 3785 continue; 3786 if(child.hidden) 3787 continue; 3788 static if(calcingV) { 3789 auto minimum = childStyle.minHeight(); 3790 auto stretch = childStyle.heightShrinkiness(); 3791 } else { 3792 auto minimum = childStyle.minWidth(); 3793 auto stretch = childStyle.widthShrinkiness(); 3794 } 3795 3796 if(mixin("child._" ~ relevantMeasure) <= minimum) 3797 continue; 3798 // import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch); 3799 3800 mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh. 3801 3802 spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum; 3803 } 3804 } 3805 3806 // stretch to fill space 3807 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3808 auto spacePerChild = spaceRemaining / stretchinessSum; 3809 bool spreadEvenly; 3810 bool giveToBiggest; 3811 if(spacePerChild <= 0) { 3812 spacePerChild = spaceRemaining / stretchyChildSum; 3813 spreadEvenly = true; 3814 } 3815 if(spacePerChild <= 0) { 3816 giveToBiggest = true; 3817 } 3818 int previousSpaceRemaining = spaceRemaining; 3819 stretchinessSum = 0; 3820 Widget mostStretchy; 3821 int mostStretchyS; 3822 foreach(child; parent.children) { 3823 auto childStyle = child.getComputedStyle(); 3824 if(cast(StaticPosition) child) 3825 continue; 3826 if(child.hidden) 3827 continue; 3828 static if(calcingV) { 3829 auto maximum = childStyle.maxHeight(); 3830 } else { 3831 auto maximum = childStyle.maxWidth(); 3832 } 3833 3834 if(mixin("child." ~ relevantMeasure) >= maximum) { 3835 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3836 mixin("child._" ~ relevantMeasure) -= adj; 3837 spaceRemaining += adj; 3838 continue; 3839 } 3840 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3841 if(s <= 0) 3842 continue; 3843 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3844 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3845 spaceRemaining -= spaceAdjustment; 3846 if(mixin("child." ~ relevantMeasure) > maximum) { 3847 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3848 mixin("child._" ~ relevantMeasure) -= diff; 3849 spaceRemaining += diff; 3850 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3851 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3852 if(mostStretchy is null || s >= mostStretchyS) { 3853 mostStretchy = child; 3854 mostStretchyS = s; 3855 } 3856 } 3857 } 3858 3859 if(giveToBiggest && mostStretchy !is null) { 3860 auto child = mostStretchy; 3861 auto childStyle = child.getComputedStyle(); 3862 int spaceAdjustment = spaceRemaining; 3863 3864 static if(calcingV) 3865 auto maximum = childStyle.maxHeight(); 3866 else 3867 auto maximum = childStyle.maxWidth(); 3868 3869 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3870 spaceRemaining -= spaceAdjustment; 3871 if(mixin("child._" ~ relevantMeasure) > maximum) { 3872 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3873 mixin("child._" ~ relevantMeasure) -= diff; 3874 spaceRemaining += diff; 3875 } 3876 } 3877 3878 if(spaceRemaining == previousSpaceRemaining) { 3879 if(mostStretchy !is null) { 3880 static if(calcingV) 3881 auto maximum = mostStretchy.maxHeight(); 3882 else 3883 auto maximum = mostStretchy.maxWidth(); 3884 3885 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3886 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3887 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3888 } 3889 break; // apparently nothing more we can do 3890 } 3891 } 3892 3893 foreach(child; parent.children) { 3894 auto childStyle = child.getComputedStyle(); 3895 if(cast(StaticPosition) child) 3896 continue; 3897 if(child.hidden) 3898 continue; 3899 3900 static if(calcingV) 3901 auto maximum = childStyle.maxHeight(); 3902 else 3903 auto maximum = childStyle.maxWidth(); 3904 if(mixin("child._" ~ relevantMeasure) > maximum) 3905 mixin("child._" ~ relevantMeasure) = maximum; 3906 } 3907 3908 // position 3909 lastMargin = 0; 3910 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3911 foreach(child; parent.children) { 3912 auto childStyle = child.getComputedStyle(); 3913 if(cast(StaticPosition) child) { 3914 child.recomputeChildLayout(); 3915 continue; 3916 } 3917 if(child.hidden) 3918 continue; 3919 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3920 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3921 currentPos += thisMargin; 3922 static if(calcingV) { 3923 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3924 child.y = currentPos; 3925 } else { 3926 child.x = currentPos; 3927 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3928 3929 } 3930 currentPos += mixin("child." ~ relevantMeasure); 3931 currentPos += margin; 3932 lastMargin = margin; 3933 3934 child.recomputeChildLayout(); 3935 } 3936 } 3937 3938 int mymax(int a, int b) { return a > b ? a : b; } 3939 int mymax(int a, int b, int c) { 3940 auto d = mymax(a, b); 3941 return c > d ? c : d; 3942 } 3943 3944 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3945 // and here, it must be integrable with the layout, the event system, and not be painted over. 3946 version(win32_widgets) { 3947 3948 // this function just does stuff that a parent window needs for redirection 3949 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3950 this_.hookedWndProc(msg, wParam, lParam); 3951 3952 switch(msg) { 3953 3954 case WM_VSCROLL, WM_HSCROLL: 3955 auto pos = HIWORD(wParam); 3956 auto m = LOWORD(wParam); 3957 3958 auto scrollbarHwnd = cast(HWND) lParam; 3959 3960 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3961 3962 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3963 3964 switch(m) { 3965 /+ 3966 // I don't think those messages are ever actually sent normally by the widget itself, 3967 // they are more used for the keyboard interface. methinks. 3968 case SB_BOTTOM: 3969 // writeln("end"); 3970 auto event = new Event("scrolltoend", *widgetp); 3971 event.dispatch(); 3972 //if(!event.defaultPrevented) 3973 break; 3974 case SB_TOP: 3975 // writeln("top"); 3976 auto event = new Event("scrolltobeginning", *widgetp); 3977 event.dispatch(); 3978 break; 3979 case SB_ENDSCROLL: 3980 // idk 3981 break; 3982 +/ 3983 case SB_LINEDOWN: 3984 (*widgetp).emitCommand!"scrolltonextline"(); 3985 return 0; 3986 case SB_LINEUP: 3987 (*widgetp).emitCommand!"scrolltopreviousline"(); 3988 return 0; 3989 case SB_PAGEDOWN: 3990 (*widgetp).emitCommand!"scrolltonextpage"(); 3991 return 0; 3992 case SB_PAGEUP: 3993 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3994 return 0; 3995 case SB_THUMBPOSITION: 3996 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3997 ev.dispatch(); 3998 return 0; 3999 case SB_THUMBTRACK: 4000 // eh kinda lying but i like the real time update display 4001 auto ev = new ScrollToPositionEvent(*widgetp, pos); 4002 ev.dispatch(); 4003 4004 // the event loop doesn't seem to carry on with a requested redraw.. 4005 // so we request it to get our dirty bit set... 4006 // then we need to immediately actually redraw it too for instant feedback to user 4007 SimpleWindow.processAllCustomEvents(); 4008 SimpleWindow.processAllCustomEvents(); 4009 //if(this_.parentWindow) 4010 //this_.parentWindow.actualRedraw(); 4011 4012 // and this ensures the WM_PAINT message is sent fairly quickly 4013 // still seems to lag a little in large windows but meh it basically works. 4014 if(this_.parentWindow) { 4015 // FIXME: if painting is slow, this does still lag 4016 // we probably will want to expose some user hook to ScrollWindowEx 4017 // or something. 4018 UpdateWindow(this_.parentWindow.hwnd); 4019 } 4020 return 0; 4021 default: 4022 } 4023 } 4024 break; 4025 4026 case WM_CONTEXTMENU: 4027 auto hwndFrom = cast(HWND) wParam; 4028 4029 auto xPos = cast(short) LOWORD(lParam); 4030 auto yPos = cast(short) HIWORD(lParam); 4031 4032 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 4033 POINT p; 4034 p.x = xPos; 4035 p.y = yPos; 4036 ScreenToClient(hwnd, &p); 4037 auto clientX = cast(ushort) p.x; 4038 auto clientY = cast(ushort) p.y; 4039 4040 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 4041 4042 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 4043 return 0; 4044 } 4045 } 4046 break; 4047 4048 case WM_DRAWITEM: 4049 auto dis = cast(DRAWITEMSTRUCT*) lParam; 4050 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 4051 return (*widgetp).handleWmDrawItem(dis); 4052 } 4053 break; 4054 4055 case WM_NOTIFY: 4056 auto hdr = cast(NMHDR*) lParam; 4057 auto hwndFrom = hdr.hwndFrom; 4058 auto code = hdr.code; 4059 4060 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 4061 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 4062 } 4063 break; 4064 case WM_COMMAND: 4065 auto handle = cast(HWND) lParam; 4066 auto cmd = HIWORD(wParam); 4067 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 4068 4069 default: 4070 // pass it on 4071 } 4072 return 0; 4073 } 4074 4075 4076 4077 extern(Windows) 4078 private 4079 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 4080 // but can i merge them?! 4081 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 4082 // try { writeln(iMessage); } catch(Exception e) {}; 4083 4084 if(auto te = hWnd in Widget.nativeMapping) { 4085 try { 4086 4087 te.hookedWndProc(iMessage, wParam, lParam); 4088 4089 int mustReturn; 4090 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 4091 if(mustReturn) 4092 return ret; 4093 4094 if(iMessage == WM_SETFOCUS) { 4095 auto lol = *te; 4096 while(lol !is null && lol.implicitlyCreated) 4097 lol = lol.parent; 4098 lol.focus(); 4099 //(*te).parentWindow.focusedWidget = lol; 4100 } 4101 4102 4103 if(iMessage == WM_CTLCOLOREDIT) { 4104 4105 } 4106 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 4107 SetBkMode(cast(HDC) wParam, TRANSPARENT); 4108 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 4109 //GetStockObject(NULL_BRUSH); 4110 } 4111 4112 auto pos = getChildPositionRelativeToParentOrigin(*te); 4113 lastDefaultPrevented = false; 4114 // try { writeln(typeid(*te)); } catch(Exception e) {} 4115 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 4116 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 4117 else { 4118 // it was something we recognized, should only call the window procedure if the default was not prevented 4119 } 4120 } catch(Exception e) { 4121 assert(0, e.toString()); 4122 } 4123 return 0; 4124 } 4125 assert(0, "shouldn't be receiving messages for this window...."); 4126 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 4127 } 4128 4129 extern(Windows) 4130 private 4131 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 4132 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 4133 if(iMessage == WM_ERASEBKGND) { 4134 auto dc = GetDC(hWnd); 4135 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 4136 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 4137 RECT r; 4138 GetWindowRect(hWnd, &r); 4139 // since the pen is null, to fill the whole space, we need the +1 on both. 4140 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 4141 SelectObject(dc, p); 4142 SelectObject(dc, b); 4143 ReleaseDC(hWnd, dc); 4144 InvalidateRect(hWnd, null, false); // redraw the border 4145 return 1; 4146 } 4147 return HookedWndProc(hWnd, iMessage, wParam, lParam); 4148 } 4149 4150 /++ 4151 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 4152 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 4153 of minigui's expectations. 4154 4155 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 4156 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 4157 4158 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 4159 4160 To check if you can use this, use `static if(UsingWin32Widgets)`. 4161 +/ 4162 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 4163 assert(p.parentWindow !is null); 4164 assert(p.parentWindow.win.impl.hwnd !is null); 4165 4166 auto bsgroupbox = style == BS_GROUPBOX; 4167 4168 HWND phwnd; 4169 4170 auto wtf = p.parent; 4171 while(wtf) { 4172 if(wtf.hwnd !is null) { 4173 phwnd = wtf.hwnd; 4174 break; 4175 } 4176 wtf = wtf.parent; 4177 } 4178 4179 if(phwnd is null) 4180 phwnd = p.parentWindow.win.impl.hwnd; 4181 4182 assert(phwnd !is null); 4183 4184 WCharzBuffer wt = WCharzBuffer(windowText); 4185 4186 style |= WS_VISIBLE | WS_CHILD; 4187 //if(className != WC_TABCONTROL) 4188 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 4189 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 4190 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 4191 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 4192 4193 assert(p.hwnd !is null); 4194 4195 4196 static HFONT font; 4197 if(font is null) { 4198 NONCLIENTMETRICS params; 4199 params.cbSize = params.sizeof; 4200 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 4201 font = CreateFontIndirect(¶ms.lfMessageFont); 4202 } 4203 } 4204 4205 if(font) 4206 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 4207 4208 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 4209 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 4210 Widget.nativeMapping[p.hwnd] = p; 4211 4212 if(bsgroupbox) 4213 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 4214 else 4215 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4216 4217 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 4218 4219 p.registerMovement(); 4220 } 4221 } 4222 4223 version(win32_widgets) 4224 private 4225 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 4226 if(hwnd is null || hwnd in Widget.nativeMapping) 4227 return true; 4228 auto parent = cast(Widget) cast(void*) lparam; 4229 Widget p = new Widget(null); 4230 p._parent = parent; 4231 p.parentWindow = parent.parentWindow; 4232 p.hwnd = hwnd; 4233 p.implicitlyCreated = true; 4234 Widget.nativeMapping[p.hwnd] = p; 4235 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4236 return true; 4237 } 4238 4239 /++ 4240 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 4241 +/ 4242 struct WidgetPainter { 4243 this(ScreenPainter screenPainter, Widget drawingUpon) { 4244 this.drawingUpon = drawingUpon; 4245 this.screenPainter = screenPainter; 4246 4247 this.widgetClipRectangle = screenPainter.currentClipRectangle; 4248 4249 // this.screenPainter.impl.enableXftDraw(); 4250 if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 4251 this.screenPainter.setFont(font); 4252 } 4253 4254 /++ 4255 EXPERIMENTAL. subject to change. 4256 4257 When you draw a cursor, you can draw this to notify your window of where it is, 4258 for IME systems to use. 4259 +/ 4260 void notifyCursorPosition(int x, int y, int width, int height) { 4261 if(auto a = drawingUpon.parentWindow) 4262 if(auto w = a.inputProxy) { 4263 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 4264 } 4265 } 4266 4267 private Rectangle widgetClipRectangle; 4268 4269 private Rectangle setClipRectangleForWidget(Point upperLeft, int width, int height) { 4270 widgetClipRectangle = Rectangle(upperLeft, Size(width, height)); 4271 4272 return screenPainter.setClipRectangle(widgetClipRectangle); 4273 } 4274 4275 /++ 4276 Sets the clip rectangle to the given settings. It will automatically calculate the intersection 4277 of your widget's content boundaries and your requested clip rectangle. 4278 4279 History: 4280 Before February 26, 2025, you could sometimes exceed widget boundaries, as this forwarded 4281 directly to the underlying `ScreenPainter`. It now wraps it to calculate the intersection. 4282 +/ 4283 Rectangle setClipRectangle(Rectangle rectangle) { 4284 return screenPainter.setClipRectangle(rectangle.intersectionOf(widgetClipRectangle)); 4285 } 4286 /// ditto 4287 Rectangle setClipRectangle(Point upperLeft, int width, int height) { 4288 return setClipRectangle(Rectangle(upperLeft, Size(width, height))); 4289 } 4290 /// ditto 4291 Rectangle setClipRectangle(Point upperLeft, Size size) { 4292 return setClipRectangle(Rectangle(upperLeft, size)); 4293 } 4294 4295 /// 4296 ScreenPainter screenPainter; 4297 /// Forward to the screen painter for all other methods, see [arsd.simpledisplay.ScreenPainter] for more information 4298 alias screenPainter this; 4299 4300 private Widget drawingUpon; 4301 4302 /++ 4303 This is the list of rectangles that actually need to be redrawn. 4304 4305 Not actually implemented yet. 4306 +/ 4307 Rectangle[] invalidatedRectangles; 4308 4309 private static BaseVisualTheme _visualTheme; 4310 4311 /++ 4312 Functions to access the visual theme and helpers to easily use it. 4313 4314 These are aware of the current widget's computed style out of the theme. 4315 +/ 4316 static @property BaseVisualTheme visualTheme() { 4317 if(_visualTheme is null) 4318 _visualTheme = new DefaultVisualTheme(); 4319 return _visualTheme; 4320 } 4321 4322 /// ditto 4323 static @property void visualTheme(BaseVisualTheme theme) { 4324 _visualTheme = theme; 4325 4326 // FIXME: notify all windows about the new theme, they should recompute layout and redraw. 4327 } 4328 4329 /// ditto 4330 Color themeForeground() { 4331 return drawingUpon.getComputedStyle().foregroundColor(); 4332 } 4333 4334 /// ditto 4335 Color themeBackground() { 4336 return drawingUpon.getComputedStyle().background.color; 4337 } 4338 4339 int isDarkTheme() { 4340 return 0; // unspecified, yes, no as enum. FIXME 4341 } 4342 4343 /++ 4344 Draws the general pattern of a widget if you don't need anything particularly special and/or control the other details through your widget's style theme hints. 4345 4346 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 4347 4348 If you change teh clip rectangle, you should change it back before you return. 4349 4350 4351 The sequence it uses is: 4352 background 4353 content (delegated to you) 4354 border 4355 focused outline 4356 selected overlay 4357 4358 Example code: 4359 4360 --- 4361 void paint(WidgetPainter painter) { 4362 painter.drawThemed((bounds) { 4363 return bounds; // if the selection overlay should be contained, you can return it here. 4364 }); 4365 } 4366 --- 4367 +/ 4368 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 4369 drawThemed((WidgetPainter painter, const Rectangle bounds) { 4370 return drawBody(bounds); 4371 }); 4372 } 4373 // this overload is actually mroe for setting the delegate to a virtual function 4374 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 4375 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 4376 4377 auto cs = drawingUpon.getComputedStyle(); 4378 4379 auto bg = cs.background.color; 4380 4381 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 4382 4383 rect.left += borderWidth; 4384 rect.right -= borderWidth; 4385 rect.top += borderWidth; 4386 rect.bottom -= borderWidth; 4387 4388 auto insideBorderRect = rect; 4389 4390 rect.left += cs.paddingLeft; 4391 rect.right -= cs.paddingRight; 4392 rect.top += cs.paddingTop; 4393 rect.bottom -= cs.paddingBottom; 4394 4395 this.outlineColor = this.themeForeground; 4396 this.fillColor = bg; 4397 4398 auto widgetFont = cs.fontCached; 4399 if(widgetFont !is null) 4400 this.setFont(widgetFont); 4401 4402 rect = drawBody(this, rect); 4403 4404 if(widgetFont !is null) { 4405 if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 4406 this.setFont(vtFont); 4407 else 4408 this.setFont(null); 4409 } 4410 4411 if(auto os = cs.outlineStyle()) { 4412 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 4413 this.fillColor = Color.transparent; 4414 this.drawRectangle(insideBorderRect); 4415 } 4416 } 4417 4418 /++ 4419 First, draw the background. 4420 Then draw your content. 4421 Next, draw the border. 4422 And the focused indicator. 4423 And the is-selected box. 4424 4425 If it is focused i can draw the outline too... 4426 4427 If selected i can even do the xor action but that's at the end. 4428 +/ 4429 void drawThemeBackground() { 4430 4431 } 4432 4433 void drawThemeBorder() { 4434 4435 } 4436 4437 // all this stuff is a dangerous experiment.... 4438 static class ScriptableVersion { 4439 ScreenPainterImplementation* p; 4440 int originX, originY; 4441 4442 @scriptable: 4443 void drawRectangle(int x, int y, int width, int height) { 4444 p.drawRectangle(x + originX, y + originY, width, height); 4445 } 4446 void drawLine(int x1, int y1, int x2, int y2) { 4447 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 4448 } 4449 void drawText(int x, int y, string text) { 4450 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 4451 } 4452 void setOutlineColor(int r, int g, int b) { 4453 p.pen = Pen(Color(r,g,b), 1); 4454 } 4455 void setFillColor(int r, int g, int b) { 4456 p.fillColor = Color(r,g,b); 4457 } 4458 } 4459 4460 ScriptableVersion toArsdJsvar() { 4461 auto sv = new ScriptableVersion; 4462 sv.p = this.screenPainter.impl; 4463 sv.originX = this.screenPainter.originX; 4464 sv.originY = this.screenPainter.originY; 4465 return sv; 4466 } 4467 4468 static WidgetPainter fromJsVar(T)(T t) { 4469 return WidgetPainter.init; 4470 } 4471 // done.......... 4472 } 4473 4474 4475 struct Style { 4476 static struct helper(string m, T) { 4477 enum method = m; 4478 T v; 4479 4480 mixin template MethodOverride(typeof(this) v) { 4481 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 4482 } 4483 } 4484 4485 static auto opDispatch(string method, T)(T value) { 4486 return helper!(method, T)(value); 4487 } 4488 } 4489 4490 /++ 4491 Implementation detail of the [ControlledBy] UDA. 4492 4493 History: 4494 Added Oct 28, 2020 4495 +/ 4496 struct ControlledBy_(T, Args...) { 4497 Args args; 4498 4499 static if(Args.length) 4500 this(Args args) { 4501 this.args = args; 4502 } 4503 4504 private T construct(Widget parent) { 4505 return new T(args, parent); 4506 } 4507 } 4508 4509 /++ 4510 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 4511 4512 History: 4513 Added Oct 28, 2020 4514 +/ 4515 auto ControlledBy(T, Args...)(Args args) { 4516 return ControlledBy_!(T, Args)(args); 4517 } 4518 4519 struct ContainerMeta { 4520 string name; 4521 ContainerMeta[] children; 4522 Widget function(Widget parent) factory; 4523 4524 Widget instantiate(Widget parent) { 4525 auto n = factory(parent); 4526 n.name = name; 4527 foreach(child; children) 4528 child.instantiate(n); 4529 return n; 4530 } 4531 } 4532 4533 /++ 4534 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 4535 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 4536 4537 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 4538 structures. It works fine on structs declared inside functions though. 4539 4540 See: https://issues.dlang.org/show_bug.cgi?id=21984 4541 +/ 4542 template Container(CArgs...) { 4543 static if(CArgs.length && is(CArgs[0] : Widget)) { 4544 private alias Super = CArgs[0]; 4545 private alias CArgs2 = CArgs[1 .. $]; 4546 } else { 4547 private alias Super = Layout; 4548 private alias CArgs2 = CArgs; 4549 } 4550 4551 class Container : Super { 4552 this(Widget parent) { super(parent); } 4553 4554 // just to partially support old gdc versions 4555 version(GNU) { 4556 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 4557 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 4558 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 4559 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 4560 } else mixin(q{ 4561 static foreach(Arg; CArgs2) { 4562 mixin Arg.MethodOverride!(Arg); 4563 } 4564 }); 4565 4566 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 4567 return ContainerMeta( 4568 name, 4569 children.dup, 4570 function (Widget parent) { return new typeof(this)(parent); } 4571 ); 4572 } 4573 4574 static ContainerMeta opCall(ContainerMeta[] children...) { 4575 return opCall(null, children); 4576 } 4577 } 4578 } 4579 4580 /++ 4581 The data controller widget is created by reflecting over the given 4582 data type. You can use [ControlledBy] as a UDA on a struct or 4583 just let it create things automatically. 4584 4585 Unlike [dialog], this uses real-time updating of the data and 4586 you add it to another window yourself. 4587 4588 --- 4589 struct Test { 4590 int x; 4591 int y; 4592 } 4593 4594 auto window = new Window(); 4595 auto dcw = new DataControllerWidget!Test(new Test, window); 4596 --- 4597 4598 The way it works is any public members are given a widget based 4599 on their data type, and public methods trigger an action button 4600 if no relevant parameters or a dialog action if it does have 4601 parameters, similar to the [menu] facility. 4602 4603 If you change data programmatically, without going through the 4604 DataControllerWidget methods, you will have to tell it something 4605 has changed and it needs to redraw. This is done with the `invalidate` 4606 method. 4607 4608 History: 4609 Added Oct 28, 2020 4610 +/ 4611 /// Group: generating_from_code 4612 class DataControllerWidget(T) : WidgetContainer { 4613 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 4614 private alias Tref = T; 4615 else 4616 private alias Tref = T*; 4617 4618 Tref datum; 4619 4620 /++ 4621 See_also: [addDataControllerWidget] 4622 +/ 4623 this(Tref datum, Widget parent) { 4624 this.datum = datum; 4625 4626 Widget cp = this; 4627 4628 super(parent); 4629 4630 foreach(attr; __traits(getAttributes, T)) 4631 static if(is(typeof(attr) == ContainerMeta)) { 4632 cp = attr.instantiate(this); 4633 } 4634 4635 auto def = this.getByName("default"); 4636 if(def !is null) 4637 cp = def; 4638 4639 Widget helper(string name) { 4640 auto maybe = this.getByName(name); 4641 if(maybe is null) 4642 return cp; 4643 return maybe; 4644 4645 } 4646 4647 foreach(member; __traits(allMembers, T)) 4648 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 4649 static if(is(typeof(__traits(getMember, this.datum, member)))) 4650 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 4651 void delegate() update; 4652 4653 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 4654 4655 if(update) 4656 updaters ~= update; 4657 4658 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 4659 w.addEventListener("triggered", delegate() { 4660 makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))(); 4661 notifyDataUpdated(); 4662 }); 4663 } else static if(is(typeof(w.isChecked) == bool)) { 4664 w.addEventListener(EventType.change, (Event ev) { 4665 __traits(getMember, this.datum, member) = w.isChecked; 4666 }); 4667 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 4668 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 4669 } else static if(is(typeof(w.value) == int)) { 4670 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4671 } else static if(is(typeof(w) == DropDownSelection)) { 4672 // special case for this to kinda support enums and such. coudl be better though 4673 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4674 } else { 4675 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 4676 } 4677 } 4678 } 4679 4680 /++ 4681 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 4682 4683 History: 4684 Added May 28, 2021 4685 +/ 4686 void notifyDataUpdated() { 4687 foreach(updater; updaters) 4688 updater(); 4689 4690 this.emit!(ChangeEvent!void)(delegate{}); 4691 } 4692 4693 private Widget[string] memberWidgets; 4694 private void delegate()[] updaters; 4695 4696 mixin Emits!(ChangeEvent!void); 4697 } 4698 4699 private int saturatedSum(int[] values...) { 4700 int sum; 4701 foreach(value; values) { 4702 if(value == int.max) 4703 return int.max; 4704 sum += value; 4705 } 4706 return sum; 4707 } 4708 4709 void genericSetValue(T, W)(T* where, W what) { 4710 import std.conv; 4711 *where = to!T(what); 4712 //*where = cast(T) stringToLong(what); 4713 } 4714 4715 /++ 4716 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4717 4718 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4719 4720 Note that this creates the widget but does not attach any event handlers to it. 4721 +/ 4722 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 4723 4724 string displayName = __traits(identifier, tt).beautify; 4725 4726 static if(controlledByCount!tt == 1) { 4727 foreach(i, attr; __traits(getAttributes, tt)) { 4728 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4729 auto w = attr.construct(parent); 4730 static if(__traits(compiles, w.setPosition(*valptr))) 4731 update = () { w.setPosition(*valptr); }; 4732 else static if(__traits(compiles, w.setValue(*valptr))) 4733 update = () { w.setValue(*valptr); }; 4734 4735 if(update) 4736 update(); 4737 return w; 4738 } 4739 } 4740 } else static if(controlledByCount!tt == 0) { 4741 static if(is(typeof(tt) == enum)) { 4742 // FIXME: update 4743 auto dds = new DropDownSelection(parent); 4744 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4745 dds.addOption(option); 4746 if(__traits(getMember, typeof(tt), option) == *valptr) 4747 dds.setSelection(cast(int) idx); 4748 } 4749 return dds; 4750 } else static if(is(typeof(tt) == bool)) { 4751 auto box = new Checkbox(displayName, parent); 4752 update = () { box.isChecked = *valptr; }; 4753 update(); 4754 return box; 4755 } else static if(is(typeof(tt) : const long)) { 4756 auto le = new LabeledLineEdit(displayName, parent); 4757 update = () { le.content = toInternal!string(*valptr); }; 4758 update(); 4759 return le; 4760 } else static if(is(typeof(tt) : const double)) { 4761 auto le = new LabeledLineEdit(displayName, parent); 4762 import std.conv; 4763 update = () { le.content = to!string(*valptr); }; 4764 update(); 4765 return le; 4766 } else static if(is(typeof(tt) : const string)) { 4767 auto le = new LabeledLineEdit(displayName, parent); 4768 update = () { le.content = *valptr; }; 4769 update(); 4770 return le; 4771 } else static if(is(typeof(tt) == E[], E)) { 4772 auto w = new ArrayEditingWidget!E(parent); 4773 // FIXME update 4774 return w; 4775 } else static if(is(typeof(tt) == function)) { 4776 auto w = new Button(displayName, parent); 4777 return w; 4778 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4779 return parent.addDataControllerWidget(tt); 4780 } else static assert(0, typeof(tt).stringof); 4781 } else static assert(0, "multiple controllers not yet supported"); 4782 } 4783 4784 class ArrayEditingWidget(T) : ArrayEditingWidgetBase { 4785 this(Widget parent) { 4786 super(parent); 4787 } 4788 } 4789 4790 class ArrayEditingWidgetBase : Widget { 4791 this(Widget parent) { 4792 super(parent); 4793 4794 // FIXME: a trash can to move items into to delete them? 4795 static class MyListViewItem : GenericListViewItem { 4796 this(Widget parent) { 4797 super(parent); 4798 4799 /+ 4800 drag handle 4801 left click lets you move the whole selection. if the current element is not selected, it changes the selection to it. 4802 right click here gives you the movement controls too 4803 index/key view zone 4804 left click here selects/unselects 4805 element view/edit zone 4806 delete button 4807 +/ 4808 4809 // FIXME: make sure the index is viewable 4810 4811 auto hl = new HorizontalLayout(this); 4812 4813 button = new CommandButton("d", hl); 4814 4815 label = new TextLabel("unloaded", TextAlignment.Left, hl); 4816 // if member editable, have edit view... get from the subclass. 4817 4818 // or a "..." menu? 4819 button = new CommandButton("Up", hl); // shift+click is move to top 4820 button = new CommandButton("Down", hl); // shift+click is move to bottom 4821 button = new CommandButton("Move to", hl); // move before, after, or swap 4822 button = new CommandButton("Delete", hl); 4823 4824 button.addEventListener("triggered", delegate(){ 4825 //messageBox(text("clicked ", currentIndexLoaded())); 4826 }); 4827 } 4828 override void showItem(int idx) { 4829 label.label = "Item ";// ~ to!string(idx); 4830 } 4831 4832 TextLabel label; 4833 Button button; 4834 } 4835 4836 auto outer_this = this; 4837 4838 // FIXME: make sure item count is easy to see 4839 4840 glvw = new class GenericListViewWidget { 4841 this() { 4842 super(outer_this); 4843 } 4844 override GenericListViewItem itemFactory(Widget parent) { 4845 return new MyListViewItem(parent); 4846 } 4847 override Size itemSize() { 4848 return Size(0, scaleWithDpi(80)); 4849 } 4850 4851 override Menu contextMenu(int x, int y) { 4852 return createContextMenuFromAnnotatedCode(this); 4853 } 4854 4855 @context_menu { 4856 void Select_All() { 4857 4858 } 4859 4860 void Undo() { 4861 4862 } 4863 4864 void Redo() { 4865 4866 } 4867 4868 void Cut() { 4869 4870 } 4871 4872 void Copy() { 4873 4874 } 4875 4876 void Paste() { 4877 4878 } 4879 4880 void Delete() { 4881 4882 } 4883 4884 void Find() { 4885 4886 } 4887 } 4888 }; 4889 4890 glvw.setItemCount(400); 4891 4892 auto hl = new HorizontalLayout(this); 4893 add = new FreeEntrySelection(hl); 4894 addButton = new Button("Add", hl); 4895 } 4896 4897 GenericListViewWidget glvw; 4898 ComboboxBase add; 4899 Button addButton; 4900 /+ 4901 Controls: 4902 clear (select all / delete) 4903 reset (confirmation blocked button, maybe only on the whole form? or hit undo so many times to get back there) 4904 add item 4905 palette of options to add to the array (add prolly a combo box) 4906 rearrange - move up/down, drag and drop a selection? right click can always do, left click only drags when on a selection handle. 4907 edit/input/view items (GLVW? or it could be a table view in a way.) 4908 undo/redo 4909 select whole elements (even if a struct) 4910 cut/copy/paste elements 4911 4912 could have an element picker, a details pane, and an add bare? 4913 4914 4915 put a handle on the elements for left click dragging. allow right click drag anywhere but pretty big wiggle until it enables. 4916 left click and drag should never work for plain text, i more want to change selection there and there no room to put a handle on it. 4917 the handle should let dragging w/o changing the selection, or if part of the selection, drag the whole selection i think. 4918 make it textured and use the grabby hand mouse cursor. 4919 +/ 4920 } 4921 4922 /++ 4923 A button that pops up a menu on click for working on a particular item or selection. 4924 4925 History: 4926 Added March 23, 2025 4927 +/ 4928 class MenuPopupButton : Button { 4929 /++ 4930 You might consider using [createContextMenuFromAnnotatedCode] to populate the `menu` argument. 4931 4932 You also may want to set the [prepare] delegate after construction. 4933 +/ 4934 this(Menu menu, Widget parent) { 4935 assert(menu !is null); 4936 4937 this.menu = menu; 4938 super("...", parent); 4939 } 4940 4941 private Menu menu; 4942 /++ 4943 If set, this delegate is called before popping up the window. This gives you a chance 4944 to prepare your dynamic data structures for the element(s) selected. 4945 4946 For example, if your `MenuPopupButton` is attached to a [GenericListViewItem], you can call 4947 [GenericListViewItem.currentIndexLoaded] in here and set it to a variable in the object you 4948 called [createContextMenuFromAnnotatedCode] to apply the operation to the right object. 4949 4950 (The api could probably be simpler...) 4951 +/ 4952 void delegate() prepare; 4953 4954 override void defaultEventHandler_triggered(scope Event e) { 4955 if(prepare) 4956 prepare(); 4957 showContextMenu(this.x, this.y + this.height, -2, -2, menu); 4958 } 4959 4960 override int maxHeight() { 4961 return defaultLineHeight; 4962 } 4963 4964 override int maxWidth() { 4965 return defaultLineHeight; 4966 } 4967 } 4968 4969 /++ 4970 A button that pops up an information box, similar to a tooltip, but explicitly triggered. 4971 4972 FIXME: i want to be able to easily embed these in other things too. 4973 +/ 4974 class TipPopupButton : Button { 4975 /++ 4976 +/ 4977 this(Widget delegate(Widget p) factory, Widget parent) { 4978 this.factory = factory; 4979 super("?", parent); 4980 } 4981 4982 private Widget delegate(Widget p) factory; 4983 4984 override void defaultEventHandler_triggered(scope Event e) { 4985 auto window = new TooltipWindow(factory, this); 4986 window.popup(this); 4987 } 4988 } 4989 4990 /++ 4991 History: 4992 Added March 23, 2025 4993 +/ 4994 class TooltipWindow : Window { 4995 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 4996 /+ 4997 this.menuParent = parent; 4998 4999 previouslyFocusedWidget = parent.parentWindow.focusedWidget; 5000 previouslyFocusedWidgetBelongsIn = &parent.parentWindow.focusedWidget; 5001 parent.parentWindow.focusedWidget = this; 5002 5003 int w = 150; 5004 int h = paddingTop + paddingBottom; 5005 if(this.children.length) { 5006 // hacking it to get the ideal height out of recomputeChildLayout 5007 this.width = w; 5008 this.height = h; 5009 this.recomputeChildLayoutEntry(); 5010 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 5011 h += paddingBottom; 5012 5013 h -= 2; // total hack, i just like the way it looks a bit tighter even though technically MenuItem reserves some space to center in normal circumstances 5014 } 5015 +/ 5016 5017 if(offsetY == int.min) 5018 offsetY = parent.defaultLineHeight; 5019 5020 int w = 150; 5021 int h = 50; 5022 5023 auto coord = parent.globalCoordinates(); 5024 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 5025 5026 static if(UsingSimpledisplayX11) 5027 XSync(XDisplayConnection.get, 0); 5028 5029 dropDown.visibilityChanged = (bool visible) { 5030 if(visible) { 5031 this.redraw(); 5032 dropDown.grabInput(); 5033 } else { 5034 dropDown.releaseInputGrab(); 5035 } 5036 }; 5037 5038 dropDown.show(); 5039 5040 clickListener = this.addEventListener((scope ClickEvent ev) { 5041 unpopup(); 5042 // need to unlock asap just in case other user handlers block... 5043 static if(UsingSimpledisplayX11) 5044 flushGui(); 5045 }, true /* again for asap action */); 5046 } 5047 5048 private EventListener clickListener; 5049 5050 void unpopup() { 5051 mouseLastOver = mouseLastDownOn = null; 5052 dropDown.hide(); 5053 clickListener.disconnect(); 5054 } 5055 5056 private SimpleWindow dropDown; 5057 private Widget child; 5058 5059 /// 5060 this(Widget delegate(Widget p) factory, Widget parent) { 5061 assert(parent); 5062 assert(parent.parentWindow); 5063 assert(parent.parentWindow.win); 5064 dropDown = new SimpleWindow( 5065 250, 40, 5066 null, OpenGlOptions.no, Resizability.fixedSize, 5067 WindowTypes.tooltip, 5068 WindowFlags.dontAutoShow, 5069 parent ? parent.parentWindow.win : null 5070 ); 5071 5072 super(dropDown); 5073 5074 child = factory(this); 5075 } 5076 } 5077 5078 private template controlledByCount(alias tt) { 5079 static int helper() { 5080 int count; 5081 foreach(i, attr; __traits(getAttributes, tt)) 5082 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 5083 count++; 5084 return count; 5085 } 5086 5087 enum controlledByCount = helper; 5088 } 5089 5090 /++ 5091 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 5092 5093 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 5094 5095 History: 5096 The `redrawOnChange` parameter was added on May 28, 2021. 5097 +/ 5098 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 5099 auto dcw = new DataControllerWidget!T(t, parent); 5100 initializeDataControllerWidget(dcw, redrawOnChange); 5101 return dcw; 5102 } 5103 5104 /// ditto 5105 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 5106 auto dcw = new DataControllerWidget!T(t, parent); 5107 initializeDataControllerWidget(dcw, redrawOnChange); 5108 return dcw; 5109 } 5110 5111 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 5112 if(redrawOnChange !is null) 5113 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 5114 } 5115 5116 /++ 5117 Get this through [Widget.getComputedStyle]. It provides access to the [Widget.Style] style hints and [Widget] layout hints, possibly modified through the [VisualTheme], through a unifed interface. 5118 5119 History: 5120 Finalized on June 3, 2021 for the dub v10.0 release 5121 +/ 5122 struct StyleInformation { 5123 private Widget w; 5124 private BaseVisualTheme visualTheme; 5125 5126 private this(Widget w) { 5127 this.w = w; 5128 this.visualTheme = WidgetPainter.visualTheme; 5129 } 5130 5131 /++ 5132 Forwards to [Widget.Style] 5133 5134 Bugs: 5135 It is supposed to fall back to the [VisualTheme] if 5136 the style doesn't override the default, but that is 5137 not generally implemented. Many of them may end up 5138 being explicit overloads instead of the generic 5139 opDispatch fallback, like [font] is now. 5140 +/ 5141 public @property opDispatch(string name)() { 5142 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 5143 w.useStyleProperties((scope Widget.Style props) { 5144 //visualTheme.useStyleProperties(w, (props) { 5145 prop = __traits(getMember, props, name); 5146 }); 5147 return prop; 5148 } 5149 5150 /++ 5151 Returns the cached font object associated with the widget, 5152 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 5153 5154 History: 5155 Prior to March 21, 2022 (dub v10.7), `font` went through 5156 [opDispatch], which did not use the cache. You can now call it 5157 repeatedly without guilt. 5158 +/ 5159 public @property OperatingSystemFont font() { 5160 OperatingSystemFont prop; 5161 w.useStyleProperties((scope Widget.Style props) { 5162 prop = props.fontCached; 5163 }); 5164 if(prop is null) { 5165 prop = visualTheme.defaultFontCached(w.currentDpi); 5166 } 5167 return prop; 5168 } 5169 5170 @property { 5171 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 5172 /** */ int paddingLeft() { return w.paddingLeft(); } 5173 /** */ int paddingRight() { return w.paddingRight(); } 5174 /** */ int paddingTop() { return w.paddingTop(); } 5175 /** */ int paddingBottom() { return w.paddingBottom(); } 5176 5177 /** */ int marginLeft() { return w.marginLeft(); } 5178 /** */ int marginRight() { return w.marginRight(); } 5179 /** */ int marginTop() { return w.marginTop(); } 5180 /** */ int marginBottom() { return w.marginBottom(); } 5181 5182 /** */ int maxHeight() { return w.maxHeight(); } 5183 /** */ int minHeight() { return w.minHeight(); } 5184 5185 /** */ int maxWidth() { return w.maxWidth(); } 5186 /** */ int minWidth() { return w.minWidth(); } 5187 5188 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 5189 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 5190 5191 /** */ int heightStretchiness() { return w.heightStretchiness(); } 5192 /** */ int widthStretchiness() { return w.widthStretchiness(); } 5193 5194 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 5195 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 5196 5197 // Global helpers some of these are unstable. 5198 static: 5199 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 5200 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 5201 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 5202 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 5203 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 5204 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 5205 5206 /** */ Color activeTabColor() { return lightAccentColor; } 5207 /** */ Color buttonColor() { return windowBackgroundColor; } 5208 /** */ Color depressedButtonColor() { return darkAccentColor; } 5209 /** the background color of the widget when mouse hovering over it, if it responds to mouse hovers */ Color hoveringColor() { return lightAccentColor; } 5210 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 5211 auto c = WidgetPainter.visualTheme.selectionColor(); 5212 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 5213 } 5214 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 5215 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 5216 } 5217 5218 5219 5220 /+ 5221 5222 private static auto extractStyleProperty(string name)(Widget w) { 5223 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 5224 w.useStyleProperties((props) { 5225 prop = __traits(getMember, props, name); 5226 }); 5227 return prop; 5228 } 5229 5230 // FIXME: clear this upon a X server disconnect 5231 private static OperatingSystemFont[string] fontCache; 5232 5233 T getProperty(T)(string name, lazy T default_) { 5234 if(visualTheme !is null) { 5235 auto str = visualTheme.getPropertyString(w, name); 5236 if(str is null) 5237 return default_; 5238 static if(is(T == Color)) 5239 return Color.fromString(str); 5240 else static if(is(T == Measurement)) 5241 return Measurement(cast(int) toInternal!int(str)); 5242 else static if(is(T == WidgetBackground)) 5243 return WidgetBackground.fromString(str); 5244 else static if(is(T == OperatingSystemFont)) { 5245 if(auto f = str in fontCache) 5246 return *f; 5247 else 5248 return fontCache[str] = new OperatingSystemFont(str); 5249 } else static if(is(T == FrameStyle)) { 5250 switch(str) { 5251 default: 5252 return FrameStyle.none; 5253 foreach(style; __traits(allMembers, FrameStyle)) 5254 case style: 5255 return __traits(getMember, FrameStyle, style); 5256 } 5257 } else static assert(0); 5258 } else 5259 return default_; 5260 } 5261 5262 static struct Measurement { 5263 int value; 5264 alias value this; 5265 } 5266 5267 @property: 5268 5269 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 5270 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 5271 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 5272 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 5273 5274 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 5275 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 5276 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 5277 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 5278 5279 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 5280 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 5281 5282 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 5283 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 5284 5285 5286 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 5287 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 5288 5289 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 5290 5291 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 5292 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 5293 5294 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 5295 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 5296 5297 5298 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 5299 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 5300 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 5301 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 5302 5303 Color activeTabColor() { return lightAccentColor; } 5304 Color buttonColor() { return windowBackgroundColor; } 5305 Color depressedButtonColor() { return darkAccentColor; } 5306 Color hoveringColor() { return Color(228, 228, 228); } 5307 Color activeListXorColor() { 5308 auto c = WidgetPainter.visualTheme.selectionColor(); 5309 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 5310 } 5311 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 5312 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 5313 +/ 5314 } 5315 5316 5317 5318 // pragma(msg, __traits(classInstanceSize, Widget)); 5319 5320 /*private*/ template EventString(E) { 5321 static if(is(typeof(E.EventString))) 5322 enum EventString = E.EventString; 5323 else 5324 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 5325 } 5326 5327 /*private*/ template EventStringIdentifier(E) { 5328 string helper() { 5329 auto es = EventString!E; 5330 char[] id = new char[](es.length * 2); 5331 size_t idx; 5332 foreach(char ch; es) { 5333 id[idx++] = cast(char)('a' + (ch >> 4)); 5334 id[idx++] = cast(char)('a' + (ch & 0x0f)); 5335 } 5336 return cast(string) id; 5337 } 5338 5339 enum EventStringIdentifier = helper(); 5340 } 5341 5342 5343 template classStaticallyEmits(This, EventType) { 5344 static if(is(This Base == super)) 5345 static if(is(Base : Widget)) 5346 enum baseEmits = classStaticallyEmits!(Base, EventType); 5347 else 5348 enum baseEmits = false; 5349 else 5350 enum baseEmits = false; 5351 5352 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 5353 5354 enum classStaticallyEmits = thisEmits || baseEmits; 5355 } 5356 5357 /++ 5358 A helper to make widgets out of other native windows. 5359 5360 History: 5361 Factored out of OpenGlWidget on November 5, 2021 5362 +/ 5363 class NestedChildWindowWidget : Widget { 5364 SimpleWindow win; 5365 5366 /++ 5367 Used on X to send focus to the appropriate child window when requested by the window manager. 5368 5369 Normally returns its own nested window. Can also return another child or null to revert to the parent 5370 if you override it in a child class. 5371 5372 History: 5373 Added April 2, 2022 (dub v10.8) 5374 +/ 5375 SimpleWindow focusableWindow() { 5376 return win; 5377 } 5378 5379 /// 5380 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 5381 this(SimpleWindow win, Widget parent) { 5382 this.parentWindow = parent.parentWindow; 5383 this.win = win; 5384 5385 super(parent); 5386 windowsetup(win); 5387 } 5388 5389 static protected SimpleWindow getParentWindow(Widget parent) { 5390 assert(parent !is null); 5391 SimpleWindow pwin = parent.parentWindow.win; 5392 5393 version(win32_widgets) { 5394 HWND phwnd; 5395 auto wtf = parent; 5396 while(wtf) { 5397 if(wtf.hwnd) { 5398 phwnd = wtf.hwnd; 5399 break; 5400 } 5401 wtf = wtf.parent; 5402 } 5403 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 5404 if(phwnd) 5405 pwin = new SimpleWindow(phwnd); 5406 } 5407 5408 return pwin; 5409 } 5410 5411 /++ 5412 Called upon the nested window being destroyed. 5413 Remember the window has already been destroyed at 5414 this point, so don't use the native handle for anything. 5415 5416 History: 5417 Added April 3, 2022 (dub v10.8) 5418 +/ 5419 protected void dispose() { 5420 5421 } 5422 5423 protected void windowsetup(SimpleWindow w) { 5424 /* 5425 win.onFocusChange = (bool getting) { 5426 if(getting) 5427 this.focus(); 5428 }; 5429 */ 5430 5431 /+ 5432 win.onFocusChange = (bool getting) { 5433 if(getting) { 5434 this.parentWindow.focusedWidget = this; 5435 this.emit!FocusEvent(); 5436 this.emit!FocusInEvent(); 5437 } else { 5438 this.emit!BlurEvent(); 5439 this.emit!FocusOutEvent(); 5440 } 5441 }; 5442 +/ 5443 5444 win.onDestroyed = () { 5445 this.dispose(); 5446 }; 5447 5448 version(win32_widgets) { 5449 Widget.nativeMapping[win.hwnd] = this; 5450 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 5451 } else { 5452 win.setEventHandlers( 5453 (MouseEvent e) { 5454 Widget p = this; 5455 while(p ! is parentWindow) { 5456 e.x += p.x; 5457 e.y += p.y; 5458 p = p.parent; 5459 } 5460 parentWindow.dispatchMouseEvent(e); 5461 }, 5462 (KeyEvent e) { 5463 //writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 5464 parentWindow.dispatchKeyEvent(e); 5465 }, 5466 (dchar e) { 5467 parentWindow.dispatchCharEvent(e); 5468 }, 5469 ); 5470 } 5471 5472 } 5473 5474 override bool showOrHideIfNativeWindow(bool shouldShow) { 5475 auto cur = hidden; 5476 win.hidden = !shouldShow; 5477 if(cur != shouldShow && shouldShow) 5478 redraw(); 5479 return true; 5480 } 5481 5482 /// OpenGL widgets cannot have child widgets. Do not call this. 5483 /* @disable */ final override void addChild(Widget, int) { 5484 throw new Error("cannot add children to OpenGL widgets"); 5485 } 5486 5487 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 5488 /// Keep in mind that events like mouse coordinates are still relative to your size. 5489 override void registerMovement() { 5490 // writefln("%d %d %d %d", x,y,width,height); 5491 version(win32_widgets) 5492 auto pos = getChildPositionRelativeToParentHwnd(this); 5493 else 5494 auto pos = getChildPositionRelativeToParentOrigin(this); 5495 win.moveResize(pos[0], pos[1], width, height); 5496 5497 registerMovementAdditionalWork(); 5498 sendResizeEvent(); 5499 } 5500 5501 abstract void registerMovementAdditionalWork(); 5502 } 5503 5504 /++ 5505 Nests an opengl capable window inside this window as a widget. 5506 5507 You may also just want to create an additional [SimpleWindow] with 5508 [OpenGlOptions.yes] yourself. 5509 5510 An OpenGL widget cannot have child widgets. It will throw if you try. 5511 +/ 5512 static if(OpenGlEnabled) 5513 class OpenGlWidget : NestedChildWindowWidget { 5514 5515 override void registerMovementAdditionalWork() { 5516 win.setAsCurrentOpenGlContext(); 5517 } 5518 5519 /// 5520 this(Widget parent) { 5521 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 5522 super(win, parent); 5523 } 5524 5525 override void paint(WidgetPainter painter) { 5526 win.setAsCurrentOpenGlContext(); 5527 glViewport(0, 0, this.width, this.height); 5528 win.redrawOpenGlSceneNow(); 5529 } 5530 5531 void redrawOpenGlScene(void delegate() dg) { 5532 win.redrawOpenGlScene = dg; 5533 } 5534 } 5535 5536 /++ 5537 This demo shows how to draw text in an opengl scene. 5538 +/ 5539 unittest { 5540 import arsd.minigui; 5541 import arsd.ttf; 5542 5543 void main() { 5544 auto window = new Window(); 5545 5546 auto widget = new OpenGlWidget(window); 5547 5548 // old means non-shader code so compatible with glBegin etc. 5549 // tbh I haven't implemented new one in font yet... 5550 // anyway, declaring here, will construct soon. 5551 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 5552 5553 // this is a little bit awkward, calling some methods through 5554 // the underlying SimpleWindow `win` method, and you can't do this 5555 // on a nanovega widget due to conflicts so I should probably fix 5556 // the api to be a bit easier. But here it will work. 5557 // 5558 // Alternatively, you could load the font on the first draw, inside 5559 // the redrawOpenGlScene, and keep a flag so you don't do it every 5560 // time. That'd be a bit easier since the lib sets up the context 5561 // by then guaranteed. 5562 // 5563 // But still, I wanna show this. 5564 widget.win.visibleForTheFirstTime = delegate { 5565 // must set the opengl context 5566 widget.win.setAsCurrentOpenGlContext(); 5567 5568 // if you were doing a OpenGL 3+ shader, this 5569 // gets especially important to do in order. With 5570 // old-style opengl, I think you can even do it 5571 // in main(), but meh, let's show it more correctly. 5572 5573 // Anyway, now it is time to load the font from the 5574 // OS (you can alternatively load one from a .ttf file 5575 // you bundle with the application), then load the 5576 // font into texture for drawing. 5577 5578 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 5579 5580 assert(!osfont.isNull()); // make sure it actually loaded 5581 5582 // using typeof to avoid repeating the long name lol 5583 glfont = new typeof(glfont)( 5584 // get the raw data from the font for loading in here 5585 // since it doesn't use the OS function to draw the 5586 // text, we gotta treat it more as a file than as 5587 // a drawing api. 5588 osfont.getTtfBytes(), 5589 18, // need to respecify size since opengl world is different coordinate system 5590 5591 // these last two numbers are why it is called 5592 // "Limited" font. It only loads the characters 5593 // in the given range, since the texture atlas 5594 // it references is all a big image generated ahead 5595 // of time. You could maybe do the whole thing but 5596 // idk how much memory that is. 5597 // 5598 // But here, 0-128 represents the ASCII range, so 5599 // good enough for most English things, numeric labels, 5600 // etc. 5601 0, 5602 128 5603 ); 5604 }; 5605 5606 widget.redrawOpenGlScene = () { 5607 // now we can use the glfont's drawString function 5608 5609 // first some opengl setup. You can do this in one place 5610 // on window first visible too in many cases, just showing 5611 // here cuz it is easier for me. 5612 5613 // gonna need some alpha blending or it just looks awful 5614 glEnable(GL_BLEND); 5615 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 5616 glClearColor(0,0,0,0); 5617 glDepthFunc(GL_LEQUAL); 5618 5619 // Also need to enable 2d textures, since it draws the 5620 // font characters as images baked in 5621 glMatrixMode(GL_MODELVIEW); 5622 glLoadIdentity(); 5623 glDisable(GL_DEPTH_TEST); 5624 glEnable(GL_TEXTURE_2D); 5625 5626 // the orthographic matrix is best for 2d things like text 5627 // so let's set that up. This matrix makes the coordinates 5628 // in the opengl scene be one-to-one with the actual pixels 5629 // on screen. (Not necessarily best, you may wish to scale 5630 // things, but it does help keep fonts looking normal.) 5631 glMatrixMode(GL_PROJECTION); 5632 glLoadIdentity(); 5633 glOrtho(0, widget.width, widget.height, 0, 0, 1); 5634 5635 // you can do other glScale, glRotate, glTranslate, etc 5636 // to the matrix here of course if you want. 5637 5638 // note the x,y coordinates here are for the text baseline 5639 // NOT the upper-left corner. The baseline is like the line 5640 // in the notebook you write on. Most the letters are actually 5641 // above it, but some, like p and q, dip a bit below it. 5642 // 5643 // So if you're used to the upper left coordinate like the 5644 // rest of simpledisplay/minigui usually do, do the 5645 // y + glfont.ascent to bring it down a little. So this 5646 // example puts the string in the upper left of the window. 5647 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 5648 5649 // re color btw: the function sets a solid color internally, 5650 // but you actually COULD do your own thing for rainbow effects 5651 // and the sort if you wanted too, by pulling its guts out. 5652 // Just view its source for an idea of how it actually draws: 5653 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 5654 5655 // it gets a bit complicated with the character positioning, 5656 // but the opengl parts are fairly simple: bind a texture, 5657 // set the color, draw a quad for each letter. 5658 5659 5660 // the last optional argument there btw is a bounding box 5661 // it will/ use to word wrap and return an object you can 5662 // use to implement scrolling or pagination; it tells how 5663 // much of the string didn't fit in the box. But for simple 5664 // labels we can just ignore that. 5665 5666 5667 // I'd suggest drawing text as the last step, after you 5668 // do your other drawing. You might use the push/pop matrix 5669 // stuff to keep your place. You, in theory, should be able 5670 // to do text in a 3d space but I've never actually tried 5671 // that.... 5672 }; 5673 5674 window.loop(); 5675 } 5676 } 5677 5678 version(custom_widgets) 5679 private class TextListViewWidget : GenericListViewWidget { 5680 static class TextListViewItem : GenericListViewItem { 5681 ListWidget controller; 5682 this(ListWidget controller, Widget parent) { 5683 this.controller = controller; 5684 this.tabStop = false; 5685 super(parent); 5686 } 5687 5688 ListWidget.Option* showing; 5689 5690 override void showItem(int idx) { 5691 showing = idx < controller.options.length ? &controller.options[idx] : null; 5692 redraw(); // is this necessary? the generic thing might call it... 5693 } 5694 5695 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 5696 if(showing is null) 5697 return bounds; 5698 painter.drawText(bounds.upperLeft, showing.label); 5699 return bounds; 5700 } 5701 5702 static class Style : Widget.Style { 5703 override WidgetBackground background() { 5704 // FIXME: change it if it is focused or not 5705 // needs to reliably detect if focused (noting the actual focus may be on a parent or child... or even sibling for FreeEntrySelection. maybe i just need a better way to proxy focus in widgets generically). also will need to redraw correctly without defaultEventHandler_focusin hacks like EditableTextWidget uses 5706 auto tlvi = cast(TextListViewItem) widget; 5707 if(tlvi && tlvi.showing && tlvi && tlvi.showing.selected) 5708 return WidgetBackground(true /*widget.parent.isFocused*/ ? WidgetPainter.visualTheme.selectionBackgroundColor : Color(128, 128, 128)); // FIXME: don't hardcode 5709 return super.background(); 5710 } 5711 5712 override Color foregroundColor() { 5713 auto tlvi = cast(TextListViewItem) widget; 5714 return tlvi && tlvi.showing && tlvi && tlvi.showing.selected ? WidgetPainter.visualTheme.selectionForegroundColor : super.foregroundColor(); 5715 } 5716 5717 override FrameStyle outlineStyle() { 5718 // FIXME: change it if it is focused or not 5719 auto tlvi = cast(TextListViewItem) widget; 5720 return (tlvi && tlvi.currentIndexLoaded() == tlvi.controller.focusOn) ? FrameStyle.dotted : super.outlineStyle(); 5721 } 5722 } 5723 mixin OverrideStyle!Style; 5724 5725 mixin Padding!q{2}; 5726 5727 override void defaultEventHandler_click(ClickEvent event) { 5728 if(event.button == MouseButton.left) { 5729 controller.setSelection(currentIndexLoaded()); 5730 controller.focusOn = currentIndexLoaded(); 5731 } 5732 } 5733 5734 } 5735 5736 ListWidget controller; 5737 5738 this(ListWidget parent) { 5739 this.controller = parent; 5740 this.tabStop = false; // this is only used as a child of the ListWidget 5741 super(parent); 5742 5743 smw.movementPerButtonClick(1, itemSize().height); 5744 } 5745 5746 override Size itemSize() { 5747 return Size(0, defaultLineHeight + scaleWithDpi(4 /* the top and bottom padding */)); 5748 } 5749 5750 override GenericListViewItem itemFactory(Widget parent) { 5751 return new TextListViewItem(controller, parent); 5752 } 5753 5754 static class Style : Widget.Style { 5755 override FrameStyle borderStyle() { 5756 return FrameStyle.sunk; 5757 } 5758 5759 override WidgetBackground background() { 5760 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 5761 } 5762 } 5763 mixin OverrideStyle!Style; 5764 } 5765 5766 /++ 5767 A list widget contains a list of strings that the user can examine and select. 5768 5769 5770 In the future, items in the list may be possible to be more than just strings. 5771 5772 See_Also: 5773 [TableView] 5774 +/ 5775 class ListWidget : Widget { 5776 /// Sends a change event when the selection changes, but the data is not attached to the event. You must instead loop the options to see if they are selected. 5777 mixin Emits!(ChangeEvent!void); 5778 5779 version(custom_widgets) 5780 TextListViewWidget glvw; 5781 5782 static struct Option { 5783 string label; 5784 bool selected; 5785 void* tag; 5786 } 5787 private Option[] options; 5788 5789 /++ 5790 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 5791 +/ 5792 void setSelection(int y) { 5793 if(!multiSelect) 5794 foreach(ref opt; options) 5795 opt.selected = false; 5796 if(y >= 0 && y < options.length) 5797 options[y].selected = !options[y].selected; 5798 5799 version(custom_widgets) 5800 focusOn = y; 5801 5802 this.emit!(ChangeEvent!void)(delegate {}); 5803 5804 version(custom_widgets) 5805 redraw(); 5806 } 5807 5808 /++ 5809 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 5810 Returns -1 if nothing is selected. 5811 +/ 5812 int getSelection() 5813 { 5814 foreach(i, opt; options) { 5815 if (opt.selected) 5816 return cast(int) i; 5817 } 5818 return -1; 5819 } 5820 5821 version(custom_widgets) 5822 private int focusOn; 5823 5824 this(Widget parent) { 5825 super(parent); 5826 5827 version(custom_widgets) 5828 glvw = new TextListViewWidget(this); 5829 5830 version(win32_widgets) 5831 createWin32Window(this, WC_LISTBOX, "", 5832 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 5833 } 5834 5835 version(win32_widgets) 5836 override void handleWmCommand(ushort code, ushort id) { 5837 switch(code) { 5838 case LBN_SELCHANGE: 5839 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 5840 setSelection(cast(int) sel); 5841 break; 5842 default: 5843 } 5844 } 5845 5846 5847 void addOption(string text, void* tag = null) { 5848 options ~= Option(text, false, tag); 5849 version(win32_widgets) { 5850 WCharzBuffer buffer = WCharzBuffer(text); 5851 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 5852 } 5853 version(custom_widgets) { 5854 glvw.setItemCount(cast(int) options.length); 5855 //setContentSize(width, cast(int) (options.length * defaultLineHeight)); 5856 redraw(); 5857 } 5858 } 5859 5860 void clear() { 5861 options = null; 5862 version(win32_widgets) { 5863 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 5864 {} 5865 5866 } else version(custom_widgets) { 5867 focusOn = -1; 5868 glvw.setItemCount(0); 5869 redraw(); 5870 } 5871 } 5872 5873 version(custom_widgets) 5874 override void defaultEventHandler_keydown(KeyDownEvent kde) { 5875 void changedFocusOn() { 5876 scrollFocusIntoView(); 5877 if(multiSelect) 5878 redraw(); 5879 else 5880 setSelection(focusOn); 5881 } 5882 switch(kde.key) { 5883 case Key.Up: 5884 if(focusOn) { 5885 focusOn--; 5886 changedFocusOn(); 5887 } 5888 break; 5889 case Key.Down: 5890 if(focusOn + 1 < options.length) { 5891 focusOn++; 5892 changedFocusOn(); 5893 } 5894 break; 5895 case Key.Home: 5896 if(focusOn) { 5897 focusOn = 0; 5898 changedFocusOn(); 5899 } 5900 break; 5901 case Key.End: 5902 if(options.length && focusOn + 1 != options.length) { 5903 focusOn = cast(int) options.length - 1; 5904 changedFocusOn(); 5905 } 5906 break; 5907 case Key.PageUp: 5908 auto n = glvw.numberOfCurrentlyFullyVisibleItems; 5909 focusOn -= n; 5910 if(focusOn < 0) 5911 focusOn = 0; 5912 changedFocusOn(); 5913 break; 5914 case Key.PageDown: 5915 if(options.length == 0) 5916 break; 5917 auto n = glvw.numberOfCurrentlyFullyVisibleItems; 5918 focusOn += n; 5919 if(focusOn >= options.length) 5920 focusOn = cast(int) options.length - 1; 5921 changedFocusOn(); 5922 break; 5923 5924 default: 5925 } 5926 } 5927 5928 version(custom_widgets) 5929 override void defaultEventHandler_char(CharEvent ce) { 5930 if(ce.character == '\n' || ce.character == ' ') { 5931 setSelection(focusOn); 5932 } else { 5933 // search for the item that best matches and jump to it 5934 // FIXME this sucks in tons of ways. the normal thing toolkits 5935 // do here is to search for a substring on a timer, but i'd kinda 5936 // rather make an actual little dialog with some options. still meh for now. 5937 dchar search = ce.character; 5938 if(search >= 'A' && search <= 'Z') 5939 search += 32; 5940 foreach(idx, option; options) { 5941 auto ch = option.label.length ? option.label[0] : 0; 5942 if(ch >= 'A' && ch <= 'Z') 5943 ch += 32; 5944 if(ch == search) { 5945 setSelection(cast(int) idx); 5946 scrollSelectionIntoView(); 5947 break; 5948 } 5949 } 5950 5951 } 5952 } 5953 5954 version(win32_widgets) 5955 enum multiSelect = false; /// not implemented yet 5956 else 5957 bool multiSelect; 5958 5959 override int heightStretchiness() { return 6; } 5960 5961 version(custom_widgets) 5962 void scrollFocusIntoView() { 5963 glvw.ensureItemVisibleInScroll(focusOn); 5964 } 5965 5966 void scrollSelectionIntoView() { 5967 // FIXME: implement on Windows 5968 5969 version(custom_widgets) 5970 glvw.ensureItemVisibleInScroll(getSelection()); 5971 } 5972 5973 /* 5974 version(custom_widgets) 5975 override void defaultEventHandler_focusout(Event foe) { 5976 glvw.redraw(); 5977 } 5978 5979 version(custom_widgets) 5980 override void defaultEventHandler_focusin(Event foe) { 5981 glvw.redraw(); 5982 } 5983 */ 5984 5985 } 5986 5987 5988 5989 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 5990 /// NEVER USED 5991 enum ScrollBarShowPolicy { 5992 automatic, /// automatically show the scroll bar if it is necessary 5993 never, /// never show the scroll bar (scrolling must be done programmatically) 5994 always /// always show the scroll bar, even if it is disabled 5995 } 5996 5997 /++ 5998 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 5999 6000 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 6001 +/ 6002 // FIXME ScrollBarShowPolicy 6003 // FIXME: use the ScrollMessageWidget in here now that it exists 6004 deprecated("Use ScrollMessageWidget or ScrollableContainerWidget instead") // ugh compiler won't let me do it 6005 class ScrollableWidget : Widget { 6006 // FIXME: make line size configurable 6007 // FIXME: add keyboard controls 6008 version(win32_widgets) { 6009 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 6010 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 6011 auto pos = HIWORD(wParam); 6012 auto m = LOWORD(wParam); 6013 6014 // FIXME: I can reintroduce the 6015 // scroll bars now by using this 6016 // in the top-level window handler 6017 // to forward comamnds 6018 auto scrollbarHwnd = lParam; 6019 switch(m) { 6020 case SB_BOTTOM: 6021 if(msg == WM_HSCROLL) 6022 horizontalScrollTo(contentWidth_); 6023 else 6024 verticalScrollTo(contentHeight_); 6025 break; 6026 case SB_TOP: 6027 if(msg == WM_HSCROLL) 6028 horizontalScrollTo(0); 6029 else 6030 verticalScrollTo(0); 6031 break; 6032 case SB_ENDSCROLL: 6033 // idk 6034 break; 6035 case SB_LINEDOWN: 6036 if(msg == WM_HSCROLL) 6037 horizontalScroll(scaleWithDpi(16)); 6038 else 6039 verticalScroll(scaleWithDpi(16)); 6040 break; 6041 case SB_LINEUP: 6042 if(msg == WM_HSCROLL) 6043 horizontalScroll(scaleWithDpi(-16)); 6044 else 6045 verticalScroll(scaleWithDpi(-16)); 6046 break; 6047 case SB_PAGEDOWN: 6048 if(msg == WM_HSCROLL) 6049 horizontalScroll(scaleWithDpi(100)); 6050 else 6051 verticalScroll(scaleWithDpi(100)); 6052 break; 6053 case SB_PAGEUP: 6054 if(msg == WM_HSCROLL) 6055 horizontalScroll(scaleWithDpi(-100)); 6056 else 6057 verticalScroll(scaleWithDpi(-100)); 6058 break; 6059 case SB_THUMBPOSITION: 6060 case SB_THUMBTRACK: 6061 if(msg == WM_HSCROLL) 6062 horizontalScrollTo(pos); 6063 else 6064 verticalScrollTo(pos); 6065 6066 if(m == SB_THUMBTRACK) { 6067 // the event loop doesn't seem to carry on with a requested redraw.. 6068 // so we request it to get our dirty bit set... 6069 redraw(); 6070 6071 // then we need to immediately actually redraw it too for instant feedback to user 6072 6073 SimpleWindow.processAllCustomEvents(); 6074 //if(parentWindow) 6075 //parentWindow.actualRedraw(); 6076 } 6077 break; 6078 default: 6079 } 6080 } 6081 return super.hookedWndProc(msg, wParam, lParam); 6082 } 6083 } 6084 /// 6085 this(Widget parent) { 6086 this.parentWindow = parent.parentWindow; 6087 6088 version(win32_widgets) { 6089 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 6090 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 6091 super(parent); 6092 } else version(custom_widgets) { 6093 outerContainer = new InternalScrollableContainerWidget(this, parent); 6094 super(outerContainer); 6095 } else static assert(0); 6096 } 6097 6098 version(custom_widgets) 6099 InternalScrollableContainerWidget outerContainer; 6100 6101 override void defaultEventHandler_click(ClickEvent event) { 6102 if(event.button == MouseButton.wheelUp) 6103 verticalScroll(scaleWithDpi(-16)); 6104 if(event.button == MouseButton.wheelDown) 6105 verticalScroll(scaleWithDpi(16)); 6106 super.defaultEventHandler_click(event); 6107 } 6108 6109 override void defaultEventHandler_keydown(KeyDownEvent event) { 6110 switch(event.key) { 6111 case Key.Left: 6112 horizontalScroll(scaleWithDpi(-16)); 6113 break; 6114 case Key.Right: 6115 horizontalScroll(scaleWithDpi(16)); 6116 break; 6117 case Key.Up: 6118 verticalScroll(scaleWithDpi(-16)); 6119 break; 6120 case Key.Down: 6121 verticalScroll(scaleWithDpi(16)); 6122 break; 6123 case Key.Home: 6124 verticalScrollTo(0); 6125 break; 6126 case Key.End: 6127 verticalScrollTo(contentHeight); 6128 break; 6129 case Key.PageUp: 6130 verticalScroll(scaleWithDpi(-160)); 6131 break; 6132 case Key.PageDown: 6133 verticalScroll(scaleWithDpi(160)); 6134 break; 6135 default: 6136 } 6137 super.defaultEventHandler_keydown(event); 6138 } 6139 6140 6141 version(win32_widgets) 6142 override void recomputeChildLayout() { 6143 super.recomputeChildLayout(); 6144 SCROLLINFO info; 6145 info.cbSize = info.sizeof; 6146 info.nPage = viewportHeight; 6147 info.fMask = SIF_PAGE | SIF_RANGE; 6148 info.nMin = 0; 6149 info.nMax = contentHeight_; 6150 SetScrollInfo(hwnd, SB_VERT, &info, true); 6151 6152 info.cbSize = info.sizeof; 6153 info.nPage = viewportWidth; 6154 info.fMask = SIF_PAGE | SIF_RANGE; 6155 info.nMin = 0; 6156 info.nMax = contentWidth_; 6157 SetScrollInfo(hwnd, SB_HORZ, &info, true); 6158 } 6159 6160 /* 6161 Scrolling 6162 ------------ 6163 6164 You are assigned a width and a height by the layout engine, which 6165 is your viewport box. However, you may draw more than that by setting 6166 a contentWidth and contentHeight. 6167 6168 If these can be contained by the viewport, no scrollbar is displayed. 6169 If they cannot fit though, it will automatically show scroll as necessary. 6170 6171 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 6172 is zero, no vertical scrolling is performed. 6173 6174 If scrolling is necessary, the lib will automatically work with the bars. 6175 When you redraw, the origin and clipping info in the painter is set so if 6176 you just draw everything, it will work, but you can be more efficient by checking 6177 the viewportWidth, viewportHeight, and scrollOrigin members. 6178 */ 6179 6180 /// 6181 final @property int viewportWidth() { 6182 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 6183 } 6184 /// 6185 final @property int viewportHeight() { 6186 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 6187 } 6188 6189 // FIXME property 6190 Point scrollOrigin_; 6191 6192 /// 6193 final const(Point) scrollOrigin() { 6194 return scrollOrigin_; 6195 } 6196 6197 // the user sets these two 6198 private int contentWidth_ = 0; 6199 private int contentHeight_ = 0; 6200 6201 /// 6202 int contentWidth() { return contentWidth_; } 6203 /// 6204 int contentHeight() { return contentHeight_; } 6205 6206 /// 6207 void setContentSize(int width, int height) { 6208 contentWidth_ = width; 6209 contentHeight_ = height; 6210 6211 version(custom_widgets) { 6212 if(showingVerticalScroll || showingHorizontalScroll) { 6213 outerContainer.queueRecomputeChildLayout(); 6214 } 6215 6216 if(showingVerticalScroll()) 6217 outerContainer.verticalScrollBar.redraw(); 6218 if(showingHorizontalScroll()) 6219 outerContainer.horizontalScrollBar.redraw(); 6220 } else version(win32_widgets) { 6221 queueRecomputeChildLayout(); 6222 } else static assert(0); 6223 } 6224 6225 /// 6226 void verticalScroll(int delta) { 6227 verticalScrollTo(scrollOrigin.y + delta); 6228 } 6229 /// 6230 void verticalScrollTo(int pos) { 6231 scrollOrigin_.y = pos; 6232 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 6233 scrollOrigin_.y = contentHeight - viewportHeight; 6234 6235 if(scrollOrigin_.y < 0) 6236 scrollOrigin_.y = 0; 6237 6238 version(win32_widgets) { 6239 SCROLLINFO info; 6240 info.cbSize = info.sizeof; 6241 info.fMask = SIF_POS; 6242 info.nPos = scrollOrigin_.y; 6243 SetScrollInfo(hwnd, SB_VERT, &info, true); 6244 } else version(custom_widgets) { 6245 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 6246 } else static assert(0); 6247 6248 redraw(); 6249 } 6250 6251 /// 6252 void horizontalScroll(int delta) { 6253 horizontalScrollTo(scrollOrigin.x + delta); 6254 } 6255 /// 6256 void horizontalScrollTo(int pos) { 6257 scrollOrigin_.x = pos; 6258 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 6259 scrollOrigin_.x = contentWidth - viewportWidth; 6260 6261 if(scrollOrigin_.x < 0) 6262 scrollOrigin_.x = 0; 6263 6264 version(win32_widgets) { 6265 SCROLLINFO info; 6266 info.cbSize = info.sizeof; 6267 info.fMask = SIF_POS; 6268 info.nPos = scrollOrigin_.x; 6269 SetScrollInfo(hwnd, SB_HORZ, &info, true); 6270 } else version(custom_widgets) { 6271 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 6272 } else static assert(0); 6273 6274 redraw(); 6275 } 6276 /// 6277 void scrollTo(Point p) { 6278 verticalScrollTo(p.y); 6279 horizontalScrollTo(p.x); 6280 } 6281 6282 /// 6283 void ensureVisibleInScroll(Point p) { 6284 auto rect = viewportRectangle(); 6285 if(rect.contains(p)) 6286 return; 6287 if(p.x < rect.left) 6288 horizontalScroll(p.x - rect.left); 6289 else if(p.x > rect.right) 6290 horizontalScroll(p.x - rect.right); 6291 6292 if(p.y < rect.top) 6293 verticalScroll(p.y - rect.top); 6294 else if(p.y > rect.bottom) 6295 verticalScroll(p.y - rect.bottom); 6296 } 6297 6298 /// 6299 void ensureVisibleInScroll(Rectangle rect) { 6300 ensureVisibleInScroll(rect.upperLeft); 6301 ensureVisibleInScroll(rect.lowerRight); 6302 } 6303 6304 /// 6305 Rectangle viewportRectangle() { 6306 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 6307 } 6308 6309 /// 6310 bool showingHorizontalScroll() { 6311 return contentWidth > width; 6312 } 6313 /// 6314 bool showingVerticalScroll() { 6315 return contentHeight > height; 6316 } 6317 6318 /// This is called before the ordinary paint delegate, 6319 /// giving you a chance to draw the window frame, etc, 6320 /// before the scroll clip takes effect 6321 void paintFrameAndBackground(WidgetPainter painter) { 6322 version(win32_widgets) { 6323 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 6324 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 6325 // since the pen is null, to fill the whole space, we need the +1 on both. 6326 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 6327 SelectObject(painter.impl.hdc, p); 6328 SelectObject(painter.impl.hdc, b); 6329 } 6330 6331 } 6332 6333 // make space for the scroll bar, and that's it. 6334 final override int paddingRight() { return scaleWithDpi(16); } 6335 final override int paddingBottom() { return scaleWithDpi(16); } 6336 6337 /* 6338 END SCROLLING 6339 */ 6340 6341 override WidgetPainter draw() { 6342 int x = this.x, y = this.y; 6343 auto parent = this.parent; 6344 while(parent) { 6345 x += parent.x; 6346 y += parent.y; 6347 parent = parent.parent; 6348 } 6349 6350 //version(win32_widgets) { 6351 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 6352 //} else { 6353 auto painter = parentWindow.win.draw(true); 6354 //} 6355 painter.originX = x; 6356 painter.originY = y; 6357 6358 painter.originX = painter.originX - scrollOrigin.x; 6359 painter.originY = painter.originY - scrollOrigin.y; 6360 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 6361 6362 return WidgetPainter(painter, this); 6363 } 6364 6365 override void addScrollPosition(ref int x, ref int y) { 6366 x += scrollOrigin.x; 6367 y += scrollOrigin.y; 6368 } 6369 6370 mixin ScrollableChildren; 6371 } 6372 6373 // you need to have a Point scrollOrigin in the class somewhere 6374 // and a paintFrameAndBackground 6375 private mixin template ScrollableChildren() { 6376 static assert(!__traits(isSame, this.addScrollPosition, Widget.addScrollPosition), "Your widget should provide `Point scrollOrigin()` and `override void addScrollPosition`"); 6377 6378 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 6379 if(hidden) 6380 return; 6381 6382 //version(win32_widgets) 6383 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 6384 6385 painter.originX = lox + x; 6386 painter.originY = loy + y; 6387 6388 bool actuallyPainted = false; 6389 6390 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 6391 if(clip == Rectangle.init) 6392 return; 6393 6394 if(force || redrawRequested) { 6395 //painter.setClipRectangle(scrollOrigin, width, height); 6396 painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 6397 paintFrameAndBackground(painter); 6398 } 6399 6400 /+ 6401 version(win32_widgets) { 6402 if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW); 6403 } 6404 +/ 6405 6406 painter.originX = painter.originX - scrollOrigin.x; 6407 painter.originY = painter.originY - scrollOrigin.y; 6408 if(force || redrawRequested) { 6409 painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 6410 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 6411 6412 //erase(painter); // we paintFrameAndBackground above so no need 6413 if(painter.visualTheme) 6414 painter.visualTheme.doPaint(this, painter); 6415 else 6416 paint(painter); 6417 6418 if(invalidate) { 6419 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 6420 // children are contained inside this, so no need to do extra work 6421 invalidate = false; 6422 } 6423 6424 6425 actuallyPainted = true; 6426 redrawRequested = false; 6427 } 6428 6429 foreach(child; children) { 6430 if(cast(FixedPosition) child) 6431 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 6432 else 6433 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 6434 } 6435 } 6436 } 6437 6438 private class InternalScrollableContainerInsideWidget : ContainerWidget { 6439 ScrollableContainerWidget scw; 6440 6441 this(ScrollableContainerWidget parent) { 6442 scw = parent; 6443 super(parent); 6444 } 6445 6446 version(custom_widgets) 6447 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 6448 if(hidden) 6449 return; 6450 6451 bool actuallyPainted = false; 6452 6453 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 6454 6455 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 6456 if(clip == Rectangle.init) 6457 return; 6458 6459 painter.originX = lox + x - scrollOrigin.x; 6460 painter.originY = loy + y - scrollOrigin.y; 6461 if(force || redrawRequested) { 6462 painter.setClipRectangleForWidget(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 6463 6464 erase(painter); 6465 if(painter.visualTheme) 6466 painter.visualTheme.doPaint(this, painter); 6467 else 6468 paint(painter); 6469 6470 if(invalidate) { 6471 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 6472 // children are contained inside this, so no need to do extra work 6473 invalidate = false; 6474 } 6475 6476 actuallyPainted = true; 6477 redrawRequested = false; 6478 } 6479 foreach(child; children) { 6480 if(cast(FixedPosition) child) 6481 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 6482 else 6483 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 6484 } 6485 } 6486 6487 version(custom_widgets) 6488 override protected void addScrollPosition(ref int x, ref int y) { 6489 x += scw.scrollX_; 6490 y += scw.scrollY_; 6491 } 6492 } 6493 6494 /++ 6495 A widget meant to contain other widgets that may need to scroll. 6496 6497 Currently buggy. 6498 6499 History: 6500 Added July 1, 2021 (dub v10.2) 6501 6502 On January 3, 2022, I tried to use it in a few other cases 6503 and found it only worked well in the original test case. Since 6504 it still sucks, I think I'm going to rewrite it again. 6505 +/ 6506 class ScrollableContainerWidget : ContainerWidget { 6507 /// 6508 this(Widget parent) { 6509 super(parent); 6510 6511 container = new InternalScrollableContainerInsideWidget(this); 6512 hsb = new HorizontalScrollbar(this); 6513 vsb = new VerticalScrollbar(this); 6514 6515 tabStop = false; 6516 container.tabStop = false; 6517 magic = true; 6518 6519 6520 vsb.addEventListener("scrolltonextline", () { 6521 scrollBy(0, scaleWithDpi(16)); 6522 }); 6523 vsb.addEventListener("scrolltopreviousline", () { 6524 scrollBy(0,scaleWithDpi( -16)); 6525 }); 6526 vsb.addEventListener("scrolltonextpage", () { 6527 scrollBy(0, container.height); 6528 }); 6529 vsb.addEventListener("scrolltopreviouspage", () { 6530 scrollBy(0, -container.height); 6531 }); 6532 vsb.addEventListener((scope ScrollToPositionEvent spe) { 6533 scrollTo(scrollX_, spe.value); 6534 }); 6535 6536 this.addEventListener(delegate (scope ClickEvent e) { 6537 if(e.button == MouseButton.wheelUp) { 6538 if(!e.defaultPrevented) 6539 scrollBy(0, scaleWithDpi(-16)); 6540 e.stopPropagation(); 6541 } else if(e.button == MouseButton.wheelDown) { 6542 if(!e.defaultPrevented) 6543 scrollBy(0, scaleWithDpi(16)); 6544 e.stopPropagation(); 6545 } 6546 }); 6547 } 6548 6549 /+ 6550 override void defaultEventHandler_click(ClickEvent e) { 6551 } 6552 +/ 6553 6554 override void removeAllChildren() { 6555 container.removeAllChildren(); 6556 } 6557 6558 void scrollTo(int x, int y) { 6559 scrollBy(x - scrollX_, y - scrollY_); 6560 } 6561 6562 void scrollBy(int x, int y) { 6563 auto ox = scrollX_; 6564 auto oy = scrollY_; 6565 6566 auto nx = ox + x; 6567 auto ny = oy + y; 6568 6569 if(nx < 0) 6570 nx = 0; 6571 if(ny < 0) 6572 ny = 0; 6573 6574 auto maxX = hsb.max - container.width; 6575 if(maxX < 0) maxX = 0; 6576 auto maxY = vsb.max - container.height; 6577 if(maxY < 0) maxY = 0; 6578 6579 if(nx > maxX) 6580 nx = maxX; 6581 if(ny > maxY) 6582 ny = maxY; 6583 6584 auto dx = nx - ox; 6585 auto dy = ny - oy; 6586 6587 if(dx || dy) { 6588 version(win32_widgets) 6589 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 6590 else { 6591 redraw(); 6592 } 6593 6594 hsb.setPosition = nx; 6595 vsb.setPosition = ny; 6596 6597 scrollX_ = nx; 6598 scrollY_ = ny; 6599 } 6600 } 6601 6602 private int scrollX_; 6603 private int scrollY_; 6604 6605 void setTotalArea(int width, int height) { 6606 hsb.setMax(width); 6607 vsb.setMax(height); 6608 } 6609 6610 /// 6611 void setViewableArea(int width, int height) { 6612 hsb.setViewableArea(width); 6613 vsb.setViewableArea(height); 6614 } 6615 6616 private bool magic; 6617 override void addChild(Widget w, int position = int.max) { 6618 if(magic) 6619 container.addChild(w, position); 6620 else 6621 super.addChild(w, position); 6622 } 6623 6624 override void recomputeChildLayout() { 6625 if(hsb is null || vsb is null || container is null) return; 6626 6627 /+ 6628 writeln(x, " ", y , " ", width, " ", height); 6629 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 6630 +/ 6631 6632 registerMovement(); 6633 6634 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 6635 hsb.x = 0; 6636 hsb.y = this.height - hsb.height; 6637 hsb.width = this.width - scaleWithDpi(16); 6638 hsb.recomputeChildLayout(); 6639 6640 vsb.width = scaleWithDpi(16); // FIXME? 6641 vsb.x = this.width - vsb.width; 6642 vsb.y = 0; 6643 vsb.height = this.height - scaleWithDpi(16); 6644 vsb.recomputeChildLayout(); 6645 6646 container.x = 0; 6647 container.y = 0; 6648 container.width = this.width - vsb.width; 6649 container.height = this.height - hsb.height; 6650 container.recomputeChildLayout(); 6651 6652 scrollX_ = 0; 6653 scrollY_ = 0; 6654 6655 hsb.setPosition(0); 6656 vsb.setPosition(0); 6657 6658 int mw, mh; 6659 Widget c = container; 6660 // FIXME: hack here to handle a layout inside... 6661 if(c.children.length == 1 && cast(Layout) c.children[0]) 6662 c = c.children[0]; 6663 foreach(child; c.children) { 6664 auto w = child.x + child.width; 6665 auto h = child.y + child.height; 6666 6667 if(w > mw) mw = w; 6668 if(h > mh) mh = h; 6669 } 6670 6671 setTotalArea(mw, mh); 6672 setViewableArea(width, height); 6673 } 6674 6675 override int minHeight() { return scaleWithDpi(64); } 6676 6677 HorizontalScrollbar hsb; 6678 VerticalScrollbar vsb; 6679 ContainerWidget container; 6680 } 6681 6682 6683 version(custom_widgets) 6684 deprecated 6685 private class InternalScrollableContainerWidget : Widget { 6686 6687 ScrollableWidget sw; 6688 6689 VerticalScrollbar verticalScrollBar; 6690 HorizontalScrollbar horizontalScrollBar; 6691 6692 this(ScrollableWidget sw, Widget parent) { 6693 this.sw = sw; 6694 6695 this.tabStop = false; 6696 6697 super(parent); 6698 6699 horizontalScrollBar = new HorizontalScrollbar(this); 6700 verticalScrollBar = new VerticalScrollbar(this); 6701 6702 horizontalScrollBar.showing_ = false; 6703 verticalScrollBar.showing_ = false; 6704 6705 horizontalScrollBar.addEventListener("scrolltonextline", { 6706 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 6707 sw.horizontalScrollTo(horizontalScrollBar.position); 6708 }); 6709 horizontalScrollBar.addEventListener("scrolltopreviousline", { 6710 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 6711 sw.horizontalScrollTo(horizontalScrollBar.position); 6712 }); 6713 verticalScrollBar.addEventListener("scrolltonextline", { 6714 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 6715 sw.verticalScrollTo(verticalScrollBar.position); 6716 }); 6717 verticalScrollBar.addEventListener("scrolltopreviousline", { 6718 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 6719 sw.verticalScrollTo(verticalScrollBar.position); 6720 }); 6721 horizontalScrollBar.addEventListener("scrolltonextpage", { 6722 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 6723 sw.horizontalScrollTo(horizontalScrollBar.position); 6724 }); 6725 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 6726 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 6727 sw.horizontalScrollTo(horizontalScrollBar.position); 6728 }); 6729 verticalScrollBar.addEventListener("scrolltonextpage", { 6730 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 6731 sw.verticalScrollTo(verticalScrollBar.position); 6732 }); 6733 verticalScrollBar.addEventListener("scrolltopreviouspage", { 6734 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 6735 sw.verticalScrollTo(verticalScrollBar.position); 6736 }); 6737 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 6738 horizontalScrollBar.setPosition(event.intValue); 6739 sw.horizontalScrollTo(horizontalScrollBar.position); 6740 }); 6741 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 6742 verticalScrollBar.setPosition(event.intValue); 6743 sw.verticalScrollTo(verticalScrollBar.position); 6744 }); 6745 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 6746 horizontalScrollBar.setPosition(event.intValue); 6747 sw.horizontalScrollTo(horizontalScrollBar.position); 6748 }); 6749 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 6750 verticalScrollBar.setPosition(event.intValue); 6751 }); 6752 } 6753 6754 // this is supposed to be basically invisible... 6755 override int minWidth() { return sw.minWidth; } 6756 override int minHeight() { return sw.minHeight; } 6757 override int maxWidth() { return sw.maxWidth; } 6758 override int maxHeight() { return sw.maxHeight; } 6759 override int widthStretchiness() { return sw.widthStretchiness; } 6760 override int heightStretchiness() { return sw.heightStretchiness; } 6761 override int marginLeft() { return sw.marginLeft; } 6762 override int marginRight() { return sw.marginRight; } 6763 override int marginTop() { return sw.marginTop; } 6764 override int marginBottom() { return sw.marginBottom; } 6765 override int paddingLeft() { return sw.paddingLeft; } 6766 override int paddingRight() { return sw.paddingRight; } 6767 override int paddingTop() { return sw.paddingTop; } 6768 override int paddingBottom() { return sw.paddingBottom; } 6769 override void focus() { sw.focus(); } 6770 6771 6772 override void recomputeChildLayout() { 6773 // The stupid thing needs to calculate if a scroll bar is needed... 6774 recomputeChildLayoutHelper(); 6775 // then running it again will position things correctly if the bar is NOT needed 6776 recomputeChildLayoutHelper(); 6777 6778 // this sucks but meh it barely works 6779 } 6780 6781 private void recomputeChildLayoutHelper() { 6782 if(sw is null) return; 6783 6784 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 6785 if(horizontalScrollBar && verticalScrollBar) { 6786 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 6787 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 6788 horizontalScrollBar.x = 0; 6789 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 6790 6791 verticalScrollBar.width = verticalScrollBar.minWidth(); 6792 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 6793 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 6794 verticalScrollBar.y = 0 + 2; 6795 6796 sw.x = 0; 6797 sw.y = 0; 6798 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 6799 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 6800 6801 if(sw.contentWidth_ <= this.width) 6802 sw.scrollOrigin_.x = 0; 6803 if(sw.contentHeight_ <= this.height) 6804 sw.scrollOrigin_.y = 0; 6805 6806 horizontalScrollBar.recomputeChildLayout(); 6807 verticalScrollBar.recomputeChildLayout(); 6808 sw.recomputeChildLayout(); 6809 } 6810 6811 if(sw.contentWidth_ <= this.width) 6812 sw.scrollOrigin_.x = 0; 6813 if(sw.contentHeight_ <= this.height) 6814 sw.scrollOrigin_.y = 0; 6815 6816 if(sw.showingHorizontalScroll()) 6817 horizontalScrollBar.showing(true, false); 6818 else 6819 horizontalScrollBar.showing(false, false); 6820 if(sw.showingVerticalScroll()) 6821 verticalScrollBar.showing(true, false); 6822 else 6823 verticalScrollBar.showing(false, false); 6824 6825 verticalScrollBar.setViewableArea(sw.viewportHeight()); 6826 verticalScrollBar.setMax(sw.contentHeight); 6827 verticalScrollBar.setPosition(sw.scrollOrigin.y); 6828 6829 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 6830 horizontalScrollBar.setMax(sw.contentWidth); 6831 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 6832 } 6833 } 6834 6835 /* 6836 class ScrollableClientWidget : Widget { 6837 this(Widget parent) { 6838 super(parent); 6839 } 6840 override void paint(WidgetPainter p) { 6841 parent.paint(p); 6842 } 6843 } 6844 */ 6845 6846 /++ 6847 A slider, also known as a trackbar control, is commonly used in applications like volume controls where you want the user to select a value between a min and a max without needing a specific value or otherwise precise input. 6848 +/ 6849 abstract class Slider : Widget { 6850 this(int min, int max, int step, Widget parent) { 6851 min_ = min; 6852 max_ = max; 6853 step_ = step; 6854 page_ = step; 6855 super(parent); 6856 } 6857 6858 private int min_; 6859 private int max_; 6860 private int step_; 6861 private int position_; 6862 private int page_; 6863 6864 // selection start and selection end 6865 // tics 6866 // tooltip? 6867 // some way to see and just type the value 6868 // win32 buddy controls are labels 6869 6870 /// 6871 void setMin(int a) { 6872 min_ = a; 6873 version(custom_widgets) 6874 redraw(); 6875 version(win32_widgets) 6876 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 6877 } 6878 /// 6879 int min() { 6880 return min_; 6881 } 6882 /// 6883 void setMax(int a) { 6884 max_ = a; 6885 version(custom_widgets) 6886 redraw(); 6887 version(win32_widgets) 6888 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 6889 } 6890 /// 6891 int max() { 6892 return max_; 6893 } 6894 /// 6895 void setPosition(int a) { 6896 if(a > max) 6897 a = max; 6898 if(a < min) 6899 a = min; 6900 position_ = a; 6901 version(custom_widgets) 6902 setPositionCustom(a); 6903 6904 version(win32_widgets) 6905 setPositionWindows(a); 6906 } 6907 version(win32_widgets) { 6908 protected abstract void setPositionWindows(int a); 6909 } 6910 6911 protected abstract int win32direction(); 6912 6913 /++ 6914 Alias for [position] for better compatibility with generic code. 6915 6916 History: 6917 Added October 5, 2021 6918 +/ 6919 @property int value() { 6920 return position; 6921 } 6922 6923 /// 6924 int position() { 6925 return position_; 6926 } 6927 /// 6928 void setStep(int a) { 6929 step_ = a; 6930 version(win32_widgets) 6931 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 6932 } 6933 /// 6934 int step() { 6935 return step_; 6936 } 6937 /// 6938 void setPageSize(int a) { 6939 page_ = a; 6940 version(win32_widgets) 6941 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 6942 } 6943 /// 6944 int pageSize() { 6945 return page_; 6946 } 6947 6948 private void notify() { 6949 auto event = new ChangeEvent!int(this, &this.position); 6950 event.dispatch(); 6951 } 6952 6953 version(win32_widgets) 6954 void win32Setup(int style) { 6955 createWin32Window(this, TRACKBAR_CLASS, "", 6956 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 6957 6958 // the trackbar sends the same messages as scroll, which 6959 // our other layer sends as these... just gonna translate 6960 // here 6961 this.addDirectEventListener("scrolltoposition", (Event event) { 6962 event.stopPropagation(); 6963 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 6964 notify(); 6965 }); 6966 this.addDirectEventListener("scrolltonextline", (Event event) { 6967 event.stopPropagation(); 6968 this.setPosition(this.position + this.step_ * this.win32direction); 6969 notify(); 6970 }); 6971 this.addDirectEventListener("scrolltopreviousline", (Event event) { 6972 event.stopPropagation(); 6973 this.setPosition(this.position - this.step_ * this.win32direction); 6974 notify(); 6975 }); 6976 this.addDirectEventListener("scrolltonextpage", (Event event) { 6977 event.stopPropagation(); 6978 this.setPosition(this.position + this.page_ * this.win32direction); 6979 notify(); 6980 }); 6981 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 6982 event.stopPropagation(); 6983 this.setPosition(this.position - this.page_ * this.win32direction); 6984 notify(); 6985 }); 6986 6987 setMin(min_); 6988 setMax(max_); 6989 setStep(step_); 6990 setPageSize(page_); 6991 } 6992 6993 version(custom_widgets) { 6994 protected MouseTrackingWidget thumb; 6995 6996 protected abstract void setPositionCustom(int a); 6997 6998 override void defaultEventHandler_keydown(KeyDownEvent event) { 6999 switch(event.key) { 7000 case Key.Up: 7001 case Key.Right: 7002 setPosition(position() - step() * win32direction); 7003 changed(); 7004 break; 7005 case Key.Down: 7006 case Key.Left: 7007 setPosition(position() + step() * win32direction); 7008 changed(); 7009 break; 7010 case Key.Home: 7011 setPosition(win32direction > 0 ? min() : max()); 7012 changed(); 7013 break; 7014 case Key.End: 7015 setPosition(win32direction > 0 ? max() : min()); 7016 changed(); 7017 break; 7018 case Key.PageUp: 7019 setPosition(position() - pageSize() * win32direction); 7020 changed(); 7021 break; 7022 case Key.PageDown: 7023 setPosition(position() + pageSize() * win32direction); 7024 changed(); 7025 break; 7026 default: 7027 } 7028 super.defaultEventHandler_keydown(event); 7029 } 7030 7031 protected void changed() { 7032 auto ev = new ChangeEvent!int(this, &position); 7033 ev.dispatch(); 7034 } 7035 } 7036 } 7037 7038 /++ 7039 7040 +/ 7041 class VerticalSlider : Slider { 7042 this(int min, int max, int step, Widget parent) { 7043 version(custom_widgets) 7044 initialize(); 7045 7046 super(min, max, step, parent); 7047 7048 version(win32_widgets) 7049 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 7050 } 7051 7052 protected override int win32direction() { 7053 return -1; 7054 } 7055 7056 version(win32_widgets) 7057 protected override void setPositionWindows(int a) { 7058 // the windows thing makes the top 0 and i don't like that. 7059 SendMessage(hwnd, TBM_SETPOS, true, max - a); 7060 } 7061 7062 version(custom_widgets) 7063 private void initialize() { 7064 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 7065 7066 thumb.tabStop = false; 7067 7068 thumb.thumbWidth = width; 7069 thumb.thumbHeight = scaleWithDpi(16); 7070 7071 thumb.addEventListener(EventType.change, () { 7072 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 7073 sx = max - sx; 7074 //informProgramThatUserChangedPosition(sx); 7075 7076 position_ = sx; 7077 7078 changed(); 7079 }); 7080 } 7081 7082 version(custom_widgets) 7083 override void recomputeChildLayout() { 7084 thumb.thumbWidth = this.width; 7085 super.recomputeChildLayout(); 7086 setPositionCustom(position_); 7087 } 7088 7089 version(custom_widgets) 7090 protected override void setPositionCustom(int a) { 7091 if(max()) 7092 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 7093 redraw(); 7094 } 7095 } 7096 7097 /++ 7098 7099 +/ 7100 class HorizontalSlider : Slider { 7101 this(int min, int max, int step, Widget parent) { 7102 version(custom_widgets) 7103 initialize(); 7104 7105 super(min, max, step, parent); 7106 7107 version(win32_widgets) 7108 win32Setup(TBS_HORZ); 7109 } 7110 7111 version(win32_widgets) 7112 protected override void setPositionWindows(int a) { 7113 SendMessage(hwnd, TBM_SETPOS, true, a); 7114 } 7115 7116 protected override int win32direction() { 7117 return 1; 7118 } 7119 7120 version(custom_widgets) 7121 private void initialize() { 7122 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 7123 7124 thumb.tabStop = false; 7125 7126 thumb.thumbWidth = scaleWithDpi(16); 7127 thumb.thumbHeight = height; 7128 7129 thumb.addEventListener(EventType.change, () { 7130 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 7131 //informProgramThatUserChangedPosition(sx); 7132 7133 position_ = sx; 7134 7135 changed(); 7136 }); 7137 } 7138 7139 version(custom_widgets) 7140 override void recomputeChildLayout() { 7141 thumb.thumbHeight = this.height; 7142 super.recomputeChildLayout(); 7143 setPositionCustom(position_); 7144 } 7145 7146 version(custom_widgets) 7147 protected override void setPositionCustom(int a) { 7148 if(max()) 7149 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 7150 redraw(); 7151 } 7152 } 7153 7154 7155 /// 7156 abstract class ScrollbarBase : Widget { 7157 /// 7158 this(Widget parent) { 7159 super(parent); 7160 tabStop = false; 7161 step_ = scaleWithDpi(16); 7162 } 7163 7164 private int viewableArea_; 7165 private int max_; 7166 private int step_;// = 16; 7167 private int position_; 7168 7169 /// 7170 bool atEnd() { 7171 return position_ + viewableArea_ >= max_; 7172 } 7173 7174 /// 7175 bool atStart() { 7176 return position_ == 0; 7177 } 7178 7179 /// 7180 void setViewableArea(int a) { 7181 viewableArea_ = a; 7182 version(custom_widgets) 7183 redraw(); 7184 } 7185 /// 7186 void setMax(int a) { 7187 max_ = a; 7188 version(custom_widgets) 7189 redraw(); 7190 } 7191 /// 7192 int max() { 7193 return max_; 7194 } 7195 /// 7196 void setPosition(int a) { 7197 auto logicalMax = max_ - viewableArea_; 7198 if(a == int.max) 7199 a = logicalMax; 7200 7201 if(a > logicalMax) 7202 a = logicalMax; 7203 if(a < 0) 7204 a = 0; 7205 7206 position_ = a; 7207 7208 version(custom_widgets) 7209 redraw(); 7210 } 7211 /// 7212 int position() { 7213 return position_; 7214 } 7215 /// 7216 void setStep(int a) { 7217 step_ = a; 7218 } 7219 /// 7220 int step() { 7221 return step_; 7222 } 7223 7224 // FIXME: remove this.... maybe 7225 /+ 7226 protected void informProgramThatUserChangedPosition(int n) { 7227 position_ = n; 7228 auto evt = new Event(EventType.change, this); 7229 evt.intValue = n; 7230 evt.dispatch(); 7231 } 7232 +/ 7233 7234 version(custom_widgets) { 7235 enum MIN_THUMB_SIZE = 8; 7236 7237 abstract protected int getBarDim(); 7238 int thumbSize() { 7239 if(viewableArea_ >= max_ || max_ == 0) 7240 return getBarDim(); 7241 7242 int res = viewableArea_ * getBarDim() / max_; 7243 7244 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 7245 res = scaleWithDpi(MIN_THUMB_SIZE); 7246 7247 return res; 7248 } 7249 7250 int thumbPosition() { 7251 /* 7252 viewableArea_ is the viewport height/width 7253 position_ is where we are 7254 */ 7255 //if(position_ + viewableArea_ >= max_) 7256 //return getBarDim - thumbSize; 7257 7258 auto maximumPossibleValue = getBarDim() - thumbSize; 7259 auto maximiumLogicalValue = max_ - viewableArea_; 7260 7261 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 7262 7263 return p; 7264 } 7265 } 7266 } 7267 7268 //public import mgt; 7269 7270 /++ 7271 A mouse tracking widget is one that follows the mouse when dragged inside it. 7272 7273 Concrete subclasses may include a scrollbar thumb and a volume control. 7274 +/ 7275 //version(custom_widgets) 7276 class MouseTrackingWidget : Widget { 7277 7278 /// 7279 int positionX() { return positionX_; } 7280 /// 7281 int positionY() { return positionY_; } 7282 7283 /// 7284 void positionX(int p) { positionX_ = p; } 7285 /// 7286 void positionY(int p) { positionY_ = p; } 7287 7288 private int positionX_; 7289 private int positionY_; 7290 7291 /// 7292 enum Orientation { 7293 horizontal, /// 7294 vertical, /// 7295 twoDimensional, /// 7296 } 7297 7298 private int thumbWidth_; 7299 private int thumbHeight_; 7300 7301 /// 7302 int thumbWidth() { return thumbWidth_; } 7303 /// 7304 int thumbHeight() { return thumbHeight_; } 7305 /// 7306 int thumbWidth(int a) { return thumbWidth_ = a; } 7307 /// 7308 int thumbHeight(int a) { return thumbHeight_ = a; } 7309 7310 private bool dragging; 7311 private bool hovering; 7312 private int startMouseX, startMouseY; 7313 7314 /// 7315 this(Orientation orientation, Widget parent) { 7316 super(parent); 7317 7318 //assert(parentWindow !is null); 7319 7320 addEventListener((MouseDownEvent event) { 7321 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 7322 dragging = true; 7323 startMouseX = event.clientX - positionX; 7324 startMouseY = event.clientY - positionY; 7325 parentWindow.captureMouse(this); 7326 } else { 7327 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 7328 positionX = event.clientX - thumbWidth / 2; 7329 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 7330 positionY = event.clientY - thumbHeight / 2; 7331 7332 if(positionX + thumbWidth > this.width) 7333 positionX = this.width - thumbWidth; 7334 if(positionY + thumbHeight > this.height) 7335 positionY = this.height - thumbHeight; 7336 7337 if(positionX < 0) 7338 positionX = 0; 7339 if(positionY < 0) 7340 positionY = 0; 7341 7342 7343 // this.emit!(ChangeEvent!void)(); 7344 auto evt = new Event(EventType.change, this); 7345 evt.sendDirectly(); 7346 7347 redraw(); 7348 7349 } 7350 }); 7351 7352 addEventListener(EventType.mouseup, (Event event) { 7353 dragging = false; 7354 parentWindow.releaseMouseCapture(); 7355 }); 7356 7357 addEventListener(EventType.mouseout, (Event event) { 7358 if(!hovering) 7359 return; 7360 hovering = false; 7361 redraw(); 7362 }); 7363 7364 int lpx, lpy; 7365 7366 addEventListener((MouseMoveEvent event) { 7367 auto oh = hovering; 7368 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 7369 hovering = true; 7370 } else { 7371 hovering = false; 7372 } 7373 if(!dragging) { 7374 if(hovering != oh) 7375 redraw(); 7376 return; 7377 } 7378 7379 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 7380 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 7381 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 7382 positionY = event.clientY - startMouseY; 7383 7384 if(positionX + thumbWidth > this.width) 7385 positionX = this.width - thumbWidth; 7386 if(positionY + thumbHeight > this.height) 7387 positionY = this.height - thumbHeight; 7388 7389 if(positionX < 0) 7390 positionX = 0; 7391 if(positionY < 0) 7392 positionY = 0; 7393 7394 if(positionX != lpx || positionY != lpy) { 7395 lpx = positionX; 7396 lpy = positionY; 7397 7398 auto evt = new Event(EventType.change, this); 7399 evt.sendDirectly(); 7400 } 7401 7402 redraw(); 7403 }); 7404 } 7405 7406 version(custom_widgets) 7407 override void paint(WidgetPainter painter) { 7408 auto cs = getComputedStyle(); 7409 auto c = darken(cs.windowBackgroundColor, 0.2); 7410 painter.outlineColor = c; 7411 painter.fillColor = c; 7412 painter.drawRectangle(Point(0, 0), this.width, this.height); 7413 7414 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 7415 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 7416 } 7417 } 7418 7419 //version(custom_widgets) 7420 //private 7421 class HorizontalScrollbar : ScrollbarBase { 7422 7423 version(custom_widgets) { 7424 private MouseTrackingWidget thumb; 7425 7426 override int getBarDim() { 7427 return thumb.width; 7428 } 7429 } 7430 7431 override void setViewableArea(int a) { 7432 super.setViewableArea(a); 7433 7434 version(win32_widgets) { 7435 SCROLLINFO info; 7436 info.cbSize = info.sizeof; 7437 info.nPage = a + 1; 7438 info.fMask = SIF_PAGE; 7439 SetScrollInfo(hwnd, SB_CTL, &info, true); 7440 } else version(custom_widgets) { 7441 thumb.positionX = thumbPosition; 7442 thumb.thumbWidth = thumbSize; 7443 thumb.redraw(); 7444 } else static assert(0); 7445 7446 } 7447 7448 override void setMax(int a) { 7449 super.setMax(a); 7450 version(win32_widgets) { 7451 SCROLLINFO info; 7452 info.cbSize = info.sizeof; 7453 info.nMin = 0; 7454 info.nMax = max; 7455 info.fMask = SIF_RANGE; 7456 SetScrollInfo(hwnd, SB_CTL, &info, true); 7457 } else version(custom_widgets) { 7458 thumb.positionX = thumbPosition; 7459 thumb.thumbWidth = thumbSize; 7460 thumb.redraw(); 7461 } 7462 } 7463 7464 override void setPosition(int a) { 7465 super.setPosition(a); 7466 version(win32_widgets) { 7467 SCROLLINFO info; 7468 info.cbSize = info.sizeof; 7469 info.fMask = SIF_POS; 7470 info.nPos = position; 7471 SetScrollInfo(hwnd, SB_CTL, &info, true); 7472 } else version(custom_widgets) { 7473 thumb.positionX = thumbPosition(); 7474 thumb.thumbWidth = thumbSize; 7475 thumb.redraw(); 7476 } else static assert(0); 7477 } 7478 7479 this(Widget parent) { 7480 super(parent); 7481 7482 version(win32_widgets) { 7483 createWin32Window(this, "Scrollbar"w, "", 7484 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 7485 } else version(custom_widgets) { 7486 auto vl = new HorizontalLayout(this); 7487 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 7488 leftButton.setClickRepeat(scrollClickRepeatInterval); 7489 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 7490 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 7491 rightButton.setClickRepeat(scrollClickRepeatInterval); 7492 7493 leftButton.tabStop = false; 7494 rightButton.tabStop = false; 7495 thumb.tabStop = false; 7496 7497 leftButton.addEventListener(EventType.triggered, () { 7498 this.emitCommand!"scrolltopreviousline"(); 7499 //informProgramThatUserChangedPosition(position - step()); 7500 }); 7501 rightButton.addEventListener(EventType.triggered, () { 7502 this.emitCommand!"scrolltonextline"(); 7503 //informProgramThatUserChangedPosition(position + step()); 7504 }); 7505 7506 thumb.thumbWidth = this.minWidth; 7507 thumb.thumbHeight = scaleWithDpi(16); 7508 7509 thumb.addEventListener(EventType.change, () { 7510 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 7511 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 7512 7513 //informProgramThatUserChangedPosition(sx); 7514 7515 auto ev = new ScrollToPositionEvent(this, sx); 7516 ev.dispatch(); 7517 }); 7518 } 7519 } 7520 7521 override int minHeight() { return scaleWithDpi(16); } 7522 override int maxHeight() { return scaleWithDpi(16); } 7523 override int minWidth() { return scaleWithDpi(48); } 7524 } 7525 7526 final class ScrollToPositionEvent : Event { 7527 enum EventString = "scrolltoposition"; 7528 7529 this(Widget target, int value) { 7530 this.value = value; 7531 super(EventString, target); 7532 } 7533 7534 immutable int value; 7535 7536 override @property int intValue() { 7537 return value; 7538 } 7539 } 7540 7541 //version(custom_widgets) 7542 //private 7543 class VerticalScrollbar : ScrollbarBase { 7544 7545 version(custom_widgets) { 7546 override int getBarDim() { 7547 return thumb.height; 7548 } 7549 7550 private MouseTrackingWidget thumb; 7551 } 7552 7553 override void setViewableArea(int a) { 7554 super.setViewableArea(a); 7555 7556 version(win32_widgets) { 7557 SCROLLINFO info; 7558 info.cbSize = info.sizeof; 7559 info.nPage = a + 1; 7560 info.fMask = SIF_PAGE; 7561 SetScrollInfo(hwnd, SB_CTL, &info, true); 7562 } else version(custom_widgets) { 7563 thumb.positionY = thumbPosition; 7564 thumb.thumbHeight = thumbSize; 7565 thumb.redraw(); 7566 } else static assert(0); 7567 7568 } 7569 7570 override void setMax(int a) { 7571 super.setMax(a); 7572 version(win32_widgets) { 7573 SCROLLINFO info; 7574 info.cbSize = info.sizeof; 7575 info.nMin = 0; 7576 info.nMax = max; 7577 info.fMask = SIF_RANGE; 7578 SetScrollInfo(hwnd, SB_CTL, &info, true); 7579 } else version(custom_widgets) { 7580 thumb.positionY = thumbPosition; 7581 thumb.thumbHeight = thumbSize; 7582 thumb.redraw(); 7583 } 7584 } 7585 7586 override void setPosition(int a) { 7587 super.setPosition(a); 7588 version(win32_widgets) { 7589 SCROLLINFO info; 7590 info.cbSize = info.sizeof; 7591 info.fMask = SIF_POS; 7592 info.nPos = position; 7593 SetScrollInfo(hwnd, SB_CTL, &info, true); 7594 } else version(custom_widgets) { 7595 thumb.positionY = thumbPosition; 7596 thumb.thumbHeight = thumbSize; 7597 thumb.redraw(); 7598 } else static assert(0); 7599 } 7600 7601 this(Widget parent) { 7602 super(parent); 7603 7604 version(win32_widgets) { 7605 createWin32Window(this, "Scrollbar"w, "", 7606 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 7607 } else version(custom_widgets) { 7608 auto vl = new VerticalLayout(this); 7609 auto upButton = new ArrowButton(ArrowDirection.up, vl); 7610 upButton.setClickRepeat(scrollClickRepeatInterval); 7611 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 7612 auto downButton = new ArrowButton(ArrowDirection.down, vl); 7613 downButton.setClickRepeat(scrollClickRepeatInterval); 7614 7615 upButton.addEventListener(EventType.triggered, () { 7616 this.emitCommand!"scrolltopreviousline"(); 7617 //informProgramThatUserChangedPosition(position - step()); 7618 }); 7619 downButton.addEventListener(EventType.triggered, () { 7620 this.emitCommand!"scrolltonextline"(); 7621 //informProgramThatUserChangedPosition(position + step()); 7622 }); 7623 7624 thumb.thumbWidth = this.minWidth; 7625 thumb.thumbHeight = scaleWithDpi(16); 7626 7627 thumb.addEventListener(EventType.change, () { 7628 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 7629 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 7630 7631 auto ev = new ScrollToPositionEvent(this, sy); 7632 ev.dispatch(); 7633 7634 //informProgramThatUserChangedPosition(sy); 7635 }); 7636 7637 upButton.tabStop = false; 7638 downButton.tabStop = false; 7639 thumb.tabStop = false; 7640 } 7641 } 7642 7643 override int minWidth() { return scaleWithDpi(16); } 7644 override int maxWidth() { return scaleWithDpi(16); } 7645 override int minHeight() { return scaleWithDpi(48); } 7646 } 7647 7648 7649 /++ 7650 EXPERIMENTAL 7651 7652 A widget specialized for being a container for other widgets. 7653 7654 History: 7655 Added May 29, 2021. Not stabilized at this time. 7656 +/ 7657 class WidgetContainer : Widget { 7658 this(Widget parent) { 7659 tabStop = false; 7660 super(parent); 7661 } 7662 7663 override int maxHeight() { 7664 if(this.children.length == 1) { 7665 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 7666 } else { 7667 return int.max; 7668 } 7669 } 7670 7671 override int maxWidth() { 7672 if(this.children.length == 1) { 7673 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 7674 } else { 7675 return int.max; 7676 } 7677 } 7678 7679 /+ 7680 7681 override int minHeight() { 7682 int largest = 0; 7683 int margins = 0; 7684 int lastMargin = 0; 7685 foreach(child; children) { 7686 auto mh = child.minHeight(); 7687 if(mh > largest) 7688 largest = mh; 7689 margins += mymax(lastMargin, child.marginTop()); 7690 lastMargin = child.marginBottom(); 7691 } 7692 return largest + margins; 7693 } 7694 7695 override int maxHeight() { 7696 int largest = 0; 7697 int margins = 0; 7698 int lastMargin = 0; 7699 foreach(child; children) { 7700 auto mh = child.maxHeight(); 7701 if(mh == int.max) 7702 return int.max; 7703 if(mh > largest) 7704 largest = mh; 7705 margins += mymax(lastMargin, child.marginTop()); 7706 lastMargin = child.marginBottom(); 7707 } 7708 return largest + margins; 7709 } 7710 7711 override int minWidth() { 7712 int min; 7713 foreach(child; children) { 7714 auto cm = child.minWidth; 7715 if(cm > min) 7716 min = cm; 7717 } 7718 return min + paddingLeft + paddingRight; 7719 } 7720 7721 override int minHeight() { 7722 int min; 7723 foreach(child; children) { 7724 auto cm = child.minHeight; 7725 if(cm > min) 7726 min = cm; 7727 } 7728 return min + paddingTop + paddingBottom; 7729 } 7730 7731 override int maxHeight() { 7732 int largest = 0; 7733 int margins = 0; 7734 int lastMargin = 0; 7735 foreach(child; children) { 7736 auto mh = child.maxHeight(); 7737 if(mh == int.max) 7738 return int.max; 7739 if(mh > largest) 7740 largest = mh; 7741 margins += mymax(lastMargin, child.marginTop()); 7742 lastMargin = child.marginBottom(); 7743 } 7744 return largest + margins; 7745 } 7746 7747 override int heightStretchiness() { 7748 int max; 7749 foreach(child; children) { 7750 auto c = child.heightStretchiness; 7751 if(c > max) 7752 max = c; 7753 } 7754 return max; 7755 } 7756 7757 override int marginTop() { 7758 if(this.children.length) 7759 return this.children[0].marginTop; 7760 return 0; 7761 } 7762 +/ 7763 } 7764 7765 /// 7766 abstract class Layout : Widget { 7767 this(Widget parent) { 7768 tabStop = false; 7769 super(parent); 7770 } 7771 } 7772 7773 /++ 7774 Makes all children minimum width and height, placing them down 7775 left to right, top to bottom. 7776 7777 Useful if you want to make a list of buttons that automatically 7778 wrap to a new line when necessary. 7779 +/ 7780 class InlineBlockLayout : Layout { 7781 /// 7782 this(Widget parent) { super(parent); } 7783 7784 override void recomputeChildLayout() { 7785 registerMovement(); 7786 7787 int x = this.paddingLeft, y = this.paddingTop; 7788 7789 int lineHeight; 7790 int previousMargin = 0; 7791 int previousMarginBottom = 0; 7792 7793 foreach(child; children) { 7794 if(child.hidden) 7795 continue; 7796 if(cast(FixedPosition) child) { 7797 child.recomputeChildLayout(); 7798 continue; 7799 } 7800 child.width = child.flexBasisWidth(); 7801 if(child.width == 0) 7802 child.width = child.minWidth(); 7803 if(child.width == 0) 7804 child.width = 32; 7805 7806 child.height = child.flexBasisHeight(); 7807 if(child.height == 0) 7808 child.height = child.minHeight(); 7809 if(child.height == 0) 7810 child.height = 32; 7811 7812 if(x + child.width + paddingRight > this.width) { 7813 x = this.paddingLeft; 7814 y += lineHeight; 7815 lineHeight = 0; 7816 previousMargin = 0; 7817 previousMarginBottom = 0; 7818 } 7819 7820 auto margin = child.marginLeft; 7821 if(previousMargin > margin) 7822 margin = previousMargin; 7823 7824 x += margin; 7825 7826 child.x = x; 7827 child.y = y; 7828 7829 int marginTopApplied; 7830 if(child.marginTop > previousMarginBottom) { 7831 child.y += child.marginTop; 7832 marginTopApplied = child.marginTop; 7833 } 7834 7835 x += child.width; 7836 previousMargin = child.marginRight; 7837 7838 if(child.marginBottom > previousMarginBottom) 7839 previousMarginBottom = child.marginBottom; 7840 7841 auto h = child.height + previousMarginBottom + marginTopApplied; 7842 if(h > lineHeight) 7843 lineHeight = h; 7844 7845 child.recomputeChildLayout(); 7846 } 7847 7848 } 7849 7850 override int minWidth() { 7851 int min; 7852 foreach(child; children) { 7853 auto cm = child.minWidth; 7854 if(cm > min) 7855 min = cm; 7856 } 7857 return min + paddingLeft + paddingRight; 7858 } 7859 7860 override int minHeight() { 7861 int min; 7862 foreach(child; children) { 7863 auto cm = child.minHeight; 7864 if(cm > min) 7865 min = cm; 7866 } 7867 return min + paddingTop + paddingBottom; 7868 } 7869 } 7870 7871 /++ 7872 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 7873 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 7874 the [TabWidget] will automatically change pages of child widgets. 7875 7876 This allows you to react to it however you see fit rather than having to 7877 be tied to just the new sets of child widgets. 7878 7879 It sends the message in the form of `this.emitCommand!"changetab"();`. 7880 7881 History: 7882 Added December 24, 2021 (dub v10.5) 7883 +/ 7884 class TabMessageWidget : Widget { 7885 7886 protected void tabIndexClicked(int item) { 7887 this.emitCommand!"changetab"(); 7888 } 7889 7890 /++ 7891 Adds the a new tab to the control with the given title. 7892 7893 Returns: 7894 The index of the newly added tab. You will need to know 7895 this index to refer to it later and to know which tab to 7896 change to when you get a changetab message. 7897 +/ 7898 int addTab(string title, int pos = int.max) { 7899 version(win32_widgets) { 7900 TCITEM item; 7901 item.mask = TCIF_TEXT; 7902 WCharzBuffer buf = WCharzBuffer(title); 7903 item.pszText = buf.ptr; 7904 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 7905 } else version(custom_widgets) { 7906 if(pos >= tabs.length) { 7907 tabs ~= title; 7908 redraw(); 7909 return cast(int) tabs.length - 1; 7910 } else if(pos <= 0) { 7911 tabs = title ~ tabs; 7912 redraw(); 7913 return 0; 7914 } else { 7915 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 7916 redraw(); 7917 return pos; 7918 } 7919 } 7920 } 7921 7922 override void addChild(Widget child, int pos = int.max) { 7923 if(container) 7924 container.addChild(child, pos); 7925 else 7926 super.addChild(child, pos); 7927 } 7928 7929 protected Widget makeContainer() { 7930 return new Widget(this); 7931 } 7932 7933 private Widget container; 7934 7935 override void recomputeChildLayout() { 7936 version(win32_widgets) { 7937 this.registerMovement(); 7938 7939 RECT rect; 7940 GetWindowRect(hwnd, &rect); 7941 7942 auto left = rect.left; 7943 auto top = rect.top; 7944 7945 TabCtrl_AdjustRect(hwnd, false, &rect); 7946 foreach(child; children) { 7947 if(!child.showing) continue; 7948 child.x = rect.left - left; 7949 child.y = rect.top - top; 7950 child.width = rect.right - rect.left; 7951 child.height = rect.bottom - rect.top; 7952 child.recomputeChildLayout(); 7953 } 7954 } else version(custom_widgets) { 7955 this.registerMovement(); 7956 foreach(child; children) { 7957 if(!child.showing) continue; 7958 child.x = 2; 7959 child.y = tabBarHeight + 2; // for the border 7960 child.width = width - 4; // for the border 7961 child.height = height - tabBarHeight - 2 - 2; // for the border 7962 child.recomputeChildLayout(); 7963 } 7964 } else static assert(0); 7965 } 7966 7967 version(custom_widgets) 7968 string[] tabs; 7969 7970 this(Widget parent) { 7971 super(parent); 7972 7973 tabStop = false; 7974 7975 version(win32_widgets) { 7976 createWin32Window(this, WC_TABCONTROL, "", 0); 7977 } else version(custom_widgets) { 7978 addEventListener((ClickEvent event) { 7979 if(event.target !is this) 7980 return; 7981 if(event.clientY >= 0 && event.clientY < tabBarHeight) { 7982 auto t = (event.clientX / tabWidth); 7983 if(t >= 0 && t < tabs.length) { 7984 currentTab_ = t; 7985 tabIndexClicked(t); 7986 redraw(); 7987 } 7988 } 7989 }); 7990 } else static assert(0); 7991 7992 this.container = makeContainer(); 7993 } 7994 7995 override int marginTop() { return 4; } 7996 override int paddingBottom() { return 4; } 7997 7998 override int minHeight() { 7999 int max = 0; 8000 foreach(child; children) 8001 max = mymax(child.minHeight, max); 8002 8003 8004 version(win32_widgets) { 8005 RECT rect; 8006 rect.right = this.width; 8007 rect.bottom = max; 8008 TabCtrl_AdjustRect(hwnd, true, &rect); 8009 8010 max = rect.bottom; 8011 } else { 8012 max += defaultLineHeight + 4; 8013 } 8014 8015 8016 return max; 8017 } 8018 8019 version(win32_widgets) 8020 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 8021 switch(code) { 8022 case TCN_SELCHANGE: 8023 auto sel = TabCtrl_GetCurSel(hwnd); 8024 tabIndexClicked(sel); 8025 break; 8026 default: 8027 } 8028 return 0; 8029 } 8030 8031 version(custom_widgets) { 8032 private int currentTab_; 8033 private int tabBarHeight() { return defaultLineHeight; } 8034 int tabWidth() { return scaleWithDpi(80); } 8035 } 8036 8037 version(win32_widgets) 8038 override void paint(WidgetPainter painter) {} 8039 8040 version(custom_widgets) 8041 override void paint(WidgetPainter painter) { 8042 auto cs = getComputedStyle(); 8043 8044 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 8045 8046 int posX = 0; 8047 foreach(idx, title; tabs) { 8048 auto isCurrent = idx == getCurrentTab(); 8049 8050 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 8051 8052 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 8053 painter.outlineColor = cs.foregroundColor; 8054 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 8055 8056 if(isCurrent) { 8057 painter.outlineColor = cs.windowBackgroundColor; 8058 painter.fillColor = Color.transparent; 8059 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 8060 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 8061 8062 painter.outlineColor = Color.white; 8063 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 8064 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 8065 painter.outlineColor = cs.activeTabColor; 8066 painter.drawPixel(Point(posX, tabBarHeight - 1)); 8067 } 8068 8069 posX += tabWidth - 2; 8070 } 8071 } 8072 8073 /// 8074 @scriptable 8075 void setCurrentTab(int item) { 8076 version(win32_widgets) 8077 TabCtrl_SetCurSel(hwnd, item); 8078 else version(custom_widgets) 8079 currentTab_ = item; 8080 else static assert(0); 8081 8082 tabIndexClicked(item); 8083 } 8084 8085 /// 8086 @scriptable 8087 int getCurrentTab() { 8088 version(win32_widgets) 8089 return TabCtrl_GetCurSel(hwnd); 8090 else version(custom_widgets) 8091 return currentTab_; // FIXME 8092 else static assert(0); 8093 } 8094 8095 /// 8096 @scriptable 8097 void removeTab(int item) { 8098 if(item && item == getCurrentTab()) 8099 setCurrentTab(item - 1); 8100 8101 version(win32_widgets) { 8102 TabCtrl_DeleteItem(hwnd, item); 8103 } 8104 8105 for(int a = item; a < children.length - 1; a++) 8106 this._children[a] = this._children[a + 1]; 8107 this._children = this._children[0 .. $-1]; 8108 } 8109 8110 } 8111 8112 8113 /++ 8114 A tab widget is a set of clickable tab buttons followed by a content area. 8115 8116 8117 Tabs can change existing content or can be new pages. 8118 8119 When the user picks a different tab, a `change` message is generated. 8120 +/ 8121 class TabWidget : TabMessageWidget { 8122 this(Widget parent) { 8123 super(parent); 8124 } 8125 8126 override protected Widget makeContainer() { 8127 return null; 8128 } 8129 8130 override void addChild(Widget child, int pos = int.max) { 8131 if(auto twp = cast(TabWidgetPage) child) { 8132 Widget.addChild(child, pos); 8133 if(pos == int.max) 8134 pos = cast(int) this.children.length - 1; 8135 8136 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 8137 8138 if(pos != getCurrentTab) { 8139 child.showing = false; 8140 } 8141 } else { 8142 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 8143 } 8144 } 8145 8146 // FIXME: add tab icons at some point, Windows supports them 8147 /++ 8148 Adds a page and its associated tab with the given label to the widget. 8149 8150 Returns: 8151 The added page object, to which you can add other widgets. 8152 +/ 8153 @scriptable 8154 TabWidgetPage addPage(string title) { 8155 return new TabWidgetPage(title, this); 8156 } 8157 8158 /++ 8159 Gets the page at the given tab index, or `null` if the index is bad. 8160 8161 History: 8162 Added December 24, 2021. 8163 +/ 8164 TabWidgetPage getPage(int index) { 8165 if(index < this.children.length) 8166 return null; 8167 return cast(TabWidgetPage) this.children[index]; 8168 } 8169 8170 /++ 8171 While you can still use the addTab from the parent class, 8172 *strongly* recommend you use [addPage] insteaad. 8173 8174 History: 8175 Added December 24, 2021 to fulful the interface 8176 requirement that came from adding [TabMessageWidget]. 8177 8178 You should not use it though since the [addPage] function 8179 is much easier to use here. 8180 +/ 8181 override int addTab(string title, int pos = int.max) { 8182 auto p = addPage(title); 8183 foreach(idx, child; this.children) 8184 if(child is p) 8185 return cast(int) idx; 8186 return -1; 8187 } 8188 8189 protected override void tabIndexClicked(int item) { 8190 foreach(idx, child; children) { 8191 child.showing(false, false); // batch the recalculates for the end 8192 } 8193 8194 foreach(idx, child; children) { 8195 if(idx == item) { 8196 child.showing(true, false); 8197 if(parentWindow) { 8198 auto f = parentWindow.getFirstFocusable(child); 8199 if(f) 8200 f.focus(); 8201 } 8202 recomputeChildLayout(); 8203 } 8204 } 8205 8206 version(win32_widgets) { 8207 InvalidateRect(hwnd, null, true); 8208 } else version(custom_widgets) { 8209 this.redraw(); 8210 } 8211 } 8212 8213 } 8214 8215 /++ 8216 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 8217 8218 You add [TabWidgetPage]s to it. 8219 +/ 8220 class PageWidget : Widget { 8221 this(Widget parent) { 8222 super(parent); 8223 } 8224 8225 override int minHeight() { 8226 int max = 0; 8227 foreach(child; children) 8228 max = mymax(child.minHeight, max); 8229 8230 return max; 8231 } 8232 8233 8234 override void addChild(Widget child, int pos = int.max) { 8235 if(auto twp = cast(TabWidgetPage) child) { 8236 super.addChild(child, pos); 8237 if(pos == int.max) 8238 pos = cast(int) this.children.length - 1; 8239 8240 if(pos != getCurrentTab) { 8241 child.showing = false; 8242 } 8243 } else { 8244 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 8245 } 8246 } 8247 8248 override void recomputeChildLayout() { 8249 this.registerMovement(); 8250 foreach(child; children) { 8251 child.x = 0; 8252 child.y = 0; 8253 child.width = width; 8254 child.height = height; 8255 child.recomputeChildLayout(); 8256 } 8257 } 8258 8259 private int currentTab_; 8260 8261 /// 8262 @scriptable 8263 void setCurrentTab(int item) { 8264 currentTab_ = item; 8265 8266 showOnly(item); 8267 } 8268 8269 /// 8270 @scriptable 8271 int getCurrentTab() { 8272 return currentTab_; 8273 } 8274 8275 /// 8276 @scriptable 8277 void removeTab(int item) { 8278 if(item && item == getCurrentTab()) 8279 setCurrentTab(item - 1); 8280 8281 for(int a = item; a < children.length - 1; a++) 8282 this._children[a] = this._children[a + 1]; 8283 this._children = this._children[0 .. $-1]; 8284 } 8285 8286 /// 8287 @scriptable 8288 TabWidgetPage addPage(string title) { 8289 return new TabWidgetPage(title, this); 8290 } 8291 8292 private void showOnly(int item) { 8293 foreach(idx, child; children) 8294 if(idx == item) { 8295 child.show(); 8296 child.queueRecomputeChildLayout(); 8297 } else { 8298 child.hide(); 8299 } 8300 } 8301 } 8302 8303 /++ 8304 8305 +/ 8306 class TabWidgetPage : Widget { 8307 string title; 8308 this(string title, Widget parent) { 8309 this.title = title; 8310 this.tabStop = false; 8311 super(parent); 8312 8313 ///* 8314 version(win32_widgets) { 8315 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 8316 } 8317 //*/ 8318 } 8319 8320 override int minHeight() { 8321 int sum = 0; 8322 foreach(child; children) 8323 sum += child.minHeight(); 8324 return sum; 8325 } 8326 } 8327 8328 version(none) 8329 /++ 8330 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 8331 8332 I think I need to modify the layout algorithms to support this. 8333 +/ 8334 class CollapsableSidebar : Widget { 8335 8336 } 8337 8338 /// Stacks the widgets vertically, taking all the available width for each child. 8339 class VerticalLayout : Layout { 8340 // most of this is intentionally blank - widget's default is vertical layout right now 8341 /// 8342 this(Widget parent) { super(parent); } 8343 8344 /++ 8345 Sets a max width for the layout so you don't have to subclass. The max width 8346 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 8347 8348 History: 8349 Added November 29, 2021 (dub v10.5) 8350 +/ 8351 this(int maxWidth, Widget parent) { 8352 this.mw = maxWidth; 8353 super(parent); 8354 } 8355 8356 private int mw = int.max; 8357 8358 override int maxWidth() { return scaleWithDpi(mw); } 8359 } 8360 8361 /// Stacks the widgets horizontally, taking all the available height for each child. 8362 class HorizontalLayout : Layout { 8363 /// 8364 this(Widget parent) { super(parent); } 8365 8366 /++ 8367 Sets a max height for the layout so you don't have to subclass. The max height 8368 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 8369 8370 History: 8371 Added November 29, 2021 (dub v10.5) 8372 +/ 8373 this(int maxHeight, Widget parent) { 8374 this.mh = maxHeight; 8375 super(parent); 8376 } 8377 8378 private int mh = 0; 8379 8380 8381 8382 override void recomputeChildLayout() { 8383 .recomputeChildLayout!"width"(this); 8384 } 8385 8386 override int minHeight() { 8387 int largest = 0; 8388 int margins = 0; 8389 int lastMargin = 0; 8390 foreach(child; children) { 8391 auto mh = child.minHeight(); 8392 if(mh > largest) 8393 largest = mh; 8394 margins += mymax(lastMargin, child.marginTop()); 8395 lastMargin = child.marginBottom(); 8396 } 8397 return largest + margins; 8398 } 8399 8400 override int maxHeight() { 8401 if(mh != 0) 8402 return mymax(minHeight, scaleWithDpi(mh)); 8403 8404 int largest = 0; 8405 int margins = 0; 8406 int lastMargin = 0; 8407 foreach(child; children) { 8408 auto mh = child.maxHeight(); 8409 if(mh == int.max) 8410 return int.max; 8411 if(mh > largest) 8412 largest = mh; 8413 margins += mymax(lastMargin, child.marginTop()); 8414 lastMargin = child.marginBottom(); 8415 } 8416 return largest + margins; 8417 } 8418 8419 override int heightStretchiness() { 8420 int max; 8421 foreach(child; children) { 8422 auto c = child.heightStretchiness; 8423 if(c > max) 8424 max = c; 8425 } 8426 return max; 8427 } 8428 } 8429 8430 version(win32_widgets) 8431 private 8432 extern(Windows) 8433 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 8434 Widget* pwin = hwnd in Widget.nativeMapping; 8435 if(pwin is null) 8436 return DefWindowProc(hwnd, message, wparam, lparam); 8437 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 8438 if(win is null) 8439 return DefWindowProc(hwnd, message, wparam, lparam); 8440 8441 switch(message) { 8442 case WM_SIZE: 8443 auto width = LOWORD(lparam); 8444 auto height = HIWORD(lparam); 8445 8446 auto hdc = GetDC(hwnd); 8447 auto hdcBmp = CreateCompatibleDC(hdc); 8448 8449 // FIXME: could this be more efficient? it never relinquishes a large bitmap 8450 if(width > win.bmpWidth || height > win.bmpHeight) { 8451 auto oldBuffer = win.buffer; 8452 win.buffer = CreateCompatibleBitmap(hdc, width, height); 8453 8454 if(oldBuffer) 8455 DeleteObject(oldBuffer); 8456 8457 win.bmpWidth = width; 8458 win.bmpHeight = height; 8459 } 8460 8461 // just always erase it upon resizing so minigui can draw over with a clean slate 8462 auto oldBmp = SelectObject(hdcBmp, win.buffer); 8463 8464 auto brush = GetSysColorBrush(COLOR_3DFACE); 8465 RECT r; 8466 r.left = 0; 8467 r.top = 0; 8468 r.right = width; 8469 r.bottom = height; 8470 FillRect(hdcBmp, &r, brush); 8471 8472 SelectObject(hdcBmp, oldBmp); 8473 DeleteDC(hdcBmp); 8474 ReleaseDC(hwnd, hdc); 8475 break; 8476 case WM_PAINT: 8477 if(win.buffer is null) 8478 goto default; 8479 8480 BITMAP bm; 8481 PAINTSTRUCT ps; 8482 8483 HDC hdc = BeginPaint(hwnd, &ps); 8484 8485 HDC hdcMem = CreateCompatibleDC(hdc); 8486 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 8487 8488 GetObject(win.buffer, bm.sizeof, &bm); 8489 8490 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 8491 8492 SelectObject(hdcMem, hbmOld); 8493 DeleteDC(hdcMem); 8494 EndPaint(hwnd, &ps); 8495 break; 8496 default: 8497 return DefWindowProc(hwnd, message, wparam, lparam); 8498 } 8499 8500 return 0; 8501 } 8502 8503 private wstring Win32Class(wstring name)() { 8504 static bool classRegistered; 8505 if(!classRegistered) { 8506 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 8507 WNDCLASSEX wc; 8508 wc.cbSize = wc.sizeof; 8509 wc.hInstance = hInstance; 8510 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 8511 wc.lpfnWndProc = &DoubleBufferWndProc; 8512 wc.lpszClassName = name.ptr; 8513 if(!RegisterClassExW(&wc)) 8514 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 8515 classRegistered = true; 8516 } 8517 8518 return name; 8519 } 8520 8521 /+ 8522 version(win32_widgets) 8523 extern(Windows) 8524 private 8525 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 8526 switch(iMessage) { 8527 case WM_PAINT: 8528 if(auto te = hWnd in Widget.nativeMapping) { 8529 try { 8530 //te.redraw(); 8531 writeln(te, " drawing"); 8532 } catch(Exception) {} 8533 } 8534 return DefWindowProc(hWnd, iMessage, wParam, lParam); 8535 default: 8536 return DefWindowProc(hWnd, iMessage, wParam, lParam); 8537 } 8538 } 8539 +/ 8540 8541 8542 /++ 8543 A widget specifically designed to hold other widgets. 8544 8545 History: 8546 Added July 1, 2021 8547 +/ 8548 class ContainerWidget : Widget { 8549 this(Widget parent) { 8550 super(parent); 8551 this.tabStop = false; 8552 8553 version(win32_widgets) { 8554 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 8555 } 8556 } 8557 } 8558 8559 /++ 8560 A widget that takes your widget, puts scroll bars around it, and sends 8561 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 8562 no effort to automatically scroll or clip its child widgets - it just sends 8563 the messages. 8564 8565 8566 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 8567 The scroll coordinates are all given in a unit you interpret as you wish. One 8568 of these units is moved on each press of the arrow buttons and represents the 8569 smallest amount the user can scroll. The intention is for this to be one line, 8570 one item in a list, one row in a table, etc. Whatever makes sense for your widget 8571 in each direction that the user might be interested in. 8572 8573 You can set a "page size" with the [step] property. (Yes, I regret the name...) 8574 This is the amount it jumps when the user pressed page up and page down, or clicks 8575 in the exposed part of the scroll bar. 8576 8577 You should add child content to the ScrollMessageWidget. However, it is important to 8578 note that the coordinates are always independent of the scroll position! It is YOUR 8579 responsibility to do any necessary transforms, clipping, etc., while drawing the 8580 content and interpreting mouse events if they are supposed to change with the scroll. 8581 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 8582 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 8583 you more control (which can be considerably more efficient and adapted to your actual data) 8584 at the expense of you also needing to be aware of its reality. 8585 8586 Please note that it does NOT react to mouse wheel events or various keyboard events as of 8587 version 10.3. Maybe this will change in the future.... but for now you must call 8588 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 8589 +/ 8590 class ScrollMessageWidget : Widget { 8591 this(Widget parent) { 8592 super(parent); 8593 8594 container = new Widget(this); 8595 hsb = new HorizontalScrollbar(this); 8596 vsb = new VerticalScrollbar(this); 8597 8598 hsb.addEventListener("scrolltonextline", { 8599 hsb.setPosition(hsb.position + movementPerButtonClickH_); 8600 notify(); 8601 }); 8602 hsb.addEventListener("scrolltopreviousline", { 8603 hsb.setPosition(hsb.position - movementPerButtonClickH_); 8604 notify(); 8605 }); 8606 vsb.addEventListener("scrolltonextline", { 8607 vsb.setPosition(vsb.position + movementPerButtonClickV_); 8608 notify(); 8609 }); 8610 vsb.addEventListener("scrolltopreviousline", { 8611 vsb.setPosition(vsb.position - movementPerButtonClickV_); 8612 notify(); 8613 }); 8614 hsb.addEventListener("scrolltonextpage", { 8615 hsb.setPosition(hsb.position + hsb.step_); 8616 notify(); 8617 }); 8618 hsb.addEventListener("scrolltopreviouspage", { 8619 hsb.setPosition(hsb.position - hsb.step_); 8620 notify(); 8621 }); 8622 vsb.addEventListener("scrolltonextpage", { 8623 vsb.setPosition(vsb.position + vsb.step_); 8624 notify(); 8625 }); 8626 vsb.addEventListener("scrolltopreviouspage", { 8627 vsb.setPosition(vsb.position - vsb.step_); 8628 notify(); 8629 }); 8630 hsb.addEventListener("scrolltoposition", (Event event) { 8631 hsb.setPosition(event.intValue); 8632 notify(); 8633 }); 8634 vsb.addEventListener("scrolltoposition", (Event event) { 8635 vsb.setPosition(event.intValue); 8636 notify(); 8637 }); 8638 8639 8640 tabStop = false; 8641 container.tabStop = false; 8642 magic = true; 8643 } 8644 8645 private int movementPerButtonClickH_ = 1; 8646 private int movementPerButtonClickV_ = 1; 8647 public void movementPerButtonClick(int h, int v) { 8648 movementPerButtonClickH_ = h; 8649 movementPerButtonClickV_ = v; 8650 } 8651 8652 /++ 8653 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 8654 8655 8656 The defaults for [addDefaultWheelListeners] are: 8657 8658 $(LIST 8659 * Mouse wheel scrolls vertically 8660 * Alt key + mouse wheel scrolls horiontally 8661 * Shift + mouse wheel scrolls faster. 8662 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 8663 ) 8664 8665 The defaults for [addDefaultKeyboardListeners] are: 8666 8667 $(LIST 8668 * Arrow keys scroll by the given amounts 8669 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 8670 * Page up and down scroll by the vertical viewable area 8671 * Home and end scroll to the start and end of the verticle viewable area. 8672 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 8673 ) 8674 8675 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 8676 8677 Params: 8678 horizontalArrowScrollAmount = 8679 verticalArrowScrollAmount = 8680 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 8681 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 8682 shiftMultiplier = multiplies the scroll amount by this when shift is held 8683 +/ 8684 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 8685 defaultKeyboardListener_verticalArrowScrollAmount = verticalArrowScrollAmount; 8686 defaultKeyboardListener_horizontalArrowScrollAmount = horizontalArrowScrollAmount; 8687 defaultKeyboardListener_shiftMultiplier = shiftMultiplier; 8688 8689 container.addEventListener(&defaultKeyboardListener); 8690 } 8691 8692 /// ditto 8693 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 8694 auto _this = this; 8695 container.addEventListener((scope ClickEvent ce) { 8696 8697 //if(ce.target && ce.target.tabStop) 8698 //ce.target.focus(); 8699 8700 // ctrl is reserved for the application 8701 if(ce.ctrlKey) 8702 return; 8703 8704 if(horizontalWheelScrollAmount == 0 && ce.altKey) 8705 return; 8706 8707 if(shiftMultiplier == 0 && ce.shiftKey) 8708 return; 8709 8710 if(ce.button == MouseButton.wheelDown) { 8711 if(ce.altKey) 8712 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8713 else 8714 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8715 } else if(ce.button == MouseButton.wheelUp) { 8716 if(ce.altKey) 8717 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8718 else 8719 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 8720 } 8721 }); 8722 } 8723 8724 int defaultKeyboardListener_verticalArrowScrollAmount = 1; 8725 int defaultKeyboardListener_horizontalArrowScrollAmount = 1; 8726 int defaultKeyboardListener_shiftMultiplier = 3; 8727 8728 void defaultKeyboardListener(scope KeyDownEvent ke) { 8729 switch(ke.key) { 8730 case Key.Left: 8731 this.scrollLeft(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8732 break; 8733 case Key.Right: 8734 this.scrollRight(defaultKeyboardListener_horizontalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8735 break; 8736 case Key.Up: 8737 this.scrollUp(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8738 break; 8739 case Key.Down: 8740 this.scrollDown(defaultKeyboardListener_verticalArrowScrollAmount * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8741 break; 8742 case Key.PageUp: 8743 if(ke.altKey) 8744 this.scrollLeft(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8745 else 8746 this.scrollUp(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8747 break; 8748 case Key.PageDown: 8749 if(ke.altKey) 8750 this.scrollRight(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8751 else 8752 this.scrollDown(this.vsb.viewableArea_ * (ke.shiftKey ? defaultKeyboardListener_shiftMultiplier : 1)); 8753 break; 8754 case Key.Home: 8755 if(ke.altKey) 8756 this.scrollLeft(short.max * 16); 8757 else 8758 this.scrollUp(short.max * 16); 8759 break; 8760 case Key.End: 8761 if(ke.altKey) 8762 this.scrollRight(short.max * 16); 8763 else 8764 this.scrollDown(short.max * 16); 8765 break; 8766 8767 default: 8768 // ignore, not for us. 8769 } 8770 } 8771 8772 /++ 8773 Scrolls the given amount. 8774 8775 History: 8776 The scroll up and down functions was here in the initial release of the class, but the `amount` parameter and left/right functions were added on September 28, 2021. 8777 +/ 8778 void scrollUp(int amount = 1) { 8779 vsb.setPosition(vsb.position.NonOverflowingInt - amount); 8780 notify(); 8781 } 8782 /// ditto 8783 void scrollDown(int amount = 1) { 8784 vsb.setPosition(vsb.position.NonOverflowingInt + amount); 8785 notify(); 8786 } 8787 /// ditto 8788 void scrollLeft(int amount = 1) { 8789 hsb.setPosition(hsb.position.NonOverflowingInt - amount); 8790 notify(); 8791 } 8792 /// ditto 8793 void scrollRight(int amount = 1) { 8794 hsb.setPosition(hsb.position.NonOverflowingInt + amount); 8795 notify(); 8796 } 8797 8798 /// 8799 VerticalScrollbar verticalScrollBar() { return vsb; } 8800 /// 8801 HorizontalScrollbar horizontalScrollBar() { return hsb; } 8802 8803 void notify() { 8804 static bool insideNotify; 8805 8806 if(insideNotify) 8807 return; // avoid the recursive call, even if it isn't strictly correct 8808 8809 insideNotify = true; 8810 scope(exit) insideNotify = false; 8811 8812 this.emit!ScrollEvent(); 8813 } 8814 8815 mixin Emits!ScrollEvent; 8816 8817 /// 8818 Point position() { 8819 return Point(hsb.position, vsb.position); 8820 } 8821 8822 /// 8823 void setPosition(int x, int y) { 8824 hsb.setPosition(x); 8825 vsb.setPosition(y); 8826 } 8827 8828 /// 8829 void setPageSize(int unitsX, int unitsY) { 8830 hsb.setStep(unitsX); 8831 vsb.setStep(unitsY); 8832 } 8833 8834 /// Always call this BEFORE setViewableArea 8835 void setTotalArea(int width, int height) { 8836 hsb.setMax(width); 8837 vsb.setMax(height); 8838 } 8839 8840 /++ 8841 Always set the viewable area AFTER setitng the total area if you are going to change both. 8842 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 8843 If you need to do that, use [queueRecomputeChildLayout]. 8844 +/ 8845 void setViewableArea(int width, int height) { 8846 8847 // actually there IS A need to dothis cuz the max might have changed since then 8848 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 8849 //return; // no need to do what is already done 8850 hsb.setViewableArea(width); 8851 vsb.setViewableArea(height); 8852 8853 bool needsNotify = false; 8854 8855 // FIXME: if at any point the rhs is outside the scrollbar, we need 8856 // to reset to 0. but it should remember the old position in case the 8857 // window resizes again, so it can kinda return ot where it was. 8858 // 8859 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 8860 if(width >= hsb.max) { 8861 // there's plenty of room to display it all so we need to reset to zero 8862 // FIXME: adjust so it matches the note above 8863 hsb.setPosition(0); 8864 needsNotify = true; 8865 } 8866 if(height >= vsb.max) { 8867 // there's plenty of room to display it all so we need to reset to zero 8868 // FIXME: adjust so it matches the note above 8869 vsb.setPosition(0); 8870 needsNotify = true; 8871 } 8872 if(needsNotify) 8873 notify(); 8874 } 8875 8876 private bool magic; 8877 override void addChild(Widget w, int position = int.max) { 8878 if(magic) 8879 container.addChild(w, position); 8880 else 8881 super.addChild(w, position); 8882 } 8883 8884 override void recomputeChildLayout() { 8885 if(hsb is null || vsb is null || container is null) return; 8886 8887 registerMovement(); 8888 8889 enum BUTTON_SIZE = 16; 8890 8891 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 8892 hsb.x = 0; 8893 hsb.y = this.height - hsb.height; 8894 8895 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 8896 vsb.x = this.width - vsb.width; 8897 vsb.y = 0; 8898 8899 auto vsb_width = vsb.showing ? vsb.width : 0; 8900 auto hsb_height = hsb.showing ? hsb.height : 0; 8901 8902 hsb.width = this.width - vsb_width; 8903 vsb.height = this.height - hsb_height; 8904 8905 hsb.recomputeChildLayout(); 8906 vsb.recomputeChildLayout(); 8907 8908 if(this.header is null) { 8909 container.x = 0; 8910 container.y = 0; 8911 container.width = this.width - vsb_width; 8912 container.height = this.height - hsb_height; 8913 container.recomputeChildLayout(); 8914 } else { 8915 header.x = 0; 8916 header.y = 0; 8917 header.width = this.width - vsb_width; 8918 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 8919 header.recomputeChildLayout(); 8920 8921 container.x = 0; 8922 container.y = scaleWithDpi(BUTTON_SIZE); 8923 container.width = this.width - vsb_width; 8924 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 8925 container.recomputeChildLayout(); 8926 } 8927 } 8928 8929 private HorizontalScrollbar hsb; 8930 private VerticalScrollbar vsb; 8931 Widget container; 8932 private Widget header; 8933 8934 /++ 8935 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 8936 8937 History: 8938 Added September 27, 2021 (dub v10.3) 8939 +/ 8940 Widget getHeader() { 8941 if(this.header is null) { 8942 magic = false; 8943 scope(exit) magic = true; 8944 this.header = new Widget(this); 8945 queueRecomputeChildLayout(); 8946 } 8947 return this.header; 8948 } 8949 8950 /++ 8951 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 8952 8953 History: 8954 Added January 3, 2023 (dub v11.0) 8955 +/ 8956 void scrollIntoView(Rectangle rect) { 8957 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 8958 8959 // import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1))); 8960 8961 // the lower right is exclusive normally 8962 auto test = rect.lowerRight; 8963 if(test.x > 0) test.x--; 8964 if(test.y > 0) test.y--; 8965 8966 if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) { 8967 // try to scroll only one dimension at a time if we can 8968 if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y))) 8969 setPosition(rect.upperLeft.x, position.y); 8970 if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y))) 8971 setPosition(position.x, rect.upperLeft.y); 8972 } 8973 8974 } 8975 8976 override int minHeight() { 8977 int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0)); 8978 if(header !is null) 8979 min += header.minHeight; 8980 if(horizontalScrollBar.showing) 8981 min += horizontalScrollBar.minHeight; 8982 return min; 8983 } 8984 8985 override int maxHeight() { 8986 int max = container ? container.maxHeight : int.max; 8987 if(max == int.max) 8988 return max; 8989 if(horizontalScrollBar.showing) 8990 max += horizontalScrollBar.minHeight; 8991 return max; 8992 } 8993 8994 static class Style : Widget.Style { 8995 override WidgetBackground background() { 8996 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 8997 } 8998 } 8999 mixin OverrideStyle!Style; 9000 } 9001 9002 /++ 9003 $(IMG //arsdnet.net/minigui-screenshots/windows/ScrollMessageWidget.png, A box saying "baby will" with three round buttons inside it for the options of "eat", "cry", and "sleep") 9004 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 9005 +/ 9006 version(minigui_screenshots) 9007 @Screenshot("ScrollMessageWidget") 9008 unittest { 9009 auto window = new Window("ScrollMessageWidget"); 9010 9011 auto smw = new ScrollMessageWidget(window); 9012 smw.addDefaultKeyboardListeners(); 9013 smw.addDefaultWheelListeners(); 9014 9015 window.loop(); 9016 } 9017 9018 /++ 9019 Bypasses automatic layout for its children, using manual positioning and sizing only. 9020 While you need to manually position them, you must ensure they are inside the StaticLayout's 9021 bounding box to avoid undefined behavior. 9022 9023 You should almost never use this. 9024 +/ 9025 class StaticLayout : Layout { 9026 /// 9027 this(Widget parent) { super(parent); } 9028 override void recomputeChildLayout() { 9029 registerMovement(); 9030 foreach(child; children) 9031 child.recomputeChildLayout(); 9032 } 9033 } 9034 9035 /++ 9036 Bypasses automatic positioning when being laid out. It is your responsibility to make 9037 room for this widget in the parent layout. 9038 9039 Its children are laid out normally, unless there is exactly one, in which case it takes 9040 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 9041 can do that with `padding`). 9042 +/ 9043 class StaticPosition : Layout { 9044 /// 9045 this(Widget parent) { super(parent); } 9046 9047 override void recomputeChildLayout() { 9048 registerMovement(); 9049 if(this.children.length == 1) { 9050 auto child = children[0]; 9051 child.x = 0; 9052 child.y = 0; 9053 child.width = this.width; 9054 child.height = this.height; 9055 child.recomputeChildLayout(); 9056 } else 9057 foreach(child; children) 9058 child.recomputeChildLayout(); 9059 } 9060 9061 alias width = typeof(super).width; 9062 alias height = typeof(super).height; 9063 9064 @property int width(int w) @nogc pure @safe nothrow { 9065 return this._width = w; 9066 } 9067 9068 @property int height(int w) @nogc pure @safe nothrow { 9069 return this._height = w; 9070 } 9071 9072 } 9073 9074 /++ 9075 FixedPosition is like [StaticPosition], but its coordinates 9076 are always relative to the viewport, meaning they do not scroll with 9077 the parent content. 9078 +/ 9079 class FixedPosition : StaticPosition { 9080 /// 9081 this(Widget parent) { super(parent); } 9082 } 9083 9084 version(win32_widgets) 9085 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 9086 if(true) { 9087 // cmd == 0 = menu, cmd == 1 = accelerator 9088 if(auto item = idm in Action.mapping) { 9089 foreach(handler; (*item).triggered) 9090 handler(); 9091 /* 9092 auto event = new Event("triggered", *item); 9093 event.button = idm; 9094 event.dispatch(); 9095 */ 9096 return 0; 9097 } 9098 } 9099 if(handle) 9100 if(auto widgetp = handle in Widget.nativeMapping) { 9101 (*widgetp).handleWmCommand(cmd, idm); 9102 return 0; 9103 } 9104 return 1; 9105 } 9106 9107 9108 /// 9109 class Window : Widget { 9110 Widget[] mouseCapturedBy; 9111 void captureMouse(Widget byWhom) { 9112 assert(byWhom !is null); 9113 if(mouseCapturedBy.length > 0) { 9114 auto cc = mouseCapturedBy[$-1]; 9115 if(cc is byWhom) 9116 return; // or should it throw? 9117 auto par = byWhom; 9118 while(par) { 9119 if(cc is par) 9120 goto allowed; 9121 par = par.parent; 9122 } 9123 9124 throw new Exception("mouse is already captured by other widget"); 9125 } 9126 allowed: 9127 mouseCapturedBy ~= byWhom; 9128 if(mouseCapturedBy.length == 1) 9129 win.grabInput(false, true, false); 9130 //void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) { 9131 } 9132 void releaseMouseCapture() { 9133 if(mouseCapturedBy.length == 0) 9134 return; // or should it throw? 9135 mouseCapturedBy = mouseCapturedBy[0 .. $-1]; 9136 mouseCapturedBy.assumeSafeAppend(); 9137 if(mouseCapturedBy.length == 0) 9138 win.releaseInputGrab(); 9139 } 9140 9141 9142 /++ 9143 9144 +/ 9145 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 9146 return .messageBox(this, title, message, style, icon); 9147 } 9148 9149 /// ditto 9150 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 9151 return messageBox(null, message, style, icon); 9152 } 9153 9154 9155 /++ 9156 Sets the window icon which is often seen in title bars and taskbars. 9157 9158 A future plan is to offer an overload that takes an array too for multiple sizes, but right now you should probably set 16x16 or 32x32 images here. 9159 9160 History: 9161 Added April 5, 2022 (dub v10.8) 9162 +/ 9163 @property void icon(MemoryImage icon) { 9164 if(win && icon) 9165 win.icon = icon; 9166 } 9167 9168 // forwarder to the top-level icon thing so this doesn't conflict too much with the UDAs seen inside the class ins ome older examples 9169 // this does NOT change the icon on the window! That's what the other overload is for 9170 static @property .icon icon(GenericIcons i) { 9171 return .icon(i); 9172 } 9173 9174 /// 9175 @scriptable 9176 @property bool focused() { 9177 return win.focused; 9178 } 9179 9180 static class Style : Widget.Style { 9181 override WidgetBackground background() { 9182 version(custom_widgets) 9183 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 9184 else version(win32_widgets) 9185 return WidgetBackground(Color.transparent); 9186 else static assert(0); 9187 } 9188 } 9189 mixin OverrideStyle!Style; 9190 9191 /++ 9192 Gives the height of a line according to the default font. You should try to use your computed font instead of this, but until May 8, 2021, this was the only real option. 9193 +/ 9194 deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() { 9195 return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback(); 9196 } 9197 9198 private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() { 9199 OperatingSystemFont font; 9200 if(auto vt = WidgetPainter.visualTheme) { 9201 font = vt.defaultFontCached(96); // FIXME 9202 } 9203 9204 if(font is null) { 9205 static int defaultHeightCache; 9206 if(defaultHeightCache == 0) { 9207 font = new OperatingSystemFont; 9208 font.loadDefault; 9209 defaultHeightCache = font.height();// * 5 / 4; 9210 } 9211 return defaultHeightCache; 9212 } 9213 9214 return font.height();// * 5 / 4; 9215 } 9216 9217 Widget focusedWidget; 9218 9219 private SimpleWindow win_; 9220 9221 @property { 9222 /++ 9223 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 9224 9225 History: 9226 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 9227 +/ 9228 public SimpleWindow win() { 9229 return win_; 9230 } 9231 /// 9232 protected void win(SimpleWindow w) { 9233 win_ = w; 9234 } 9235 } 9236 9237 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 9238 this(Widget p) { 9239 tabStop = false; 9240 super(p); 9241 } 9242 9243 private void actualRedraw() { 9244 if(recomputeChildLayoutRequired) 9245 recomputeChildLayoutEntry(); 9246 if(!showing) return; 9247 9248 assert(parentWindow !is null); 9249 9250 auto w = drawableWindow; 9251 if(w is null) 9252 w = parentWindow.win; 9253 9254 if(w.closed()) 9255 return; 9256 9257 auto ugh = this.parent; 9258 int lox, loy; 9259 while(ugh) { 9260 lox += ugh.x; 9261 loy += ugh.y; 9262 ugh = ugh.parent; 9263 } 9264 auto painter = w.draw(true); 9265 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 9266 } 9267 9268 9269 private bool skipNextChar = false; 9270 9271 /++ 9272 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 9273 9274 This constructor is intended primarily for internal use and may be changed to `protected` later. 9275 +/ 9276 this(SimpleWindow win) { 9277 9278 static if(UsingSimpledisplayX11) { 9279 win.discardAdditionalConnectionState = &discardXConnectionState; 9280 win.recreateAdditionalConnectionState = &recreateXConnectionState; 9281 } 9282 9283 tabStop = false; 9284 super(null); 9285 this.win = win; 9286 9287 win.addEventListener((Widget.RedrawEvent) { 9288 if(win.eventQueued!RecomputeEvent) { 9289 // writeln("skipping"); 9290 return; // let the recompute event do the actual redraw 9291 } 9292 this.actualRedraw(); 9293 }); 9294 9295 win.addEventListener((Widget.RecomputeEvent) { 9296 recomputeChildLayoutEntry(); 9297 if(win.eventQueued!RedrawEvent) 9298 return; // let the queued one do it 9299 else { 9300 // writeln("drawing"); 9301 this.actualRedraw(); // if not queued, it needs to be done now anyway 9302 } 9303 }); 9304 9305 this.width = win.width; 9306 this.height = win.height; 9307 this.parentWindow = this; 9308 9309 win.closeQuery = () { 9310 if(this.emit!ClosingEvent()) 9311 win.close(); 9312 }; 9313 win.onClosing = () { 9314 this.emit!ClosedEvent(); 9315 }; 9316 9317 win.windowResized = (int w, int h) { 9318 this.width = w; 9319 this.height = h; 9320 queueRecomputeChildLayout(); 9321 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 9322 //version(win32_widgets) 9323 //InvalidateRect(hwnd, null, true); 9324 redraw(); 9325 }; 9326 9327 win.onFocusChange = (bool getting) { 9328 // sdpyPrintDebugString("onFocusChange ", getting, " ", this.toString); 9329 if(this.focusedWidget) { 9330 if(getting) { 9331 this.focusedWidget.emit!FocusEvent(); 9332 this.focusedWidget.emit!FocusInEvent(); 9333 } else { 9334 this.focusedWidget.emit!BlurEvent(); 9335 this.focusedWidget.emit!FocusOutEvent(); 9336 } 9337 } 9338 9339 if(getting) { 9340 this.emit!FocusEvent(); 9341 this.emit!FocusInEvent(); 9342 } else { 9343 this.emit!BlurEvent(); 9344 this.emit!FocusOutEvent(); 9345 } 9346 }; 9347 9348 win.onDpiChanged = { 9349 this.queueRecomputeChildLayout(); 9350 auto event = new DpiChangedEvent(this); 9351 event.sendDirectly(); 9352 9353 privateDpiChanged(); 9354 }; 9355 9356 win.setEventHandlers( 9357 (MouseEvent e) { 9358 dispatchMouseEvent(e); 9359 }, 9360 (KeyEvent e) { 9361 //writefln("%x %s", cast(uint) e.key, e.key); 9362 dispatchKeyEvent(e); 9363 }, 9364 (dchar e) { 9365 if(e == 13) e = 10; // hack? 9366 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 9367 dispatchCharEvent(e); 9368 }, 9369 ); 9370 9371 addEventListener("char", (Widget, Event ev) { 9372 if(skipNextChar) { 9373 ev.preventDefault(); 9374 skipNextChar = false; 9375 } 9376 }); 9377 9378 version(win32_widgets) 9379 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 9380 if(hwnd !is this.win.impl.hwnd) 9381 return 1; // we don't care... pass it on 9382 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 9383 if(mustReturn) 9384 return ret; 9385 return 1; // pass it on 9386 }; 9387 9388 if(Window.newWindowCreated) 9389 Window.newWindowCreated(this); 9390 } 9391 9392 version(custom_widgets) 9393 override void defaultEventHandler_click(ClickEvent event) { 9394 if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) { 9395 if(event.target && event.target.tabStop) 9396 event.target.focus(); 9397 } 9398 } 9399 9400 private static void delegate(Window) newWindowCreated; 9401 9402 version(win32_widgets) 9403 override void paint(WidgetPainter painter) { 9404 /* 9405 RECT rect; 9406 rect.right = this.width; 9407 rect.bottom = this.height; 9408 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 9409 */ 9410 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 9411 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 9412 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 9413 // since the pen is null, to fill the whole space, we need the +1 on both. 9414 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 9415 SelectObject(painter.impl.hdc, p); 9416 SelectObject(painter.impl.hdc, b); 9417 } 9418 version(custom_widgets) 9419 override void paint(WidgetPainter painter) { 9420 auto cs = getComputedStyle(); 9421 painter.fillColor = cs.windowBackgroundColor; 9422 painter.outlineColor = cs.windowBackgroundColor; 9423 painter.drawRectangle(Point(0, 0), this.width, this.height); 9424 } 9425 9426 9427 override void defaultEventHandler_keydown(KeyDownEvent event) { 9428 Widget _this = event.target; 9429 9430 if(event.key == Key.Tab) { 9431 /* Window tab ordering is a recursive thingy with each group */ 9432 9433 // FIXME inefficient 9434 Widget[] helper(Widget p) { 9435 if(p.hidden) 9436 return null; 9437 Widget[] childOrdering; 9438 9439 auto children = p.children.dup; 9440 9441 while(true) { 9442 // UIs should be generally small, so gonna brute force it a little 9443 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 9444 9445 Widget smallestTab; 9446 foreach(ref c; children) { 9447 if(c is null) continue; 9448 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 9449 smallestTab = c; 9450 c = null; 9451 } 9452 } 9453 if(smallestTab !is null) { 9454 if(smallestTab.tabStop && !smallestTab.hidden) 9455 childOrdering ~= smallestTab; 9456 if(!smallestTab.hidden) 9457 childOrdering ~= helper(smallestTab); 9458 } else 9459 break; 9460 9461 } 9462 9463 return childOrdering; 9464 } 9465 9466 Widget[] tabOrdering = helper(this); 9467 9468 Widget recipient; 9469 9470 if(tabOrdering.length) { 9471 bool seenThis = false; 9472 Widget previous; 9473 foreach(idx, child; tabOrdering) { 9474 if(child is focusedWidget) { 9475 9476 if(event.shiftKey) { 9477 if(idx == 0) 9478 recipient = tabOrdering[$-1]; 9479 else 9480 recipient = tabOrdering[idx - 1]; 9481 break; 9482 } 9483 9484 seenThis = true; 9485 if(idx + 1 == tabOrdering.length) { 9486 // we're at the end, either move to the next group 9487 // or start back over 9488 recipient = tabOrdering[0]; 9489 } 9490 continue; 9491 } 9492 if(seenThis) { 9493 recipient = child; 9494 break; 9495 } 9496 previous = child; 9497 } 9498 } 9499 9500 if(recipient !is null) { 9501 // writeln(typeid(recipient)); 9502 recipient.focus(); 9503 9504 skipNextChar = true; 9505 } 9506 } 9507 9508 debug if(event.key == Key.F12) { 9509 if(devTools) { 9510 devTools.close(); 9511 devTools = null; 9512 } else { 9513 devTools = new DevToolWindow(this); 9514 devTools.show(); 9515 } 9516 } 9517 } 9518 9519 debug DevToolWindow devTools; 9520 9521 9522 /++ 9523 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 9524 9525 History: 9526 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 9527 9528 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 9529 +/ 9530 this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) { 9531 if(title is null) { 9532 import core.runtime; 9533 if(Runtime.args.length) 9534 title = Runtime.args[0]; 9535 } 9536 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent); 9537 9538 static if(UsingSimpledisplayX11) 9539 if(windowFlags & WindowFlags.managesChildWindowFocus) { 9540 ///+ 9541 // for input proxy 9542 auto display = XDisplayConnection.get; 9543 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 9544 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 9545 XMapWindow(display, inputProxy); 9546 // writefln("input proxy: 0x%0x", inputProxy); 9547 this.inputProxy = new SimpleWindow(inputProxy); 9548 9549 /+ 9550 this.inputProxy.onFocusChange = (bool getting) { 9551 sdpyPrintDebugString("input proxy focus change ", getting); 9552 }; 9553 +/ 9554 9555 XEvent lastEvent; 9556 this.inputProxy.handleNativeEvent = (XEvent ev) { 9557 lastEvent = ev; 9558 return 1; 9559 }; 9560 this.inputProxy.setEventHandlers( 9561 (MouseEvent e) { 9562 dispatchMouseEvent(e); 9563 }, 9564 (KeyEvent e) { 9565 //writefln("%x %s", cast(uint) e.key, e.key); 9566 if(dispatchKeyEvent(e)) { 9567 // FIXME: i should trap error 9568 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 9569 auto thing = nw.focusableWindow(); 9570 if(thing && thing.window) { 9571 lastEvent.xkey.window = thing.window; 9572 // writeln("sending event ", lastEvent.xkey); 9573 trapXErrors( { 9574 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 9575 }); 9576 } 9577 } 9578 } 9579 }, 9580 (dchar e) { 9581 if(e == 13) e = 10; // hack? 9582 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 9583 dispatchCharEvent(e); 9584 }, 9585 ); 9586 9587 this.inputProxy.populateXic(); 9588 // done 9589 //+/ 9590 } 9591 9592 9593 9594 win.setRequestedInputFocus = &this.setRequestedInputFocus; 9595 9596 this(win); 9597 } 9598 9599 SimpleWindow inputProxy; 9600 9601 private SimpleWindow setRequestedInputFocus() { 9602 return inputProxy; 9603 } 9604 9605 /// ditto 9606 this(string title, int width = 500, int height = 500) { 9607 this(width, height, title); 9608 } 9609 9610 /// 9611 @property string title() { return parentWindow.win.title; } 9612 /// 9613 @property void title(string title) { parentWindow.win.title = title; } 9614 9615 /// 9616 @scriptable 9617 void close() { 9618 win.close(); 9619 // I synchronize here upon window closing to ensure all child windows 9620 // get updated too before the event loop. This avoids some random X errors. 9621 static if(UsingSimpledisplayX11) { 9622 runInGuiThread( { 9623 XSync(XDisplayConnection.get, false); 9624 }); 9625 } 9626 } 9627 9628 bool dispatchKeyEvent(KeyEvent ev) { 9629 auto wid = focusedWidget; 9630 if(wid is null) 9631 wid = this; 9632 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 9633 event.originalKeyEvent = ev; 9634 event.key = ev.key; 9635 event.state = ev.modifierState; 9636 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 9637 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 9638 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 9639 event.dispatch(); 9640 9641 return !event.propagationStopped; 9642 } 9643 9644 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 9645 bool dispatchCharEvent(dchar ch) { 9646 if(focusedWidget) { 9647 auto event = new CharEvent(focusedWidget, ch); 9648 event.dispatch(); 9649 return !event.propagationStopped; 9650 } 9651 return true; 9652 } 9653 9654 Widget mouseLastOver; 9655 Widget mouseLastDownOn; 9656 bool lastWasDoubleClick; 9657 bool dispatchMouseEvent(MouseEvent ev) { 9658 auto eleR = widgetAtPoint(this, ev.x, ev.y); 9659 auto ele = eleR.widget; 9660 9661 auto captureEle = ele; 9662 9663 auto mouseCapturedBy = this.mouseCapturedBy.length ? this.mouseCapturedBy[$-1] : null; 9664 if(mouseCapturedBy !is null) { 9665 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 9666 captureEle = mouseCapturedBy; 9667 } 9668 9669 // a hack to get it relative to the widget. 9670 eleR.x = ev.x; 9671 eleR.y = ev.y; 9672 auto pain = captureEle; 9673 9674 auto vpx = eleR.x; 9675 auto vpy = eleR.y; 9676 9677 while(pain) { 9678 eleR.x -= pain.x; 9679 eleR.y -= pain.y; 9680 pain.addScrollPosition(eleR.x, eleR.y); 9681 9682 vpx -= pain.x; 9683 vpy -= pain.y; 9684 9685 pain = pain.parent; 9686 } 9687 9688 void populateMouseEventBase(MouseEventBase event) { 9689 event.button = ev.button; 9690 event.buttonLinear = ev.buttonLinear; 9691 event.state = ev.modifierState; 9692 event.clientX = eleR.x; 9693 event.clientY = eleR.y; 9694 9695 event.viewportX = vpx; 9696 event.viewportY = vpy; 9697 9698 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 9699 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 9700 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 9701 } 9702 9703 if(ev.type == MouseEventType.buttonPressed) { 9704 { 9705 auto event = new MouseDownEvent(captureEle); 9706 populateMouseEventBase(event); 9707 event.dispatch(); 9708 } 9709 9710 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 9711 auto event = new DoubleClickEvent(captureEle); 9712 populateMouseEventBase(event); 9713 event.dispatch(); 9714 lastWasDoubleClick = ev.doubleClick; 9715 } else { 9716 lastWasDoubleClick = false; 9717 } 9718 9719 mouseLastDownOn = ele; 9720 } else if(ev.type == MouseEventType.buttonReleased) { 9721 { 9722 auto event = new MouseUpEvent(captureEle); 9723 populateMouseEventBase(event); 9724 event.dispatch(); 9725 } 9726 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 9727 auto event = new ClickEvent(captureEle); 9728 populateMouseEventBase(event); 9729 event.dispatch(); 9730 } 9731 } else if(ev.type == MouseEventType.motion) { 9732 // motion 9733 { 9734 auto event = new MouseMoveEvent(captureEle); 9735 populateMouseEventBase(event); // fills in button which is meaningless but meh 9736 event.dispatch(); 9737 } 9738 9739 if(mouseLastOver !is ele) { 9740 if(ele !is null) { 9741 if(!isAParentOf(ele, mouseLastOver)) { 9742 ele.setDynamicState(DynamicState.hover, true); 9743 auto event = new MouseEnterEvent(ele); 9744 event.relatedTarget = mouseLastOver; 9745 event.sendDirectly(); 9746 9747 ele.useStyleProperties((scope Widget.Style s) { 9748 ele.parentWindow.win.cursor = s.cursor; 9749 }); 9750 } 9751 } 9752 9753 if(mouseLastOver !is null) { 9754 if(!isAParentOf(mouseLastOver, ele)) { 9755 mouseLastOver.setDynamicState(DynamicState.hover, false); 9756 auto event = new MouseLeaveEvent(mouseLastOver); 9757 event.relatedTarget = ele; 9758 event.sendDirectly(); 9759 } 9760 } 9761 9762 if(ele !is null) { 9763 auto event = new MouseOverEvent(ele); 9764 event.relatedTarget = mouseLastOver; 9765 event.dispatch(); 9766 } 9767 9768 if(mouseLastOver !is null) { 9769 auto event = new MouseOutEvent(mouseLastOver); 9770 event.relatedTarget = ele; 9771 event.dispatch(); 9772 } 9773 9774 mouseLastOver = ele; 9775 } 9776 } 9777 9778 return true; // FIXME: the event default prevented? 9779 } 9780 9781 /++ 9782 Shows the window and runs the application event loop. 9783 9784 Blocks until this window is closed. 9785 9786 Bugs: 9787 9788 $(PITFALL 9789 You should always have one event loop live for your application. 9790 If you make two windows in sequence, the second call to loop (or 9791 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 9792 might fail: 9793 9794 --- 9795 // don't do this! 9796 auto window = new Window(); 9797 window.loop(); 9798 9799 // or new Window or new MainWindow, all the same 9800 auto window2 = new SimpleWindow(); 9801 window2.eventLoop(0); // problematic! might crash 9802 --- 9803 9804 simpledisplay's current implementation assumes that final cleanup is 9805 done when the event loop refcount reaches zero. So after the first 9806 eventLoop returns, when there isn't already another one active, it assumes 9807 the program will exit soon and cleans up. 9808 9809 This is arguably a bug that it doesn't reinitialize, and I'll probably change 9810 it eventually, but in the mean time, there's an easy solution: 9811 9812 --- 9813 // do this 9814 EventLoop mainEventLoop = EventLoop.get; // just add this line 9815 9816 auto window = new Window(); 9817 window.loop(); 9818 9819 // or any other type of Window etc. 9820 auto window2 = new Window(); 9821 window2.loop(); // perfectly fine since mainEventLoop still alive 9822 --- 9823 9824 By adding a top-level reference to the event loop, it ensures the final cleanup 9825 is not performed until it goes out of scope too, letting the individual window loops 9826 work without trouble despite the bug. 9827 ) 9828 9829 History: 9830 The [BlockingMode] parameter was added on December 8, 2021. 9831 The default behavior is to block until the application quits 9832 (so all windows have been closed), unless another minigui or 9833 simpledisplay event loop is already running, in which case it 9834 will block until this window closes specifically. 9835 +/ 9836 @scriptable 9837 void loop(BlockingMode bm = BlockingMode.automatic) { 9838 if(win.closed) 9839 return; // otherwise show will throw 9840 show(); 9841 win.eventLoopWithBlockingMode(bm, 0); 9842 } 9843 9844 private bool firstShow = true; 9845 9846 @scriptable 9847 override void show() { 9848 bool rd = false; 9849 if(firstShow) { 9850 firstShow = false; 9851 queueRecomputeChildLayout(); 9852 // unless the programmer already called focus on something, pick something ourselves 9853 auto f = focusedWidget is null ? getFirstFocusable(this) : focusedWidget; // FIXME: autofocus? 9854 if(f) 9855 f.focus(); 9856 redraw(); 9857 } 9858 win.show(); 9859 super.show(); 9860 } 9861 @scriptable 9862 override void hide() { 9863 win.hide(); 9864 super.hide(); 9865 } 9866 9867 static Widget getFirstFocusable(Widget start) { 9868 if(start is null) 9869 return null; 9870 9871 foreach(widget; &start.focusableWidgets) { 9872 return widget; 9873 } 9874 9875 return null; 9876 } 9877 9878 static Widget getLastFocusable(Widget start) { 9879 if(start is null) 9880 return null; 9881 9882 Widget last; 9883 foreach(widget; &start.focusableWidgets) { 9884 last = widget; 9885 } 9886 9887 return last; 9888 } 9889 9890 9891 mixin Emits!ClosingEvent; 9892 mixin Emits!ClosedEvent; 9893 } 9894 9895 /++ 9896 History: 9897 Added January 12, 2022 9898 9899 Made `final` on January 3, 2025 9900 +/ 9901 final class DpiChangedEvent : Event { 9902 enum EventString = "dpichanged"; 9903 9904 this(Widget target) { 9905 super(EventString, target); 9906 } 9907 } 9908 9909 debug private class DevToolWindow : Window { 9910 Window p; 9911 9912 TextEdit parentList; 9913 TextEdit logWindow; 9914 TextLabel clickX, clickY; 9915 9916 this(Window p) { 9917 this.p = p; 9918 super(400, 300, "Developer Toolbox"); 9919 9920 logWindow = new TextEdit(this); 9921 parentList = new TextEdit(this); 9922 9923 auto hl = new HorizontalLayout(this); 9924 clickX = new TextLabel("", TextAlignment.Right, hl); 9925 clickY = new TextLabel("", TextAlignment.Right, hl); 9926 9927 parentListeners ~= p.addEventListener("*", (Event ev) { 9928 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 9929 }); 9930 9931 parentListeners ~= p.addEventListener((ClickEvent ev) { 9932 auto s = ev.srcElement; 9933 9934 string list; 9935 9936 void addInfo(Widget s) { 9937 list ~= s.toString(); 9938 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 9939 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 9940 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 9941 list ~= "\n\theight: " ~ toInternal!string(s.height); 9942 list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth); 9943 list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth); 9944 list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness); 9945 list ~= "\n\twidth: " ~ toInternal!string(s.width); 9946 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 9947 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 9948 } 9949 9950 addInfo(s); 9951 9952 s = s.parent; 9953 while(s) { 9954 list ~= "\n"; 9955 addInfo(s); 9956 s = s.parent; 9957 } 9958 parentList.content = list; 9959 9960 clickX.label = toInternal!string(ev.clientX); 9961 clickY.label = toInternal!string(ev.clientY); 9962 }); 9963 } 9964 9965 EventListener[] parentListeners; 9966 9967 override void close() { 9968 assert(p !is null); 9969 foreach(p; parentListeners) 9970 p.disconnect(); 9971 parentListeners = null; 9972 p.devTools = null; 9973 p = null; 9974 super.close(); 9975 } 9976 9977 override void defaultEventHandler_keydown(KeyDownEvent ev) { 9978 if(ev.key == Key.F12) { 9979 this.close(); 9980 if(p) 9981 p.devTools = null; 9982 } else { 9983 super.defaultEventHandler_keydown(ev); 9984 } 9985 } 9986 9987 void log(T...)(T t) { 9988 string str; 9989 import std.conv; 9990 foreach(i; t) 9991 str ~= to!string(i); 9992 str ~= "\n"; 9993 logWindow.addText(str); 9994 logWindow.scrollToBottom(); 9995 9996 //version(custom_widgets) 9997 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 9998 } 9999 } 10000 10001 /++ 10002 A dialog is a transient window that intends to get information from 10003 the user before being dismissed. 10004 +/ 10005 class Dialog : Window { 10006 /// 10007 this(Window parent, int width, int height, string title = null) { 10008 super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win); 10009 10010 // this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) { 10011 } 10012 10013 /// 10014 this(Window parent, string title, int width, int height) { 10015 this(parent, width, height, title); 10016 } 10017 10018 deprecated("Pass an explicit parent window, even if it is `null`") 10019 this(int width, int height, string title = null) { 10020 this(null, width, height, title); 10021 } 10022 10023 /// 10024 void OK() { 10025 10026 } 10027 10028 /// 10029 void Cancel() { 10030 this.close(); 10031 } 10032 } 10033 10034 /++ 10035 A custom widget similar to the HTML5 <details> tag. 10036 +/ 10037 version(none) 10038 class DetailsView : Widget { 10039 10040 } 10041 10042 // FIXME: maybe i should expose the other list views Windows offers too 10043 10044 /++ 10045 A TableView is a widget made to display a table of data strings. 10046 10047 10048 Future_Directions: 10049 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 10050 10051 I will add a selection changed event at some point, as well as item clicked events. 10052 History: 10053 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 10054 See_Also: 10055 [ListWidget] which displays a list of strings without additional columns. 10056 +/ 10057 class TableView : Widget { 10058 /++ 10059 10060 +/ 10061 this(Widget parent) { 10062 super(parent); 10063 10064 version(win32_widgets) { 10065 // LVS_EX_LABELTIP might be worth too 10066 // LVS_OWNERDRAWFIXED 10067 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//, LVS_EX_TRACKSELECT); // ex style for for LVN_HOTTRACK 10068 } else version(custom_widgets) { 10069 auto smw = new ScrollMessageWidget(this); 10070 smw.addDefaultKeyboardListeners(); 10071 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 10072 tvwi = new TableViewWidgetInner(this, smw); 10073 } 10074 } 10075 10076 // FIXME: auto-size columns on double click of header thing like in Windows 10077 // it need only make the currently displayed things fit well. 10078 10079 10080 private ColumnInfo[] columns; 10081 private int itemCount; 10082 10083 version(custom_widgets) private { 10084 TableViewWidgetInner tvwi; 10085 } 10086 10087 /// Passed to [setColumnInfo] 10088 static struct ColumnInfo { 10089 const(char)[] name; /// the name displayed in the header 10090 /++ 10091 The default width, in pixels. As a special case, you can set this to -1 10092 if you want the system to try to automatically size the width to fit visible 10093 content. If it can't, it will try to pick a sensible default size. 10094 10095 Any other negative value is not allowed and may lead to unpredictable results. 10096 10097 History: 10098 The -1 behavior was specified on December 3, 2021. It actually worked before 10099 anyway on Win32 but now it is a formal feature with partial Linux support. 10100 10101 Bugs: 10102 It doesn't actually attempt to calculate a best-fit width on Linux as of 10103 December 3, 2021. I do plan to fix this in the future, but Windows is the 10104 priority right now. At least it doesn't break things when you use it now. 10105 +/ 10106 int width; 10107 10108 /++ 10109 Alignment of the text in the cell. Applies to the header as well as all data in this 10110 column. 10111 10112 Bugs: 10113 On Windows, the first column ignores this member and is always left aligned. 10114 You can work around this by inserting a dummy first column with width = 0 10115 then putting your actual data in the second column, which does respect the 10116 alignment. 10117 10118 This is a quirk of the operating system's implementation going back a very 10119 long time and is unlikely to ever be fixed. 10120 +/ 10121 TextAlignment alignment; 10122 10123 /++ 10124 After all the pixel widths have been assigned, any left over 10125 space is divided up among all columns and distributed to according 10126 to the widthPercent field. 10127 10128 10129 For example, if you have two fields, both with width 50 and one with 10130 widthPercent of 25 and the other with widthPercent of 75, and the 10131 container is 200 pixels wide, first both get their width of 50. 10132 then the 100 remaining pixels are split up, so the one gets a total 10133 of 75 pixels and the other gets a total of 125. 10134 10135 This is automatically applied as the window is resized. 10136 10137 If there is not enough space - that is, when a horizontal scrollbar 10138 needs to appear - there are 0 pixels divided up, and thus everyone 10139 gets 0. This can cause a column to shrink out of proportion when 10140 passing the scroll threshold. 10141 10142 It is important to still set a fixed width (that is, to populate the 10143 `width` field) even if you use the percents because that will be the 10144 default minimum in the event of a scroll bar appearing. 10145 10146 The percents total in the column can never exceed 100 or be less than 0. 10147 Doing this will trigger an assert error. 10148 10149 Implementation note: 10150 10151 Please note that percentages are only recalculated 1) upon original 10152 construction and 2) upon resizing the control. If the user adjusts the 10153 width of a column, the percentage items will not be updated. 10154 10155 On the other hand, if the user adjusts the width of a percentage column 10156 then resizes the window, it is recalculated, meaning their hand adjustment 10157 is discarded. This specific behavior may change in the future as it is 10158 arguably a bug, but I'm not certain yet. 10159 10160 History: 10161 Added November 10, 2021 (dub v10.4) 10162 +/ 10163 int widthPercent; 10164 10165 10166 private int calculatedWidth; 10167 } 10168 /++ 10169 Sets the number of columns along with information about the headers. 10170 10171 Please note: on Windows, the first column ignores your alignment preference 10172 and is always left aligned. 10173 +/ 10174 void setColumnInfo(ColumnInfo[] columns...) { 10175 10176 foreach(ref c; columns) { 10177 c.name = c.name.idup; 10178 } 10179 this.columns = columns.dup; 10180 10181 updateCalculatedWidth(false); 10182 10183 version(custom_widgets) { 10184 tvwi.header.updateHeaders(); 10185 tvwi.updateScrolls(); 10186 } else version(win32_widgets) 10187 foreach(i, column; this.columns) { 10188 LVCOLUMN lvColumn; 10189 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 10190 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 10191 10192 auto bfr = WCharzBuffer(column.name); 10193 lvColumn.pszText = bfr.ptr; 10194 10195 if(column.alignment & TextAlignment.Center) 10196 lvColumn.fmt = LVCFMT_CENTER; 10197 else if(column.alignment & TextAlignment.Right) 10198 lvColumn.fmt = LVCFMT_RIGHT; 10199 else 10200 lvColumn.fmt = LVCFMT_LEFT; 10201 10202 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 10203 throw new WindowsApiException("Insert Column Fail", GetLastError()); 10204 } 10205 } 10206 10207 version(custom_widgets) 10208 private int getColumnSizeForContent(size_t columnIndex) { 10209 // FIXME: idk where the problem is but with a 2x scale the horizontal scroll is insuffiicent. i think the SMW is doing it wrong. 10210 // might also want a user-defined max size too 10211 int padding = scaleWithDpi(6); 10212 int m = this.defaultTextWidth(this.columns[columnIndex].name) + padding; 10213 10214 if(getData !is null) 10215 foreach(row; 0 .. itemCount) 10216 getData(row, cast(int) columnIndex, (txt) { 10217 m = mymax(m, this.defaultTextWidth(txt) + padding); 10218 }); 10219 10220 if(m < 32) 10221 m = 32; 10222 10223 return m; 10224 } 10225 10226 /++ 10227 History: 10228 Added February 26, 2025 10229 +/ 10230 void autoSizeColumnsToContent() { 10231 version(custom_widgets) { 10232 foreach(idx, ref c; columns) { 10233 c.width = getColumnSizeForContent(idx); 10234 } 10235 updateCalculatedWidth(false); 10236 tvwi.updateScrolls(); 10237 } else version(win32_widgets) { 10238 foreach(i, c; columns) 10239 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, LVSCW_AUTOSIZE); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 10240 } 10241 } 10242 10243 /++ 10244 History: 10245 Added March 1, 2025 10246 +/ 10247 bool supportsPerCellAlignment() { 10248 version(custom_widgets) 10249 return true; 10250 else version(win32_widgets) 10251 return false; 10252 return false; 10253 } 10254 10255 private int getActualSetSize(size_t i, bool askWindows) { 10256 version(win32_widgets) 10257 if(askWindows) 10258 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 10259 auto w = columns[i].width; 10260 if(w == -1) 10261 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 10262 return w; 10263 } 10264 10265 private void updateCalculatedWidth(bool informWindows) { 10266 int padding; 10267 version(win32_widgets) 10268 padding = 4; 10269 int remaining = this.width; 10270 foreach(i, column; columns) 10271 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 10272 remaining -= padding; 10273 if(remaining < 0) 10274 remaining = 0; 10275 10276 int percentTotal; 10277 foreach(i, ref column; columns) { 10278 percentTotal += column.widthPercent; 10279 10280 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 10281 10282 column.calculatedWidth = c; 10283 10284 version(win32_widgets) 10285 if(informWindows) 10286 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 10287 } 10288 10289 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 10290 assert(percentTotal <= 100, "The total percents in your column definitions exceeded 100. They must add up to no more than 100 (can be less though)."); 10291 10292 10293 } 10294 10295 override void registerMovement() { 10296 super.registerMovement(); 10297 10298 updateCalculatedWidth(true); 10299 } 10300 10301 /++ 10302 Tells the view how many items are in it. It uses this to set the scroll bar, but the items are not added per se; it calls [getData] as-needed. 10303 +/ 10304 void setItemCount(int count) { 10305 this.itemCount = count; 10306 version(custom_widgets) { 10307 tvwi.updateScrolls(); 10308 redraw(); 10309 } else version(win32_widgets) { 10310 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 10311 } 10312 } 10313 10314 /++ 10315 Clears all items; 10316 +/ 10317 void clear() { 10318 this.itemCount = 0; 10319 this.columns = null; 10320 version(custom_widgets) { 10321 tvwi.header.updateHeaders(); 10322 tvwi.updateScrolls(); 10323 redraw(); 10324 } else version(win32_widgets) { 10325 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 10326 } 10327 } 10328 10329 /+ 10330 version(win32_widgets) 10331 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 10332 auto itemId = dis.itemID; 10333 auto hdc = dis.hDC; 10334 auto rect = dis.rcItem; 10335 switch(dis.itemAction) { 10336 case ODA_DRAWENTIRE: 10337 10338 // FIXME: do other items 10339 // FIXME: do the focus rectangle i guess 10340 // FIXME: alignment 10341 // FIXME: column width 10342 // FIXME: padding left 10343 // FIXME: check dpi scaling 10344 // FIXME: don't owner draw unless it is necessary. 10345 10346 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 10347 RECT itemRect; 10348 itemRect.top = 1; // subitem idx, 1-based 10349 itemRect.left = LVIR_BOUNDS; 10350 10351 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 10352 itemRect.left += padding; 10353 10354 getData(itemId, 0, (in char[] data) { 10355 auto wdata = WCharzBuffer(data); 10356 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 10357 10358 }); 10359 goto case; 10360 case ODA_FOCUS: 10361 if(dis.itemState & ODS_FOCUS) 10362 DrawFocusRect(hdc, &rect); 10363 break; 10364 case ODA_SELECT: 10365 // itemState & ODS_SELECTED 10366 break; 10367 default: 10368 } 10369 return 1; 10370 } 10371 +/ 10372 10373 version(win32_widgets) { 10374 CellStyle last; 10375 COLORREF defaultColor; 10376 COLORREF defaultBackground; 10377 } 10378 10379 version(win32_widgets) 10380 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 10381 switch(code) { 10382 case NM_CUSTOMDRAW: 10383 auto s = cast(NMLVCUSTOMDRAW*) hdr; 10384 switch(s.nmcd.dwDrawStage) { 10385 case CDDS_PREPAINT: 10386 if(getCellStyle is null) 10387 return 0; 10388 10389 mustReturn = true; 10390 return CDRF_NOTIFYITEMDRAW; 10391 case CDDS_ITEMPREPAINT: 10392 mustReturn = true; 10393 return CDRF_NOTIFYSUBITEMDRAW; 10394 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 10395 mustReturn = true; 10396 10397 if(getCellStyle is null) // this SHOULD never happen... 10398 return 0; 10399 10400 if(s.iSubItem == 0) { 10401 // Windows resets it per row so we'll use item 0 as a chance 10402 // to capture these for later 10403 defaultColor = s.clrText; 10404 defaultBackground = s.clrTextBk; 10405 } 10406 10407 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 10408 // if no special style and no reset needed... 10409 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 10410 return 0; // allow default processing to continue 10411 10412 last = style; 10413 10414 // might still need to reset or use the preference. 10415 10416 if(style.flags & CellStyle.Flags.textColorSet) 10417 s.clrText = style.textColor.asWindowsColorRef; 10418 else 10419 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 10420 if(style.flags & CellStyle.Flags.backgroundColorSet) 10421 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 10422 else 10423 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 10424 10425 return CDRF_NEWFONT; 10426 default: 10427 return 0; 10428 10429 } 10430 case NM_RETURN: // no need since i subclass keydown 10431 break; 10432 case LVN_COLUMNCLICK: 10433 auto info = cast(LPNMLISTVIEW) hdr; 10434 this.emit!HeaderClickedEvent(info.iSubItem); 10435 break; 10436 case (LVN_FIRST-21) /* LVN_HOTTRACK */: 10437 // requires LVS_EX_TRACKSELECT 10438 // sdpyPrintDebugString("here"); 10439 mustReturn = 1; // override Windows' auto selection 10440 break; 10441 case NM_CLICK: 10442 NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr; 10443 this.emit!CellClickedEvent(info.iItem, info.iSubItem, MouseButton.left, MouseButtonLinear.left, info.ptAction.x, info.ptAction.y, !!(info.uKeyFlags & LVKF_ALT), !!(info.uKeyFlags & LVKF_CONTROL), !!(info.uKeyFlags & LVKF_SHIFT), false); 10444 break; 10445 case NM_DBLCLK: 10446 NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr; 10447 this.emit!CellClickedEvent(info.iItem, info.iSubItem, MouseButton.left, MouseButtonLinear.left, info.ptAction.x, info.ptAction.y, !!(info.uKeyFlags & LVKF_ALT), !!(info.uKeyFlags & LVKF_CONTROL), !!(info.uKeyFlags & LVKF_SHIFT), true); 10448 break; 10449 case NM_RCLICK: 10450 NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr; 10451 this.emit!CellClickedEvent(info.iItem, info.iSubItem, MouseButton.right, MouseButtonLinear.left, info.ptAction.x, info.ptAction.y, !!(info.uKeyFlags & LVKF_ALT), !!(info.uKeyFlags & LVKF_CONTROL), !!(info.uKeyFlags & LVKF_SHIFT), false); 10452 break; 10453 case NM_RDBLCLK: 10454 NMITEMACTIVATE* info = cast(NMITEMACTIVATE*) hdr; 10455 this.emit!CellClickedEvent(info.iItem, info.iSubItem, MouseButton.right, MouseButtonLinear.left, info.ptAction.x, info.ptAction.y, !!(info.uKeyFlags & LVKF_ALT), !!(info.uKeyFlags & LVKF_CONTROL), !!(info.uKeyFlags & LVKF_SHIFT), true); 10456 break; 10457 case LVN_GETDISPINFO: 10458 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 10459 if(info.item.mask & LVIF_TEXT) { 10460 if(getData) { 10461 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 10462 auto bfr = WCharzBuffer(dataReceived); 10463 auto len = info.item.cchTextMax; 10464 if(bfr.length < len) 10465 len = cast(typeof(len)) bfr.length; 10466 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 10467 info.item.pszText[len] = 0; 10468 }); 10469 } else { 10470 info.item.pszText[0] = 0; 10471 } 10472 //info.item.iItem 10473 //if(info.item.iSubItem) 10474 } 10475 break; 10476 default: 10477 } 10478 return 0; 10479 } 10480 10481 // FIXME: this throws off mouse calculations, it should only happen when we're at the top level or something idk 10482 override bool encapsulatedChildren() { 10483 return true; 10484 } 10485 10486 /++ 10487 Informs the control that content has changed. 10488 10489 History: 10490 Added November 10, 2021 (dub v10.4) 10491 +/ 10492 void update() { 10493 version(custom_widgets) 10494 redraw(); 10495 else { 10496 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 10497 UpdateWindow(hwnd); 10498 } 10499 10500 10501 } 10502 10503 /++ 10504 Called by the system to request the text content of an individual cell. You 10505 should pass the text into the provided `sink` delegate. This function will be 10506 called for each visible cell as-needed when drawing. 10507 +/ 10508 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 10509 10510 /++ 10511 Available per-cell style customization options. Use one of the constructors 10512 provided to set the values conveniently, or default construct it and set individual 10513 values yourself. Just remember to set the `flags` so your values are actually used. 10514 If the flag isn't set, the field is ignored and the system default is used instead. 10515 10516 This is returned by the [getCellStyle] delegate. 10517 10518 Examples: 10519 --- 10520 // assumes you have a variables called `my_data` which is an array of arrays of numbers 10521 auto table = new TableView(window); 10522 // snip: you would set up columns here 10523 10524 // this is how you provide data to the table view class 10525 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 10526 import std.conv; 10527 sink(to!string(my_data[row][column])); 10528 }; 10529 10530 // and this is how you customize the colors 10531 table.getCellStyle = delegate(int row, int column) { 10532 return (my_data[row][column] < 0) ? 10533 TableView.CellStyle(Color.red); // make negative numbers red 10534 : TableView.CellStyle.init; // leave the rest alone 10535 }; 10536 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 10537 --- 10538 10539 History: 10540 Added November 27, 2021 (dub v10.4) 10541 +/ 10542 struct CellStyle { 10543 /// Sets just a custom text color, leaving the background as the default. Use caution with certain colors as it may have illeglible contrast on the (unknown to you) background color. 10544 this(Color textColor) { 10545 this.textColor = textColor; 10546 this.flags |= Flags.textColorSet; 10547 } 10548 /// Sets a custom text and background color. 10549 this(Color textColor, Color backgroundColor) { 10550 this.textColor = textColor; 10551 this.backgroundColor = backgroundColor; 10552 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 10553 } 10554 /++ 10555 Alignment is only supported on some platforms. 10556 +/ 10557 this(TextAlignment alignment) { 10558 this.alignment = alignment; 10559 this.flags |= Flags.alignmentSet; 10560 } 10561 /// ditto 10562 this(TextAlignment alignment, Color textColor) { 10563 this.alignment = alignment; 10564 this.textColor = textColor; 10565 this.flags |= Flags.alignmentSet | Flags.textColorSet; 10566 } 10567 /// ditto 10568 this(TextAlignment alignment, Color textColor, Color backgroundColor) { 10569 this.alignment = alignment; 10570 this.textColor = textColor; 10571 this.backgroundColor = backgroundColor; 10572 this.flags |= Flags.alignmentSet | Flags.textColorSet | Flags.backgroundColorSet; 10573 } 10574 10575 TextAlignment alignment; 10576 Color textColor; 10577 Color backgroundColor; 10578 int flags; /// bitmask of [Flags] 10579 /// available options to combine into [flags] 10580 enum Flags { 10581 textColorSet = 1 << 0, 10582 backgroundColorSet = 1 << 1, 10583 alignmentSet = 1 << 2, 10584 } 10585 } 10586 /++ 10587 Companion delegate to [getData] that allows you to custom style each 10588 cell of the table. 10589 10590 Returns: 10591 A [CellStyle] structure that describes the desired style for the 10592 given cell. `return CellStyle.init` if you want the default style. 10593 10594 History: 10595 Added November 27, 2021 (dub v10.4) 10596 +/ 10597 CellStyle delegate(int row, int column) getCellStyle; 10598 10599 // i want to be able to do things like draw little colored things to show red for negative numbers 10600 // or background color indicators or even in-cell charts 10601 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 10602 10603 /++ 10604 When the user clicks on a header, this event is emitted. It has a member to identify which header (by index) was clicked. 10605 +/ 10606 mixin Emits!HeaderClickedEvent; 10607 10608 /++ 10609 History: 10610 Added March 2, 2025 10611 +/ 10612 mixin Emits!CellClickedEvent; 10613 } 10614 10615 /++ 10616 This is emitted by the [TableView] when a user clicks on a column header. 10617 10618 Its member `columnIndex` has the zero-based index of the column that was clicked. 10619 10620 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 10621 10622 History: 10623 Added November 27, 2021 (dub v10.4) 10624 10625 Made `final` on January 3, 2025 10626 +/ 10627 final class HeaderClickedEvent : Event { 10628 enum EventString = "HeaderClicked"; 10629 this(Widget target, int columnIndex) { 10630 this.columnIndex = columnIndex; 10631 super(EventString, target); 10632 } 10633 10634 /// The index of the column 10635 int columnIndex; 10636 10637 /// 10638 override @property int intValue() { 10639 return columnIndex; 10640 } 10641 } 10642 10643 /++ 10644 History: 10645 Added March 2, 2025 10646 +/ 10647 final class CellClickedEvent : MouseEventBase { 10648 enum EventString = "CellClicked"; 10649 this(Widget target, int rowIndex, int columnIndex, MouseButton button, MouseButtonLinear mouseButtonLinear, int x, int y, bool altKey, bool ctrlKey, bool shiftKey, bool isDoubleClick) { 10650 this.rowIndex = rowIndex; 10651 this.columnIndex = columnIndex; 10652 this.button = button; 10653 this.buttonLinear = mouseButtonLinear; 10654 this.isDoubleClick = isDoubleClick; 10655 this.clientX = x; 10656 this.clientY = y; 10657 10658 this.altKey = altKey; 10659 this.ctrlKey = ctrlKey; 10660 this.shiftKey = shiftKey; 10661 10662 // import std.stdio; std.stdio.writeln(rowIndex, "x", columnIndex, " @ ", x, ",", y, " ", button, " ", isDoubleClick, " ", altKey, " ", ctrlKey, " ", shiftKey); 10663 10664 // FIXME: x, y, state, altButton etc? 10665 super(EventString, target); 10666 } 10667 10668 /++ 10669 See also: [button] inherited from the base class. 10670 10671 clientX and clientY are irrespective of scrolling - FIXME is that sane? 10672 +/ 10673 int columnIndex; 10674 10675 /// ditto 10676 int rowIndex; 10677 10678 /// ditto 10679 bool isDoubleClick; 10680 10681 /+ 10682 // i could do intValue as a linear index if we know the width 10683 // and a stringValue with the string in the cell. but idk if worth. 10684 override @property int intValue() { 10685 return columnIndex; 10686 } 10687 +/ 10688 10689 } 10690 10691 version(custom_widgets) 10692 private class TableViewWidgetInner : Widget { 10693 10694 // wrap this thing in a ScrollMessageWidget 10695 10696 TableView tvw; 10697 ScrollMessageWidget smw; 10698 HeaderWidget header; 10699 10700 this(TableView tvw, ScrollMessageWidget smw) { 10701 this.tvw = tvw; 10702 this.smw = smw; 10703 super(smw); 10704 10705 this.tabStop = true; 10706 10707 header = new HeaderWidget(this, smw.getHeader()); 10708 10709 smw.addEventListener("scroll", () { 10710 this.redraw(); 10711 header.redraw(); 10712 }); 10713 10714 10715 // I need headers outside the scroll area but rendered on the same line as the up arrow 10716 // FIXME: add a fixed header to the SMW 10717 } 10718 10719 enum padding = 3; 10720 10721 void updateScrolls() { 10722 int w; 10723 foreach(idx, column; tvw.columns) { 10724 w += column.calculatedWidth; 10725 } 10726 smw.setTotalArea(w, tvw.itemCount); 10727 columnsWidth = w; 10728 } 10729 10730 private int columnsWidth; 10731 10732 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 10733 10734 override void registerMovement() { 10735 super.registerMovement(); 10736 // FIXME: actual column width. it might need to be done per-pixel instead of per-column 10737 smw.setViewableArea(this.width, this.height / lh); 10738 } 10739 10740 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 10741 int x; 10742 int y; 10743 10744 int row = smw.position.y; 10745 10746 foreach(lol; 0 .. this.height / lh) { 10747 if(row >= tvw.itemCount) 10748 break; 10749 x = 0; 10750 foreach(columnNumber, column; tvw.columns) { 10751 auto x2 = x + column.calculatedWidth; 10752 auto smwx = smw.position.x; 10753 10754 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 10755 auto startX = x; 10756 auto endX = x + column.calculatedWidth; 10757 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 10758 case TextAlignment.Left: startX += padding; break; 10759 case TextAlignment.Center: startX += padding; endX -= padding; break; 10760 case TextAlignment.Right: endX -= padding; break; 10761 default: /* broken */ break; 10762 } 10763 if(column.width != 0) // no point drawing an invisible column 10764 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 10765 auto endClip = endX - smw.position.x; 10766 if(endClip > this.width - padding) 10767 endClip = this.width - padding; 10768 auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endClip, y + lh))); 10769 10770 void dotext(WidgetPainter painter, TextAlignment alignment) { 10771 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x - padding, y + lh), alignment); 10772 } 10773 10774 if(tvw.getCellStyle !is null) { 10775 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 10776 10777 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 10778 auto tempPainter = painter; 10779 tempPainter.fillColor = style.backgroundColor; 10780 tempPainter.outlineColor = style.backgroundColor; 10781 10782 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 10783 Point(endX - smw.position.x, y + lh)); 10784 } 10785 auto tempPainter = painter; 10786 if(style.flags & TableView.CellStyle.Flags.textColorSet) 10787 tempPainter.outlineColor = style.textColor; 10788 10789 auto alignment = column.alignment; 10790 if(style.flags & TableView.CellStyle.Flags.alignmentSet) 10791 alignment = style.alignment; 10792 dotext(tempPainter, alignment); 10793 } else { 10794 dotext(painter, column.alignment); 10795 } 10796 }); 10797 } 10798 10799 x += column.calculatedWidth; 10800 } 10801 row++; 10802 y += lh; 10803 } 10804 return bounds; 10805 } 10806 10807 static class Style : Widget.Style { 10808 override WidgetBackground background() { 10809 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 10810 } 10811 } 10812 mixin OverrideStyle!Style; 10813 10814 private static class HeaderWidget : Widget { 10815 /+ 10816 maybe i should do a splitter thing on top of the other widgets 10817 so the splitter itself isn't really drawn but still replies to mouse events? 10818 +/ 10819 this(TableViewWidgetInner tvw, Widget parent) { 10820 super(parent); 10821 this.tvw = tvw; 10822 10823 this.remainder = new Button("", this); 10824 10825 this.addEventListener((scope ClickEvent ev) { 10826 int header = -1; 10827 foreach(idx, child; this.children[1 .. $]) { 10828 if(child is ev.target) { 10829 header = cast(int) idx; 10830 break; 10831 } 10832 } 10833 10834 if(header != -1) { 10835 auto hce = new HeaderClickedEvent(tvw.tvw, header); 10836 hce.dispatch(); 10837 } 10838 10839 }); 10840 } 10841 10842 override int minHeight() { 10843 return defaultLineHeight + 4; // same as Button 10844 } 10845 10846 void updateHeaders() { 10847 foreach(child; children[1 .. $]) 10848 child.removeWidget(); 10849 10850 foreach(column; tvw.tvw.columns) { 10851 // the cast is ok because I dup it above, just the type is never changed. 10852 // all this is private so it should never get messed up. 10853 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 10854 } 10855 } 10856 10857 Button remainder; 10858 TableViewWidgetInner tvw; 10859 10860 override void recomputeChildLayout() { 10861 registerMovement(); 10862 int pos; 10863 foreach(idx, child; children[1 .. $]) { 10864 if(idx >= tvw.tvw.columns.length) 10865 continue; 10866 child.x = pos; 10867 child.y = 0; 10868 child.width = tvw.tvw.columns[idx].calculatedWidth; 10869 child.height = scaleWithDpi(16);// this.height; 10870 pos += child.width; 10871 10872 child.recomputeChildLayout(); 10873 } 10874 10875 if(remainder is null) 10876 return; 10877 10878 remainder.x = pos; 10879 remainder.y = 0; 10880 if(pos < this.width) 10881 remainder.width = this.width - pos;// + 4; 10882 else 10883 remainder.width = 0; 10884 remainder.height = scaleWithDpi(16); 10885 10886 remainder.recomputeChildLayout(); 10887 } 10888 10889 // for the scrollable children mixin 10890 Point scrollOrigin() { 10891 return Point(tvw.smw.position.x, 0); 10892 } 10893 void paintFrameAndBackground(WidgetPainter painter) { } 10894 10895 // for mouse event dispatching 10896 override protected void addScrollPosition(ref int x, ref int y) { 10897 x += scrollOrigin.x; 10898 y += scrollOrigin.y; 10899 } 10900 10901 mixin ScrollableChildren; 10902 } 10903 10904 private void emitCellClickedEvent(scope MouseEventBase event, bool isDoubleClick) { 10905 int mx = event.clientX + smw.position.x; 10906 int my = event.clientY; 10907 10908 Widget par = this; 10909 while(par && !par.encapsulatedChildren) { 10910 my -= par.y; // to undo the encapsulatedChildren adjustClientCoordinates effect 10911 par = par.parent; 10912 } 10913 if(par is null) 10914 my = event.clientY; // encapsulatedChildren not present? 10915 10916 int row = my / lh + smw.position.y; // scrolling here is done per-item, not per pixel 10917 if(row > tvw.itemCount) 10918 row = -1; 10919 10920 int column = -1; 10921 if(row != -1) { 10922 int pos; 10923 foreach(idx, col; tvw.columns) { 10924 pos += col.calculatedWidth; 10925 if(mx < pos) { 10926 column = cast(int) idx; 10927 break; 10928 } 10929 } 10930 } 10931 10932 // wtf are these casts about? 10933 tvw.emit!CellClickedEvent(row, column, cast(MouseButton) event.button, cast(MouseButtonLinear) event.buttonLinear, event.clientX, event.clientY, event.altKey, event.ctrlKey, event.shiftKey, isDoubleClick); 10934 } 10935 10936 override void defaultEventHandler_click(scope ClickEvent ce) { 10937 // FIXME: should i filter mouse wheel events? Windows doesn't send them but i can. 10938 emitCellClickedEvent(ce, false); 10939 } 10940 10941 override void defaultEventHandler_dblclick(scope DoubleClickEvent ce) { 10942 emitCellClickedEvent(ce, true); 10943 } 10944 } 10945 10946 /+ 10947 10948 // given struct / array / number / string / etc, make it viewable and editable 10949 class DataViewerWidget : Widget { 10950 10951 } 10952 +/ 10953 10954 /++ 10955 A line edit box with an associated label. 10956 10957 History: 10958 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 10959 10960 ``` 10961 Old: ________ 10962 10963 New: 10964 ____________ 10965 ``` 10966 10967 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 10968 10969 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 10970 horizontal label but left aligned. You may also consider a [GridLayout]. 10971 +/ 10972 alias LabeledLineEdit = Labeled!LineEdit; 10973 10974 private int widthThatWouldFitChildLabels(Widget w) { 10975 if(w is null) 10976 return 0; 10977 10978 int max; 10979 10980 if(auto label = cast(TextLabel) w) { 10981 return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight(); 10982 } else { 10983 foreach(child; w.children) { 10984 max = mymax(max, widthThatWouldFitChildLabels(child)); 10985 } 10986 } 10987 10988 return max; 10989 } 10990 10991 /++ 10992 History: 10993 Added May 19, 2021 10994 +/ 10995 class Labeled(T) : Widget { 10996 /// 10997 this(string label, Widget parent) { 10998 super(parent); 10999 initialize!VerticalLayout(label, TextAlignment.Left, parent); 11000 } 11001 11002 /++ 11003 History: 11004 The alignment parameter was added May 17, 2021 11005 +/ 11006 this(string label, TextAlignment alignment, Widget parent) { 11007 super(parent); 11008 initialize!HorizontalLayout(label, alignment, parent); 11009 } 11010 11011 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 11012 tabStop = false; 11013 horizontal = is(L == HorizontalLayout); 11014 auto hl = new L(this); 11015 if(horizontal) { 11016 static class SpecialTextLabel : TextLabel { 11017 Widget outerParent; 11018 11019 this(string label, TextAlignment alignment, Widget outerParent, Widget parent) { 11020 this.outerParent = outerParent; 11021 super(label, alignment, parent); 11022 } 11023 11024 override int flexBasisWidth() { 11025 return widthThatWouldFitChildLabels(outerParent); 11026 } 11027 /+ 11028 override int widthShrinkiness() { return 0; } 11029 override int widthStretchiness() { return 1; } 11030 +/ 11031 11032 override int paddingRight() { return 6; } 11033 override int paddingLeft() { return 9; } 11034 11035 override int paddingTop() { return 3; } 11036 } 11037 this.label = new SpecialTextLabel(label, alignment, parent, hl); 11038 } else 11039 this.label = new TextLabel(label, alignment, hl); 11040 this.lineEdit = new T(hl); 11041 11042 this.label.labelFor = this.lineEdit; 11043 } 11044 11045 private bool horizontal; 11046 11047 TextLabel label; /// 11048 T lineEdit; /// 11049 11050 override int flexBasisWidth() { return 250; } 11051 override int widthShrinkiness() { return 1; } 11052 11053 override int minHeight() { 11054 return this.children[0].minHeight; 11055 } 11056 override int maxHeight() { return minHeight(); } 11057 override int marginTop() { return 4; } 11058 override int marginBottom() { return 4; } 11059 11060 // FIXME: i should prolly call it value as well as content tbh 11061 11062 /// 11063 @property string content() { 11064 return lineEdit.content; 11065 } 11066 /// 11067 @property void content(string c) { 11068 return lineEdit.content(c); 11069 } 11070 11071 /// 11072 void selectAll() { 11073 lineEdit.selectAll(); 11074 } 11075 11076 override void focus() { 11077 lineEdit.focus(); 11078 } 11079 } 11080 11081 /++ 11082 A labeled password edit. 11083 11084 History: 11085 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 11086 11087 The default parameters for the constructors were also removed on May 19, 2021 11088 +/ 11089 alias LabeledPasswordEdit = Labeled!PasswordEdit; 11090 11091 private string toMenuLabel(string s) { 11092 string n; 11093 n.reserve(s.length); 11094 foreach(c; s) 11095 if(c == '_') 11096 n ~= ' '; 11097 else 11098 n ~= c; 11099 return n; 11100 } 11101 11102 private void autoExceptionHandler(Exception e) { 11103 messageBox(e.msg); 11104 } 11105 11106 void callAsIfClickedFromMenu(alias fn)(auto ref __traits(parent, fn) _this, Window window) { 11107 makeAutomaticHandler!(fn)(window, &__traits(child, _this, fn))(); 11108 } 11109 11110 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) { 11111 static if(is(T : void delegate())) { 11112 return () { 11113 try 11114 t(); 11115 catch(Exception e) 11116 autoExceptionHandler(e); 11117 }; 11118 } else static if(is(typeof(fn) Params == __parameters)) { 11119 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 11120 return () { 11121 void onOK(string s) { 11122 member = s; 11123 try 11124 t(Params[0](s)); 11125 catch(Exception e) 11126 autoExceptionHandler(e); 11127 } 11128 11129 if( 11130 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 11131 || type == FileDialogType.Save) 11132 { 11133 getSaveFileName(window, &onOK, member, filters, null); 11134 } else 11135 getOpenFileName(window, &onOK, member, filters, null); 11136 }; 11137 } else { 11138 struct S { 11139 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 11140 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 11141 } else mixin(q{ 11142 static foreach(idx, ignore; Params) { 11143 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 11144 } 11145 }); 11146 } 11147 return () { 11148 dialog(window, (S s) { 11149 try { 11150 static if(is(typeof(t) Ret == return)) { 11151 static if(is(Ret == void)) { 11152 t(s.tupleof); 11153 } else { 11154 auto ret = t(s.tupleof); 11155 import std.conv; 11156 messageBox(to!string(ret), "Returned Value"); 11157 } 11158 } 11159 } catch(Exception e) 11160 autoExceptionHandler(e); 11161 }, null, __traits(identifier, fn)); 11162 }; 11163 } 11164 } 11165 } 11166 11167 private template hasAnyRelevantAnnotations(a...) { 11168 bool helper() { 11169 bool any; 11170 foreach(attr; a) { 11171 static if(is(typeof(attr) == .menu)) 11172 any = true; 11173 else static if(is(typeof(attr) == .toolbar)) 11174 any = true; 11175 else static if(is(attr == .separator)) 11176 any = true; 11177 else static if(is(typeof(attr) == .accelerator)) 11178 any = true; 11179 else static if(is(typeof(attr) == .hotkey)) 11180 any = true; 11181 else static if(is(typeof(attr) == .icon)) 11182 any = true; 11183 else static if(is(typeof(attr) == .label)) 11184 any = true; 11185 else static if(is(typeof(attr) == .tip)) 11186 any = true; 11187 } 11188 return any; 11189 } 11190 11191 enum bool hasAnyRelevantAnnotations = helper(); 11192 } 11193 11194 /++ 11195 A `MainWindow` is a window that includes turnkey support for a menu bar, tool bar, and status bar automatically positioned around a client area where you put your widgets. 11196 +/ 11197 class MainWindow : Window { 11198 /// 11199 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 11200 super(initialWidth, initialHeight, title); 11201 11202 _clientArea = new ClientAreaWidget(); 11203 _clientArea.x = 0; 11204 _clientArea.y = 0; 11205 _clientArea.width = this.width; 11206 _clientArea.height = this.height; 11207 _clientArea.tabStop = false; 11208 11209 super.addChild(_clientArea); 11210 11211 statusBar = new StatusBar(this); 11212 } 11213 11214 /++ 11215 Adds a menu and toolbar from annotated functions. It uses the top-level annotations from this module, so it is better to put the commands in a separate struct instad of in your window subclass, to avoid potential conflicts with method names (if you do hit one though, you can use `@(.icon(...))` instead of plain `@icon(...)` to disambiguate, though). 11216 11217 The only required annotation on a function is `@menu("Label")` to make it appear, but there are several optional ones I'd recommend considering, including `@toolbar("group name")`, `@icon()`, `@accelerator("keyboard shortcut string")`, and `@hotkey('char')`. 11218 11219 You can also use `@separator` to put a separating line in the menu before the function. 11220 11221 Functions may have zero or one argument. If they have an argument, an automatic dialog box (see: [dialog]) will be created to request the data from the user before calling your function. Some types have special treatment, like [FileName], will invoke the file dialog, assuming open or save based on the name of your function. 11222 11223 Let's look at a complete example: 11224 11225 --- 11226 import arsd.minigui; 11227 11228 void main() { 11229 auto window = new MainWindow(); 11230 11231 // we can add widgets before or after setting the menu, either way is fine. 11232 // i'll do it before here so the local variables are available to the commands. 11233 11234 auto textEdit = new TextEdit(window); 11235 11236 // Remember, in D, you can define structs inside of functions 11237 // and those structs can access the function's local variables. 11238 // 11239 // Of course, you might also want to do this separately, and if you 11240 // do, make sure you keep a reference to the window as a struct data 11241 // member so you can refer to it in cases like this Exit function. 11242 struct Commands { 11243 // the & in the string indicates that the next letter is the hotkey 11244 // to access it from the keyboard (so here, alt+f will open the 11245 // file menu) 11246 @menu("&File") { 11247 @accelerator("Ctrl+N") 11248 @hotkey('n') 11249 @icon(GenericIcons.New) // add an icon to the action 11250 @toolbar("File") // adds it to a toolbar. 11251 // The toolbar name is never visible to the user, but is used to group icons. 11252 void New() { 11253 previousFileReferenced = null; 11254 textEdit.content = ""; 11255 } 11256 11257 @icon(GenericIcons.Open) 11258 @toolbar("File") 11259 @hotkey('s') 11260 @accelerator("Ctrl+O") 11261 void Open(FileName!() filename) { 11262 import std.file; 11263 textEdit.content = std.file.readText(filename); 11264 } 11265 11266 @icon(GenericIcons.Save) 11267 @toolbar("File") 11268 @accelerator("Ctrl+S") 11269 @hotkey('s') 11270 void Save() { 11271 // these are still functions, so of course you can 11272 // still call them yourself too 11273 Save_As(previousFileReferenced); 11274 } 11275 11276 // underscores translate to spaces in the visible name 11277 @hotkey('a') 11278 void Save_As(FileName!() filename) { 11279 import std.file; 11280 std.file.write(previousFileReferenced, textEdit.content); 11281 } 11282 11283 // you can put the annotations before or after the function name+args and it works the same way 11284 @separator 11285 void Exit() @accelerator("Alt+F4") @hotkey('x') { 11286 window.close(); 11287 } 11288 } 11289 11290 @menu("&Edit") { 11291 // not putting accelerators here because the text edit widget 11292 // does it locally, so no need to duplicate it globally. 11293 11294 @icon(GenericIcons.Undo) 11295 void Undo() @toolbar("Undo") { 11296 textEdit.undo(); 11297 } 11298 11299 @separator 11300 11301 @icon(GenericIcons.Cut) 11302 void Cut() @toolbar("Edit") { 11303 textEdit.cut(); 11304 } 11305 @icon(GenericIcons.Copy) 11306 void Copy() @toolbar("Edit") { 11307 textEdit.copy(); 11308 } 11309 @icon(GenericIcons.Paste) 11310 void Paste() @toolbar("Edit") { 11311 textEdit.paste(); 11312 } 11313 11314 @separator 11315 void Select_All() { 11316 textEdit.selectAll(); 11317 } 11318 } 11319 11320 @menu("Help") { 11321 void About() @accelerator("F1") { 11322 window.messageBox("A minigui sample program."); 11323 } 11324 11325 // @label changes the name in the menu from what is in the code 11326 @label("In Menu Name") 11327 void otherNameInCode() {} 11328 } 11329 } 11330 11331 // declare the object that holds the commands, and set 11332 // and members you want from it 11333 Commands commands; 11334 11335 // and now tell minigui to do its magic and create the ui for it! 11336 window.setMenuAndToolbarFromAnnotatedCode(commands); 11337 11338 // then, loop the window normally; 11339 window.loop(); 11340 11341 // important to note that the `commands` variable must live through the window's whole life cycle, 11342 // or you can have crashes. If you declare the variable and loop in different functions, make sure 11343 // you do `new Commands` so the garbage collector can take over management of it for you. 11344 } 11345 --- 11346 11347 Note that you can call this function multiple times and it will add the items in order to the given items. 11348 11349 +/ 11350 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 11351 setMenuAndToolbarFromAnnotatedCode_internal(t); 11352 } 11353 /// ditto 11354 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 11355 setMenuAndToolbarFromAnnotatedCode_internal(t); 11356 } 11357 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 11358 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 11359 Menu[string] mcs; 11360 11361 alias ToolbarSection = ToolBar.ToolbarSection; 11362 ToolbarSection[] toolbarSections; 11363 11364 foreach(menu; menuBar.subMenus) { 11365 mcs[menu.label] = menu; 11366 } 11367 11368 foreach(memberName; __traits(derivedMembers, T)) { 11369 static if(memberName != "this") 11370 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 11371 .menu menu; 11372 .toolbar toolbar; 11373 bool separator; 11374 .accelerator accelerator; 11375 .hotkey hotkey; 11376 .icon icon; 11377 string label; 11378 string tip; 11379 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 11380 static if(is(typeof(attr) == .menu)) 11381 menu = attr; 11382 else static if(is(typeof(attr) == .toolbar)) 11383 toolbar = attr; 11384 else static if(is(attr == .separator)) 11385 separator = true; 11386 else static if(is(typeof(attr) == .accelerator)) 11387 accelerator = attr; 11388 else static if(is(typeof(attr) == .hotkey)) 11389 hotkey = attr; 11390 else static if(is(typeof(attr) == .icon)) 11391 icon = attr; 11392 else static if(is(typeof(attr) == .label)) 11393 label = attr.label; 11394 else static if(is(typeof(attr) == .tip)) 11395 tip = attr.tip; 11396 } 11397 11398 if(menu !is .menu.init || toolbar !is .toolbar.init) { 11399 ushort correctIcon = icon.id; // FIXME 11400 if(label.length == 0) 11401 label = memberName.toMenuLabel; 11402 11403 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName)); 11404 11405 auto action = new Action(label, correctIcon, handler); 11406 11407 if(accelerator.keyString.length) { 11408 auto ke = KeyEvent.parse(accelerator.keyString); 11409 action.accelerator = ke; 11410 accelerators[ke.toStr] = handler; 11411 } 11412 11413 if(toolbar !is .toolbar.init) { 11414 bool found; 11415 foreach(ref section; toolbarSections) 11416 if(section.name == toolbar.groupName) { 11417 section.actions ~= action; 11418 found = true; 11419 break; 11420 } 11421 if(!found) { 11422 toolbarSections ~= ToolbarSection(toolbar.groupName, [action]); 11423 } 11424 } 11425 if(menu !is .menu.init) { 11426 Menu mc; 11427 if(menu.name in mcs) { 11428 mc = mcs[menu.name]; 11429 } else { 11430 mc = new Menu(menu.name, this); 11431 menuBar.addItem(mc); 11432 mcs[menu.name] = mc; 11433 } 11434 11435 if(separator) 11436 mc.addSeparator(); 11437 auto mi = mc.addItem(new MenuItem(action)); 11438 11439 if(hotkey !is .hotkey.init) 11440 mi.hotkey = hotkey.ch; 11441 } 11442 } 11443 } 11444 } 11445 11446 this.menuBar = menuBar; 11447 11448 if(toolbarSections.length) { 11449 auto tb = new ToolBar(toolbarSections, this); 11450 } 11451 } 11452 11453 void delegate()[string] accelerators; 11454 11455 override void defaultEventHandler_keydown(KeyDownEvent event) { 11456 auto str = event.originalKeyEvent.toStr; 11457 if(auto acl = str in accelerators) 11458 (*acl)(); 11459 11460 // Windows this this automatically so only on custom need we implement it 11461 version(custom_widgets) { 11462 if(event.altKey && this.menuBar) { 11463 foreach(item; this.menuBar.items) { 11464 if(item.hotkey == keyToLetterCharAssumingLotsOfThingsThatYouMightBetterNotAssume(event.key)) { 11465 // FIXME this kinda sucks but meh just pretending to click on it to trigger other existing mediocre code 11466 item.dynamicState = DynamicState.hover | DynamicState.depressed; 11467 item.redraw(); 11468 auto e = new MouseDownEvent(item); 11469 e.dispatch(); 11470 break; 11471 } 11472 } 11473 } 11474 11475 if(event.key == Key.Menu) { 11476 showContextMenu(-1, -1); 11477 } 11478 } 11479 11480 super.defaultEventHandler_keydown(event); 11481 } 11482 11483 override void defaultEventHandler_mouseover(MouseOverEvent event) { 11484 super.defaultEventHandler_mouseover(event); 11485 if(this.statusBar !is null && event.target.statusTip.length) 11486 this.statusBar.parts[0].content = event.target.statusTip; 11487 else if(this.statusBar !is null && this.statusTip.length) 11488 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 11489 } 11490 11491 override void addChild(Widget c, int position = int.max) { 11492 if(auto tb = cast(ToolBar) c) 11493 version(win32_widgets) 11494 super.addChild(c, 0); 11495 else version(custom_widgets) 11496 super.addChild(c, menuBar ? 1 : 0); 11497 else static assert(0); 11498 else 11499 clientArea.addChild(c, position); 11500 } 11501 11502 ToolBar _toolBar; 11503 /// 11504 ToolBar toolBar() { return _toolBar; } 11505 /// 11506 ToolBar toolBar(ToolBar t) { 11507 _toolBar = t; 11508 foreach(child; this.children) 11509 if(child is t) 11510 return t; 11511 version(win32_widgets) 11512 super.addChild(t, 0); 11513 else version(custom_widgets) 11514 super.addChild(t, menuBar ? 1 : 0); 11515 else static assert(0); 11516 return t; 11517 } 11518 11519 MenuBar _menu; 11520 /// 11521 MenuBar menuBar() { return _menu; } 11522 /// 11523 MenuBar menuBar(MenuBar m) { 11524 if(m is _menu) { 11525 version(custom_widgets) 11526 queueRecomputeChildLayout(); 11527 return m; 11528 } 11529 11530 if(_menu !is null) { 11531 // make sure it is sanely removed 11532 // FIXME 11533 } 11534 11535 _menu = m; 11536 11537 version(win32_widgets) { 11538 SetMenu(parentWindow.win.impl.hwnd, m.handle); 11539 } else version(custom_widgets) { 11540 super.addChild(m, 0); 11541 11542 // clientArea.y = menu.height; 11543 // clientArea.height = this.height - menu.height; 11544 11545 queueRecomputeChildLayout(); 11546 } else static assert(false); 11547 11548 return _menu; 11549 } 11550 private Widget _clientArea; 11551 /// 11552 @property Widget clientArea() { return _clientArea; } 11553 protected @property void clientArea(Widget wid) { 11554 _clientArea = wid; 11555 } 11556 11557 private StatusBar _statusBar; 11558 /++ 11559 Returns the window's [StatusBar]. Be warned it may be `null`. 11560 +/ 11561 @property StatusBar statusBar() { return _statusBar; } 11562 /// ditto 11563 @property void statusBar(StatusBar bar) { 11564 if(_statusBar !is null) 11565 _statusBar.removeWidget(); 11566 _statusBar = bar; 11567 if(bar !is null) 11568 super.addChild(_statusBar); 11569 } 11570 } 11571 11572 /+ 11573 This is really an implementation detail of [MainWindow] 11574 +/ 11575 private class ClientAreaWidget : Widget { 11576 this() { 11577 this.tabStop = false; 11578 super(null); 11579 //sa = new ScrollableWidget(this); 11580 } 11581 /* 11582 ScrollableWidget sa; 11583 override void addChild(Widget w, int position) { 11584 if(sa is null) 11585 super.addChild(w, position); 11586 else { 11587 sa.addChild(w, position); 11588 sa.setContentSize(this.minWidth + 1, this.minHeight); 11589 writeln(sa.contentWidth, "x", sa.contentHeight); 11590 } 11591 } 11592 */ 11593 } 11594 11595 /** 11596 Toolbars are lists of buttons (typically icons) that appear under the menu. 11597 Each button ought to correspond to a menu item, represented by [Action] objects. 11598 */ 11599 class ToolBar : Widget { 11600 version(win32_widgets) { 11601 private int idealHeight; 11602 override int minHeight() { return idealHeight; } 11603 override int maxHeight() { return idealHeight; } 11604 } else version(custom_widgets) { 11605 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 11606 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 11607 } else static assert(false); 11608 override int heightStretchiness() { return 0; } 11609 11610 static struct ToolbarSection { 11611 string name; 11612 Action[] actions; 11613 } 11614 11615 version(win32_widgets) { 11616 HIMAGELIST imageListSmall; 11617 HIMAGELIST imageListLarge; 11618 } 11619 11620 this(Widget parent) { 11621 this(cast(ToolbarSection[]) null, parent); 11622 } 11623 11624 version(win32_widgets) 11625 void changeIconSize(bool useLarge) { 11626 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall)); 11627 11628 /+ 11629 SIZE size; 11630 import core.sys.windows.commctrl; 11631 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 11632 idealHeight = size.cy + 4; // the plus 4 is a hack 11633 +/ 11634 11635 idealHeight = useLarge ? 34 : 26; 11636 11637 if(parent) { 11638 parent.queueRecomputeChildLayout(); 11639 parent.redraw(); 11640 } 11641 11642 SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4)); 11643 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 11644 } 11645 11646 /++ 11647 History: 11648 The `ToolbarSection` overload was added December 31, 2024 11649 +/ 11650 this(Action[] actions, Widget parent) { 11651 this([ToolbarSection(null, actions)], parent); 11652 } 11653 11654 /// ditto 11655 this(ToolbarSection[] sections, Widget parent) { 11656 super(parent); 11657 11658 tabStop = false; 11659 11660 version(win32_widgets) { 11661 // so i like how the flat thing looks on windows, but not on wine 11662 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 11663 // leave it commented 11664 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 11665 11666 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 11667 11668 imageListSmall = ImageList_Create( 11669 // width, height 11670 16, 16, 11671 ILC_COLOR16 | ILC_MASK, 11672 16 /*numberOfButtons*/, 0); 11673 11674 imageListLarge = ImageList_Create( 11675 // width, height 11676 24, 24, 11677 ILC_COLOR16 | ILC_MASK, 11678 16 /*numberOfButtons*/, 0); 11679 11680 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall); 11681 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 11682 11683 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge); 11684 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL); 11685 11686 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 11687 11688 TBBUTTON[] buttons; 11689 11690 // FIXME: I_IMAGENONE is if here is no icon 11691 foreach(sidx, section; sections) { 11692 if(sidx) 11693 buttons ~= TBBUTTON( 11694 scaleWithDpi(4), 11695 0, 11696 TBSTATE_ENABLED, // state 11697 TBSTYLE_SEP | BTNS_SEP, // style 11698 0, // reserved array, just zero it out 11699 0, // dwData 11700 -1 11701 ); 11702 11703 foreach(action; section.actions) 11704 buttons ~= TBBUTTON( 11705 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 11706 action.id, 11707 TBSTATE_ENABLED, // state 11708 0, // style 11709 0, // reserved array, just zero it out 11710 0, // dwData 11711 cast(size_t) toWstringzInternal(action.label) // INT_PTR 11712 ); 11713 } 11714 11715 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 11716 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 11717 11718 /* 11719 RECT rect; 11720 GetWindowRect(hwnd, &rect); 11721 idealHeight = rect.bottom - rect.top + 10; // the +10 is a hack since the size right now doesn't look right on a real Windows XP box 11722 */ 11723 11724 dpiChanged(); // to load the things calling changeIconSize the first time 11725 11726 assert(idealHeight); 11727 } else version(custom_widgets) { 11728 foreach(sidx, section; sections) { 11729 if(sidx) 11730 new HorizontalSpacer(4, this); 11731 foreach(action; section.actions) 11732 new ToolButton(action, this); 11733 } 11734 } else static assert(false); 11735 } 11736 11737 override void recomputeChildLayout() { 11738 .recomputeChildLayout!"width"(this); 11739 } 11740 11741 11742 version(win32_widgets) 11743 override protected void dpiChanged() { 11744 auto sz = scaleWithDpi(16); 11745 if(sz >= 20) 11746 changeIconSize(true); 11747 else 11748 changeIconSize(false); 11749 } 11750 } 11751 11752 /// An implementation helper for [ToolBar]. Generally, you shouldn't create these yourself and instead just pass [Action]s to [ToolBar]'s constructor and let it create the buttons for you. 11753 class ToolButton : Button { 11754 /// 11755 this(Action action, Widget parent) { 11756 super(action.label, parent); 11757 tabStop = false; 11758 this.action = action; 11759 } 11760 11761 version(custom_widgets) 11762 override void defaultEventHandler_click(ClickEvent event) { 11763 foreach(handler; action.triggered) 11764 handler(); 11765 } 11766 11767 Action action; 11768 11769 override int maxWidth() { return toolbarIconSize; } 11770 override int minWidth() { return toolbarIconSize; } 11771 override int maxHeight() { return toolbarIconSize; } 11772 override int minHeight() { return toolbarIconSize; } 11773 11774 version(custom_widgets) 11775 override void paint(WidgetPainter painter) { 11776 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 11777 painter.outlineColor = Color.black; 11778 11779 immutable multiplier = toolbarIconSize / 4; 11780 immutable divisor = 16 / 4; 11781 11782 int ScaledNumber(int n) { 11783 // return n * multiplier / divisor; 11784 auto s = n * multiplier; 11785 auto it = s / divisor; 11786 auto rem = s % divisor; 11787 if(rem && n >= 8) // cuz the original used 0 .. 16 and we want to try to stay centered so things in the bottom half tend to be added a it 11788 it++; 11789 return it; 11790 } 11791 11792 arsd.color.Point Point(int x, int y) { 11793 return arsd.color.Point(ScaledNumber(x), ScaledNumber(y)); 11794 } 11795 11796 switch(action.iconId) { 11797 case GenericIcons.New: 11798 painter.fillColor = Color.white; 11799 painter.drawPolygon( 11800 Point(3, 2), Point(3, 13), Point(12, 13), Point(12, 6), 11801 Point(8, 2), Point(8, 6), Point(12, 6), Point(8, 2), 11802 Point(3, 2), Point(3, 13) 11803 ); 11804 break; 11805 case GenericIcons.Save: 11806 painter.fillColor = Color.white; 11807 painter.outlineColor = Color.black; 11808 painter.drawRectangle(Point(2, 2), Point(13, 13)); 11809 11810 // the label 11811 painter.drawRectangle(Point(4, 8), Point(11, 13)); 11812 11813 // the slider 11814 painter.fillColor = Color.black; 11815 painter.outlineColor = Color.black; 11816 painter.drawRectangle(Point(4, 3), Point(10, 6)); 11817 11818 painter.fillColor = Color.white; 11819 painter.outlineColor = Color.white; 11820 // the disc window 11821 painter.drawRectangle(Point(5, 3), Point(6, 5)); 11822 break; 11823 case GenericIcons.Open: 11824 painter.fillColor = Color.white; 11825 painter.drawPolygon( 11826 Point(4, 4), Point(4, 12), Point(13, 12), Point(13, 3), 11827 Point(9, 3), Point(9, 4), Point(4, 4)); 11828 painter.drawPolygon( 11829 Point(2, 6), Point(11, 6), 11830 Point(12, 12), Point(4, 12), 11831 Point(2, 6)); 11832 //painter.drawLine(Point(9, 6), Point(13, 7)); 11833 break; 11834 case GenericIcons.Copy: 11835 painter.fillColor = Color.white; 11836 painter.drawRectangle(Point(3, 2), Point(9, 10)); 11837 painter.drawRectangle(Point(6, 5), Point(12, 13)); 11838 break; 11839 case GenericIcons.Cut: 11840 painter.fillColor = Color.transparent; 11841 painter.outlineColor = getComputedStyle.foregroundColor(); 11842 painter.drawLine(Point(3, 2), Point(10, 9)); 11843 painter.drawLine(Point(4, 9), Point(11, 2)); 11844 painter.drawRectangle(Point(3, 9), Point(5, 13)); 11845 painter.drawRectangle(Point(9, 9), Point(11, 12)); 11846 break; 11847 case GenericIcons.Paste: 11848 painter.fillColor = Color.white; 11849 painter.drawRectangle(Point(2, 3), Point(11, 11)); 11850 painter.drawRectangle(Point(6, 8), Point(13, 13)); 11851 painter.drawLine(Point(6, 2), Point(4, 5)); 11852 painter.drawLine(Point(6, 2), Point(9, 5)); 11853 painter.fillColor = Color.black; 11854 painter.drawRectangle(Point(4, 5), Point(9, 6)); 11855 break; 11856 case GenericIcons.Help: 11857 painter.outlineColor = getComputedStyle.foregroundColor(); 11858 painter.drawText(arsd.color.Point(0, 0), "?", arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 11859 break; 11860 case GenericIcons.Undo: 11861 painter.fillColor = Color.transparent; 11862 painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64); 11863 painter.outlineColor = Color.black; 11864 painter.fillColor = Color.black; 11865 painter.drawPolygon( 11866 Point(4, 4), 11867 Point(8, 2), 11868 Point(8, 6), 11869 Point(4, 4), 11870 ); 11871 break; 11872 case GenericIcons.Redo: 11873 painter.fillColor = Color.transparent; 11874 painter.drawArc(Point(3, 4), ScaledNumber(9), ScaledNumber(9), 0, 360 * 64); 11875 painter.outlineColor = Color.black; 11876 painter.fillColor = Color.black; 11877 painter.drawPolygon( 11878 Point(10, 4), 11879 Point(6, 2), 11880 Point(6, 6), 11881 Point(10, 4), 11882 ); 11883 break; 11884 default: 11885 painter.outlineColor = getComputedStyle.foregroundColor; 11886 painter.drawText(arsd.color.Point(0, 0), action.label, arsd.color.Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 11887 } 11888 return bounds; 11889 }); 11890 } 11891 11892 } 11893 11894 11895 /++ 11896 You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode]. 11897 +/ 11898 class MenuBar : Widget { 11899 MenuItem[] items; 11900 Menu[] subMenus; 11901 11902 version(win32_widgets) { 11903 HMENU handle; 11904 /// 11905 this(Widget parent = null) { 11906 super(parent); 11907 11908 handle = CreateMenu(); 11909 tabStop = false; 11910 } 11911 } else version(custom_widgets) { 11912 /// 11913 this(Widget parent = null) { 11914 tabStop = false; // these are selected some other way 11915 super(parent); 11916 } 11917 11918 mixin Padding!q{2}; 11919 } else static assert(false); 11920 11921 version(custom_widgets) 11922 override void paint(WidgetPainter painter) { 11923 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 11924 } 11925 11926 /// 11927 MenuItem addItem(MenuItem item) { 11928 this.addChild(item); 11929 items ~= item; 11930 version(win32_widgets) { 11931 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 11932 } 11933 return item; 11934 } 11935 11936 11937 /// 11938 Menu addItem(Menu item) { 11939 11940 subMenus ~= item; 11941 11942 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 11943 11944 addChild(mbItem); 11945 items ~= mbItem; 11946 11947 version(win32_widgets) { 11948 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 11949 } else version(custom_widgets) { 11950 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 11951 item.popup(mbItem); 11952 }; 11953 } else static assert(false); 11954 11955 return item; 11956 } 11957 11958 override void recomputeChildLayout() { 11959 .recomputeChildLayout!"width"(this); 11960 } 11961 11962 override int maxHeight() { return defaultLineHeight + 4; } 11963 override int minHeight() { return defaultLineHeight + 4; } 11964 } 11965 11966 11967 /** 11968 Status bars appear at the bottom of a MainWindow. 11969 They are made out of Parts, with a width and content. 11970 11971 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 11972 11973 11974 sb.parts[0].content = "Status bar text!"; 11975 */ 11976 // https://learn.microsoft.com/en-us/windows/win32/controls/status-bars#owner-drawn-status-bars 11977 class StatusBar : Widget { 11978 private Part[] partsArray; 11979 /// 11980 struct Parts { 11981 @disable this(); 11982 this(StatusBar owner) { this.owner = owner; } 11983 //@disable this(this); 11984 /// 11985 @property int length() { return cast(int) owner.partsArray.length; } 11986 private StatusBar owner; 11987 private this(StatusBar owner, Part[] parts) { 11988 this.owner.partsArray = parts; 11989 this.owner = owner; 11990 } 11991 /// 11992 Part opIndex(int p) { 11993 if(owner.partsArray.length == 0) 11994 this ~= new StatusBar.Part(0); 11995 return owner.partsArray[p]; 11996 } 11997 11998 /// 11999 Part opOpAssign(string op : "~" )(Part p) { 12000 assert(owner.partsArray.length < 255); 12001 p.owner = this.owner; 12002 p.idx = cast(int) owner.partsArray.length; 12003 owner.partsArray ~= p; 12004 12005 owner.queueRecomputeChildLayout(); 12006 12007 version(win32_widgets) { 12008 int[256] pos; 12009 int cpos; 12010 foreach(idx, part; owner.partsArray) { 12011 if(idx + 1 == owner.partsArray.length) 12012 pos[idx] = -1; 12013 else { 12014 cpos += part.currentlyAssignedWidth; 12015 pos[idx] = cpos; 12016 } 12017 } 12018 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 12019 } else version(custom_widgets) { 12020 owner.redraw(); 12021 } else static assert(false); 12022 12023 return p; 12024 } 12025 12026 /++ 12027 Sets up proportional parts in one function call. You can use negative numbers to indicate device-independent pixels, and positive numbers to indicate proportions. 12028 12029 No given item should be 0. 12030 12031 History: 12032 Added December 31, 2024 12033 +/ 12034 void setSizes(int[] proportions...) { 12035 assert(this.owner); 12036 this.owner.partsArray = null; 12037 12038 foreach(n; proportions) { 12039 assert(n, "do not give 0 to statusBar.parts.set, it would make an invisible part. Try 1 instead."); 12040 12041 this.opOpAssign!"~"(new StatusBar.Part(n > 0 ? n : -n, n > 0 ? StatusBar.Part.WidthUnits.Proportional : StatusBar.Part.WidthUnits.DeviceIndependentPixels)); 12042 } 12043 12044 } 12045 } 12046 12047 private Parts _parts; 12048 /// 12049 final @property Parts parts() { 12050 return _parts; 12051 } 12052 12053 /++ 12054 12055 +/ 12056 static class Part { 12057 /++ 12058 History: 12059 Added September 1, 2023 (dub v11.1) 12060 +/ 12061 enum WidthUnits { 12062 /++ 12063 Unscaled pixels as they appear on screen. 12064 12065 If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui. 12066 +/ 12067 DeviceDependentPixels, 12068 /++ 12069 Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui. 12070 +/ 12071 DeviceIndependentPixels, 12072 /++ 12073 An approximate character count in the currently selected font (at layout time) of the status bar. This will use the x-width (similar to css `ch`). 12074 +/ 12075 ApproximateCharacters, 12076 /++ 12077 These take a proportion of the remaining space in the window after all other parts have been assigned. The sum of all proportional parts is then divided by the current item to get the amount of space it uses. 12078 12079 If you pass 0, it will assume that this item takes an average of all remaining proportional space. This is there primarily to provide compatibility with code written against older versions of minigui. 12080 +/ 12081 Proportional 12082 } 12083 private WidthUnits units; 12084 private int width; 12085 private StatusBar owner; 12086 12087 private int currentlyAssignedWidth; 12088 12089 /++ 12090 History: 12091 Prior to September 1, 2023, this took a default value of 100 and was interpreted as pixels, unless the value was 0 and it was the last item in the list, in which case it would use the remaining space in the window. 12092 12093 It now allows you to provide your own value for [WidthUnits]. 12094 12095 Additionally, the default value used to be an arbitrary value of 100. It is now 0, to take advantage of the automatic proportional calculator in the new version. If you want the old behavior, pass `100, StatusBar.Part.WidthUnits.DeviceIndependentPixels`. 12096 +/ 12097 this(int w, WidthUnits units = WidthUnits.Proportional) { 12098 this.units = units; 12099 this.width = w; 12100 } 12101 12102 /// ditto 12103 this(int w = 0) { 12104 if(w == 0) 12105 this(w, WidthUnits.Proportional); 12106 else 12107 this(w, WidthUnits.DeviceDependentPixels); 12108 } 12109 12110 private int idx; 12111 private string _content; 12112 /// 12113 @property string content() { return _content; } 12114 /// 12115 @property void content(string s) { 12116 version(win32_widgets) { 12117 _content = s; 12118 WCharzBuffer bfr = WCharzBuffer(s); 12119 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 12120 } else version(custom_widgets) { 12121 if(_content != s) { 12122 _content = s; 12123 owner.redraw(); 12124 } 12125 } else static assert(false); 12126 } 12127 } 12128 string simpleModeContent; 12129 bool inSimpleMode; 12130 12131 12132 /// 12133 this(Widget parent) { 12134 super(null); // FIXME 12135 _parts = Parts(this); 12136 tabStop = false; 12137 version(win32_widgets) { 12138 parentWindow = parent.parentWindow; 12139 createWin32Window(this, "msctls_statusbar32"w, "", 0); 12140 12141 RECT rect; 12142 GetWindowRect(hwnd, &rect); 12143 idealHeight = rect.bottom - rect.top; 12144 assert(idealHeight); 12145 } else version(custom_widgets) { 12146 } else static assert(false); 12147 } 12148 12149 override void recomputeChildLayout() { 12150 int remainingLength = this.width; 12151 12152 int proportionalSum; 12153 int proportionalCount; 12154 foreach(idx, part; this.partsArray) { 12155 with(Part.WidthUnits) 12156 final switch(part.units) { 12157 case DeviceDependentPixels: 12158 part.currentlyAssignedWidth = part.width; 12159 remainingLength -= part.currentlyAssignedWidth; 12160 break; 12161 case DeviceIndependentPixels: 12162 part.currentlyAssignedWidth = scaleWithDpi(part.width); 12163 remainingLength -= part.currentlyAssignedWidth; 12164 break; 12165 case ApproximateCharacters: 12166 auto cs = getComputedStyle(); 12167 auto font = cs.font; 12168 12169 part.currentlyAssignedWidth = font.averageWidth * this.width; 12170 remainingLength -= part.currentlyAssignedWidth; 12171 break; 12172 case Proportional: 12173 proportionalSum += part.width; 12174 proportionalCount ++; 12175 break; 12176 } 12177 } 12178 12179 foreach(part; this.partsArray) { 12180 if(part.units == Part.WidthUnits.Proportional) { 12181 auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width; 12182 if(proportion == 0) 12183 proportion = 1; 12184 12185 if(proportionalSum == 0) 12186 proportionalSum = proportionalCount; 12187 12188 part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum; 12189 } 12190 } 12191 12192 super.recomputeChildLayout(); 12193 } 12194 12195 version(win32_widgets) 12196 override protected void dpiChanged() { 12197 RECT rect; 12198 GetWindowRect(hwnd, &rect); 12199 idealHeight = rect.bottom - rect.top; 12200 assert(idealHeight); 12201 } 12202 12203 version(custom_widgets) 12204 override void paint(WidgetPainter painter) { 12205 auto cs = getComputedStyle(); 12206 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 12207 int cpos = 0; 12208 foreach(idx, part; this.partsArray) { 12209 auto partWidth = part.currentlyAssignedWidth; 12210 // part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 12211 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 12212 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 12213 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 12214 12215 painter.outlineColor = cs.foregroundColor(); 12216 painter.fillColor = cs.foregroundColor(); 12217 12218 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 12219 cpos += partWidth; 12220 } 12221 } 12222 12223 12224 version(win32_widgets) { 12225 private int idealHeight; 12226 override int maxHeight() { return idealHeight; } 12227 override int minHeight() { return idealHeight; } 12228 } else version(custom_widgets) { 12229 override int maxHeight() { return defaultLineHeight + 4; } 12230 override int minHeight() { return defaultLineHeight + 4; } 12231 } else static assert(false); 12232 } 12233 12234 /// Displays an in-progress indicator without known values 12235 version(none) 12236 class IndefiniteProgressBar : Widget { 12237 version(win32_widgets) 12238 this(Widget parent) { 12239 super(parent); 12240 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 12241 tabStop = false; 12242 } 12243 override int minHeight() { return 10; } 12244 } 12245 12246 /// A progress bar with a known endpoint and completion amount 12247 class ProgressBar : Widget { 12248 /++ 12249 History: 12250 Added March 16, 2022 (dub v10.7) 12251 +/ 12252 this(int min, int max, Widget parent) { 12253 this(parent); 12254 setRange(cast(ushort) min, cast(ushort) max); // FIXME 12255 } 12256 this(Widget parent) { 12257 version(win32_widgets) { 12258 super(parent); 12259 createWin32Window(this, "msctls_progress32"w, "", 0); 12260 tabStop = false; 12261 } else version(custom_widgets) { 12262 super(parent); 12263 max = 100; 12264 step = 10; 12265 tabStop = false; 12266 } else static assert(0); 12267 } 12268 12269 version(custom_widgets) 12270 override void paint(WidgetPainter painter) { 12271 auto cs = getComputedStyle(); 12272 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 12273 painter.fillColor = cs.progressBarColor; 12274 painter.drawRectangle(Point(0, 0), width * current / max, height); 12275 } 12276 12277 12278 version(custom_widgets) { 12279 int current; 12280 int max; 12281 int step; 12282 } 12283 12284 /// 12285 void advanceOneStep() { 12286 version(win32_widgets) 12287 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 12288 else version(custom_widgets) 12289 addToPosition(step); 12290 else static assert(false); 12291 } 12292 12293 /// 12294 void setStepIncrement(int increment) { 12295 version(win32_widgets) 12296 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 12297 else version(custom_widgets) 12298 step = increment; 12299 else static assert(false); 12300 } 12301 12302 /// 12303 void addToPosition(int amount) { 12304 version(win32_widgets) 12305 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 12306 else version(custom_widgets) 12307 setPosition(current + amount); 12308 else static assert(false); 12309 } 12310 12311 /// 12312 void setPosition(int pos) { 12313 version(win32_widgets) 12314 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 12315 else version(custom_widgets) { 12316 current = pos; 12317 if(current > max) 12318 current = max; 12319 redraw(); 12320 } 12321 else static assert(false); 12322 } 12323 12324 /// 12325 void setRange(ushort min, ushort max) { 12326 version(win32_widgets) 12327 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 12328 else version(custom_widgets) { 12329 this.max = max; 12330 } 12331 else static assert(false); 12332 } 12333 12334 override int minHeight() { return 10; } 12335 } 12336 12337 version(custom_widgets) 12338 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 12339 thisLabel.reserve(label.length); 12340 bool justSawAmpersand; 12341 foreach(ch; label) { 12342 if(justSawAmpersand) { 12343 justSawAmpersand = false; 12344 if(ch == '&') { 12345 goto plain; 12346 } 12347 thisAccelerator = ch; 12348 } else { 12349 if(ch == '&') { 12350 justSawAmpersand = true; 12351 continue; 12352 } 12353 plain: 12354 thisLabel ~= ch; 12355 } 12356 } 12357 } 12358 12359 /++ 12360 Creates the fieldset (also known as a group box) with the given label. A fieldset is generally used a container for mutually exclusive [Radiobox]s. 12361 12362 12363 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 12364 12365 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 12366 12367 History: 12368 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 12369 +/ 12370 class Fieldset : Widget { 12371 // FIXME: on Windows,it doesn't draw the background on the label 12372 // on X, it doesn't fix the clipping rectangle for it 12373 version(win32_widgets) 12374 override int paddingTop() { return defaultLineHeight; } 12375 else version(custom_widgets) 12376 override int paddingTop() { return defaultLineHeight + 2; } 12377 else static assert(false); 12378 override int paddingBottom() { return 6; } 12379 override int paddingLeft() { return 6; } 12380 override int paddingRight() { return 6; } 12381 12382 override int marginLeft() { return 6; } 12383 override int marginRight() { return 6; } 12384 override int marginTop() { return 2; } 12385 override int marginBottom() { return 2; } 12386 12387 string legend; 12388 12389 version(custom_widgets) private dchar accelerator; 12390 12391 this(string legend, Widget parent) { 12392 version(win32_widgets) { 12393 super(parent); 12394 this.legend = legend; 12395 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 12396 tabStop = false; 12397 } else version(custom_widgets) { 12398 super(parent); 12399 tabStop = false; 12400 12401 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 12402 } else static assert(0); 12403 } 12404 12405 version(custom_widgets) 12406 override void paint(WidgetPainter painter) { 12407 auto dlh = defaultLineHeight; 12408 12409 painter.fillColor = Color.transparent; 12410 auto cs = getComputedStyle(); 12411 painter.pen = Pen(cs.foregroundColor, 1); 12412 painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2); 12413 12414 auto tx = painter.textSize(legend); 12415 painter.outlineColor = Color.transparent; 12416 12417 version(Windows) { 12418 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 12419 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 12420 SelectObject(painter.impl.hdc, b); 12421 } else static if(UsingSimpledisplayX11) { 12422 painter.fillColor = getComputedStyle().windowBackgroundColor; 12423 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 12424 } 12425 painter.outlineColor = cs.foregroundColor; 12426 painter.drawText(Point(8, 0), legend); 12427 } 12428 12429 override int maxHeight() { 12430 auto m = paddingTop() + paddingBottom(); 12431 foreach(child; children) { 12432 auto mh = child.maxHeight(); 12433 if(mh == int.max) 12434 return int.max; 12435 m += mh; 12436 m += child.marginBottom(); 12437 m += child.marginTop(); 12438 } 12439 m += 6; 12440 if(m < minHeight) 12441 return minHeight; 12442 return m; 12443 } 12444 12445 override int minHeight() { 12446 auto m = paddingTop() + paddingBottom(); 12447 foreach(child; children) { 12448 m += child.minHeight(); 12449 m += child.marginBottom(); 12450 m += child.marginTop(); 12451 } 12452 return m + 6; 12453 } 12454 12455 override int minWidth() { 12456 return 6 + cast(int) this.legend.length * 7; 12457 } 12458 } 12459 12460 /++ 12461 $(IMG //arsdnet.net/minigui-screenshots/windows/Fieldset.png, A box saying "baby will" with three round buttons inside it for the options of "eat", "cry", and "sleep") 12462 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 12463 +/ 12464 version(minigui_screenshots) 12465 @Screenshot("Fieldset") 12466 unittest { 12467 auto window = new Window(200, 100); 12468 auto set = new Fieldset("Baby will", window); 12469 auto option1 = new Radiobox("Eat", set); 12470 auto option2 = new Radiobox("Cry", set); 12471 auto option3 = new Radiobox("Sleep", set); 12472 window.loop(); 12473 } 12474 12475 /// Draws a line 12476 class HorizontalRule : Widget { 12477 mixin Margin!q{ 2 }; 12478 override int minHeight() { return 2; } 12479 override int maxHeight() { return 2; } 12480 12481 /// 12482 this(Widget parent) { 12483 super(parent); 12484 } 12485 12486 override void paint(WidgetPainter painter) { 12487 auto cs = getComputedStyle(); 12488 painter.outlineColor = cs.darkAccentColor; 12489 painter.drawLine(Point(0, 0), Point(width, 0)); 12490 painter.outlineColor = cs.lightAccentColor; 12491 painter.drawLine(Point(0, 1), Point(width, 1)); 12492 } 12493 } 12494 12495 version(minigui_screenshots) 12496 @Screenshot("HorizontalRule") 12497 /++ 12498 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 12499 12500 +/ 12501 unittest { 12502 auto window = new Window(200, 100); 12503 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 12504 new HorizontalRule(window); 12505 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 12506 window.loop(); 12507 } 12508 12509 /// ditto 12510 class VerticalRule : Widget { 12511 mixin Margin!q{ 2 }; 12512 override int minWidth() { return 2; } 12513 override int maxWidth() { return 2; } 12514 12515 /// 12516 this(Widget parent) { 12517 super(parent); 12518 } 12519 12520 override void paint(WidgetPainter painter) { 12521 auto cs = getComputedStyle(); 12522 painter.outlineColor = cs.darkAccentColor; 12523 painter.drawLine(Point(0, 0), Point(0, height)); 12524 painter.outlineColor = cs.lightAccentColor; 12525 painter.drawLine(Point(1, 0), Point(1, height)); 12526 } 12527 } 12528 12529 12530 /// 12531 class Menu : Window { 12532 void remove() { 12533 foreach(i, child; parentWindow.children) 12534 if(child is this) { 12535 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 12536 break; 12537 } 12538 parentWindow.redraw(); 12539 12540 parentWindow.releaseMouseCapture(); 12541 } 12542 12543 /// 12544 void addSeparator() { 12545 version(win32_widgets) 12546 AppendMenu(handle, MF_SEPARATOR, 0, null); 12547 else version(custom_widgets) 12548 auto hr = new HorizontalRule(this); 12549 else static assert(0); 12550 } 12551 12552 override int paddingTop() { return 4; } 12553 override int paddingBottom() { return 4; } 12554 override int paddingLeft() { return 2; } 12555 override int paddingRight() { return 2; } 12556 12557 version(win32_widgets) {} 12558 else version(custom_widgets) { 12559 12560 Widget previouslyFocusedWidget; 12561 Widget* previouslyFocusedWidgetBelongsIn; 12562 12563 SimpleWindow dropDown; 12564 Widget menuParent; 12565 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 12566 this.menuParent = parent; 12567 12568 previouslyFocusedWidget = parent.parentWindow.focusedWidget; 12569 previouslyFocusedWidgetBelongsIn = &parent.parentWindow.focusedWidget; 12570 parent.parentWindow.focusedWidget = this; 12571 12572 int w = 150; 12573 int h = paddingTop + paddingBottom; 12574 if(this.children.length) { 12575 // hacking it to get the ideal height out of recomputeChildLayout 12576 this.width = w; 12577 this.height = h; 12578 this.recomputeChildLayoutEntry(); 12579 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 12580 h += paddingBottom; 12581 12582 h -= 2; // total hack, i just like the way it looks a bit tighter even though technically MenuItem reserves some space to center in normal circumstances 12583 } 12584 12585 if(offsetY == int.min) 12586 offsetY = parent.defaultLineHeight; 12587 12588 auto coord = parent.globalCoordinates(); 12589 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 12590 this.x = 0; 12591 this.y = 0; 12592 this.width = dropDown.width; 12593 this.height = dropDown.height; 12594 this.drawableWindow = dropDown; 12595 this.recomputeChildLayoutEntry(); 12596 12597 static if(UsingSimpledisplayX11) 12598 XSync(XDisplayConnection.get, 0); 12599 12600 dropDown.visibilityChanged = (bool visible) { 12601 if(visible) { 12602 this.redraw(); 12603 dropDown.grabInput(); 12604 } else { 12605 dropDown.releaseInputGrab(); 12606 } 12607 }; 12608 12609 dropDown.show(); 12610 12611 clickListener = this.addEventListener((scope ClickEvent ev) { 12612 unpopup(); 12613 // need to unlock asap just in case other user handlers block... 12614 static if(UsingSimpledisplayX11) 12615 flushGui(); 12616 }, true /* again for asap action */); 12617 } 12618 12619 EventListener clickListener; 12620 } 12621 else static assert(false); 12622 12623 version(custom_widgets) 12624 void unpopup() { 12625 mouseLastOver = mouseLastDownOn = null; 12626 dropDown.hide(); 12627 if(!menuParent.parentWindow.win.closed) { 12628 if(auto maw = cast(MouseActivatedWidget) menuParent) { 12629 maw.setDynamicState(DynamicState.depressed, false); 12630 maw.setDynamicState(DynamicState.hover, false); 12631 maw.redraw(); 12632 } 12633 // menuParent.parentWindow.win.focus(); 12634 } 12635 clickListener.disconnect(); 12636 12637 if(previouslyFocusedWidgetBelongsIn) 12638 *previouslyFocusedWidgetBelongsIn = previouslyFocusedWidget; 12639 } 12640 12641 MenuItem[] items; 12642 12643 /// 12644 MenuItem addItem(MenuItem item) { 12645 addChild(item); 12646 items ~= item; 12647 version(win32_widgets) { 12648 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 12649 } 12650 return item; 12651 } 12652 12653 string label; 12654 12655 version(win32_widgets) { 12656 HMENU handle; 12657 /// 12658 this(string label, Widget parent) { 12659 // not actually passing the parent since it effs up the drawing 12660 super(cast(Widget) null);// parent); 12661 this.label = label; 12662 handle = CreatePopupMenu(); 12663 } 12664 } else version(custom_widgets) { 12665 /// 12666 this(string label, Widget parent) { 12667 12668 if(dropDown) { 12669 dropDown.close(); 12670 } 12671 dropDown = new SimpleWindow( 12672 150, 4, 12673 // FIXME: what if it is a popupMenu ? 12674 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 12675 12676 this.label = label; 12677 12678 super(dropDown); 12679 } 12680 } else static assert(false); 12681 12682 override int maxHeight() { return defaultLineHeight; } 12683 override int minHeight() { return defaultLineHeight; } 12684 12685 version(custom_widgets) { 12686 Widget currentPlace; 12687 12688 void changeCurrentPlace(Widget n) { 12689 if(currentPlace) { 12690 currentPlace.dynamicState = 0; 12691 } 12692 12693 if(n) { 12694 n.dynamicState = DynamicState.hover; 12695 } 12696 12697 currentPlace = n; 12698 } 12699 12700 override void paint(WidgetPainter painter) { 12701 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 12702 } 12703 12704 override void defaultEventHandler_keydown(KeyDownEvent ke) { 12705 switch(ke.key) { 12706 case Key.Down: 12707 Widget next; 12708 Widget first; 12709 foreach(w; this.children) { 12710 if((cast(MenuItem) w) is null) 12711 continue; 12712 12713 if(first is null) 12714 first = w; 12715 12716 if(next !is null) { 12717 next = w; 12718 break; 12719 } 12720 12721 if(currentPlace is null) { 12722 next = w; 12723 break; 12724 } 12725 12726 if(w is currentPlace) { 12727 next = w; 12728 } 12729 } 12730 12731 if(next is currentPlace) 12732 next = first; 12733 12734 changeCurrentPlace(next); 12735 break; 12736 case Key.Up: 12737 Widget prev; 12738 foreach(w; this.children) { 12739 if((cast(MenuItem) w) is null) 12740 continue; 12741 if(w is currentPlace) { 12742 if(prev is null) { 12743 foreach_reverse(c; this.children) { 12744 if((cast(MenuItem) c) !is null) { 12745 prev = c; 12746 break; 12747 } 12748 } 12749 } 12750 break; 12751 } 12752 prev = w; 12753 } 12754 changeCurrentPlace(prev); 12755 break; 12756 case Key.Left: 12757 case Key.Right: 12758 if(menuParent) { 12759 Menu first; 12760 Menu last; 12761 Menu prev; 12762 Menu next; 12763 bool found; 12764 12765 size_t prev_idx; 12766 size_t next_idx; 12767 12768 MenuBar mb = cast(MenuBar) menuParent.parent; 12769 12770 if(mb) { 12771 foreach(idx, menu; mb.subMenus) { 12772 if(first is null) 12773 first = menu; 12774 last = menu; 12775 if(found && next is null) { 12776 next = menu; 12777 next_idx = idx; 12778 } 12779 if(menu is this) 12780 found = true; 12781 if(!found) { 12782 prev = menu; 12783 prev_idx = idx; 12784 } 12785 } 12786 12787 Menu nextMenu; 12788 size_t nextMenuIdx; 12789 if(ke.key == Key.Left) { 12790 nextMenu = prev ? prev : last; 12791 nextMenuIdx = prev ? prev_idx : mb.subMenus.length - 1; 12792 } else { 12793 nextMenu = next ? next : first; 12794 nextMenuIdx = next ? next_idx : 0; 12795 } 12796 12797 unpopup(); 12798 12799 auto rent = mb.children[nextMenuIdx]; // FIXME thsi is not necessarily right 12800 rent.dynamicState = DynamicState.depressed | DynamicState.hover; 12801 nextMenu.popup(rent); 12802 } 12803 } 12804 break; 12805 case Key.Enter: 12806 case Key.PadEnter: 12807 // because the key up and char events will go back to the other window after we unpopup! 12808 // we will wait for the char event to come (in the following method) 12809 break; 12810 case Key.Escape: 12811 unpopup(); 12812 break; 12813 default: 12814 } 12815 } 12816 override void defaultEventHandler_char(CharEvent ke) { 12817 // if one is selected, enter activates it 12818 if(currentPlace) { 12819 if(ke.character == '\n') { 12820 // enter selects 12821 auto event = new Event(EventType.triggered, currentPlace); 12822 event.dispatch(); 12823 unpopup(); 12824 return; 12825 } 12826 } 12827 12828 // otherwise search for a hotkey 12829 foreach(item; items) { 12830 if(item.hotkey == ke.character) { 12831 auto event = new Event(EventType.triggered, item); 12832 event.dispatch(); 12833 unpopup(); 12834 return; 12835 } 12836 } 12837 } 12838 override void defaultEventHandler_mouseover(MouseOverEvent moe) { 12839 if(moe.target && moe.target.parent is this) 12840 changeCurrentPlace(moe.target); 12841 } 12842 } 12843 } 12844 12845 /++ 12846 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 12847 +/ 12848 class MenuItem : MouseActivatedWidget { 12849 Menu submenu; 12850 12851 Action action; 12852 string label; 12853 dchar hotkey; 12854 12855 override int paddingLeft() { return 4; } 12856 12857 override int maxHeight() { return defaultLineHeight + 4; } 12858 override int minHeight() { return defaultLineHeight + 4; } 12859 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 12860 override int maxWidth() { 12861 if(cast(MenuBar) parent) { 12862 return minWidth(); 12863 } 12864 return int.max; 12865 } 12866 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 12867 this(string lbl, Widget parent = null) { 12868 super(parent); 12869 //label = lbl; // FIXME 12870 foreach(idx, char ch; lbl) // FIXME 12871 if(ch != '&') { // FIXME 12872 label ~= ch; // FIXME 12873 } else { 12874 if(idx + 1 < lbl.length) { 12875 hotkey = lbl[idx + 1]; 12876 if(hotkey >= 'A' && hotkey <= 'Z') 12877 hotkey += 32; 12878 } 12879 } 12880 tabStop = false; // these are selected some other way 12881 } 12882 12883 /// 12884 this(Action action, Widget parent = null) { 12885 assert(action !is null); 12886 this(action.label, parent); 12887 this.action = action; 12888 tabStop = false; // these are selected some other way 12889 } 12890 12891 version(custom_widgets) 12892 override void paint(WidgetPainter painter) { 12893 auto cs = getComputedStyle(); 12894 if(dynamicState & DynamicState.depressed) 12895 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 12896 else { 12897 if(dynamicState & DynamicState.hover) { 12898 painter.fillColor = cs.hoveringColor; 12899 painter.outlineColor = Color.transparent; 12900 } else { 12901 painter.fillColor = cs.background.color; 12902 painter.outlineColor = Color.transparent; 12903 } 12904 12905 painter.drawRectangle(Point(0, 0), Size(this.width, this.height)); 12906 } 12907 12908 if(dynamicState & DynamicState.hover) 12909 painter.outlineColor = cs.activeMenuItemColor; 12910 else 12911 painter.outlineColor = cs.foregroundColor; 12912 painter.fillColor = Color.transparent; 12913 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 12914 if(action && action.accelerator !is KeyEvent.init) { 12915 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 12916 12917 } 12918 } 12919 12920 static class Style : Widget.Style { 12921 override bool variesWithState(ulong dynamicStateFlags) { 12922 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 12923 } 12924 } 12925 mixin OverrideStyle!Style; 12926 12927 override void defaultEventHandler_triggered(Event event) { 12928 if(action) 12929 foreach(handler; action.triggered) 12930 handler(); 12931 12932 if(auto pmenu = cast(Menu) this.parent) 12933 pmenu.remove(); 12934 12935 super.defaultEventHandler_triggered(event); 12936 } 12937 } 12938 12939 version(win32_widgets) 12940 /// A "mouse activiated widget" is really just an abstract variant of button. 12941 class MouseActivatedWidget : Widget { 12942 @property bool isChecked() { 12943 assert(hwnd); 12944 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 12945 12946 } 12947 @property void isChecked(bool state) { 12948 assert(hwnd); 12949 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 12950 12951 } 12952 12953 override void handleWmCommand(ushort cmd, ushort id) { 12954 if(cmd == 0) { 12955 auto event = new Event(EventType.triggered, this); 12956 event.dispatch(); 12957 } 12958 } 12959 12960 this(Widget parent) { 12961 super(parent); 12962 } 12963 } 12964 else version(custom_widgets) 12965 /// ditto 12966 class MouseActivatedWidget : Widget { 12967 @property bool isChecked() { return isChecked_; } 12968 @property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;} 12969 12970 private bool isChecked_; 12971 12972 this(Widget parent) { 12973 super(parent); 12974 12975 addEventListener((MouseDownEvent ev) { 12976 if(ev.button == MouseButton.left) { 12977 setDynamicState(DynamicState.depressed, true); 12978 setDynamicState(DynamicState.hover, true); 12979 redraw(); 12980 } 12981 }); 12982 12983 addEventListener((MouseUpEvent ev) { 12984 if(ev.button == MouseButton.left) { 12985 setDynamicState(DynamicState.depressed, false); 12986 setDynamicState(DynamicState.hover, false); 12987 redraw(); 12988 } 12989 }); 12990 12991 addEventListener((MouseMoveEvent mme) { 12992 if(!(mme.state & ModifierState.leftButtonDown)) { 12993 if(dynamicState_ & DynamicState.depressed) { 12994 setDynamicState(DynamicState.depressed, false); 12995 redraw(); 12996 } 12997 } 12998 }); 12999 } 13000 13001 override void defaultEventHandler_focus(FocusEvent ev) { 13002 super.defaultEventHandler_focus(ev); 13003 this.redraw(); 13004 } 13005 override void defaultEventHandler_blur(BlurEvent ev) { 13006 super.defaultEventHandler_blur(ev); 13007 setDynamicState(DynamicState.depressed, false); 13008 this.redraw(); 13009 } 13010 override void defaultEventHandler_keydown(KeyDownEvent ev) { 13011 super.defaultEventHandler_keydown(ev); 13012 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 13013 setDynamicState(DynamicState.depressed, true); 13014 setDynamicState(DynamicState.hover, true); 13015 this.redraw(); 13016 } 13017 } 13018 override void defaultEventHandler_keyup(KeyUpEvent ev) { 13019 super.defaultEventHandler_keyup(ev); 13020 if(!(dynamicState & DynamicState.depressed)) 13021 return; 13022 setDynamicState(DynamicState.depressed, false); 13023 setDynamicState(DynamicState.hover, false); 13024 this.redraw(); 13025 13026 auto event = new Event(EventType.triggered, this); 13027 event.sendDirectly(); 13028 } 13029 override void defaultEventHandler_click(ClickEvent ev) { 13030 super.defaultEventHandler_click(ev); 13031 if(ev.button == MouseButton.left) { 13032 auto event = new Event(EventType.triggered, this); 13033 event.sendDirectly(); 13034 } 13035 } 13036 13037 } 13038 else static assert(false); 13039 13040 /* 13041 /++ 13042 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 13043 13044 Basically the same as a checkbox. 13045 +/ 13046 class OnOffSwitch : MouseActivatedWidget { 13047 13048 } 13049 */ 13050 13051 /++ 13052 History: 13053 Added June 15, 2021 (dub v10.1) 13054 +/ 13055 struct ImageLabel { 13056 /++ 13057 Defines a label+image combo used by some widgets. 13058 13059 If you provide just a text label, that is all the widget will try to 13060 display. Or just an image will display just that. If you provide both, 13061 it may display both text and image side by side or display the image 13062 and offer text on an input event depending on the widget. 13063 13064 History: 13065 The `alignment` parameter was added on September 27, 2021 13066 +/ 13067 this(string label, TextAlignment alignment = TextAlignment.Center) { 13068 this.label = label; 13069 this.displayFlags = DisplayFlags.displayText; 13070 this.alignment = alignment; 13071 } 13072 13073 /// ditto 13074 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 13075 this.label = label; 13076 this.image = image; 13077 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 13078 this.alignment = alignment; 13079 } 13080 13081 /// ditto 13082 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 13083 this.image = image; 13084 this.displayFlags = DisplayFlags.displayImage; 13085 this.alignment = alignment; 13086 } 13087 13088 /// ditto 13089 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 13090 this.label = label; 13091 this.image = image; 13092 this.alignment = alignment; 13093 this.displayFlags = displayFlags; 13094 } 13095 13096 string label; 13097 MemoryImage image; 13098 13099 enum DisplayFlags { 13100 displayText = 1 << 0, 13101 displayImage = 1 << 1, 13102 } 13103 13104 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 13105 13106 TextAlignment alignment; 13107 } 13108 13109 /++ 13110 A basic checked or not checked box with an attached label. 13111 13112 13113 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 13114 13115 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 13116 13117 History: 13118 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 13119 +/ 13120 class Checkbox : MouseActivatedWidget { 13121 version(win32_widgets) { 13122 override int maxHeight() { return scaleWithDpi(16); } 13123 override int minHeight() { return scaleWithDpi(16); } 13124 } else version(custom_widgets) { 13125 private enum buttonSize = 16; 13126 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 13127 override int minHeight() { return maxHeight(); } 13128 } else static assert(0); 13129 13130 override int marginLeft() { return 4; } 13131 13132 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 13133 13134 /++ 13135 Just an alias because I keep typing checked out of web habit. 13136 13137 History: 13138 Added May 31, 2021 13139 +/ 13140 alias checked = isChecked; 13141 13142 private string label; 13143 private dchar accelerator; 13144 13145 /++ 13146 +/ 13147 this(string label, Widget parent) { 13148 this(ImageLabel(label), Appearance.checkbox, parent); 13149 } 13150 13151 /// ditto 13152 this(string label, Appearance appearance, Widget parent) { 13153 this(ImageLabel(label), appearance, parent); 13154 } 13155 13156 /++ 13157 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 13158 13159 History: 13160 Added June 29, 2021 (dub v10.2) 13161 +/ 13162 enum Appearance { 13163 checkbox, /// a normal checkbox 13164 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 13165 //sliderswitch, 13166 } 13167 private Appearance appearance; 13168 13169 /// ditto 13170 private this(ImageLabel label, Appearance appearance, Widget parent) { 13171 super(parent); 13172 version(win32_widgets) { 13173 this.label = label.label; 13174 13175 uint extraStyle; 13176 final switch(appearance) { 13177 case Appearance.checkbox: 13178 break; 13179 case Appearance.pushbutton: 13180 extraStyle |= BS_PUSHLIKE; 13181 break; 13182 } 13183 13184 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 13185 } else version(custom_widgets) { 13186 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 13187 } else static assert(0); 13188 } 13189 13190 version(custom_widgets) 13191 override void paint(WidgetPainter painter) { 13192 auto cs = getComputedStyle(); 13193 if(isFocused()) { 13194 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 13195 painter.fillColor = cs.windowBackgroundColor; 13196 painter.drawRectangle(Point(0, 0), width, height); 13197 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 13198 } else { 13199 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 13200 painter.fillColor = cs.windowBackgroundColor; 13201 painter.drawRectangle(Point(0, 0), width, height); 13202 } 13203 13204 13205 painter.outlineColor = Color.black; 13206 painter.fillColor = Color.white; 13207 enum rectOffset = 2; 13208 painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset)); 13209 13210 if(isChecked) { 13211 auto size = scaleWithDpi(2); 13212 painter.pen = Pen(Color.black, size); 13213 // I'm using height so the checkbox is square 13214 enum padding = 3; 13215 painter.drawLine( 13216 scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)), 13217 scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2) 13218 ); 13219 painter.drawLine( 13220 scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0), 13221 scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 - size % 2) 13222 ); 13223 13224 painter.pen = Pen(Color.black, 1); 13225 } 13226 13227 if(label !is null) { 13228 painter.outlineColor = cs.foregroundColor(); 13229 painter.fillColor = cs.foregroundColor(); 13230 13231 // i want the centerline of the text to be aligned with the centerline of the checkbox 13232 /+ 13233 auto font = cs.font(); 13234 auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2; 13235 painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label); 13236 +/ 13237 painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter); 13238 } 13239 } 13240 13241 override void defaultEventHandler_triggered(Event ev) { 13242 isChecked = !isChecked; 13243 13244 this.emit!(ChangeEvent!bool)(&isChecked); 13245 13246 redraw(); 13247 } 13248 13249 /// Emits a change event with the checked state 13250 mixin Emits!(ChangeEvent!bool); 13251 } 13252 13253 /// Adds empty space to a layout. 13254 class VerticalSpacer : Widget { 13255 private int mh; 13256 13257 /++ 13258 History: 13259 The overload with `maxHeight` was added on December 31, 2024 13260 +/ 13261 this(Widget parent) { 13262 this(0, parent); 13263 } 13264 13265 /// ditto 13266 this(int maxHeight, Widget parent) { 13267 this.mh = maxHeight; 13268 super(parent); 13269 this.tabStop = false; 13270 } 13271 13272 override int maxHeight() { 13273 return mh ? scaleWithDpi(mh) : super.maxHeight(); 13274 } 13275 } 13276 13277 13278 /// ditto 13279 class HorizontalSpacer : Widget { 13280 private int mw; 13281 13282 /++ 13283 History: 13284 The overload with `maxWidth` was added on December 31, 2024 13285 +/ 13286 this(Widget parent) { 13287 this(0, parent); 13288 } 13289 13290 /// ditto 13291 this(int maxWidth, Widget parent) { 13292 this.mw = maxWidth; 13293 super(parent); 13294 this.tabStop = false; 13295 } 13296 13297 override int maxWidth() { 13298 return mw ? scaleWithDpi(mw) : super.maxWidth(); 13299 } 13300 } 13301 13302 13303 /++ 13304 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 13305 13306 13307 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 13308 13309 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 13310 13311 History: 13312 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 13313 +/ 13314 class Radiobox : MouseActivatedWidget { 13315 13316 version(win32_widgets) { 13317 override int maxHeight() { return scaleWithDpi(16); } 13318 override int minHeight() { return scaleWithDpi(16); } 13319 } else version(custom_widgets) { 13320 private enum buttonSize = 16; 13321 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 13322 override int minHeight() { return maxHeight(); } 13323 } else static assert(0); 13324 13325 override int marginLeft() { return 4; } 13326 13327 // FIXME: make a label getter 13328 private string label; 13329 private dchar accelerator; 13330 13331 /++ 13332 13333 +/ 13334 this(string label, Widget parent) { 13335 super(parent); 13336 version(win32_widgets) { 13337 this.label = label; 13338 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 13339 } else version(custom_widgets) { 13340 label.extractWindowsStyleLabel(this.label, this.accelerator); 13341 height = 16; 13342 width = height + 4 + cast(int) label.length * 16; 13343 } 13344 } 13345 13346 version(custom_widgets) 13347 override void paint(WidgetPainter painter) { 13348 auto cs = getComputedStyle(); 13349 13350 if(isFocused) { 13351 painter.fillColor = cs.windowBackgroundColor; 13352 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 13353 } else { 13354 painter.fillColor = cs.windowBackgroundColor; 13355 painter.outlineColor = cs.windowBackgroundColor; 13356 } 13357 painter.drawRectangle(Point(0, 0), width, height); 13358 13359 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 13360 13361 painter.outlineColor = Color.black; 13362 painter.fillColor = Color.white; 13363 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 13364 if(isChecked) { 13365 painter.outlineColor = Color.black; 13366 painter.fillColor = Color.black; 13367 // I'm using height so the checkbox is square 13368 auto size = scaleWithDpi(2); 13369 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2)); 13370 } 13371 13372 painter.outlineColor = cs.foregroundColor(); 13373 painter.fillColor = cs.foregroundColor(); 13374 13375 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 13376 } 13377 13378 13379 override void defaultEventHandler_triggered(Event ev) { 13380 isChecked = true; 13381 13382 if(this.parent) { 13383 foreach(child; this.parent.children) { 13384 if(child is this) continue; 13385 if(auto rb = cast(Radiobox) child) { 13386 rb.isChecked = false; 13387 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 13388 rb.redraw(); 13389 } 13390 } 13391 } 13392 13393 this.emit!(ChangeEvent!bool)(&this.isChecked); 13394 13395 redraw(); 13396 } 13397 13398 /// Emits a change event with if it is checked. Note that when you select one in a group, that one will emit changed with value == true, and the previous one will emit changed with value == false right before. A button group may catch this and change the event. 13399 mixin Emits!(ChangeEvent!bool); 13400 } 13401 13402 13403 /++ 13404 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 13405 13406 13407 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 13408 13409 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 13410 13411 History: 13412 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 13413 +/ 13414 class Button : MouseActivatedWidget { 13415 override int heightStretchiness() { return 3; } 13416 override int widthStretchiness() { return 3; } 13417 13418 /++ 13419 If true, this button will emit trigger events on double (and other quick events, if added) click events as well as on normal single click events. 13420 13421 History: 13422 Added July 2, 2021 13423 +/ 13424 public bool triggersOnMultiClick; 13425 13426 private string label_; 13427 private TextAlignment alignment; 13428 private dchar accelerator; 13429 13430 /// 13431 string label() { return label_; } 13432 /// 13433 void label(string l) { 13434 label_ = l; 13435 version(win32_widgets) { 13436 WCharzBuffer bfr = WCharzBuffer(l); 13437 SetWindowTextW(hwnd, bfr.ptr); 13438 } else version(custom_widgets) { 13439 redraw(); 13440 } 13441 } 13442 13443 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 13444 super.defaultEventHandler_dblclick(ev); 13445 if(triggersOnMultiClick) { 13446 if(ev.button == MouseButton.left) { 13447 auto event = new Event(EventType.triggered, this); 13448 event.sendDirectly(); 13449 } 13450 } 13451 } 13452 13453 private Sprite sprite; 13454 private int displayFlags; 13455 13456 protected bool needsOwnerDraw() { 13457 return &this.paint !is &Button.paint || &this.useStyleProperties !is &Button.useStyleProperties || &this.paintContent !is &Button.paintContent; 13458 } 13459 13460 version(win32_widgets) 13461 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) { 13462 auto itemId = dis.itemID; 13463 auto hdc = dis.hDC; 13464 auto rect = dis.rcItem; 13465 switch(dis.itemAction) { 13466 // skipping setDynamicState because i don't want to queue the redraw unnecessarily 13467 case ODA_SELECT: 13468 dynamicState_ &= ~DynamicState.depressed; 13469 if(dis.itemState & ODS_SELECTED) 13470 dynamicState_ |= DynamicState.depressed; 13471 goto case; 13472 case ODA_FOCUS: 13473 dynamicState_ &= ~DynamicState.focus; 13474 if(dis.itemState & ODS_FOCUS) 13475 dynamicState_ |= DynamicState.focus; 13476 goto case; 13477 case ODA_DRAWENTIRE: 13478 auto painter = WidgetPainter(this.simpleWindowWrappingHwnd.draw(true), this); 13479 //painter.impl.hdc = hdc; 13480 paint(painter); 13481 break; 13482 default: 13483 } 13484 return 1; 13485 13486 } 13487 13488 /++ 13489 Creates a push button with the given label, which may be an image or some text. 13490 13491 Bugs: 13492 If the image is bigger than the button, it may not be displayed in the right position on Linux. 13493 13494 History: 13495 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 13496 13497 The button with label and image will respect requests to show both on Windows as 13498 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 13499 +/ 13500 this(string label, Widget parent) { 13501 this(ImageLabel(label), parent); 13502 } 13503 13504 /// ditto 13505 this(ImageLabel label, Widget parent) { 13506 bool needsImage; 13507 version(win32_widgets) { 13508 super(parent); 13509 13510 // BS_BITMAP is set when we want image only, so checking for exactly that combination 13511 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 13512 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 13513 13514 // could also do a virtual method needsOwnerDraw which default returns true and we control it here. typeid(this) == typeid(Button) for override check. 13515 13516 if(needsOwnerDraw) { 13517 extraStyle |= BS_OWNERDRAW; 13518 needsImage = true; 13519 } 13520 13521 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 13522 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 13523 13524 if(label.image) { 13525 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 13526 13527 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 13528 } 13529 13530 this.label = label.label; 13531 } else version(custom_widgets) { 13532 super(parent); 13533 13534 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 13535 needsImage = true; 13536 } 13537 13538 13539 if(needsImage && label.image) { 13540 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 13541 this.displayFlags = label.displayFlags; 13542 } 13543 13544 this.alignment = label.alignment; 13545 } 13546 13547 override int minHeight() { return defaultLineHeight + 4; } 13548 13549 static class Style : Widget.Style { 13550 override WidgetBackground background() { 13551 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 13552 13553 auto pressed = DynamicState.depressed | DynamicState.hover; 13554 if((widget.dynamicState & pressed) == pressed) { 13555 return WidgetBackground(cs.depressedButtonColor()); 13556 } else if(widget.dynamicState & DynamicState.hover) { 13557 return WidgetBackground(cs.hoveringColor()); 13558 } else { 13559 return WidgetBackground(cs.buttonColor()); 13560 } 13561 } 13562 13563 override FrameStyle borderStyle() { 13564 auto pressed = DynamicState.depressed | DynamicState.hover; 13565 if((widget.dynamicState & pressed) == pressed) { 13566 return FrameStyle.sunk; 13567 } else { 13568 return FrameStyle.risen; 13569 } 13570 13571 } 13572 13573 override bool variesWithState(ulong dynamicStateFlags) { 13574 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 13575 } 13576 } 13577 mixin OverrideStyle!Style; 13578 13579 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 13580 if(sprite) { 13581 sprite.drawAt( 13582 painter, 13583 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 13584 Point(0, 0) 13585 ); 13586 } else { 13587 Point pos = bounds.upperLeft; 13588 if(this.height == 16) 13589 pos.y -= 2; // total hack omg 13590 painter.drawText(pos, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 13591 } 13592 return bounds; 13593 } 13594 13595 override int flexBasisWidth() { 13596 version(win32_widgets) { 13597 SIZE size; 13598 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 13599 if(size.cx == 0) 13600 goto fallback; 13601 return size.cx + scaleWithDpi(16); 13602 } 13603 fallback: 13604 return scaleWithDpi(cast(int) label.length * 8 + 16); 13605 } 13606 13607 override int flexBasisHeight() { 13608 version(win32_widgets) { 13609 SIZE size; 13610 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 13611 if(size.cy == 0) 13612 goto fallback; 13613 return size.cy + scaleWithDpi(6); 13614 } 13615 fallback: 13616 return defaultLineHeight + 4; 13617 } 13618 } 13619 13620 /++ 13621 A button with a custom appearance, even on systems where there is a standard button. You can subclass it to override its style, paint, or paintContent functions, or you can modify its members for common changes. 13622 13623 History: 13624 Added January 14, 2024 13625 +/ 13626 class CustomButton : Button { 13627 this(ImageLabel label, Widget parent) { 13628 super(label, parent); 13629 } 13630 13631 this(string label, Widget parent) { 13632 super(label, parent); 13633 } 13634 13635 version(win32_widgets) 13636 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 13637 // paint is driven by handleWmDrawItem instead of minigui's redraw events 13638 if(hwnd) 13639 InvalidateRect(hwnd, null, false); // get Windows to trigger the actual redraw 13640 return; 13641 } 13642 13643 override void paint(WidgetPainter painter) { 13644 // the parent does `if(hwnd) return;` because 13645 // normally we don't want to draw on standard controls, 13646 // but this is an exception if it is an owner drawn button 13647 // (which is determined in the constructor by testing, 13648 // at runtime, for the existence of an overridden paint 13649 // member anyway, so this needed to trigger BS_OWNERDRAW) 13650 // sdpyPrintDebugString("drawing"); 13651 painter.drawThemed(&paintContent); 13652 } 13653 } 13654 13655 /++ 13656 A button with a consistent size, suitable for user commands like OK and CANCEL. 13657 +/ 13658 class CommandButton : Button { 13659 this(string label, Widget parent) { 13660 super(label, parent); 13661 } 13662 13663 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 13664 13665 override int maxHeight() { 13666 return defaultLineHeight + 4; 13667 } 13668 13669 override int maxWidth() { 13670 return defaultLineHeight * 4; 13671 } 13672 13673 override int marginLeft() { return 12; } 13674 override int marginRight() { return 12; } 13675 override int marginTop() { return 12; } 13676 override int marginBottom() { return 12; } 13677 } 13678 13679 /// 13680 enum ArrowDirection { 13681 left, /// 13682 right, /// 13683 up, /// 13684 down /// 13685 } 13686 13687 /// 13688 version(custom_widgets) 13689 class ArrowButton : Button { 13690 /// 13691 this(ArrowDirection direction, Widget parent) { 13692 super("", parent); 13693 this.direction = direction; 13694 triggersOnMultiClick = true; 13695 } 13696 13697 private ArrowDirection direction; 13698 13699 override int minHeight() { return scaleWithDpi(16); } 13700 override int maxHeight() { return scaleWithDpi(16); } 13701 override int minWidth() { return scaleWithDpi(16); } 13702 override int maxWidth() { return scaleWithDpi(16); } 13703 13704 override void paint(WidgetPainter painter) { 13705 super.paint(painter); 13706 13707 auto cs = getComputedStyle(); 13708 13709 painter.outlineColor = cs.foregroundColor; 13710 painter.fillColor = cs.foregroundColor; 13711 13712 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 13713 13714 final switch(direction) { 13715 case ArrowDirection.up: 13716 painter.drawPolygon( 13717 scaleWithDpi(Point(2, 10) + offset), 13718 scaleWithDpi(Point(7, 5) + offset), 13719 scaleWithDpi(Point(12, 10) + offset), 13720 scaleWithDpi(Point(2, 10) + offset) 13721 ); 13722 break; 13723 case ArrowDirection.down: 13724 painter.drawPolygon( 13725 scaleWithDpi(Point(2, 6) + offset), 13726 scaleWithDpi(Point(7, 11) + offset), 13727 scaleWithDpi(Point(12, 6) + offset), 13728 scaleWithDpi(Point(2, 6) + offset) 13729 ); 13730 break; 13731 case ArrowDirection.left: 13732 painter.drawPolygon( 13733 scaleWithDpi(Point(10, 2) + offset), 13734 scaleWithDpi(Point(5, 7) + offset), 13735 scaleWithDpi(Point(10, 12) + offset), 13736 scaleWithDpi(Point(10, 2) + offset) 13737 ); 13738 break; 13739 case ArrowDirection.right: 13740 painter.drawPolygon( 13741 scaleWithDpi(Point(6, 2) + offset), 13742 scaleWithDpi(Point(11, 7) + offset), 13743 scaleWithDpi(Point(6, 12) + offset), 13744 scaleWithDpi(Point(6, 2) + offset) 13745 ); 13746 break; 13747 } 13748 } 13749 } 13750 13751 private 13752 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 13753 int x, y; 13754 Widget par = c; 13755 while(par) { 13756 x += par.x; 13757 y += par.y; 13758 par = par.parent; 13759 } 13760 return [x, y]; 13761 } 13762 13763 version(win32_widgets) 13764 private 13765 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 13766 // MapWindowPoints? 13767 int x, y; 13768 Widget par = c; 13769 while(par) { 13770 x += par.x; 13771 y += par.y; 13772 par = par.parent; 13773 if(par !is null && par.useNativeDrawing()) 13774 break; 13775 } 13776 return [x, y]; 13777 } 13778 13779 /// 13780 class ImageBox : Widget { 13781 private MemoryImage image_; 13782 13783 override int widthStretchiness() { return 1; } 13784 override int heightStretchiness() { return 1; } 13785 override int widthShrinkiness() { return 1; } 13786 override int heightShrinkiness() { return 1; } 13787 13788 override int flexBasisHeight() { 13789 return image_.height; 13790 } 13791 13792 override int flexBasisWidth() { 13793 return image_.width; 13794 } 13795 13796 /// 13797 public void setImage(MemoryImage image){ 13798 this.image_ = image; 13799 if(this.parentWindow && this.parentWindow.win) { 13800 if(sprite) 13801 sprite.dispose(); 13802 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 13803 } 13804 redraw(); 13805 } 13806 13807 /// How to fit the image in the box if they aren't an exact match in size? 13808 enum HowToFit { 13809 center, /// centers the image, cropping around all the edges as needed 13810 crop, /// always draws the image in the upper left, cropping the lower right if needed 13811 // stretch, /// not implemented 13812 } 13813 13814 private Sprite sprite; 13815 private HowToFit howToFit_; 13816 13817 private Color backgroundColor_; 13818 13819 /// 13820 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 13821 this.image_ = image; 13822 this.tabStop = false; 13823 this.howToFit_ = howToFit; 13824 this.backgroundColor_ = backgroundColor; 13825 super(parent); 13826 updateSprite(); 13827 } 13828 13829 /// ditto 13830 this(MemoryImage image, HowToFit howToFit, Widget parent) { 13831 this(image, howToFit, Color.transparent, parent); 13832 } 13833 13834 private void updateSprite() { 13835 if(sprite is null && this.parentWindow && this.parentWindow.win) { 13836 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 13837 } 13838 } 13839 13840 override void paint(WidgetPainter painter) { 13841 updateSprite(); 13842 if(backgroundColor_.a) { 13843 painter.fillColor = backgroundColor_; 13844 painter.drawRectangle(Point(0, 0), width, height); 13845 } 13846 if(howToFit_ == HowToFit.crop) 13847 sprite.drawAt(painter, Point(0, 0)); 13848 else if(howToFit_ == HowToFit.center) { 13849 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 13850 } 13851 } 13852 } 13853 13854 /// 13855 class TextLabel : Widget { 13856 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; } 13857 override int maxHeight() { return minHeight; } 13858 override int minWidth() { return 32; } 13859 13860 override int flexBasisHeight() { return minHeight(); } 13861 override int flexBasisWidth() { return defaultTextWidth(label); } 13862 13863 string label_; 13864 13865 /++ 13866 Indicates which other control this label is here for. Similar to HTML `for` attribute. 13867 13868 In practice this means a click on the label will focus the `labelFor`. In future versions 13869 it will also set screen reader hints but that is not yet implemented. 13870 13871 History: 13872 Added October 3, 2021 (dub v10.4) 13873 +/ 13874 Widget labelFor; 13875 13876 /// 13877 @scriptable 13878 string label() { return label_; } 13879 13880 /// 13881 @scriptable 13882 void label(string l) { 13883 label_ = l; 13884 version(win32_widgets) { 13885 WCharzBuffer bfr = WCharzBuffer(l); 13886 SetWindowTextW(hwnd, bfr.ptr); 13887 } else version(custom_widgets) 13888 redraw(); 13889 } 13890 13891 override void defaultEventHandler_click(scope ClickEvent ce) { 13892 if(this.labelFor !is null) 13893 this.labelFor.focus(); 13894 } 13895 13896 /++ 13897 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 13898 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 13899 +/ 13900 this(string label, TextAlignment alignment, Widget parent) { 13901 this.label_ = label; 13902 this.alignment = alignment; 13903 this.tabStop = false; 13904 super(parent); 13905 13906 version(win32_widgets) 13907 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 13908 } 13909 13910 /// ditto 13911 this(string label, Widget parent) { 13912 this(label, TextAlignment.Right, parent); 13913 } 13914 13915 TextAlignment alignment; 13916 13917 version(custom_widgets) 13918 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 13919 painter.outlineColor = getComputedStyle().foregroundColor; 13920 painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment); 13921 return bounds; 13922 } 13923 } 13924 13925 class TextDisplayHelper : Widget { 13926 protected TextLayouter l; 13927 protected ScrollMessageWidget smw; 13928 13929 private const(TextLayouter.State)*[] undoStack; 13930 private const(TextLayouter.State)*[] redoStack; 13931 13932 private string preservedPrimaryText; 13933 protected void selectionChanged() { 13934 // sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString()); 13935 static if(UsingSimpledisplayX11) 13936 with(l.selection()) { 13937 if(!isEmpty()) { 13938 //sdpyPrintDebugString("!isEmpty"); 13939 13940 getPrimarySelection(parentWindow.win, (in char[] txt) { 13941 // sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")"); 13942 // import std.stdio; writeln("txt: ", txt, " sel: ", getContentString); 13943 if(txt.length) { 13944 preservedPrimaryText = txt.idup; 13945 // writeln(preservedPrimaryText); 13946 } 13947 13948 setPrimarySelection(parentWindow.win, getContentString()); 13949 }); 13950 } 13951 } 13952 } 13953 13954 final TextLayouter layouter() { 13955 return l; 13956 } 13957 13958 bool readonly; 13959 bool caretNavigation; // scroll lock can flip this 13960 bool singleLine; 13961 bool acceptsTabInput; 13962 13963 private Menu ctx; 13964 override Menu contextMenu(int x, int y) { 13965 if(ctx is null) { 13966 ctx = new Menu("Actions", this); 13967 if(!readonly) { 13968 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 13969 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 13970 ctx.addSeparator(); 13971 } 13972 if(!readonly) 13973 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 13974 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 13975 if(!readonly) 13976 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 13977 if(!readonly) 13978 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 13979 ctx.addSeparator(); 13980 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 13981 } 13982 return ctx; 13983 } 13984 13985 override void defaultEventHandler_blur(BlurEvent ev) { 13986 super.defaultEventHandler_blur(ev); 13987 if(l.wasMutated()) { 13988 auto evt = new ChangeEvent!string(this, &this.content); 13989 evt.dispatch(); 13990 l.clearWasMutatedFlag(); 13991 } 13992 } 13993 13994 private string content() { 13995 return l.getTextString(); 13996 } 13997 13998 void undo() { 13999 if(readonly) return; 14000 if(undoStack.length) { 14001 auto state = undoStack[$-1]; 14002 undoStack = undoStack[0 .. $-1]; 14003 undoStack.assumeSafeAppend(); 14004 redoStack ~= l.saveState(); 14005 l.restoreState(state); 14006 adjustScrollbarSizes(); 14007 scrollForCaret(); 14008 redraw(); 14009 stateCheckpoint = true; 14010 } 14011 } 14012 14013 void redo() { 14014 if(readonly) return; 14015 if(redoStack.length) { 14016 doStateCheckpoint(); 14017 auto state = redoStack[$-1]; 14018 redoStack = redoStack[0 .. $-1]; 14019 redoStack.assumeSafeAppend(); 14020 l.restoreState(state); 14021 adjustScrollbarSizes(); 14022 scrollForCaret(); 14023 redraw(); 14024 stateCheckpoint = true; 14025 } 14026 } 14027 14028 void cut() { 14029 if(readonly) return; 14030 with(l.selection()) { 14031 if(!isEmpty()) { 14032 setClipboardText(parentWindow.win, getContentString()); 14033 doStateCheckpoint(); 14034 replaceContent(""); 14035 adjustScrollbarSizes(); 14036 scrollForCaret(); 14037 this.redraw(); 14038 } 14039 } 14040 14041 } 14042 14043 void copy() { 14044 with(l.selection()) { 14045 if(!isEmpty()) { 14046 setClipboardText(parentWindow.win, getContentString()); 14047 this.redraw(); 14048 } 14049 } 14050 } 14051 14052 void paste() { 14053 if(readonly) return; 14054 getClipboardText(parentWindow.win, (txt) { 14055 doStateCheckpoint(); 14056 if(singleLine) 14057 l.selection.replaceContent(txt.stripInternal()); 14058 else 14059 l.selection.replaceContent(txt); 14060 adjustScrollbarSizes(); 14061 scrollForCaret(); 14062 this.redraw(); 14063 }); 14064 } 14065 14066 void deleteContentOfSelection() { 14067 if(readonly) return; 14068 doStateCheckpoint(); 14069 l.selection.replaceContent(""); 14070 l.selection.setUserXCoordinate(); 14071 adjustScrollbarSizes(); 14072 scrollForCaret(); 14073 redraw(); 14074 } 14075 14076 void selectAll() { 14077 with(l.selection) { 14078 moveToStartOfDocument(); 14079 setAnchor(); 14080 moveToEndOfDocument(); 14081 setFocus(); 14082 14083 selectionChanged(); 14084 } 14085 redraw(); 14086 } 14087 14088 protected bool stateCheckpoint = true; 14089 14090 protected void doStateCheckpoint() { 14091 if(stateCheckpoint) { 14092 undoStack ~= l.saveState(); 14093 stateCheckpoint = false; 14094 } 14095 } 14096 14097 protected void adjustScrollbarSizes() { 14098 // FIXME: will want a content area helper function instead of doing all these subtractions myself 14099 auto borderWidth = 2; 14100 this.smw.setTotalArea(l.width, l.height); 14101 this.smw.setViewableArea( 14102 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 14103 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 14104 } 14105 14106 protected void scrollForCaret() { 14107 // writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 14108 smw.scrollIntoView(l.selection.focusBoundingBox()); 14109 } 14110 14111 // FIXME: this should be a theme changed event listener instead 14112 private BaseVisualTheme currentTheme; 14113 override void recomputeChildLayout() { 14114 if(currentTheme is null) 14115 currentTheme = WidgetPainter.visualTheme; 14116 if(WidgetPainter.visualTheme !is currentTheme) { 14117 currentTheme = WidgetPainter.visualTheme; 14118 auto ds = this.l.defaultStyle; 14119 if(auto ms = cast(MyTextStyle) ds) { 14120 auto cs = getComputedStyle(); 14121 auto font = cs.font(); 14122 if(font !is null) 14123 ms.font_ = font; 14124 else { 14125 auto osc = new OperatingSystemFont(); 14126 osc.loadDefault; 14127 ms.font_ = osc; 14128 } 14129 } 14130 } 14131 super.recomputeChildLayout(); 14132 } 14133 14134 private Point adjustForSingleLine(Point p) { 14135 if(singleLine) 14136 return Point(p.x, this.height / 2); 14137 else 14138 return p; 14139 } 14140 14141 private bool wordWrapEnabled_; 14142 14143 this(TextLayouter l, ScrollMessageWidget parent) { 14144 this.smw = parent; 14145 14146 smw.addDefaultWheelListeners(16, 16, 8); 14147 smw.movementPerButtonClick(16, 16); 14148 14149 this.defaultPadding = Rectangle(2, 2, 2, 2); 14150 14151 this.l = l; 14152 super(parent); 14153 14154 smw.addEventListener((scope ScrollEvent se) { 14155 this.redraw(); 14156 }); 14157 14158 this.addEventListener((scope ResizeEvent re) { 14159 // FIXME: I should add a method to give this client area width thing 14160 if(wordWrapEnabled_) 14161 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 14162 14163 adjustScrollbarSizes(); 14164 scrollForCaret(); 14165 14166 this.redraw(); 14167 }); 14168 14169 } 14170 14171 private { 14172 bool mouseDown; 14173 bool mouseActuallyMoved; 14174 14175 Point downAt; 14176 14177 Timer autoscrollTimer; 14178 int autoscrollDirection; 14179 int autoscrollAmount; 14180 14181 void autoscroll() { 14182 switch(autoscrollDirection) { 14183 case 0: smw.scrollUp(autoscrollAmount); break; 14184 case 1: smw.scrollDown(autoscrollAmount); break; 14185 case 2: smw.scrollLeft(autoscrollAmount); break; 14186 case 3: smw.scrollRight(autoscrollAmount); break; 14187 default: assert(0); 14188 } 14189 14190 this.redraw(); 14191 } 14192 14193 void setAutoscrollTimer(int direction, int amount) { 14194 if(autoscrollTimer is null) { 14195 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 14196 } 14197 14198 autoscrollDirection = direction; 14199 autoscrollAmount = amount; 14200 } 14201 14202 void stopAutoscrollTimer() { 14203 if(autoscrollTimer !is null) { 14204 autoscrollTimer.dispose(); 14205 autoscrollTimer = null; 14206 } 14207 autoscrollAmount = 0; 14208 autoscrollDirection = 0; 14209 } 14210 } 14211 14212 override void defaultEventHandler_mousemove(scope MouseMoveEvent ce) { 14213 if(mouseDown) { 14214 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 14215 14216 // FIXME: when scrolling i actually do want a timer. 14217 // i also want a zone near the sides of the window where i can auto scroll 14218 14219 auto scrollMultiplier = scaleWithDpi(16); 14220 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 14221 14222 if(!singleLine && movedTo.y < 4) { 14223 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 14224 } else 14225 if(!singleLine && (movedTo.y + 6) > this.height) { 14226 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 14227 } else 14228 if(movedTo.x < 4) { 14229 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 14230 } else 14231 if((movedTo.x + 6) > this.width) { 14232 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 14233 } else 14234 stopAutoscrollTimer(); 14235 14236 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 14237 l.selection.setFocus(); 14238 mouseActuallyMoved = true; 14239 this.redraw(); 14240 } 14241 14242 super.defaultEventHandler_mousemove(ce); 14243 } 14244 14245 override void defaultEventHandler_mouseup(scope MouseUpEvent ce) { 14246 // FIXME: assert primary selection 14247 if(mouseDown && ce.button == MouseButton.left) { 14248 stateCheckpoint = true; 14249 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 14250 //l.selection.setFocus(); 14251 mouseDown = false; 14252 parentWindow.releaseMouseCapture(); 14253 stopAutoscrollTimer(); 14254 this.redraw(); 14255 14256 if(mouseActuallyMoved) 14257 selectionChanged(); 14258 } 14259 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 14260 14261 super.defaultEventHandler_mouseup(ce); 14262 } 14263 14264 static if(UsingSimpledisplayX11) 14265 override void defaultEventHandler_click(scope ClickEvent ce) { 14266 if(ce.button == MouseButton.middle) { 14267 parentWindow.win.getPrimarySelection((txt) { 14268 doStateCheckpoint(); 14269 14270 // import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText); 14271 14272 if(txt == l.selection.getContentString && preservedPrimaryText.length) 14273 l.selection.replaceContent(preservedPrimaryText); 14274 else 14275 l.selection.replaceContent(txt); 14276 redraw(); 14277 }); 14278 } 14279 14280 super.defaultEventHandler_click(ce); 14281 } 14282 14283 override void defaultEventHandler_dblclick(scope DoubleClickEvent dce) { 14284 if(dce.button == MouseButton.left) { 14285 with(l.selection()) { 14286 // FIXME: for a url or file picker i might wanna use / as a separator intead 14287 scope dg = delegate const(char)[] (scope return const(char)[] ch) { 14288 if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r") 14289 return ch; 14290 return null; 14291 }; 14292 find(dg, 1, true).moveToEnd.setAnchor; 14293 find(dg, 1, false).moveTo.setFocus; 14294 selectionChanged(); 14295 redraw(); 14296 } 14297 } 14298 14299 super.defaultEventHandler_dblclick(dce); 14300 } 14301 14302 override void defaultEventHandler_mousedown(scope MouseDownEvent ce) { 14303 if(ce.button == MouseButton.left) { 14304 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 14305 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 14306 if(ce.shiftKey) 14307 l.selection.setFocus(); 14308 else 14309 l.selection.setAnchor(); 14310 mouseDown = true; 14311 mouseActuallyMoved = false; 14312 parentWindow.captureMouse(this); 14313 this.redraw(); 14314 } 14315 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 14316 14317 super.defaultEventHandler_mousedown(ce); 14318 } 14319 14320 override void defaultEventHandler_char(scope CharEvent ce) { 14321 super.defaultEventHandler_char(ce); 14322 14323 if(readonly) 14324 return; 14325 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 14326 return; // skip the ctrl+x characters we don't care about as plain text 14327 14328 if(singleLine && ce.character == '\n') 14329 return; 14330 if(!acceptsTabInput && ce.character == '\t') 14331 return; 14332 14333 doStateCheckpoint(); 14334 14335 char[4] buffer; 14336 import arsd.core; 14337 auto stride = encodeUtf8(buffer, ce.character); 14338 l.selection.replaceContent(buffer[0 .. stride]); 14339 l.selection.setUserXCoordinate(); 14340 adjustScrollbarSizes(); 14341 scrollForCaret(); 14342 redraw(); 14343 14344 } 14345 14346 override void defaultEventHandler_keydown(scope KeyDownEvent kde) { 14347 switch(kde.key) { 14348 case Key.Up, Key.Down, Key.Left, Key.Right: 14349 case Key.Home, Key.End: 14350 stateCheckpoint = true; 14351 bool setPosition = false; 14352 switch(kde.key) { 14353 case Key.Up: l.selection.moveUp(); break; 14354 case Key.Down: l.selection.moveDown(); break; 14355 case Key.Left: l.selection.moveLeft(); setPosition = true; break; 14356 case Key.Right: l.selection.moveRight(); setPosition = true; break; 14357 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 14358 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 14359 default: assert(0); 14360 } 14361 14362 if(kde.shiftKey) 14363 l.selection.setFocus(); 14364 else 14365 l.selection.setAnchor(); 14366 14367 selectionChanged(); 14368 14369 if(setPosition) 14370 l.selection.setUserXCoordinate(); 14371 scrollForCaret(); 14372 redraw(); 14373 break; 14374 case Key.PageUp, Key.PageDown: 14375 // want to act like the user clicked on the caret again 14376 // after the scroll operation completed, so it would remain at 14377 // about the same place on the viewport 14378 auto oldY = smw.vsb.position; 14379 smw.defaultKeyboardListener(kde); 14380 auto newY = smw.vsb.position; 14381 with(l.selection) { 14382 auto uc = getUserCoordinate(); 14383 uc.y += newY - oldY; 14384 moveTo(uc); 14385 14386 if(kde.shiftKey) 14387 setFocus(); 14388 else 14389 setAnchor(); 14390 } 14391 break; 14392 case Key.Delete: 14393 if(l.selection.isEmpty()) { 14394 l.selection.setAnchor(); 14395 l.selection.moveRight(); 14396 l.selection.setFocus(); 14397 } 14398 deleteContentOfSelection(); 14399 adjustScrollbarSizes(); 14400 scrollForCaret(); 14401 break; 14402 case Key.Insert: 14403 break; 14404 case Key.A: 14405 if(kde.ctrlKey) 14406 selectAll(); 14407 break; 14408 case Key.F: 14409 // find 14410 break; 14411 case Key.Z: 14412 if(kde.ctrlKey) 14413 undo(); 14414 break; 14415 case Key.R: 14416 if(kde.ctrlKey) 14417 redo(); 14418 break; 14419 case Key.X: 14420 if(kde.ctrlKey) 14421 cut(); 14422 break; 14423 case Key.C: 14424 if(kde.ctrlKey) 14425 copy(); 14426 break; 14427 case Key.V: 14428 if(kde.ctrlKey) 14429 paste(); 14430 break; 14431 case Key.F1: 14432 with(l.selection()) { 14433 moveToStartOfLine(); 14434 setAnchor(); 14435 moveToEndOfLine(); 14436 moveToIncludeAdjacentEndOfLineMarker(); 14437 setFocus(); 14438 replaceContent(""); 14439 } 14440 14441 redraw(); 14442 break; 14443 /* 14444 case Key.F2: 14445 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 14446 //(cast(MyTextStyle) old).font, 14447 font2, 14448 Color.red))); 14449 redraw(); 14450 break; 14451 */ 14452 case Key.Tab: 14453 // we process the char event, so don't want to change focus on it, unless the user overrides that with ctrl 14454 if(acceptsTabInput && !kde.ctrlKey) 14455 kde.preventDefault(); 14456 break; 14457 default: 14458 } 14459 14460 if(!kde.defaultPrevented) 14461 super.defaultEventHandler_keydown(kde); 14462 } 14463 14464 // we want to delegate all the Widget.Style stuff up to the other class that the user can see 14465 override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) { 14466 // this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidget Parent 14467 if(parent && parent.parent && parent.parent.parent) 14468 parent.parent.parent.useStyleProperties(dg); 14469 else 14470 super.useStyleProperties(dg); 14471 } 14472 14473 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; } 14474 override int maxHeight() { 14475 if(singleLine) 14476 return minHeight; 14477 else 14478 return super.maxHeight(); 14479 } 14480 14481 void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 14482 painter.setFont(myStyle.font); 14483 painter.drawText(upperLeft, text); 14484 } 14485 14486 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 14487 //painter.setFont(font); 14488 14489 auto cs = getComputedStyle(); 14490 auto defaultColor = cs.foregroundColor; 14491 14492 auto old = painter.setClipRectangleForWidget(bounds.upperLeft, bounds.width, bounds.height); 14493 scope(exit) painter.setClipRectangleForWidget(old.upperLeft, old.width, old.height); 14494 14495 l.getDrawableText(delegate bool(txt, style, info, carets...) { 14496 //writeln("Segment: ", txt); 14497 assert(style !is null); 14498 14499 if(info.selections && info.boundingBox.width > 0) { 14500 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 14501 painter.fillColor = color; 14502 painter.outlineColor = color; 14503 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 14504 painter.outlineColor = cs.selectionForegroundColor; 14505 //painter.fillColor = Color.white; 14506 } else { 14507 painter.outlineColor = defaultColor; 14508 } 14509 14510 if(this.isFocused) 14511 foreach(idx, caret; carets) { 14512 if(idx == 0) 14513 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 14514 painter.drawLine( 14515 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 14516 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 14517 ); 14518 } 14519 14520 if(txt.stripInternal.length) { 14521 // defaultColor = myStyle.color; // FIXME: so wrong 14522 if(auto myStyle = cast(MyTextStyle) style) 14523 drawTextSegment(myStyle, painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 14524 else if(auto myStyle = cast(MyImageStyle) style) 14525 myStyle.draw(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 14526 } 14527 14528 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) { 14529 return false; 14530 } else { 14531 return true; 14532 } 14533 }, Rectangle(smw.position(), bounds.size)); 14534 14535 /+ 14536 int place = 0; 14537 int y = 75; 14538 foreach(width; widths) { 14539 painter.fillColor = Color.red; 14540 painter.drawRectangle(Point(place, y), Size(width, 75)); 14541 //y += 15; 14542 place += width; 14543 } 14544 +/ 14545 14546 return bounds; 14547 } 14548 14549 static class MyTextStyle : TextStyle { 14550 OperatingSystemFont font_; 14551 this(OperatingSystemFont font, bool passwordMode = false) { 14552 this.font_ = font; 14553 } 14554 14555 override OperatingSystemFont font() { 14556 return font_; 14557 } 14558 14559 bool foregroundColorOverridden; 14560 bool backgroundColorOverridden; 14561 Color foregroundColor; 14562 Color backgroundColor; // should this be inline segment or the whole paragraph block? 14563 bool italic; 14564 bool bold; 14565 bool underline; 14566 bool strikeout; 14567 bool subscript; 14568 bool superscript; 14569 } 14570 14571 static class MyImageStyle : TextStyle, MeasurableFont { 14572 MemoryImage image_; 14573 Image converted; 14574 this(MemoryImage image) { 14575 this.image_ = image; 14576 this.converted = Image.fromMemoryImage(image); 14577 } 14578 14579 bool isMonospace() { return false; } 14580 int averageWidth() { return image_.width; } 14581 int height() { return image_.height; } 14582 int ascent() { return image_.height; } 14583 int descent() { return 0; } 14584 14585 int stringWidth(scope const(char)[] s, SimpleWindow window = null) { 14586 return image_.width; 14587 } 14588 14589 override MeasurableFont font() { 14590 return this; 14591 } 14592 14593 void draw(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 14594 painter.drawImage(upperLeft, converted); 14595 } 14596 } 14597 } 14598 14599 /+ 14600 class TextWidget : Widget { 14601 TextLayouter l; 14602 ScrollMessageWidget smw; 14603 TextDisplayHelper helper; 14604 this(TextLayouter l, Widget parent) { 14605 this.l = l; 14606 super(parent); 14607 14608 smw = new ScrollMessageWidget(this); 14609 //smw.horizontalScrollBar.hide; 14610 //smw.verticalScrollBar.hide; 14611 smw.addDefaultWheelListeners(16, 16, 8); 14612 smw.movementPerButtonClick(16, 16); 14613 helper = new TextDisplayHelper(l, smw); 14614 14615 // no need to do this here since there's gonna be a resize 14616 // event immediately before any drawing 14617 // smw.setTotalArea(l.width, l.height); 14618 smw.setViewableArea( 14619 this.width - this.paddingLeft - this.paddingRight, 14620 this.height - this.paddingTop - this.paddingBottom); 14621 14622 /+ 14623 writeln(l.width, "x", l.height); 14624 +/ 14625 } 14626 } 14627 +/ 14628 14629 14630 14631 14632 /+ 14633 make sure it calls parentWindow.inputProxy.setIMEPopupLocation too 14634 +/ 14635 14636 /++ 14637 Contains the implementation of text editing and shared basic api. You should construct one of the child classes instead, like [TextEdit], [LineEdit], or [PasswordEdit]. 14638 +/ 14639 abstract class EditableTextWidget : Widget { 14640 protected this(Widget parent) { 14641 version(custom_widgets) 14642 this(true, parent); 14643 else 14644 this(false, parent); 14645 } 14646 14647 private bool useCustomWidget; 14648 14649 protected this(bool useCustomWidget, Widget parent) { 14650 this.useCustomWidget = useCustomWidget; 14651 14652 super(parent); 14653 14654 if(useCustomWidget) 14655 setupCustomTextEditing(); 14656 } 14657 14658 private bool wordWrapEnabled_; 14659 /++ 14660 Enables or disables wrapping of long lines on word boundaries. 14661 +/ 14662 void wordWrapEnabled(bool enabled) { 14663 if(useCustomWidget) { 14664 wordWrapEnabled_ = enabled; 14665 if(tdh) 14666 tdh.wordWrapEnabled_ = true; 14667 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 14668 } else version(win32_widgets) { 14669 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 14670 } 14671 } 14672 14673 override int minWidth() { return scaleWithDpi(16); } 14674 override int widthStretchiness() { return 7; } 14675 override int widthShrinkiness() { return 1; } 14676 14677 override int maxHeight() { 14678 if(useCustomWidget) 14679 return tdh.maxHeight; 14680 else 14681 return super.maxHeight(); 14682 } 14683 14684 override void focus() { 14685 if(useCustomWidget && tdh) 14686 tdh.focus(); 14687 else 14688 super.focus(); 14689 } 14690 14691 override void defaultEventHandler_focusout(FocusOutEvent foe) { 14692 if(tdh !is null && foe.target is tdh) 14693 tdh.redraw(); 14694 } 14695 14696 override void defaultEventHandler_focusin(FocusInEvent foe) { 14697 if(tdh !is null && foe.target is tdh) 14698 tdh.redraw(); 14699 } 14700 14701 14702 /++ 14703 Selects all the text in the control, as if the user did it themselves. When the user types in a widget, the selected text is replaced with the new input, so this might be useful for putting in default text that is easy for the user to replace. 14704 +/ 14705 void selectAll() { 14706 if(useCustomWidget) { 14707 tdh.selectAll(); 14708 } else version(win32_widgets) { 14709 SendMessage(hwnd, EM_SETSEL, 0, -1); 14710 } 14711 } 14712 14713 /++ 14714 Basic clipboard operations. 14715 14716 History: 14717 Added December 31, 2024 14718 +/ 14719 void copy() { 14720 if(useCustomWidget) { 14721 tdh.copy(); 14722 } else version(win32_widgets) { 14723 SendMessage(hwnd, WM_COPY, 0, 0); 14724 } 14725 } 14726 14727 /// ditto 14728 void cut() { 14729 if(useCustomWidget) { 14730 tdh.cut(); 14731 } else version(win32_widgets) { 14732 SendMessage(hwnd, WM_CUT, 0, 0); 14733 } 14734 } 14735 14736 /// ditto 14737 void paste() { 14738 if(useCustomWidget) { 14739 tdh.paste(); 14740 } else version(win32_widgets) { 14741 SendMessage(hwnd, WM_PASTE, 0, 0); 14742 } 14743 } 14744 14745 /// 14746 void undo() { 14747 if(useCustomWidget) { 14748 tdh.undo(); 14749 } else version(win32_widgets) { 14750 SendMessage(hwnd, EM_UNDO, 0, 0); 14751 } 14752 } 14753 14754 // note that WM_CLEAR deletes the selection without copying it to the clipboard 14755 // also windows supports margins, modified flag, and much more 14756 14757 // EM_UNDO and EM_CANUNDO. EM_REDO is only supported in rich text boxes here 14758 14759 // EM_GETSEL, EM_REPLACESEL, and EM_SETSEL might be usable for find etc. 14760 14761 14762 14763 /*protected*/ TextDisplayHelper tdh; 14764 /*protected*/ TextLayouter textLayout; 14765 14766 /++ 14767 Gets or sets the current content of the control, as a plain text string. Setting the content will reset the cursor position and overwrite any changes the user made. 14768 +/ 14769 @property string content() { 14770 if(useCustomWidget) { 14771 return textLayout.getTextString(); 14772 } else version(win32_widgets) { 14773 wchar[4096] bufferstack; 14774 wchar[] buffer; 14775 auto len = GetWindowTextLength(hwnd); 14776 if(len < bufferstack.length) 14777 buffer = bufferstack[0 .. len + 1]; 14778 else 14779 buffer = new wchar[](len + 1); 14780 14781 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 14782 if(l >= 0) 14783 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 14784 else 14785 return null; 14786 } 14787 14788 assert(0); 14789 } 14790 /// ditto 14791 @property void content(string s) { 14792 if(useCustomWidget) { 14793 with(textLayout.selection) { 14794 moveToStartOfDocument(); 14795 setAnchor(); 14796 moveToEndOfDocument(); 14797 setFocus(); 14798 replaceContent(s); 14799 } 14800 14801 tdh.adjustScrollbarSizes(); 14802 // these don't seem to help 14803 // tdh.smw.setPosition(0, 0); 14804 // tdh.scrollForCaret(); 14805 14806 redraw(); 14807 } else version(win32_widgets) { 14808 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 14809 SetWindowTextW(hwnd, bfr.ptr); 14810 } 14811 } 14812 14813 /++ 14814 Appends some text to the widget at the end, without affecting the user selection or cursor position. 14815 +/ 14816 void addText(string txt) { 14817 if(useCustomWidget) { 14818 textLayout.appendText(txt); 14819 tdh.adjustScrollbarSizes(); 14820 redraw(); 14821 } else version(win32_widgets) { 14822 // get the current selection 14823 DWORD StartPos, EndPos; 14824 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 14825 14826 // move the caret to the end of the text 14827 int outLength = GetWindowTextLengthW(hwnd); 14828 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 14829 14830 // insert the text at the new caret position 14831 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 14832 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 14833 14834 // restore the previous selection 14835 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 14836 } 14837 } 14838 14839 // EM_SCROLLCARET scrolls the caret into view 14840 14841 void scrollToBottom() { 14842 if(useCustomWidget) { 14843 tdh.smw.scrollDown(int.max); 14844 } else version(win32_widgets) { 14845 SendMessageW( hwnd, EM_LINESCROLL, 0, int.max ); 14846 } 14847 } 14848 14849 protected TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 14850 return new TextDisplayHelper(textLayout, smw); 14851 } 14852 14853 protected TextStyle defaultTextStyle() { 14854 return new TextDisplayHelper.MyTextStyle(getUsedFont()); 14855 } 14856 14857 private OperatingSystemFont getUsedFont() { 14858 auto cs = getComputedStyle(); 14859 auto font = cs.font; 14860 if(font is null) { 14861 font = new OperatingSystemFont; 14862 font.loadDefault(); 14863 } 14864 return font; 14865 } 14866 14867 protected void setupCustomTextEditing() { 14868 textLayout = new TextLayouter(defaultTextStyle()); 14869 14870 auto smw = new ScrollMessageWidget(this); 14871 if(!showingHorizontalScroll) 14872 smw.horizontalScrollBar.hide(); 14873 if(!showingVerticalScroll) 14874 smw.verticalScrollBar.hide(); 14875 this.tabStop = false; 14876 smw.tabStop = false; 14877 tdh = textDisplayHelperFactory(textLayout, smw); 14878 } 14879 14880 override void newParentWindow(Window old, Window n) { 14881 if(n is null) return; 14882 this.parentWindow.addEventListener((scope DpiChangedEvent dce) { 14883 if(textLayout) { 14884 if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) { 14885 // the dpi change can change the font, so this informs the layouter that it has changed too 14886 style.font_ = getUsedFont(); 14887 14888 // arsd.core.writeln(this.parentWindow.win.actualDpi); 14889 } 14890 } 14891 }); 14892 } 14893 14894 static class Style : Widget.Style { 14895 override WidgetBackground background() { 14896 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 14897 } 14898 14899 override Color foregroundColor() { 14900 return WidgetPainter.visualTheme.foregroundColor; 14901 } 14902 14903 override FrameStyle borderStyle() { 14904 return FrameStyle.sunk; 14905 } 14906 14907 override MouseCursor cursor() { 14908 return GenericCursor.Text; 14909 } 14910 } 14911 mixin OverrideStyle!Style; 14912 14913 version(win32_widgets) { 14914 private string lastContentBlur; 14915 14916 override void defaultEventHandler_blur(BlurEvent ev) { 14917 super.defaultEventHandler_blur(ev); 14918 14919 if(!useCustomWidget) 14920 if(this.content != lastContentBlur) { 14921 auto evt = new ChangeEvent!string(this, &this.content); 14922 evt.dispatch(); 14923 lastContentBlur = this.content; 14924 } 14925 } 14926 } 14927 14928 14929 bool showingVerticalScroll() { return true; } 14930 bool showingHorizontalScroll() { return true; } 14931 } 14932 14933 /++ 14934 A `LineEdit` is an editor of a single line of text, comparable to a HTML `<input type="text" />`. 14935 14936 A `CustomLineEdit` always uses the custom implementation, even on operating systems where the native control is implemented in minigui, which may provide more api styling features but at the cost of poorer integration with the OS and potentially worse user experience in other ways. 14937 14938 See_Also: 14939 [PasswordEdit] for a `LineEdit` that obscures its input. 14940 14941 [TextEdit] for a multi-line plain text editor widget. 14942 14943 [TextLabel] for a single line piece of static text. 14944 14945 [TextDisplay] for a read-only display of a larger piece of plain text. 14946 +/ 14947 class LineEdit : EditableTextWidget { 14948 override bool showingVerticalScroll() { return false; } 14949 override bool showingHorizontalScroll() { return false; } 14950 14951 override int flexBasisWidth() { return 250; } 14952 override int widthShrinkiness() { return 10; } 14953 14954 /// 14955 this(Widget parent) { 14956 super(parent); 14957 version(win32_widgets) { 14958 createWin32Window(this, "edit"w, "", 14959 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 14960 } else version(custom_widgets) { 14961 } else static assert(false); 14962 } 14963 14964 private this(bool useCustomWidget, Widget parent) { 14965 if(!useCustomWidget) 14966 this(parent); 14967 else 14968 super(true, parent); 14969 } 14970 14971 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 14972 auto tdh = new TextDisplayHelper(textLayout, smw); 14973 tdh.singleLine = true; 14974 return tdh; 14975 } 14976 14977 version(win32_widgets) { 14978 mixin Padding!q{0}; 14979 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 14980 override int maxHeight() { return minHeight; } 14981 } 14982 14983 /+ 14984 @property void passwordMode(bool p) { 14985 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 14986 } 14987 +/ 14988 } 14989 14990 /// ditto 14991 class CustomLineEdit : LineEdit { 14992 this(Widget parent) { 14993 super(true, parent); 14994 } 14995 } 14996 14997 /++ 14998 A [LineEdit] that displays `*` in place of the actual characters. 14999 15000 Alas, Windows requires the window to be created differently to use this style, 15001 so it had to be a new class instead of a toggle on and off on an existing object. 15002 15003 History: 15004 Added January 24, 2021 15005 15006 Implemented on Linux on January 31, 2023. 15007 +/ 15008 class PasswordEdit : EditableTextWidget { 15009 override bool showingVerticalScroll() { return false; } 15010 override bool showingHorizontalScroll() { return false; } 15011 15012 override int flexBasisWidth() { return 250; } 15013 15014 override TextStyle defaultTextStyle() { 15015 auto cs = getComputedStyle(); 15016 15017 auto osf = new class OperatingSystemFont { 15018 this() { 15019 super(cs.font); 15020 } 15021 override int stringWidth(scope const(char)[] text, SimpleWindow window = null) { 15022 int count = 0; 15023 foreach(dchar ch; text) 15024 count++; 15025 return count * super.stringWidth("*", window); 15026 } 15027 }; 15028 15029 return new TextDisplayHelper.MyTextStyle(osf); 15030 } 15031 15032 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 15033 static class TDH : TextDisplayHelper { 15034 this(TextLayouter textLayout, ScrollMessageWidget smw) { 15035 singleLine = true; 15036 super(textLayout, smw); 15037 } 15038 15039 override void drawTextSegment(MyTextStyle myStyle, WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 15040 char[256] buffer = void; 15041 int bufferLength = 0; 15042 foreach(dchar ch; text) 15043 buffer[bufferLength++] = '*'; 15044 painter.setFont(myStyle.font); 15045 painter.drawText(upperLeft, buffer[0..bufferLength]); 15046 } 15047 } 15048 15049 return new TDH(textLayout, smw); 15050 } 15051 15052 /// 15053 this(Widget parent) { 15054 super(parent); 15055 version(win32_widgets) { 15056 createWin32Window(this, "edit"w, "", 15057 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 15058 } else version(custom_widgets) { 15059 } else static assert(false); 15060 } 15061 15062 private this(bool useCustomWidget, Widget parent) { 15063 if(!useCustomWidget) 15064 this(parent); 15065 else 15066 super(true, parent); 15067 } 15068 15069 version(win32_widgets) { 15070 mixin Padding!q{2}; 15071 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 15072 override int maxHeight() { return minHeight; } 15073 } 15074 } 15075 15076 /// ditto 15077 class CustomPasswordEdit : PasswordEdit { 15078 this(Widget parent) { 15079 super(true, parent); 15080 } 15081 } 15082 15083 15084 /++ 15085 A `TextEdit` is a multi-line plain text editor, comparable to a HTML `<textarea>`. 15086 15087 See_Also: 15088 [TextDisplay] for a read-only text display. 15089 15090 [LineEdit] for a single line text editor. 15091 15092 [PasswordEdit] for a single line text editor that obscures its input. 15093 +/ 15094 class TextEdit : EditableTextWidget { 15095 /// 15096 this(Widget parent) { 15097 super(parent); 15098 version(win32_widgets) { 15099 createWin32Window(this, "edit"w, "", 15100 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 15101 } else version(custom_widgets) { 15102 } else static assert(false); 15103 } 15104 15105 private this(bool useCustomWidget, Widget parent) { 15106 if(!useCustomWidget) 15107 this(parent); 15108 else 15109 super(true, parent); 15110 } 15111 15112 override int maxHeight() { return int.max; } 15113 override int heightStretchiness() { return 7; } 15114 15115 override int flexBasisWidth() { return 250; } 15116 override int flexBasisHeight() { return 25; } 15117 } 15118 15119 /// ditto 15120 class CustomTextEdit : TextEdit { 15121 this(Widget parent) { 15122 super(true, parent); 15123 } 15124 } 15125 15126 /+ 15127 /++ 15128 15129 +/ 15130 version(none) 15131 class RichTextDisplay : Widget { 15132 @property void content(string c) {} 15133 void appendContent(string c) {} 15134 } 15135 +/ 15136 15137 /++ 15138 A read-only text display. It is based on the editable widget base, but does not allow user edits and displays it on the direct background instead of on an editable background. 15139 15140 History: 15141 Added October 31, 2023 (dub v11.3) 15142 +/ 15143 class TextDisplay : EditableTextWidget { 15144 this(string text, Widget parent) { 15145 super(true, parent); 15146 this.content = text; 15147 } 15148 15149 override int maxHeight() { return int.max; } 15150 override int minHeight() { return Window.defaultLineHeight; } 15151 override int heightStretchiness() { return 7; } 15152 override int heightShrinkiness() { return 2; } 15153 15154 override int flexBasisWidth() { 15155 return scaleWithDpi(250); 15156 } 15157 override int flexBasisHeight() { 15158 if(textLayout is null || this.tdh is null) 15159 return Window.defaultLineHeight; 15160 15161 auto textHeight = borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textLayout.height))).height; 15162 return this.tdh.borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textHeight))).height; 15163 } 15164 15165 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 15166 return new MyTextDisplayHelper(textLayout, smw); 15167 } 15168 15169 override void registerMovement() { 15170 super.registerMovement(); 15171 this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally 15172 } 15173 15174 static class MyTextDisplayHelper : TextDisplayHelper { 15175 this(TextLayouter textLayout, ScrollMessageWidget smw) { 15176 smw.verticalScrollBar.hide(); 15177 smw.horizontalScrollBar.hide(); 15178 super(textLayout, smw); 15179 this.readonly = true; 15180 } 15181 15182 override void registerMovement() { 15183 super.registerMovement(); 15184 15185 // FIXME: do the horizontal one too as needed and make sure that it does 15186 // wordwrapping again 15187 if(l.height + smw.horizontalScrollBar.height > this.height) 15188 smw.verticalScrollBar.show(); 15189 else 15190 smw.verticalScrollBar.hide(); 15191 15192 l.wordWrapWidth = this.width; 15193 15194 smw.verticalScrollBar.setPosition = 0; 15195 } 15196 } 15197 15198 class Style : Widget.Style { 15199 // just want the generic look for these 15200 } 15201 15202 mixin OverrideStyle!Style; 15203 } 15204 15205 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active 15206 /++ 15207 A scrollable viewer for an array of widgets. The widgets inside a list item can be whatever you want, and you can have any number of total items you want because only the visible widgets need to actually exist and load their data at a time, giving constantly predictable performance. 15208 15209 15210 When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`. 15211 15212 Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand. 15213 15214 Note that some state in reused widget objects may either be preserved or reset when the user isn't expecting it. It is your responsibility to handle this when you load an item (try to save it when it is unloaded, then set it when reloaded), but my recommendation would be to have minimal extra state. For example, avoid having a scrollable widget inside a list, since the scroll state might change as it goes out and into view. Instead, I'd suggest making the list be a loader for a details pane on the side. 15215 15216 History: 15217 Added August 12, 2024 (dub v11.6) 15218 +/ 15219 abstract class GenericListViewWidget : Widget { 15220 /++ 15221 15222 +/ 15223 this(Widget parent) { 15224 super(parent); 15225 15226 smw = new ScrollMessageWidget(this); 15227 smw.addDefaultKeyboardListeners(itemSize.height, itemSize.width); 15228 smw.addDefaultWheelListeners(itemSize.height, itemSize.width); 15229 smw.hsb.hide(); // FIXME: this might actually be useful but we can't really communicate that yet 15230 15231 inner = new GenericListViewWidgetInner(this, smw, new GenericListViewInnerContainer(smw)); 15232 inner.tabStop = this.tabStop; 15233 this.tabStop = false; 15234 } 15235 15236 private ScrollMessageWidget smw; 15237 private GenericListViewWidgetInner inner; 15238 15239 /++ 15240 15241 +/ 15242 abstract GenericListViewItem itemFactory(Widget parent); 15243 // in device-dependent pixels 15244 /++ 15245 15246 +/ 15247 abstract Size itemSize(); // use 0 to indicate it can stretch? 15248 15249 enum LayoutMode { 15250 rows, 15251 columns, 15252 gridRowsFirst, 15253 gridColumnsFirst 15254 } 15255 LayoutMode layoutMode() { 15256 return LayoutMode.rows; 15257 } 15258 15259 private int itemCount_; 15260 15261 /++ 15262 Sets the count of available items in the list. This will not allocate any items, but it will adjust the scroll bars and try to load items up to this count on-demand as they appear visible. 15263 +/ 15264 void setItemCount(int count) { 15265 smw.setTotalArea(inner.width, count * itemSize().height); 15266 smw.setViewableArea(inner.width, inner.height); 15267 this.itemCount_ = count; 15268 } 15269 15270 /++ 15271 Returns the current count of items expected to available in the list. 15272 +/ 15273 int itemCount() { 15274 return this.itemCount_; 15275 } 15276 15277 /++ 15278 Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data. 15279 15280 Note you must $(I also) call [setItemCount] if the total item count has changed. 15281 +/ 15282 void notifyItemsChanged(int index, int count = 1) { 15283 } 15284 /// ditto 15285 void notifyItemsInserted(int index, int count = 1) { 15286 } 15287 /// ditto 15288 void notifyItemsRemoved(int index, int count = 1) { 15289 } 15290 /// ditto 15291 void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) { 15292 } 15293 15294 /++ 15295 History: 15296 Added January 1, 2025 15297 +/ 15298 void ensureItemVisibleInScroll(int index) { 15299 auto itemPos = index * itemSize().height; 15300 auto vsb = smw.verticalScrollBar; 15301 auto viewable = vsb.viewableArea_; 15302 15303 if(viewable == 0) { 15304 // viewable == 0 isn't actually supposed to happen, this means 15305 // this method is being called before having our size assigned, it should 15306 // probably just queue it up for later. 15307 queuedScroll = index; 15308 return; 15309 } 15310 15311 queuedScroll = int.min; 15312 15313 if(itemPos < vsb.position) { 15314 // scroll up to it 15315 vsb.setPosition(itemPos); 15316 smw.notify(); 15317 } else if(itemPos + itemSize().height > (vsb.position + viewable)) { 15318 // scroll down to it, so it is at the bottom 15319 15320 auto lastViewableItemPosition = (viewable - itemSize.height) / itemSize.height * itemSize.height; 15321 // need the itemPos to be at the lastViewableItemPosition after scrolling, so subtraction does it 15322 15323 vsb.setPosition(itemPos - lastViewableItemPosition); 15324 smw.notify(); 15325 } 15326 } 15327 15328 /++ 15329 History: 15330 Added January 1, 2025; 15331 +/ 15332 int numberOfCurrentlyFullyVisibleItems() { 15333 return smw.verticalScrollBar.viewableArea_ / itemSize.height; 15334 } 15335 15336 private int queuedScroll = int.min; 15337 15338 override void recomputeChildLayout() { 15339 super.recomputeChildLayout(); 15340 if(queuedScroll != int.min) 15341 ensureItemVisibleInScroll(queuedScroll); 15342 } 15343 15344 private GenericListViewItem[] items; 15345 15346 override void paint(WidgetPainter painter) {} 15347 } 15348 15349 /// ditto 15350 abstract class GenericListViewItem : Widget { 15351 /++ 15352 +/ 15353 this(Widget parent) { 15354 super(parent); 15355 } 15356 15357 private int _currentIndex = -1; 15358 15359 private void showItemPrivate(int idx) { 15360 showItem(idx); 15361 _currentIndex = idx; 15362 } 15363 15364 /++ 15365 Implement this to show an item from your data backing to the list. 15366 15367 Note that even if you are showing the requested index already, you should still try to reload it because it is possible the index now points to a different item (e.g. an item was added so all the indexes have changed) or if data has changed in this index and it is requesting you to update it prior to a repaint. 15368 +/ 15369 abstract void showItem(int idx); 15370 15371 /++ 15372 Maintained by the library after calling [showItem] so the object knows which data index it currently has. 15373 15374 It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent). 15375 15376 Inside the call to `showItem`, `currentIndexLoaded` is the old index, and the argument to `showItem` is the new index. You might use that to save state to the right place as needed before you overwrite it with the new item. 15377 +/ 15378 final int currentIndexLoaded() { 15379 return _currentIndex; 15380 } 15381 } 15382 15383 /// 15384 unittest { 15385 import arsd.minigui; 15386 15387 import std.conv; 15388 15389 void main() { 15390 auto mw = new MainWindow(); 15391 15392 static class MyListViewItem : GenericListViewItem { 15393 this(Widget parent) { 15394 super(parent); 15395 15396 label = new TextLabel("unloaded", TextAlignment.Left, this); 15397 button = new Button("Click", this); 15398 15399 button.addEventListener("triggered", (){ 15400 messageBox(text("clicked ", currentIndexLoaded())); 15401 }); 15402 } 15403 override void showItem(int idx) { 15404 label.label = "Item " ~ to!string(idx); 15405 } 15406 15407 TextLabel label; 15408 Button button; 15409 } 15410 15411 auto widget = new class GenericListViewWidget { 15412 this() { 15413 super(mw); 15414 } 15415 override GenericListViewItem itemFactory(Widget parent) { 15416 return new MyListViewItem(parent); 15417 } 15418 override Size itemSize() { 15419 return Size(0, scaleWithDpi(80)); 15420 } 15421 }; 15422 15423 widget.setItemCount(5000); 15424 15425 mw.loop(); 15426 } 15427 } 15428 15429 // this exists just to wrap the actual GenericListViewWidgetInner so borders 15430 // and padding and stuff can work 15431 private class GenericListViewInnerContainer : Widget { 15432 this(Widget parent) { 15433 super(parent); 15434 this.tabStop = false; 15435 } 15436 15437 override void recomputeChildLayout() { 15438 registerMovement(); 15439 15440 auto cs = getComputedStyle(); 15441 auto bw = getBorderWidth(cs.borderStyle); 15442 15443 assert(children.length < 2); 15444 foreach(child; children) { 15445 child.x = bw + paddingLeft(); 15446 child.y = bw + paddingTop(); 15447 child.width = this.width.NonOverflowingUint - bw - bw - paddingLeft() - paddingRight(); 15448 child.height = this.height.NonOverflowingUint - bw - bw - paddingTop() - paddingBottom(); 15449 15450 child.recomputeChildLayout(); 15451 } 15452 } 15453 15454 override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) { 15455 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15456 return parent.parent.parent.useStyleProperties(dg); 15457 else 15458 return super.useStyleProperties(dg); 15459 } 15460 15461 override int paddingTop() { 15462 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15463 return parent.parent.parent.paddingTop(); 15464 else 15465 return super.paddingTop(); 15466 } 15467 15468 override int paddingBottom() { 15469 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15470 return parent.parent.parent.paddingBottom(); 15471 else 15472 return super.paddingBottom(); 15473 } 15474 15475 override int paddingLeft() { 15476 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15477 return parent.parent.parent.paddingLeft(); 15478 else 15479 return super.paddingLeft(); 15480 } 15481 15482 override int paddingRight() { 15483 if(parent && parent.parent && parent.parent.parent) // ScrollMessageWidgetInner then ScrollMessageWidget then GenericListViewWidget 15484 return parent.parent.parent.paddingRight(); 15485 else 15486 return super.paddingRight(); 15487 } 15488 15489 15490 } 15491 15492 private class GenericListViewWidgetInner : Widget { 15493 this(GenericListViewWidget glvw, ScrollMessageWidget smw, GenericListViewInnerContainer parent) { 15494 super(parent); 15495 this.glvw = glvw; 15496 15497 reloadVisible(); 15498 15499 smw.addEventListener("scroll", () { 15500 reloadVisible(); 15501 }); 15502 } 15503 15504 override void registerMovement() { 15505 super.registerMovement(); 15506 if(glvw && glvw.smw) 15507 glvw.smw.setViewableArea(this.width, this.height); 15508 } 15509 15510 void reloadVisible() { 15511 auto y = glvw.smw.position.y / glvw.itemSize.height; 15512 15513 // idk why i had this here it doesn't seem to be ueful and actually made last items diasppear 15514 //int offset = glvw.smw.position.y % glvw.itemSize.height; 15515 //if(offset || y >= glvw.itemCount()) 15516 //y--; 15517 15518 if(y < 0) 15519 y = 0; 15520 15521 recomputeChildLayout(); 15522 15523 foreach(item; glvw.items) { 15524 if(y < glvw.itemCount()) { 15525 item.showItemPrivate(y); 15526 item.show(); 15527 } else { 15528 item.hide(); 15529 } 15530 y++; 15531 } 15532 15533 this.redraw(); 15534 } 15535 15536 private GenericListViewWidget glvw; 15537 15538 private bool inRcl; 15539 override void recomputeChildLayout() { 15540 if(inRcl) 15541 return; 15542 inRcl = true; 15543 scope(exit) 15544 inRcl = false; 15545 15546 registerMovement(); 15547 15548 auto ih = glvw.itemSize().height; 15549 15550 auto itemCount = this.height / ih + 2; // extra for partial display before and after 15551 bool hadNew; 15552 while(glvw.items.length < itemCount) { 15553 // FIXME: free the old items? maybe just set length 15554 glvw.items ~= glvw.itemFactory(this); 15555 hadNew = true; 15556 } 15557 15558 if(hadNew) 15559 reloadVisible(); 15560 15561 int y = -(glvw.smw.position.y % ih) + this.paddingTop(); 15562 foreach(child; children) { 15563 child.x = this.paddingLeft(); 15564 child.y = y; 15565 y += glvw.itemSize().height; 15566 child.width = this.width.NonOverflowingUint - this.paddingLeft() - this.paddingRight(); 15567 child.height = ih; 15568 15569 child.recomputeChildLayout(); 15570 } 15571 } 15572 } 15573 15574 15575 15576 /++ 15577 History: 15578 It was a child of Window before, but as of September 29, 2024, it is now a child of `Dialog`. 15579 +/ 15580 class MessageBox : Dialog { 15581 private string message; 15582 MessageBoxButton buttonPressed = MessageBoxButton.None; 15583 /++ 15584 15585 History: 15586 The overload that takes `Window originator` was added on September 29, 2024. 15587 +/ 15588 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 15589 this(null, message, buttons, buttonIds); 15590 } 15591 /// ditto 15592 this(Window originator, string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 15593 message = message.stripRightInternal; 15594 int mainWidth; 15595 15596 // estimate longest line 15597 int count; 15598 foreach(ch; message) { 15599 if(ch == '\n') { 15600 if(count > mainWidth) 15601 mainWidth = count; 15602 count = 0; 15603 } else { 15604 count++; 15605 } 15606 } 15607 mainWidth *= 8; 15608 if(mainWidth < 300) 15609 mainWidth = 300; 15610 if(mainWidth > 600) 15611 mainWidth = 600; 15612 15613 super(originator, mainWidth, 100); 15614 15615 assert(buttons.length); 15616 assert(buttons.length == buttonIds.length); 15617 15618 this.message = message; 15619 15620 auto label = new TextDisplay(message, this); 15621 15622 auto hl = new HorizontalLayout(this); 15623 auto spacer = new HorizontalSpacer(hl); // to right align 15624 15625 foreach(idx, buttonText; buttons) { 15626 auto button = new CommandButton(buttonText, hl); 15627 15628 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 15629 this.buttonPressed = buttonIds[idx]; 15630 win.close(); 15631 }; })(idx)); 15632 15633 if(idx == 0) 15634 button.focus(); 15635 } 15636 15637 if(buttons.length == 1) 15638 auto spacer2 = new HorizontalSpacer(hl); // to center it 15639 15640 auto size = label.flexBasisHeight() + hl.minHeight() + this.paddingTop + this.paddingBottom; 15641 auto max = scaleWithDpi(600); // random max height 15642 if(size > max) 15643 size = max; 15644 15645 win.resize(scaleWithDpi(mainWidth), size); 15646 15647 win.show(); 15648 redraw(); 15649 } 15650 15651 override void OK() { 15652 this.win.close(); 15653 } 15654 15655 mixin Padding!q{16}; 15656 } 15657 15658 /// 15659 enum MessageBoxStyle { 15660 OK, /// 15661 OKCancel, /// 15662 RetryCancel, /// 15663 YesNo, /// 15664 YesNoCancel, /// 15665 RetryCancelContinue /// In a multi-part process, if one part fails, ask the user if you should retry that failed step, cancel the entire process, or just continue with the next step, accepting failure on this step. 15666 } 15667 15668 /// 15669 enum MessageBoxIcon { 15670 None, /// 15671 Info, /// 15672 Warning, /// 15673 Error /// 15674 } 15675 15676 /// Identifies the button the user pressed on a message box. 15677 enum MessageBoxButton { 15678 None, /// The user closed the message box without clicking any of the buttons. 15679 OK, /// 15680 Cancel, /// 15681 Retry, /// 15682 Yes, /// 15683 No, /// 15684 Continue /// 15685 } 15686 15687 15688 /++ 15689 Displays a modal message box, blocking until the user dismisses it. These global ones are discouraged in favor of the same methods on [Window], which give better user experience since the message box is tied the parent window instead of acting independently. 15690 15691 Returns: the button pressed. 15692 +/ 15693 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15694 return messageBox(null, title, message, style, icon); 15695 } 15696 15697 /// ditto 15698 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15699 return messageBox(null, null, message, style, icon); 15700 } 15701 15702 /++ 15703 15704 +/ 15705 MessageBoxButton messageBox(Window originator, string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15706 version(win32_widgets) { 15707 WCharzBuffer t = WCharzBuffer(title); 15708 WCharzBuffer m = WCharzBuffer(message); 15709 UINT type; 15710 with(MessageBoxStyle) 15711 final switch(style) { 15712 case OK: type |= MB_OK; break; 15713 case OKCancel: type |= MB_OKCANCEL; break; 15714 case RetryCancel: type |= MB_RETRYCANCEL; break; 15715 case YesNo: type |= MB_YESNO; break; 15716 case YesNoCancel: type |= MB_YESNOCANCEL; break; 15717 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 15718 } 15719 with(MessageBoxIcon) 15720 final switch(icon) { 15721 case None: break; 15722 case Info: type |= MB_ICONINFORMATION; break; 15723 case Warning: type |= MB_ICONWARNING; break; 15724 case Error: type |= MB_ICONERROR; break; 15725 } 15726 switch(MessageBoxW(originator is null ? null : originator.win.hwnd, m.ptr, t.ptr, type)) { 15727 case IDOK: return MessageBoxButton.OK; 15728 case IDCANCEL: return MessageBoxButton.Cancel; 15729 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 15730 case IDYES: return MessageBoxButton.Yes; 15731 case IDNO: return MessageBoxButton.No; 15732 case IDCONTINUE: return MessageBoxButton.Continue; 15733 default: return MessageBoxButton.None; 15734 } 15735 } else { 15736 string[] buttons; 15737 MessageBoxButton[] buttonIds; 15738 with(MessageBoxStyle) 15739 final switch(style) { 15740 case OK: 15741 buttons = ["OK"]; 15742 buttonIds = [MessageBoxButton.OK]; 15743 break; 15744 case OKCancel: 15745 buttons = ["OK", "Cancel"]; 15746 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 15747 break; 15748 case RetryCancel: 15749 buttons = ["Retry", "Cancel"]; 15750 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 15751 break; 15752 case YesNo: 15753 buttons = ["Yes", "No"]; 15754 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 15755 break; 15756 case YesNoCancel: 15757 buttons = ["Yes", "No", "Cancel"]; 15758 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 15759 break; 15760 case RetryCancelContinue: 15761 buttons = ["Try Again", "Cancel", "Continue"]; 15762 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 15763 break; 15764 } 15765 auto mb = new MessageBox(originator, message, buttons, buttonIds); 15766 EventLoop el = EventLoop.get; 15767 el.run(() { return !mb.win.closed; }); 15768 return mb.buttonPressed; 15769 } 15770 15771 } 15772 15773 /// ditto 15774 int messageBox(Window originator, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 15775 return messageBox(originator, null, message, style, icon); 15776 } 15777 15778 15779 /// 15780 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 15781 15782 /++ 15783 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 15784 15785 History: 15786 The data members were `public` (albeit undocumented and not intended for use) prior to May 13, 2021. They are now `private`, reflecting the single intended use of this object. 15787 +/ 15788 struct EventListener { 15789 private Widget widget; 15790 private string event; 15791 private EventHandler handler; 15792 private bool useCapture; 15793 15794 /// 15795 void disconnect() { 15796 if(widget !is null && handler !is null) 15797 widget.removeEventListener(this); 15798 } 15799 } 15800 15801 /++ 15802 The purpose of this enum was to give a compile-time checked version of various standard event strings. 15803 15804 Now, I recommend you use a statically typed event object instead. 15805 15806 See_Also: [Event] 15807 +/ 15808 enum EventType : string { 15809 click = "click", /// 15810 15811 mouseenter = "mouseenter", /// 15812 mouseleave = "mouseleave", /// 15813 mousein = "mousein", /// 15814 mouseout = "mouseout", /// 15815 mouseup = "mouseup", /// 15816 mousedown = "mousedown", /// 15817 mousemove = "mousemove", /// 15818 15819 keydown = "keydown", /// 15820 keyup = "keyup", /// 15821 char_ = "char", /// 15822 15823 focus = "focus", /// 15824 blur = "blur", /// 15825 15826 triggered = "triggered", /// 15827 15828 change = "change", /// 15829 } 15830 15831 /++ 15832 Represents an event that is currently being processed. 15833 15834 15835 Minigui's event model is based on the web browser. An event has a name, a target, 15836 and an associated data object. It starts from the window and works its way down through 15837 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 15838 then goes back up again all the way back to the window, triggering bubble phase handlers. At 15839 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 15840 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 15841 was called; that just stops it from calling other handlers in the widget tree, but the default happens 15842 whenever propagation is done, not only if it gets to the end of the chain). 15843 15844 This model has several nice points: 15845 15846 $(LIST 15847 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 15848 with event handlers set, then add/remove children as much as you want without needing 15849 to manage the event handlers on them - the parent alone can manage everything. 15850 15851 * It is easy to create new custom events in your application. 15852 15853 * It is familiar to many web developers. 15854 ) 15855 15856 There's a few downsides though: 15857 15858 $(LIST 15859 * There's not a lot of type safety. 15860 15861 * You don't get a static list of what events a widget can emit. 15862 15863 * Tracing where an event got cancelled along the chain can get difficult; the downside of 15864 the central delegation benefit is it can be lead to debugging of action at a distance. 15865 ) 15866 15867 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 15868 while keeping the benefits - and most compatibility with - the existing model. The main idea is 15869 to simply use a D object type which provides a static interface as well as a built-in event name. 15870 Then, a new static interface allows you to see what an event can emit and attach handlers to it 15871 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 15872 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 15873 to having a little more help from the D compiler and documentation generator. 15874 15875 Your code would change like this: 15876 15877 --- 15878 // old 15879 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 15880 15881 // new 15882 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 15883 --- 15884 15885 The old-style code will still work, but using certain members of the [Event] class will generate deprecation warnings. Changing handlers to the new style will silence all those warnings at once without requiring any other changes to your code. 15886 15887 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 15888 15889 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 15890 15891 Thus the family of functions are: 15892 15893 [Widget.addEventListener] is the fully-flexible base method. It has two main overload families: one with the string and one without. The one with the string takes the Event object, the one without determines the string from the type you pass. The string "*" matches ALL events that pass through. 15894 15895 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 15896 15897 [Widget.setDefaultEventHandler] is what is called if no preventDefault was called. This should be called in the widget's constructor to set default behaivor. Default event handlers are only called on the event target. 15898 15899 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 15900 15901 --- 15902 class MyCheckbox : Widget { 15903 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 15904 /// It is NOT actually required but should be used whenever possible. 15905 mixin Emits!(ChangeEvent!bool); 15906 15907 this(Widget parent) { 15908 super(parent); 15909 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 15910 } 15911 15912 private bool _checked; 15913 @property bool checked() { return _checked; } 15914 @property void checked(bool set) { 15915 _checked = set; 15916 emit!(ChangeEvent!bool)(&checked); 15917 } 15918 } 15919 --- 15920 15921 ## Creating Your Own Events 15922 15923 To avoid clashing in the string namespace, your events should use your module and class name as the event string. The simple code `mixin Register;` in your Event subclass will do this for you. You should mark events `final` unless you specifically plan to use it as a shared base. Only `Widget` and final classes should actually be sent (and preferably, not even `Widget`), with few exceptions. 15924 15925 --- 15926 final class MyEvent : Event { 15927 this(Widget target) { super(EventString, target); } 15928 mixin Register; // adds EventString and other reflection information 15929 } 15930 --- 15931 15932 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 15933 15934 History: 15935 Prior to May 2021, Event had a set of pre-made members with no extensibility (outside of diy casts) and no static checks on field presence. 15936 15937 After that, those old pre-made members are deprecated accessors and the fields are moved to child classes. To transition, change string events to typed events or do a dynamic cast (don't forget the null check!) in your handler. 15938 +/ 15939 /+ 15940 15941 ## General Conventions 15942 15943 Change events should NOT be emitted when a value is changed programmatically. Indeed, methods should usually not send events. The point of an event is to know something changed and when you call a method, you already know about it. 15944 15945 15946 ## Qt-style signals and slots 15947 15948 Some events make sense to use with just name and data type. These are one-way notifications with no propagation nor default behavior and thus separate from the other event system. 15949 15950 The intention is for events to be used when 15951 15952 --- 15953 class Demo : Widget { 15954 this() { 15955 myPropertyChanged = Signal!int(this); 15956 } 15957 @property myProperty(int v) { 15958 myPropertyChanged.emit(v); 15959 } 15960 15961 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 15962 // but it can just genuinely not care about `this` since that's not really passed. 15963 } 15964 15965 class Foo : Widget { 15966 // the slot uda is not necessary, but it helps the script and ui builder find it. 15967 @slot void setValue(int v) { ... } 15968 } 15969 15970 demo.myPropertyChanged.connect(&foo.setValue); 15971 --- 15972 15973 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 15974 15975 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 15976 15977 class StringChangeEvent : ChangeEvent, Signal!string { 15978 mixin SignalImpl 15979 } 15980 15981 +/ 15982 class Event : ReflectableProperties { 15983 /// Creates an event without populating any members and without sending it. See [dispatch] 15984 this(string eventName, Widget emittedBy) { 15985 this.eventName = eventName; 15986 this.srcElement = emittedBy; 15987 } 15988 15989 15990 /// Implementations for the [ReflectableProperties] interface/ 15991 void getPropertiesList(scope void delegate(string name) sink) const {} 15992 /// ditto 15993 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 15994 /// ditto 15995 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 15996 return SetPropertyResult.notPermitted; 15997 } 15998 15999 16000 /+ 16001 /++ 16002 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 16003 16004 It is just protected so the mixin template can see it from user modules. If I made it private, even my own mixin template couldn't see it due to mixin scoping rules. 16005 +/ 16006 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 16007 if(value.length == 0) { 16008 finalSink(memberName, `""`); 16009 return; 16010 } 16011 16012 char[1024] bufferBacking; 16013 char[] buffer = bufferBacking; 16014 int bufferPosition; 16015 16016 void sink(char ch) { 16017 if(bufferPosition >= buffer.length) 16018 buffer.length = buffer.length + 1024; 16019 buffer[bufferPosition++] = ch; 16020 } 16021 16022 sink('"'); 16023 16024 foreach(ch; value) { 16025 switch(ch) { 16026 case '\\': 16027 sink('\\'); sink('\\'); 16028 break; 16029 case '"': 16030 sink('\\'); sink('"'); 16031 break; 16032 case '\n': 16033 sink('\\'); sink('n'); 16034 break; 16035 case '\r': 16036 sink('\\'); sink('r'); 16037 break; 16038 case '\t': 16039 sink('\\'); sink('t'); 16040 break; 16041 default: 16042 sink(ch); 16043 } 16044 } 16045 16046 sink('"'); 16047 16048 finalSink(memberName, buffer[0 .. bufferPosition]); 16049 } 16050 +/ 16051 16052 /+ 16053 enum EventInitiator { 16054 system, 16055 minigui, 16056 user 16057 } 16058 16059 immutable EventInitiator; initiatedBy; 16060 +/ 16061 16062 /++ 16063 Events should generally follow the propagation model, but there's some exceptions 16064 to that rule. If so, they should override this to return false. In that case, only 16065 bubbling event handlers on the target itself and capturing event handlers on the containing 16066 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 16067 capture -> target -> bubble process.) 16068 16069 History: 16070 Added May 12, 2021 16071 +/ 16072 bool propagates() const pure nothrow @nogc @safe { 16073 return true; 16074 } 16075 16076 /++ 16077 hints as to whether preventDefault will actually do anything. not entirely reliable. 16078 16079 History: 16080 Added May 14, 2021 16081 +/ 16082 bool cancelable() const pure nothrow @nogc @safe { 16083 return true; 16084 } 16085 16086 /++ 16087 You can mix this into child class to register some boilerplate. It includes the `EventString` 16088 member, a constructor, and implementations of the dynamic get data interfaces. 16089 16090 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 16091 16092 16093 You can override the default EventString by simply providing your own in the form of 16094 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 16095 which provides some namespace protection against conflicts in other libraries while still being fairly 16096 easy to use. 16097 16098 If you provide your own constructor, it will override the default constructor provided here. A constructor 16099 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 16100 first argument to your constructor. 16101 16102 History: 16103 Added May 13, 2021. 16104 +/ 16105 protected static mixin template Register() { 16106 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 16107 this(Widget target) { super(EventString, target); } 16108 16109 mixin ReflectableProperties.RegisterGetters; 16110 } 16111 16112 /++ 16113 This is the widget that emitted the event. 16114 16115 16116 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 16117 16118 History: 16119 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 16120 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 16121 so I don't intend to remove these aliases. 16122 +/ 16123 Widget source; 16124 /// ditto 16125 alias source target; 16126 /// ditto 16127 alias source srcElement; 16128 16129 Widget relatedTarget; /// Note: likely to be deprecated at some point. 16130 16131 /// Prevents the default event handler (if there is one) from being called 16132 void preventDefault() { 16133 lastDefaultPrevented = true; 16134 defaultPrevented = true; 16135 } 16136 16137 /// Stops the event propagation immediately. 16138 void stopPropagation() { 16139 propagationStopped = true; 16140 } 16141 16142 private bool defaultPrevented; 16143 private bool propagationStopped; 16144 private string eventName; 16145 16146 private bool isBubbling; 16147 16148 /// This is an internal implementation detail you should not use. It would be private if the language allowed it and it may be removed without notice. 16149 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 16150 16151 /++ 16152 this sends it only to the target. If you want propagation, use dispatch() instead. 16153 16154 This should be made private!!! 16155 16156 +/ 16157 void sendDirectly() { 16158 if(srcElement is null) 16159 return; 16160 16161 // i capturing on the parent too. The main reason for this is that gives a central place to log all events for the debug window. 16162 16163 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 16164 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 16165 16166 if(auto e = target.parentWindow) { 16167 if(auto handlers = "*" in e.capturingEventHandlers) 16168 foreach(handler; *handlers) 16169 if(handler) handler(e, this); 16170 if(auto handlers = eventName in e.capturingEventHandlers) 16171 foreach(handler; *handlers) 16172 if(handler) handler(e, this); 16173 } 16174 16175 auto e = srcElement; 16176 16177 if(auto handlers = eventName in e.bubblingEventHandlers) 16178 foreach(handler; *handlers) 16179 if(handler) handler(e, this); 16180 16181 if(auto handlers = "*" in e.bubblingEventHandlers) 16182 foreach(handler; *handlers) 16183 if(handler) handler(e, this); 16184 16185 // there's never a default for a catch-all event 16186 if(!defaultPrevented) 16187 if(eventName in e.defaultEventHandlers) 16188 e.defaultEventHandlers[eventName](e, this); 16189 } 16190 16191 /// this dispatches the element using the capture -> target -> bubble process 16192 void dispatch() { 16193 if(srcElement is null) 16194 return; 16195 16196 if(!propagates) { 16197 sendDirectly; 16198 return; 16199 } 16200 16201 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 16202 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 16203 16204 // first capture, then bubble 16205 16206 Widget[] chain; 16207 Widget curr = srcElement; 16208 while(curr) { 16209 auto l = curr; 16210 chain ~= l; 16211 curr = curr.parent; 16212 } 16213 16214 isBubbling = false; 16215 16216 foreach_reverse(e; chain) { 16217 if(auto handlers = "*" in e.capturingEventHandlers) 16218 foreach(handler; *handlers) if(handler !is null) handler(e, this); 16219 16220 if(propagationStopped) 16221 break; 16222 16223 if(auto handlers = eventName in e.capturingEventHandlers) 16224 foreach(handler; *handlers) if(handler !is null) handler(e, this); 16225 16226 // the default on capture should really be to always do nothing 16227 16228 //if(!defaultPrevented) 16229 // if(eventName in e.defaultEventHandlers) 16230 // e.defaultEventHandlers[eventName](e.element, this); 16231 16232 if(propagationStopped) 16233 break; 16234 } 16235 16236 int adjustX; 16237 int adjustY; 16238 16239 isBubbling = true; 16240 if(!propagationStopped) 16241 foreach(e; chain) { 16242 if(auto handlers = eventName in e.bubblingEventHandlers) 16243 foreach(handler; *handlers) if(handler !is null) handler(e, this); 16244 16245 if(propagationStopped) 16246 break; 16247 16248 if(auto handlers = "*" in e.bubblingEventHandlers) 16249 foreach(handler; *handlers) if(handler !is null) handler(e, this); 16250 16251 if(propagationStopped) 16252 break; 16253 16254 if(e.encapsulatedChildren()) { 16255 adjustClientCoordinates(adjustX, adjustY); 16256 target = e; 16257 } else { 16258 adjustX += e.x; 16259 adjustY += e.y; 16260 } 16261 } 16262 16263 if(!defaultPrevented) 16264 foreach(e; chain) { 16265 if(eventName in e.defaultEventHandlers) 16266 e.defaultEventHandlers[eventName](e, this); 16267 } 16268 } 16269 16270 16271 /* old compatibility things */ 16272 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 16273 final @property { 16274 Key key() { return (cast(KeyEventBase) this).key; } 16275 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 16276 16277 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 16278 bool altKey() { return (cast(KeyEventBase) this).altKey; } 16279 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 16280 } 16281 16282 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 16283 final @property { 16284 int clientX() { return (cast(MouseEventBase) this).clientX; } 16285 int clientY() { return (cast(MouseEventBase) this).clientY; } 16286 16287 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 16288 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 16289 16290 int button() { return (cast(MouseEventBase) this).button; } 16291 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 16292 } 16293 16294 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 16295 final @property { 16296 int state() { 16297 if(auto meb = cast(MouseEventBase) this) 16298 return meb.state; 16299 if(auto keb = cast(KeyEventBase) this) 16300 return keb.state; 16301 assert(0); 16302 } 16303 } 16304 16305 deprecated("Use a CharEvent instead of Event in your handler going forward") 16306 final @property { 16307 dchar character() { 16308 if(auto ce = cast(CharEvent) this) 16309 return ce.character; 16310 return dchar.init; 16311 } 16312 } 16313 16314 // for change events 16315 @property { 16316 /// 16317 int intValue() { return 0; } 16318 /// 16319 string stringValue() { return null; } 16320 } 16321 } 16322 16323 /++ 16324 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 16325 16326 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 16327 dynamic and custom events, but the static list helps ensure you get them right. 16328 16329 If this is declared, you can use [Widget.emit] to send the event. 16330 16331 All events work the same way though, following the capture->widget->bubble model described under [Event]. 16332 16333 History: 16334 Added May 4, 2021 16335 +/ 16336 mixin template Emits(EventType) { 16337 import arsd.minigui : EventString; 16338 static if(is(EventType : Event) && !is(EventType == Event)) 16339 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 16340 else 16341 static assert(0, "You can only emit subclasses of Event"); 16342 } 16343 16344 /// ditto 16345 mixin template Emits(string eventString) { 16346 mixin("private Event[0] emits_" ~ eventString ~";"); 16347 } 16348 16349 /* 16350 class SignalEvent(string name) : Event { 16351 16352 } 16353 */ 16354 16355 /++ 16356 Command Events are used with a widget wants to issue a higher-level, yet loosely coupled command do its parents and other interested listeners, for example, "scroll up". 16357 16358 16359 Command Events are a bit special in the way they're used. You don't typically refer to them by object, but instead by a name string and a set of arguments. The expectation is that they will be delegated to a parent, which "consumes" the command - it handles it and stops its propagation upward. The [consumesCommand] method will call your handler with the arguments, then stop the command event's propagation for you, meaning you don't have to call [Event.stopPropagation]. A command event should have no default behavior, so calling [Event.preventDefault] is not necessary either. 16360 16361 History: 16362 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 16363 +/ 16364 class CommandEvent : Event { 16365 enum EventString = "command"; 16366 this(Widget source, string CommandString = EventString) { 16367 super(CommandString, source); 16368 } 16369 } 16370 16371 /++ 16372 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 16373 +/ 16374 class CommandEventWithArgs(Args...) : CommandEvent { 16375 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 16376 Args args; 16377 } 16378 16379 /++ 16380 Declares that the given widget consumes a command identified by the `CommandString` AND containing `Args`. Your `handler` is called with the arguments, then the event's propagation is stopped, so it will not be seen by the consumer's parents. 16381 16382 See [CommandEvent] for more information. 16383 16384 Returns: 16385 The [EventListener] you can use to remove the handler. 16386 +/ 16387 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 16388 return w.addEventListener(CommandString, (Event ev) { 16389 if(ev.target is w) 16390 return; // it does not consume its own commands! 16391 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 16392 handler(cev.args); 16393 ev.stopPropagation(); 16394 } 16395 }); 16396 } 16397 16398 /++ 16399 Emits a command to the sender widget's parents with the given `CommandString` and `args`. You have no way of knowing if it was ever actually consumed due to the loose coupling. Instead, the consumer may broadcast a state update back toward you. 16400 +/ 16401 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 16402 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 16403 event.dispatch(); 16404 } 16405 16406 /++ 16407 Widgets emit `ResizeEvent`s any time they are resized. You check [Widget.width] and [Widget.height] upon receiving this event to know the new size. 16408 16409 If you need to know the old size, you need to store it yourself. 16410 16411 History: 16412 Made final on January 3, 2025 (dub v12.0) 16413 +/ 16414 final class ResizeEvent : Event { 16415 enum EventString = "resize"; 16416 16417 this(Widget target) { super(EventString, target); } 16418 16419 override bool propagates() const { return false; } 16420 } 16421 16422 /++ 16423 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 16424 16425 ClosedEvent happens when the window has been closed. It is already gone by the time this event fires, meaning you cannot prevent the close. Use [ClosingEvent] if you want to cancel, use [ClosedEvent] if you simply want to be notified. 16426 16427 History: 16428 Added June 21, 2021 (dub v10.1) 16429 16430 Made final on January 3, 2025 (dub v12.0) 16431 +/ 16432 final class ClosingEvent : Event { 16433 enum EventString = "closing"; 16434 16435 this(Widget target) { super(EventString, target); } 16436 16437 override bool propagates() const { return false; } 16438 override bool cancelable() const { return true; } 16439 } 16440 16441 /// ditto 16442 final class ClosedEvent : Event { 16443 enum EventString = "closed"; 16444 16445 this(Widget target) { super(EventString, target); } 16446 16447 override bool propagates() const { return false; } 16448 override bool cancelable() const { return false; } 16449 } 16450 16451 /// 16452 final class BlurEvent : Event { 16453 enum EventString = "blur"; 16454 16455 // FIXME: related target? 16456 this(Widget target) { super(EventString, target); } 16457 16458 override bool propagates() const { return false; } 16459 } 16460 16461 /// 16462 final class FocusEvent : Event { 16463 enum EventString = "focus"; 16464 16465 // FIXME: related target? 16466 this(Widget target) { super(EventString, target); } 16467 16468 override bool propagates() const { return false; } 16469 } 16470 16471 /++ 16472 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 16473 16474 History: 16475 Added July 3, 2021 16476 +/ 16477 final class FocusInEvent : Event { 16478 enum EventString = "focusin"; 16479 16480 // FIXME: related target? 16481 this(Widget target) { super(EventString, target); } 16482 16483 override bool cancelable() const { return false; } 16484 } 16485 16486 /// ditto 16487 final class FocusOutEvent : Event { 16488 enum EventString = "focusout"; 16489 16490 // FIXME: related target? 16491 this(Widget target) { super(EventString, target); } 16492 16493 override bool cancelable() const { return false; } 16494 } 16495 16496 /// 16497 final class ScrollEvent : Event { 16498 enum EventString = "scroll"; 16499 this(Widget target) { super(EventString, target); } 16500 16501 override bool cancelable() const { return false; } 16502 } 16503 16504 /++ 16505 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 16506 16507 History: 16508 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 16509 +/ 16510 final class CharEvent : Event { 16511 enum EventString = "char"; 16512 this(Widget target, dchar ch) { 16513 character = ch; 16514 super(EventString, target); 16515 } 16516 16517 immutable dchar character; 16518 } 16519 16520 /++ 16521 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 16522 +/ 16523 abstract class ChangeEventBase : Event { 16524 enum EventString = "change"; 16525 this(Widget target) { 16526 super(EventString, target); 16527 } 16528 16529 /+ 16530 // idk where or how exactly i want to do this. 16531 // i might come back to it later. 16532 16533 // If a widget itself broadcasts one of theses itself, it stops propagation going down 16534 // this way the source doesn't get too confused (think of a nested scroll widget) 16535 // 16536 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 16537 // then you consume that command and change you scroll x position to whatever. then you do 16538 // some kind of change event that is broadcast back to the children and any horizontal scroll 16539 // listeners are now able to update, without having an explicit connection between them. 16540 void broadcastToChildren(string fieldName) { 16541 16542 } 16543 +/ 16544 } 16545 16546 /++ 16547 Single-value widgets (that is, ones with a programming interface that just expose a value that the user has control over) should emit this after their value changes. 16548 16549 16550 Generally speaking, if your widget can reasonably have a `@property T value();` or `@property bool checked();` method, it should probably emit this event when that value changes to inform its parents that they can now read a new value. Whether you emit it on each keystroke or other intermediate values or only when a value is committed (e.g. when the user leaves the field) is up to the widget. You might even make that a togglable property depending on your needs (emitting events can get expensive). 16551 16552 The delegate you pass to the constructor ought to be a handle to your getter property. If your widget has `@property string value()` for example, you emit `ChangeEvent!string(&value);` 16553 16554 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 16555 16556 History: 16557 Added May 11, 2021. Prior to that, widgets would more likely just send `new Event("change")`. These typed ChangeEvents are still compatible with listeners subscribed to generic change events. 16558 +/ 16559 final class ChangeEvent(T) : ChangeEventBase { 16560 this(Widget target, T delegate() getNewValue) { 16561 assert(getNewValue !is null); 16562 this.getNewValue = getNewValue; 16563 super(target); 16564 } 16565 16566 private T delegate() getNewValue; 16567 16568 /++ 16569 Gets the new value that just changed. 16570 +/ 16571 @property T value() { 16572 return getNewValue(); 16573 } 16574 16575 /// compatibility method for old generic Events 16576 static if(is(immutable T == immutable int)) 16577 override int intValue() { return value; } 16578 /// ditto 16579 static if(is(immutable T == immutable string)) 16580 override string stringValue() { return value; } 16581 } 16582 16583 /++ 16584 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 16585 16586 16587 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16588 16589 History: 16590 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 16591 +/ 16592 abstract class KeyEventBase : Event { 16593 this(string name, Widget target) { 16594 super(name, target); 16595 } 16596 16597 // for key events 16598 Key key; /// 16599 16600 KeyEvent originalKeyEvent; 16601 16602 /++ 16603 Indicates the current state of the given keyboard modifier keys. 16604 16605 History: 16606 Added to events on April 15, 2020. 16607 +/ 16608 bool ctrlKey; 16609 16610 /// ditto 16611 bool altKey; 16612 16613 /// ditto 16614 bool shiftKey; 16615 16616 /++ 16617 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 16618 16619 See [arsd.simpledisplay.ModifierState] for other possible flags. 16620 +/ 16621 int state; 16622 16623 mixin Register; 16624 } 16625 16626 /++ 16627 Indicates that the user has pressed a key on the keyboard, or if they've been holding it long enough to repeat (key down events are sent both on the initial press then repeated by the OS on its own time.) For available properties, see [KeyEventBase]. 16628 16629 16630 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16631 16632 Please note that a `KeyDownEvent` will also often send a [CharEvent], but there is not necessarily a one-to-one relationship between them. For example, a capital letter may send KeyDownEvent for Key.Shift, then KeyDownEvent for the letter's key (this key may not match the letter due to keyboard mappings), then CharEvent for the letter, then KeyUpEvent for the letter, and finally, KeyUpEvent for shift. 16633 16634 For some characters, there are other key down events as well. A compose key can be pressed and released, followed by several letters pressed and released to generate one character. This is why [CharEvent] is a separate entity. 16635 16636 See_Also: [KeyUpEvent], [CharEvent] 16637 16638 History: 16639 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 16640 +/ 16641 final class KeyDownEvent : KeyEventBase { 16642 enum EventString = "keydown"; 16643 this(Widget target) { super(EventString, target); } 16644 } 16645 16646 /++ 16647 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 16648 16649 16650 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16651 16652 See_Also: [KeyDownEvent], [CharEvent] 16653 16654 History: 16655 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 16656 +/ 16657 final class KeyUpEvent : KeyEventBase { 16658 enum EventString = "keyup"; 16659 this(Widget target) { super(EventString, target); } 16660 } 16661 16662 /++ 16663 Contains shared properties for various mouse events; 16664 16665 16666 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16667 16668 History: 16669 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 16670 +/ 16671 abstract class MouseEventBase : Event { 16672 this(string name, Widget target) { 16673 super(name, target); 16674 } 16675 16676 // for mouse events 16677 int clientX; /// The mouse event location relative to the target widget 16678 int clientY; /// ditto 16679 16680 int viewportX; /// The mouse event location relative to the window origin 16681 int viewportY; /// ditto 16682 16683 int button; /// See: [MouseEvent.button] 16684 int buttonLinear; /// See: [MouseEvent.buttonLinear] 16685 16686 /++ 16687 Indicates the current state of the given keyboard modifier keys. 16688 16689 History: 16690 Added to mouse events on September 28, 2010. 16691 +/ 16692 bool ctrlKey; 16693 16694 /// ditto 16695 bool altKey; 16696 16697 /// ditto 16698 bool shiftKey; 16699 16700 16701 16702 int state; /// 16703 16704 /++ 16705 for consistent names with key event. 16706 16707 History: 16708 Added September 28, 2021 (dub v10.3) 16709 +/ 16710 alias modifierState = state; 16711 16712 /++ 16713 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 16714 16715 History: 16716 Added May 15, 2021 16717 +/ 16718 bool isMouseWheel() { 16719 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 16720 } 16721 16722 // private 16723 override void adjustClientCoordinates(int deltaX, int deltaY) { 16724 clientX += deltaX; 16725 clientY += deltaY; 16726 } 16727 16728 mixin Register; 16729 } 16730 16731 /++ 16732 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 16733 16734 16735 $(WARNING 16736 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 16737 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 16738 behavior. 16739 16740 Use [MouseEventBase.isMouseWheel] to filter wheel events while keeping others. 16741 ) 16742 16743 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 16744 16745 [MouseUpEvent] is sent when the user releases a mouse button. 16746 16747 [MouseMoveEvent] is sent when the mouse is moved. Please note you may not receive this in some cases unless a button is also pressed; the system is free to withhold them as an optimization. (In practice, [arsd.simpledisplay] does not request mouse motion event without a held button if it is on a remote X11 link, but does elsewhere at this time.) 16748 16749 [ClickEvent] is sent when the user clicks on the widget. It may also be sent with keyboard control, though minigui prefers to send a "triggered" event in addition to a mouse click and instead of a simulated mouse click in cases like keyboard activation of a button. 16750 16751 [DoubleClickEvent] is sent when the user clicks twice on a thing quickly, immediately after the second MouseDownEvent. The sequence is: MouseDownEvent, MouseUpEvent, ClickEvent, MouseDownEvent, DoubleClickEvent, MouseUpEvent. The second ClickEvent is NOT sent. Note that this is different than Javascript! They would send down,up,click,down,up,click,dblclick. Minigui does it differently because this is the way the Windows OS reports it. 16752 16753 [MouseOverEvent] is sent then the mouse first goes over a widget. Please note that this participates in event propagation of children! Use [MouseEnterEvent] instead if you are only interested in a specific element's whole bounding box instead of the top-most element in any particular location. 16754 16755 [MouseOutEvent] is sent when the mouse exits a target. Please note that this participates in event propagation of children! Use [MouseLeaveEvent] instead if you are only interested in a specific element's whole bounding box instead of the top-most element in any particular location. 16756 16757 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 16758 16759 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 16760 16761 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 16762 16763 Rationale: 16764 16765 If you only want to do drag, mousedown/up works just fine being consistently sent. 16766 16767 If you want click, that event does what you expect (if the user mouse downs then moves the mouse off the widget before going up, no click event happens - a click is only down and back up on the same thing). 16768 16769 If you want double click and listen to that specifically, it also just works, and if you only cared about clicks, odds are the double click should do the same thing as a single click anyway - the double was prolly accidental - so only sending the event once is prolly what user intended. 16770 16771 History: 16772 Added May 2, 2021. Previously, it was only seen as the base [Event] class on event listeners. See the member [EventString] to see what the associated string is with these elements. 16773 +/ 16774 final class MouseUpEvent : MouseEventBase { 16775 enum EventString = "mouseup"; /// 16776 this(Widget target) { super(EventString, target); } 16777 } 16778 /// ditto 16779 final class MouseDownEvent : MouseEventBase { 16780 enum EventString = "mousedown"; /// 16781 this(Widget target) { super(EventString, target); } 16782 } 16783 /// ditto 16784 final class MouseMoveEvent : MouseEventBase { 16785 enum EventString = "mousemove"; /// 16786 this(Widget target) { super(EventString, target); } 16787 } 16788 /// ditto 16789 final class ClickEvent : MouseEventBase { 16790 enum EventString = "click"; /// 16791 this(Widget target) { super(EventString, target); } 16792 } 16793 /// ditto 16794 final class DoubleClickEvent : MouseEventBase { 16795 enum EventString = "dblclick"; /// 16796 this(Widget target) { super(EventString, target); } 16797 } 16798 /// ditto 16799 final class MouseOverEvent : Event { 16800 enum EventString = "mouseover"; /// 16801 this(Widget target) { super(EventString, target); } 16802 } 16803 /// ditto 16804 final class MouseOutEvent : Event { 16805 enum EventString = "mouseout"; /// 16806 this(Widget target) { super(EventString, target); } 16807 } 16808 /// ditto 16809 final class MouseEnterEvent : Event { 16810 enum EventString = "mouseenter"; /// 16811 this(Widget target) { super(EventString, target); } 16812 16813 override bool propagates() const { return false; } 16814 } 16815 /// ditto 16816 final class MouseLeaveEvent : Event { 16817 enum EventString = "mouseleave"; /// 16818 this(Widget target) { super(EventString, target); } 16819 16820 override bool propagates() const { return false; } 16821 } 16822 16823 private bool isAParentOf(Widget a, Widget b) { 16824 if(a is null || b is null) 16825 return false; 16826 16827 while(b !is null) { 16828 if(a is b) 16829 return true; 16830 b = b.parent; 16831 } 16832 16833 return false; 16834 } 16835 16836 private struct WidgetAtPointResponse { 16837 Widget widget; 16838 16839 // x, y relative to the widget in the response. 16840 int x; 16841 int y; 16842 } 16843 16844 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 16845 assert(starting !is null); 16846 16847 starting.addScrollPosition(x, y); 16848 16849 auto child = starting.getChildAtPosition(x, y); 16850 while(child) { 16851 if(child.hidden) 16852 continue; 16853 starting = child; 16854 x -= child.x; 16855 y -= child.y; 16856 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 16857 child = r.widget; 16858 if(child is starting) 16859 break; 16860 } 16861 return WidgetAtPointResponse(starting, x, y); 16862 } 16863 16864 version(win32_widgets) { 16865 private: 16866 import core.sys.windows.commctrl; 16867 16868 pragma(lib, "comctl32"); 16869 shared static this() { 16870 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 16871 INITCOMMONCONTROLSEX ic; 16872 ic.dwSize = cast(DWORD) ic.sizeof; 16873 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 16874 if(!InitCommonControlsEx(&ic)) { 16875 //writeln("ICC failed"); 16876 } 16877 } 16878 16879 16880 // everything from here is just win32 headers copy pasta 16881 private: 16882 extern(Windows): 16883 16884 alias HANDLE HMENU; 16885 HMENU CreateMenu(); 16886 bool SetMenu(HWND, HMENU); 16887 HMENU CreatePopupMenu(); 16888 enum MF_POPUP = 0x10; 16889 enum MF_STRING = 0; 16890 16891 16892 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 16893 struct INITCOMMONCONTROLSEX { 16894 DWORD dwSize; 16895 DWORD dwICC; 16896 } 16897 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 16898 enum { 16899 IDB_STD_SMALL_COLOR, 16900 IDB_STD_LARGE_COLOR, 16901 IDB_VIEW_SMALL_COLOR = 4, 16902 IDB_VIEW_LARGE_COLOR = 5 16903 } 16904 enum { 16905 STD_CUT, 16906 STD_COPY, 16907 STD_PASTE, 16908 STD_UNDO, 16909 STD_REDOW, 16910 STD_DELETE, 16911 STD_FILENEW, 16912 STD_FILEOPEN, 16913 STD_FILESAVE, 16914 STD_PRINTPRE, 16915 STD_PROPERTIES, 16916 STD_HELP, 16917 STD_FIND, 16918 STD_REPLACE, 16919 STD_PRINT // = 14 16920 } 16921 16922 alias HANDLE HIMAGELIST; 16923 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 16924 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 16925 BOOL ImageList_Destroy(HIMAGELIST); 16926 16927 uint MAKELONG(ushort a, ushort b) { 16928 return cast(uint) ((b << 16) | a); 16929 } 16930 16931 16932 struct TBBUTTON { 16933 int iBitmap; 16934 int idCommand; 16935 BYTE fsState; 16936 BYTE fsStyle; 16937 version(Win64) 16938 BYTE[6] bReserved; 16939 else 16940 BYTE[2] bReserved; 16941 DWORD dwData; 16942 INT_PTR iString; 16943 } 16944 16945 enum { 16946 TB_ADDBUTTONSA = WM_USER + 20, 16947 TB_INSERTBUTTONA = WM_USER + 21, 16948 TB_GETIDEALSIZE = WM_USER + 99, 16949 } 16950 16951 struct SIZE { 16952 LONG cx; 16953 LONG cy; 16954 } 16955 16956 16957 enum { 16958 TBSTATE_CHECKED = 1, 16959 TBSTATE_PRESSED = 2, 16960 TBSTATE_ENABLED = 4, 16961 TBSTATE_HIDDEN = 8, 16962 TBSTATE_INDETERMINATE = 16, 16963 TBSTATE_WRAP = 32 16964 } 16965 16966 16967 16968 enum { 16969 ILC_COLOR = 0, 16970 ILC_COLOR4 = 4, 16971 ILC_COLOR8 = 8, 16972 ILC_COLOR16 = 16, 16973 ILC_COLOR24 = 24, 16974 ILC_COLOR32 = 32, 16975 ILC_COLORDDB = 254, 16976 ILC_MASK = 1, 16977 ILC_PALETTE = 2048 16978 } 16979 16980 16981 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 16982 16983 16984 enum { 16985 TB_ENABLEBUTTON = WM_USER + 1, 16986 TB_CHECKBUTTON, 16987 TB_PRESSBUTTON, 16988 TB_HIDEBUTTON, 16989 TB_INDETERMINATE, // = WM_USER + 5, 16990 TB_ISBUTTONENABLED = WM_USER + 9, 16991 TB_ISBUTTONCHECKED, 16992 TB_ISBUTTONPRESSED, 16993 TB_ISBUTTONHIDDEN, 16994 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 16995 TB_SETSTATE = WM_USER + 17, 16996 TB_GETSTATE = WM_USER + 18, 16997 TB_ADDBITMAP = WM_USER + 19, 16998 TB_DELETEBUTTON = WM_USER + 22, 16999 TB_GETBUTTON, 17000 TB_BUTTONCOUNT, 17001 TB_COMMANDTOINDEX, 17002 TB_SAVERESTOREA, 17003 TB_CUSTOMIZE, 17004 TB_ADDSTRINGA, 17005 TB_GETITEMRECT, 17006 TB_BUTTONSTRUCTSIZE, 17007 TB_SETBUTTONSIZE, 17008 TB_SETBITMAPSIZE, 17009 TB_AUTOSIZE, // = WM_USER + 33, 17010 TB_GETTOOLTIPS = WM_USER + 35, 17011 TB_SETTOOLTIPS = WM_USER + 36, 17012 TB_SETPARENT = WM_USER + 37, 17013 TB_SETROWS = WM_USER + 39, 17014 TB_GETROWS, 17015 TB_GETBITMAPFLAGS, 17016 TB_SETCMDID, 17017 TB_CHANGEBITMAP, 17018 TB_GETBITMAP, 17019 TB_GETBUTTONTEXTA, 17020 TB_REPLACEBITMAP, // = WM_USER + 46, 17021 TB_GETBUTTONSIZE = WM_USER + 58, 17022 TB_SETBUTTONWIDTH = WM_USER + 59, 17023 TB_GETBUTTONTEXTW = WM_USER + 75, 17024 TB_SAVERESTOREW = WM_USER + 76, 17025 TB_ADDSTRINGW = WM_USER + 77, 17026 } 17027 17028 extern(Windows) 17029 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 17030 17031 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 17032 17033 17034 enum { 17035 TB_SETINDENT = WM_USER + 47, 17036 TB_SETIMAGELIST, 17037 TB_GETIMAGELIST, 17038 TB_LOADIMAGES, 17039 TB_GETRECT, 17040 TB_SETHOTIMAGELIST, 17041 TB_GETHOTIMAGELIST, 17042 TB_SETDISABLEDIMAGELIST, 17043 TB_GETDISABLEDIMAGELIST, 17044 TB_SETSTYLE, 17045 TB_GETSTYLE, 17046 //TB_GETBUTTONSIZE, 17047 //TB_SETBUTTONWIDTH, 17048 TB_SETMAXTEXTROWS, 17049 TB_GETTEXTROWS // = WM_USER + 61 17050 } 17051 17052 enum { 17053 CCM_FIRST = 0x2000, 17054 CCM_LAST = CCM_FIRST + 0x200, 17055 CCM_SETBKCOLOR = 8193, 17056 CCM_SETCOLORSCHEME = 8194, 17057 CCM_GETCOLORSCHEME = 8195, 17058 CCM_GETDROPTARGET = 8196, 17059 CCM_SETUNICODEFORMAT = 8197, 17060 CCM_GETUNICODEFORMAT = 8198, 17061 CCM_SETVERSION = 0x2007, 17062 CCM_GETVERSION = 0x2008, 17063 CCM_SETNOTIFYWINDOW = 0x2009 17064 } 17065 17066 17067 enum { 17068 PBM_SETRANGE = WM_USER + 1, 17069 PBM_SETPOS, 17070 PBM_DELTAPOS, 17071 PBM_SETSTEP, 17072 PBM_STEPIT, // = WM_USER + 5 17073 PBM_SETRANGE32 = 1030, 17074 PBM_GETRANGE, 17075 PBM_GETPOS, 17076 PBM_SETBARCOLOR, // = 1033 17077 PBM_SETBKCOLOR = CCM_SETBKCOLOR 17078 } 17079 17080 enum { 17081 PBS_SMOOTH = 1, 17082 PBS_VERTICAL = 4 17083 } 17084 17085 enum { 17086 ICC_LISTVIEW_CLASSES = 1, 17087 ICC_TREEVIEW_CLASSES = 2, 17088 ICC_BAR_CLASSES = 4, 17089 ICC_TAB_CLASSES = 8, 17090 ICC_UPDOWN_CLASS = 16, 17091 ICC_PROGRESS_CLASS = 32, 17092 ICC_HOTKEY_CLASS = 64, 17093 ICC_ANIMATE_CLASS = 128, 17094 ICC_WIN95_CLASSES = 255, 17095 ICC_DATE_CLASSES = 256, 17096 ICC_USEREX_CLASSES = 512, 17097 ICC_COOL_CLASSES = 1024, 17098 ICC_STANDARD_CLASSES = 0x00004000, 17099 } 17100 17101 enum WM_USER = 1024; 17102 } 17103 17104 version(win32_widgets) 17105 pragma(lib, "comdlg32"); 17106 17107 17108 /// 17109 enum GenericIcons : ushort { 17110 None, /// 17111 // these happen to match the win32 std icons numerically if you just subtract one from the value 17112 Cut, /// 17113 Copy, /// 17114 Paste, /// 17115 Undo, /// 17116 Redo, /// 17117 Delete, /// 17118 New, /// 17119 Open, /// 17120 Save, /// 17121 PrintPreview, /// 17122 Properties, /// 17123 Help, /// 17124 Find, /// 17125 Replace, /// 17126 Print, /// 17127 } 17128 17129 enum FileDialogType { 17130 Automatic, 17131 Open, 17132 Save 17133 } 17134 17135 /++ 17136 The default string [FileName] refers to to store the last file referenced. You can use this if you like, or provide a different variable to `FileName` in your function. 17137 +/ 17138 string previousFileReferenced; 17139 17140 /++ 17141 Used in automatic menu functions to indicate that the user should be able to browse for a file. 17142 17143 Params: 17144 storage = an alias to a `static string` variable that stores the last file referenced. It will 17145 use this to pre-fill the dialog with a suggestion. 17146 17147 Please note that it MUST be `static` or you will get compile errors. 17148 17149 filters = the filters param to [getFileName] 17150 17151 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 17152 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 17153 a save dialog box. Otherwise, it will show an open dialog box. 17154 +/ 17155 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 17156 string name; 17157 alias name this; 17158 17159 @implicit this(string name) { 17160 this.name = name; 17161 } 17162 } 17163 17164 /++ 17165 Gets a file name for an open or save operation, calling your `onOK` function when the user has selected one. This function may or may not block depending on the operating system, you MUST assume it will complete asynchronously. 17166 17167 History: 17168 onCancel was added November 6, 2021. 17169 17170 The dialog itself on Linux was modified on December 2, 2021 to include 17171 a directory picker in addition to the command line completion view. 17172 17173 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 17174 17175 The `owner` argument was added September 29, 2024. The overloads without this argument are likely to be deprecated in the next major version. 17176 Future_directions: 17177 I want to add some kind of custom preview and maybe thumbnail thing in the future, 17178 at least on Linux, maybe on Windows too. 17179 +/ 17180 void getOpenFileName( 17181 Window owner, 17182 void delegate(string) onOK, 17183 string prefilledName = null, 17184 string[] filters = null, 17185 void delegate() onCancel = null, 17186 string initialDirectory = null, 17187 ) 17188 { 17189 return getFileName(owner, true, onOK, prefilledName, filters, onCancel, initialDirectory); 17190 } 17191 17192 /// ditto 17193 void getSaveFileName( 17194 Window owner, 17195 void delegate(string) onOK, 17196 string prefilledName = null, 17197 string[] filters = null, 17198 void delegate() onCancel = null, 17199 string initialDirectory = null, 17200 ) 17201 { 17202 return getFileName(owner, false, onOK, prefilledName, filters, onCancel, initialDirectory); 17203 } 17204 17205 // deprecated("Pass an explicit owner window as the first argument, even if `null`. You can usually pass the `parentWindow` member of the widget that prompted this interaction.") 17206 /// ditto 17207 void getOpenFileName( 17208 void delegate(string) onOK, 17209 string prefilledName = null, 17210 string[] filters = null, 17211 void delegate() onCancel = null, 17212 string initialDirectory = null, 17213 ) 17214 { 17215 return getFileName(null, true, onOK, prefilledName, filters, onCancel, initialDirectory); 17216 } 17217 17218 /// ditto 17219 void getSaveFileName( 17220 void delegate(string) onOK, 17221 string prefilledName = null, 17222 string[] filters = null, 17223 void delegate() onCancel = null, 17224 string initialDirectory = null, 17225 ) 17226 { 17227 return getFileName(null, false, onOK, prefilledName, filters, onCancel, initialDirectory); 17228 } 17229 17230 /++ 17231 It is possible to override or customize the file dialog in some cases. These members provide those hooks: you do `fileDialogDelegate = new YourSubclassOf_FileDialogDelegate;` and you can do your own thing. 17232 17233 This is a customization hook and you should not call methods on this class directly. Use the public functions [getOpenFileName] and [getSaveFileName], or make an automatic dialog with [FileName] instead. 17234 17235 History: 17236 Added January 1, 2025 17237 +/ 17238 class FileDialogDelegate { 17239 17240 /++ 17241 17242 +/ 17243 static abstract class PreviewWidget : Widget { 17244 /// Call this from your subclass' constructor 17245 this(Widget parent) { 17246 super(parent); 17247 } 17248 17249 /// Load the file given to you and show its preview inside the widget here 17250 abstract void previewFile(string filename); 17251 } 17252 17253 /++ 17254 Override this to add preview capabilities to the dialog for certain files. 17255 +/ 17256 protected PreviewWidget makePreviewWidget(Widget parent) { 17257 return null; 17258 } 17259 17260 /++ 17261 Override this to change the dialog entirely. 17262 17263 This function IS allowed to block, but is NOT required to. 17264 +/ 17265 protected void getFileName( 17266 Window owner, 17267 bool openOrSave, // true if open, false if save 17268 void delegate(string) onOK, 17269 string prefilledName, 17270 string[] filters, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 17271 void delegate() onCancel, 17272 string initialDirectory, 17273 ) 17274 { 17275 17276 version(win32_widgets) { 17277 import core.sys.windows.commdlg; 17278 /* 17279 Ofn.lStructSize = sizeof(OPENFILENAME); 17280 Ofn.hwndOwner = hWnd; 17281 Ofn.lpstrFilter = szFilter; 17282 Ofn.lpstrFile= szFile; 17283 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 17284 Ofn.lpstrFileTitle = szFileTitle; 17285 Ofn.nMaxFileTitle = sizeof(szFileTitle); 17286 Ofn.lpstrInitialDir = (LPSTR)NULL; 17287 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 17288 Ofn.lpstrTitle = szTitle; 17289 */ 17290 17291 17292 wchar[1024] file = 0; 17293 wchar[1024] filterBuffer = 0; 17294 makeWindowsString(prefilledName, file[]); 17295 OPENFILENAME ofn; 17296 ofn.lStructSize = ofn.sizeof; 17297 ofn.hwndOwner = owner is null ? null : owner.win.hwnd; 17298 if(filters.length) { 17299 string filter; 17300 foreach(i, f; filters) { 17301 filter ~= f; 17302 filter ~= "\0"; 17303 } 17304 filter ~= "\0"; 17305 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 17306 } 17307 ofn.lpstrFile = file.ptr; 17308 ofn.nMaxFile = file.length; 17309 17310 wchar[1024] initialDir = 0; 17311 if(initialDirectory !is null) { 17312 makeWindowsString(initialDirectory, initialDir[]); 17313 ofn.lpstrInitialDir = file.ptr; 17314 } 17315 17316 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 17317 { 17318 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 17319 if(okString.length && okString[$-1] == '\0') 17320 okString = okString[0..$-1]; 17321 onOK(okString); 17322 } else { 17323 if(onCancel) 17324 onCancel(); 17325 } 17326 } else version(custom_widgets) { 17327 filters ~= ["All Files\0*.*"]; 17328 auto picker = new FilePicker(openOrSave, prefilledName, filters, initialDirectory, owner); 17329 picker.onOK = onOK; 17330 picker.onCancel = onCancel; 17331 picker.show(); 17332 } 17333 } 17334 17335 } 17336 17337 /// ditto 17338 FileDialogDelegate fileDialogDelegate() { 17339 if(fileDialogDelegate_ is null) 17340 fileDialogDelegate_ = new FileDialogDelegate(); 17341 return fileDialogDelegate_; 17342 } 17343 17344 /// ditto 17345 void fileDialogDelegate(FileDialogDelegate replacement) { 17346 fileDialogDelegate_ = replacement; 17347 } 17348 17349 private FileDialogDelegate fileDialogDelegate_; 17350 17351 struct FileNameFilter { 17352 string description; 17353 string[] globPatterns; 17354 17355 string toString() { 17356 string ret; 17357 ret ~= description; 17358 ret ~= " ("; 17359 foreach(idx, pattern; globPatterns) { 17360 if(idx) 17361 ret ~= "; "; 17362 ret ~= pattern; 17363 } 17364 ret ~= ")"; 17365 17366 return ret; 17367 } 17368 17369 static FileNameFilter fromString(string s) { 17370 size_t end = s.length; 17371 size_t start = 0; 17372 foreach_reverse(idx, ch; s) { 17373 if(ch == ')' && end == s.length) 17374 end = idx; 17375 else if(ch == '(' && end != s.length) { 17376 start = idx + 1; 17377 break; 17378 } 17379 } 17380 17381 FileNameFilter fnf; 17382 fnf.description = s[0 .. start ? start - 1 : 0]; 17383 size_t globStart = 0; 17384 s = s[start .. end]; 17385 foreach(idx, ch; s) 17386 if(ch == ';') { 17387 auto ptn = stripInternal(s[globStart .. idx]); 17388 if(ptn.length) 17389 fnf.globPatterns ~= ptn; 17390 globStart = idx + 1; 17391 17392 } 17393 auto ptn = stripInternal(s[globStart .. $]); 17394 if(ptn.length) 17395 fnf.globPatterns ~= ptn; 17396 return fnf; 17397 } 17398 } 17399 17400 struct FileNameFilterSet { 17401 FileNameFilter[] filters; 17402 17403 static FileNameFilterSet fromWindowsFileNameFilterDescription(string[] filters) { 17404 FileNameFilter[] ret; 17405 17406 foreach(filter; filters) { 17407 FileNameFilter fnf; 17408 size_t filterStartPoint; 17409 foreach(idx, ch; filter) { 17410 if(ch == 0) { 17411 fnf.description = filter[0 .. idx]; 17412 filterStartPoint = idx + 1; 17413 } else if(filterStartPoint && ch == ';') { 17414 fnf.globPatterns ~= filter[filterStartPoint .. idx]; 17415 filterStartPoint = idx + 1; 17416 } 17417 } 17418 fnf.globPatterns ~= filter[filterStartPoint .. $]; 17419 17420 ret ~= fnf; 17421 } 17422 17423 return FileNameFilterSet(ret); 17424 } 17425 } 17426 17427 void getFileName( 17428 Window owner, 17429 bool openOrSave, 17430 void delegate(string) onOK, 17431 string prefilledName = null, 17432 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 17433 void delegate() onCancel = null, 17434 string initialDirectory = null, 17435 ) 17436 { 17437 return fileDialogDelegate().getFileName(owner, openOrSave, onOK, prefilledName, filters, onCancel, initialDirectory); 17438 } 17439 17440 version(custom_widgets) 17441 private 17442 class FilePicker : Dialog { 17443 void delegate(string) onOK; 17444 void delegate() onCancel; 17445 LabeledLineEdit lineEdit; 17446 bool isOpenDialogInsteadOfSave; 17447 17448 static struct HistoryItem { 17449 string cwd; 17450 FileNameFilter filters; 17451 } 17452 HistoryItem[] historyStack; 17453 size_t historyStackPosition; 17454 17455 void back() { 17456 if(historyStackPosition) { 17457 historyStackPosition--; 17458 currentDirectory = historyStack[historyStackPosition].cwd; 17459 currentFilter = historyStack[historyStackPosition].filters; 17460 filesOfType.content = currentFilter.toString(); 17461 loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true); 17462 lineEdit.focus(); 17463 } 17464 } 17465 17466 void forward() { 17467 if(historyStackPosition + 1 < historyStack.length) { 17468 historyStackPosition++; 17469 currentDirectory = historyStack[historyStackPosition].cwd; 17470 currentFilter = historyStack[historyStackPosition].filters; 17471 filesOfType.content = currentFilter.toString(); 17472 loadFiles(historyStack[historyStackPosition].cwd, historyStack[historyStackPosition].filters, true); 17473 lineEdit.focus(); 17474 } 17475 } 17476 17477 void up() { 17478 currentDirectory = currentDirectory ~ ".."; 17479 loadFiles(currentDirectory, currentFilter); 17480 lineEdit.focus(); 17481 } 17482 17483 void refresh() { 17484 loadFiles(currentDirectory, currentFilter); 17485 lineEdit.focus(); 17486 } 17487 17488 // returns common prefix 17489 static struct CommonPrefixInfo { 17490 string commonPrefix; 17491 int fileCount; 17492 string exactMatch; 17493 } 17494 CommonPrefixInfo loadFiles(string cwd, FileNameFilter filters, bool comingFromHistory = false) { 17495 17496 if(!comingFromHistory) { 17497 if(historyStack.length) { 17498 historyStack = historyStack[0 .. historyStackPosition + 1]; 17499 historyStack.assumeSafeAppend(); 17500 } 17501 historyStack ~= HistoryItem(cwd, filters); 17502 historyStackPosition = historyStack.length - 1; 17503 } 17504 17505 string[] files; 17506 string[] dirs; 17507 17508 dirs ~= "$HOME"; 17509 dirs ~= "$PWD"; 17510 17511 string commonPrefix; 17512 int commonPrefixCount; 17513 string exactMatch; 17514 17515 bool matchesFilter(string name) { 17516 foreach(filter; filters.globPatterns) { 17517 if( 17518 filter.length <= 1 || 17519 filter == "*.*" || // we always treat *.* the same as *, but it is a bit different than .* 17520 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 17521 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 17522 ) 17523 { 17524 if(name.length > 1 && name[0] == '.') 17525 if(filter.length == 0 || filter[0] != '.') 17526 return false; 17527 17528 return true; 17529 } 17530 } 17531 17532 return false; 17533 } 17534 17535 void considerCommonPrefix(string name, bool prefiltered) { 17536 if(!prefiltered && !matchesFilter(name)) 17537 return; 17538 17539 if(commonPrefix is null) { 17540 commonPrefix = name; 17541 commonPrefixCount = 1; 17542 exactMatch = commonPrefix; 17543 } else { 17544 foreach(idx, char i; name) { 17545 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 17546 commonPrefix = commonPrefix[0 .. idx]; 17547 commonPrefixCount ++; 17548 exactMatch = null; 17549 break; 17550 } 17551 } 17552 } 17553 } 17554 17555 bool applyFilterToDirectories = true; 17556 bool showDotFiles = false; 17557 foreach(filter; filters.globPatterns) { 17558 if(filter == ".*") 17559 showDotFiles = true; 17560 else foreach(ch; filter) 17561 if(ch == '.') { 17562 // a filter like *.exe should not apply to the directory 17563 applyFilterToDirectories = false; 17564 break; 17565 } 17566 } 17567 17568 try 17569 getFiles(cwd, (string name, bool isDirectory) { 17570 if(name == ".") 17571 return; // skip this as unnecessary 17572 if(isDirectory) { 17573 if(applyFilterToDirectories) { 17574 if(matchesFilter(name)) { 17575 dirs ~= name; 17576 considerCommonPrefix(name, false); 17577 } 17578 } else if(name != ".." && name.length > 1 && name[0] == '.') { 17579 if(showDotFiles) { 17580 dirs ~= name; 17581 considerCommonPrefix(name, false); 17582 } 17583 } else { 17584 dirs ~= name; 17585 considerCommonPrefix(name, false); 17586 } 17587 } else { 17588 if(matchesFilter(name)) { 17589 files ~= name; 17590 17591 //if(filter.length > 0 && filter[$-1] == '*') { 17592 considerCommonPrefix(name, true); 17593 //} 17594 } 17595 } 17596 }); 17597 catch(ArsdExceptionBase e) { 17598 messageBox("Unable to read requested directory"); 17599 // FIXME: give them a chance to create it? or at least go back? 17600 /+ 17601 comingFromHistory = true; 17602 back(); 17603 return null; 17604 +/ 17605 } 17606 17607 extern(C) static int comparator(scope const void* a, scope const void* b) { 17608 auto sa = *cast(string*) a; 17609 auto sb = *cast(string*) b; 17610 17611 /+ 17612 Goal here: 17613 17614 Dot first. This puts `foo.d` before `foo2.d` 17615 Then numbers , natural sort order (so 9 comes before 10) for positive numbers 17616 Then letters, in order Aa, Bb, Cc 17617 Then other symbols in ascii order 17618 +/ 17619 static int nextPiece(ref string whole) { 17620 if(whole.length == 0) 17621 return -1; 17622 17623 enum specialZoneSize = 1; 17624 17625 char current = whole[0]; 17626 if(current >= '0' && current <= '9') { 17627 int accumulator; 17628 do { 17629 whole = whole[1 .. $]; 17630 accumulator *= 10; 17631 accumulator += current - '0'; 17632 current = whole.length ? whole[0] : 0; 17633 } while (current >= '0' && current <= '9'); 17634 17635 return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols 17636 } else { 17637 whole = whole[1 .. $]; 17638 17639 if(current == '.') 17640 return 0; // the special case to put it before numbers 17641 17642 // anything above should be < specialZoneSize 17643 17644 int letterZoneSize = 26 * 2; 17645 int base = int.max - letterZoneSize - char.max; // leaves space at end for symbols too if we want them after chars 17646 17647 if(current >= 'A' && current <= 'Z') 17648 return base + (current - 'A') * 2; 17649 if(current >= 'a' && current <= 'z') 17650 return base + (current - 'a') * 2 + 1; 17651 // return base + letterZoneSize + current; // would put symbols after numbers and letters 17652 return specialZoneSize + current; // puts symbols before numbers and letters, but after the special zone 17653 } 17654 } 17655 17656 while(sa.length || sb.length) { 17657 auto pa = nextPiece(sa); 17658 auto pb = nextPiece(sb); 17659 17660 auto diff = pa - pb; 17661 if(diff) 17662 return diff; 17663 } 17664 17665 return 0; 17666 } 17667 17668 nonPhobosSort(files, &comparator); 17669 nonPhobosSort(dirs, &comparator); 17670 17671 listWidget.clear(); 17672 dirWidget.clear(); 17673 foreach(name; dirs) 17674 dirWidget.addOption(name); 17675 foreach(name; files) 17676 listWidget.addOption(name); 17677 17678 return CommonPrefixInfo(commonPrefix, commonPrefixCount, exactMatch); 17679 } 17680 17681 ListWidget listWidget; 17682 ListWidget dirWidget; 17683 17684 FreeEntrySelection filesOfType; 17685 LineEdit directoryHolder; 17686 17687 string currentDirectory_; 17688 FileNameFilter currentNonTabFilter; 17689 FileNameFilter currentFilter; 17690 FileNameFilterSet filterOptions; 17691 17692 void currentDirectory(string s) { 17693 currentDirectory_ = FilePath(s).makeAbsolute(getCurrentWorkingDirectory()).toString(); 17694 directoryHolder.content = currentDirectory_; 17695 } 17696 string currentDirectory() { 17697 return currentDirectory_; 17698 } 17699 17700 private string getUserHomeDir() { 17701 import core.stdc.stdlib; 17702 version(Windows) 17703 return (stringz(getenv("HOMEDRIVE")).borrow ~ stringz(getenv("HOMEPATH")).borrow).idup; 17704 else 17705 return (stringz(getenv("HOME")).borrow).idup; 17706 } 17707 17708 private string expandTilde(string s) { 17709 // FIXME: cannot look up other user dirs 17710 if(s.length == 1 && s == "~") 17711 return getUserHomeDir(); 17712 if(s.length > 1 && s[0] == '~' && s[1] == '/') 17713 return getUserHomeDir() ~ s[1 .. $]; 17714 return s; 17715 } 17716 17717 // FIXME: allow many files to be picked too sometimes 17718 17719 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 17720 this(bool isOpenDialogInsteadOfSave, string prefilledName, string[] filtersInWindowsFormat, string initialDirectory, Window owner = null) { 17721 this.filterOptions = FileNameFilterSet.fromWindowsFileNameFilterDescription(filtersInWindowsFormat); 17722 this.isOpenDialogInsteadOfSave = isOpenDialogInsteadOfSave; 17723 super(owner, 500, 400, "Choose File..."); // owner); 17724 17725 { 17726 auto navbar = new HorizontalLayout(24, this); 17727 auto backButton = new ToolButton(new Action("<", 0, &this.back), navbar); 17728 auto forwardButton = new ToolButton(new Action(">", 0, &this.forward), navbar); 17729 auto upButton = new ToolButton(new Action("^", 0, &this.up), navbar); // hmm with .. in the dir list we don't really need an up button 17730 17731 directoryHolder = new LineEdit(navbar); 17732 17733 directoryHolder.addEventListener(delegate(scope KeyDownEvent kde) { 17734 if(kde.key == Key.Enter || kde.key == Key.PadEnter) { 17735 kde.stopPropagation(); 17736 17737 currentDirectory = directoryHolder.content; 17738 loadFiles(currentDirectory, currentFilter); 17739 17740 lineEdit.focus(); 17741 } 17742 }); 17743 17744 auto refreshButton = new ToolButton(new Action("R", 0, &this.refresh), navbar); // can live without refresh since you can cancel and reopen but still nice. it should be automatic when it can maybe. 17745 17746 /+ 17747 auto newDirectoryButton = new ToolButton(new Action("N"), navbar); 17748 17749 // FIXME: make sure putting `.` in the dir filter goes back to the CWD 17750 // and that ~ goes back to the home dir 17751 // and blanking it goes back to the suggested dir 17752 17753 auto homeButton = new ToolButton(new Action("H"), navbar); 17754 auto cwdButton = new ToolButton(new Action("."), navbar); 17755 auto suggestedDirectoryButton = new ToolButton(new Action("*"), navbar); 17756 +/ 17757 17758 filesOfType = new class FreeEntrySelection { 17759 this() { 17760 string[] opt; 17761 foreach(option; filterOptions.filters) 17762 opt ~= option.toString; 17763 super(opt, navbar); 17764 } 17765 override int flexBasisWidth() { 17766 return scaleWithDpi(150); 17767 } 17768 override int widthStretchiness() { 17769 return 1;//super.widthStretchiness() / 2; 17770 } 17771 }; 17772 filesOfType.setSelection(0); 17773 currentFilter = filterOptions.filters[0]; 17774 currentNonTabFilter = currentFilter; 17775 } 17776 17777 { 17778 auto mainGrid = new GridLayout(4, 1, this); 17779 17780 dirWidget = new ListWidget(mainGrid); 17781 listWidget = new ListWidget(mainGrid); 17782 listWidget.tabStop = false; 17783 dirWidget.tabStop = false; 17784 17785 FileDialogDelegate.PreviewWidget previewWidget = fileDialogDelegate.makePreviewWidget(mainGrid); 17786 17787 mainGrid.setChildPosition(dirWidget, 0, 0, 1, 1); 17788 mainGrid.setChildPosition(listWidget, 1, 0, previewWidget !is null ? 2 : 3, 1); 17789 if(previewWidget) 17790 mainGrid.setChildPosition(previewWidget, 2, 0, 1, 1); 17791 17792 // double click events normally trigger something else but 17793 // here user might be clicking kinda fast and we'd rather just 17794 // keep it 17795 dirWidget.addEventListener((scope DoubleClickEvent dev) { 17796 auto ce = new ChangeEvent!void(dirWidget, () {}); 17797 ce.dispatch(); 17798 lineEdit.focus(); 17799 }); 17800 17801 dirWidget.addEventListener((scope ChangeEvent!void sce) { 17802 string v; 17803 foreach(o; dirWidget.options) 17804 if(o.selected) { 17805 v = o.label; 17806 break; 17807 } 17808 if(v.length) { 17809 if(v == "$HOME") 17810 currentDirectory = getUserHomeDir(); 17811 else if(v == "$PWD") 17812 currentDirectory = "."; 17813 else 17814 currentDirectory = currentDirectory ~ "/" ~ v; 17815 loadFiles(currentDirectory, currentFilter); 17816 } 17817 17818 dirWidget.focusOn = -1; 17819 lineEdit.focus(); 17820 }); 17821 17822 // double click here, on the other hand, selects the file 17823 // and moves on 17824 listWidget.addEventListener((scope DoubleClickEvent dev) { 17825 OK(); 17826 }); 17827 } 17828 17829 lineEdit = new LabeledLineEdit("File name:", TextAlignment.Right, this); 17830 lineEdit.focus(); 17831 lineEdit.addEventListener(delegate(CharEvent event) { 17832 if(event.character == '\t' || event.character == '\n') 17833 event.preventDefault(); 17834 }); 17835 17836 listWidget.addEventListener(EventType.change, () { 17837 foreach(o; listWidget.options) 17838 if(o.selected) 17839 lineEdit.content = o.label; 17840 }); 17841 17842 currentDirectory = initialDirectory is null ? "." : initialDirectory; 17843 17844 auto prefilledPath = FilePath(expandTilde(prefilledName)).makeAbsolute(FilePath(currentDirectory)); 17845 currentDirectory = prefilledPath.directoryName; 17846 prefilledName = prefilledPath.filename; 17847 loadFiles(currentDirectory, currentFilter); 17848 17849 filesOfType.addEventListener(delegate (FreeEntrySelection.SelectionChangedEvent ce) { 17850 currentFilter = FileNameFilter.fromString(ce.stringValue); 17851 currentNonTabFilter = currentFilter; 17852 loadFiles(currentDirectory, currentFilter); 17853 // lineEdit.focus(); // this causes a recursive crash..... 17854 }); 17855 17856 filesOfType.addEventListener(delegate(KeyDownEvent event) { 17857 if(event.key == Key.Enter) { 17858 currentFilter = FileNameFilter.fromString(filesOfType.content); 17859 currentNonTabFilter = currentFilter; 17860 loadFiles(currentDirectory, currentFilter); 17861 event.stopPropagation(); 17862 // FIXME: refocus on the line edit 17863 } 17864 }); 17865 17866 lineEdit.addEventListener((KeyDownEvent event) { 17867 if(event.key == Key.Tab && !event.ctrlKey && !event.shiftKey) { 17868 17869 auto path = FilePath(expandTilde(lineEdit.content)).makeAbsolute(FilePath(currentDirectory)); 17870 currentDirectory = path.directoryName; 17871 auto current = path.filename; 17872 17873 auto newFilter = current; 17874 if(current.length && current[0] != '*' && current[$-1] != '*') 17875 newFilter ~= "*"; 17876 else if(newFilter.length == 0) 17877 newFilter = "*"; 17878 17879 auto newFilterObj = FileNameFilter("Custom filter", [newFilter]); 17880 17881 CommonPrefixInfo commonPrefix = loadFiles(currentDirectory, newFilterObj); 17882 if(commonPrefix.fileCount == 1) { 17883 // exactly one file, let's see what it is 17884 auto specificFile = FilePath(commonPrefix.exactMatch).makeAbsolute(FilePath(currentDirectory)); 17885 if(getFileType(specificFile.toString) == FileType.dir) { 17886 // a directory means we should change to it and keep the old filter 17887 currentDirectory = specificFile.toString(); 17888 lineEdit.content = specificFile.toString() ~ "/"; 17889 loadFiles(currentDirectory, currentFilter); 17890 } else { 17891 // any other file should be selected in the list 17892 currentDirectory = specificFile.directoryName; 17893 current = specificFile.filename; 17894 lineEdit.content = current; 17895 loadFiles(currentDirectory, currentFilter); 17896 } 17897 } else if(commonPrefix.fileCount > 1) { 17898 currentFilter = newFilterObj; 17899 filesOfType.content = currentFilter.toString(); 17900 lineEdit.content = commonPrefix.commonPrefix; 17901 } else { 17902 // if there were no files, we don't really want to change the filter.. 17903 //sdpyPrintDebugString("no files"); 17904 } 17905 17906 // FIXME: if that is a directory, add the slash? or even go inside? 17907 17908 event.preventDefault(); 17909 } 17910 else if(event.key == Key.Left && event.altKey) { 17911 this.back(); 17912 event.preventDefault(); 17913 } 17914 else if(event.key == Key.Right && event.altKey) { 17915 this.forward(); 17916 event.preventDefault(); 17917 } 17918 }); 17919 17920 17921 lineEdit.content = prefilledName; 17922 17923 auto hl = new HorizontalLayout(60, this); 17924 auto cancelButton = new Button("Cancel", hl); 17925 auto okButton = new Button(isOpenDialogInsteadOfSave ? "Open" : "Save"/*"OK"*/, hl); 17926 17927 cancelButton.addEventListener(EventType.triggered, &Cancel); 17928 okButton.addEventListener(EventType.triggered, &OK); 17929 17930 this.addEventListener((KeyDownEvent event) { 17931 if(event.key == Key.Enter || event.key == Key.PadEnter) { 17932 event.preventDefault(); 17933 OK(); 17934 } 17935 else if(event.key == Key.Escape) 17936 Cancel(); 17937 else if(event.key == Key.F5) 17938 refresh(); 17939 else if(event.key == Key.Up && event.altKey) 17940 up(); // ditto 17941 else if(event.key == Key.Left && event.altKey) 17942 back(); // FIXME: it sends the key to the line edit too 17943 else if(event.key == Key.Right && event.altKey) 17944 forward(); // ditto 17945 else if(event.key == Key.Up) 17946 listWidget.setSelection(listWidget.getSelection() - 1); 17947 else if(event.key == Key.Down) 17948 listWidget.setSelection(listWidget.getSelection() + 1); 17949 }); 17950 17951 // FIXME: set the list view's focusOn to -1 on most interactions so it doesn't keep a thing highlighted 17952 // FIXME: button to create new directory 17953 // FIXME: show dirs in the files list too? idk. 17954 17955 // FIXME: support ~ as alias for home in the input 17956 // FIXME: tab complete ought to be able to change+complete dir too 17957 } 17958 17959 override void OK() { 17960 if(lineEdit.content.length) { 17961 auto c = expandTilde(lineEdit.content); 17962 17963 FilePath accepted = FilePath(c).makeAbsolute(FilePath(currentDirectory)); 17964 17965 auto ft = getFileType(accepted.toString); 17966 17967 if(ft == FileType.error && isOpenDialogInsteadOfSave) { 17968 // FIXME: tell the user why 17969 messageBox("Cannot open file: " ~ accepted.toString ~ "\nTry another or cancel."); 17970 lineEdit.focus(); 17971 return; 17972 17973 } 17974 17975 // FIXME: symlinks to dirs should prolly also get this behavior 17976 if(ft == FileType.dir) { 17977 currentDirectory = accepted.toString; 17978 17979 currentFilter = currentNonTabFilter; 17980 filesOfType.content = currentFilter.toString(); 17981 17982 loadFiles(currentDirectory, currentFilter); 17983 lineEdit.content = ""; 17984 17985 lineEdit.focus(); 17986 17987 return; 17988 } 17989 17990 if(onOK) 17991 onOK(accepted.toString); 17992 } 17993 close(); 17994 } 17995 17996 override void Cancel() { 17997 if(onCancel) 17998 onCancel(); 17999 close(); 18000 } 18001 } 18002 18003 private enum FileType { 18004 error, 18005 dir, 18006 other 18007 } 18008 18009 private FileType getFileType(string name) { 18010 version(Windows) { 18011 auto ws = WCharzBuffer(name); 18012 auto ret = GetFileAttributesW(ws.ptr); 18013 if(ret == INVALID_FILE_ATTRIBUTES) 18014 return FileType.error; 18015 return ((ret & FILE_ATTRIBUTE_DIRECTORY) != 0) ? FileType.dir : FileType.other; 18016 } else version(Posix) { 18017 import core.sys.posix.sys.stat; 18018 stat_t buf; 18019 auto ret = stat((name ~ '\0').ptr, &buf); 18020 if(ret == -1) 18021 return FileType.error; 18022 return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other; 18023 } else assert(0, "Not implemented"); 18024 } 18025 18026 /* 18027 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 18028 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 18029 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 18030 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 18031 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 18032 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 18033 http://www.sbin.org/doc/Xlib/chapt_03.html 18034 18035 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 18036 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 18037 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 18038 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 18039 */ 18040 18041 18042 // These are all for setMenuAndToolbarFromAnnotatedCode 18043 /// This item in the menu will be preceded by a separator line 18044 /// Group: generating_from_code 18045 struct separator {} 18046 deprecated("It was misspelled, use separator instead") alias seperator = separator; 18047 /// Program-wide keyboard shortcut to trigger the action 18048 /// Group: generating_from_code 18049 struct accelerator { string keyString; } // FIXME: allow multiple aliases here 18050 /// tells which menu the action will be on 18051 /// Group: generating_from_code 18052 struct menu { string name; } 18053 /// Describes which toolbar section the action appears on 18054 /// Group: generating_from_code 18055 struct toolbar { string groupName; } 18056 /// 18057 /// Group: generating_from_code 18058 struct icon { ushort id; } 18059 /// 18060 /// Group: generating_from_code 18061 struct label { string label; } 18062 /// 18063 /// Group: generating_from_code 18064 struct hotkey { dchar ch; } 18065 /// 18066 /// Group: generating_from_code 18067 struct tip { string tip; } 18068 /// 18069 /// Group: generating_from_code 18070 enum context_menu = menu.init; 18071 /++ 18072 // FIXME: the options should have both a label and a value 18073 18074 if label is null, it will try to just stringify value. 18075 18076 if type is int or size_t and it returns a string array, we can use the index but this will implicitly not allow custom, even if allowCustom is set. 18077 +/ 18078 /// Group: generating_from_code 18079 Choices!T choices(T)(T[] options, bool allowCustom = false, bool allowReordering = true, bool allowDuplicates = true) { 18080 return Choices!T(() => options, allowCustom, allowReordering, allowDuplicates); 18081 } 18082 /// ditto 18083 Choices!T choices(T)(T[] delegate() options, bool allowCustom = false, bool allowReordering = true, bool allowDuplicates = true) { 18084 return Choices!T(options, allowCustom, allowReordering, allowDuplicates); 18085 } 18086 /// ditto 18087 struct Choices(T) { 18088 /// 18089 T[] delegate() options; 18090 bool allowCustom = false; 18091 /// only relevant if attached to an array 18092 bool allowReordering = true; 18093 /// ditto 18094 bool allowDuplicates = true; 18095 /// makes no sense on a set 18096 bool requireAll = false; 18097 } 18098 18099 18100 /++ 18101 Observes and allows inspection of an object via automatic gui 18102 +/ 18103 /// Group: generating_from_code 18104 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 18105 return new ObjectInspectionWindowImpl!(T)(t); 18106 } 18107 18108 class ObjectInspectionWindow : Window { 18109 this(int a, int b, string c) { 18110 super(a, b, c); 18111 } 18112 18113 abstract void readUpdatesFromObject(); 18114 } 18115 18116 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 18117 T t; 18118 this(T t) { 18119 this.t = t; 18120 18121 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 18122 18123 foreach(memberName; __traits(derivedMembers, T)) {{ 18124 alias member = I!(__traits(getMember, t, memberName))[0]; 18125 alias type = typeof(member); 18126 static if(is(type == int)) { 18127 auto le = new LabeledLineEdit(memberName ~ ": ", this); 18128 //le.addEventListener("char", (Event ev) { 18129 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 18130 //ev.preventDefault(); 18131 //}); 18132 le.addEventListener(EventType.change, (Event ev) { 18133 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 18134 }); 18135 18136 updateMemberDelegates[memberName] = () { 18137 le.content = toInternal!string(__traits(getMember, t, memberName)); 18138 }; 18139 } 18140 }} 18141 } 18142 18143 void delegate()[string] updateMemberDelegates; 18144 18145 override void readUpdatesFromObject() { 18146 foreach(k, v; updateMemberDelegates) 18147 v(); 18148 } 18149 } 18150 18151 /++ 18152 Creates a dialog based on a data structure. 18153 18154 --- 18155 dialog(window, (YourStructure value) { 18156 // the user filled in the struct and clicked OK, 18157 // you can check the members now 18158 }); 18159 --- 18160 18161 Params: 18162 initialData = the initial value to show in the dialog. It will not modify this unless 18163 it is a class then it might, no promises. 18164 18165 History: 18166 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 18167 18168 The overloads with `parent` were added September 29, 2024. The ones without it are likely to 18169 be deprecated soon. 18170 +/ 18171 /// Group: generating_from_code 18172 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18173 dialog(null, T.init, onOK, onCancel, title); 18174 } 18175 /// ditto 18176 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18177 dialog(null, T.init, onOK, onCancel, title); 18178 } 18179 /// ditto 18180 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18181 dialog(parent, T.init, onOK, onCancel, title); 18182 } 18183 /// ditto 18184 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18185 dialog(parent, initialData, onOK, onCancel, title); 18186 } 18187 /// ditto 18188 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 18189 auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title); 18190 dg.show(); 18191 } 18192 18193 private static template I(T...) { alias I = T; } 18194 18195 18196 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 18197 if(name == "id") 18198 return allLowerCase ? name : "ID"; 18199 18200 char[160] buffer; 18201 int bufferIndex = 0; 18202 bool shouldCap = true; 18203 bool shouldSpace; 18204 bool lastWasCap; 18205 foreach(idx, char ch; name) { 18206 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 18207 18208 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 18209 if(lastWasCap) { 18210 // two caps in a row, don't change. Prolly acronym. 18211 } else { 18212 if(idx) 18213 shouldSpace = true; // new word, add space 18214 } 18215 18216 lastWasCap = true; 18217 } else { 18218 lastWasCap = false; 18219 } 18220 18221 if(shouldSpace) { 18222 buffer[bufferIndex++] = space; 18223 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 18224 shouldSpace = false; 18225 } 18226 if(shouldCap) { 18227 if(ch >= 'a' && ch <= 'z') 18228 ch -= 32; 18229 shouldCap = false; 18230 } 18231 if(allLowerCase && ch >= 'A' && ch <= 'Z') 18232 ch += 32; 18233 buffer[bufferIndex++] = ch; 18234 } 18235 return buffer[0 .. bufferIndex].idup; 18236 } 18237 18238 /++ 18239 This is the implementation for [dialog]. None of its details are guaranteed stable and may change at any time; the stable interface is just the [dialog] function at this time. 18240 +/ 18241 class AutomaticDialog(T) : Dialog { 18242 T t; 18243 18244 void delegate(T) onOK; 18245 void delegate() onCancel; 18246 18247 override int paddingTop() { return defaultLineHeight; } 18248 override int paddingBottom() { return defaultLineHeight; } 18249 override int paddingRight() { return defaultLineHeight; } 18250 override int paddingLeft() { return defaultLineHeight; } 18251 18252 this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 18253 assert(onOK !is null); 18254 18255 t = initialData; 18256 18257 static if(is(T == class)) { 18258 if(t is null) 18259 t = new T(); 18260 } 18261 this.onOK = onOK; 18262 this.onCancel = onCancel; 18263 super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title); 18264 18265 static if(is(T == class)) 18266 this.addDataControllerWidget(t); 18267 else 18268 this.addDataControllerWidget(&t); 18269 18270 auto hl = new HorizontalLayout(this); 18271 auto stretch = new HorizontalSpacer(hl); // to right align 18272 auto ok = new CommandButton("OK", hl); 18273 auto cancel = new CommandButton("Cancel", hl); 18274 ok.addEventListener(EventType.triggered, &OK); 18275 cancel.addEventListener(EventType.triggered, &Cancel); 18276 18277 this.addEventListener((KeyDownEvent ev) { 18278 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 18279 ok.focus(); 18280 OK(); 18281 ev.preventDefault(); 18282 } 18283 if(ev.key == Key.Escape) { 18284 Cancel(); 18285 ev.preventDefault(); 18286 } 18287 }); 18288 18289 this.addEventListener((scope ClosedEvent ce) { 18290 if(onCancel) 18291 onCancel(); 18292 }); 18293 18294 //this.children[0].focus(); 18295 } 18296 18297 override void OK() { 18298 onOK(t); 18299 close(); 18300 } 18301 18302 override void Cancel() { 18303 if(onCancel) 18304 onCancel(); 18305 close(); 18306 } 18307 } 18308 18309 private template baseClassCount(Class) { 18310 private int helper() { 18311 int count = 0; 18312 static if(is(Class bases == super)) { 18313 foreach(base; bases) 18314 static if(is(base == class)) 18315 count += 1 + baseClassCount!base; 18316 } 18317 return count; 18318 } 18319 18320 enum int baseClassCount = helper(); 18321 } 18322 18323 private long stringToLong(string s) { 18324 long ret; 18325 if(s.length == 0) 18326 return ret; 18327 bool negative = s[0] == '-'; 18328 if(negative) 18329 s = s[1 .. $]; 18330 foreach(ch; s) { 18331 if(ch >= '0' && ch <= '9') { 18332 ret *= 10; 18333 ret += ch - '0'; 18334 } 18335 } 18336 if(negative) 18337 ret = -ret; 18338 return ret; 18339 } 18340 18341 18342 interface ReflectableProperties { 18343 /++ 18344 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 18345 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 18346 json in the current implementation. 18347 18348 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 18349 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 18350 as of the June 2, 2021 release. 18351 18352 History: 18353 Added June 2, 2021. 18354 18355 See_Also: [getPropertyAsString], [setPropertyFromString] 18356 +/ 18357 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 18358 /++ 18359 Requests a property to be delivered to you as a string, through your `sink` delegate. 18360 18361 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 18362 be interpreted as json, otherwise, it is just a plain string. 18363 18364 The sink should always be called exactly once for each call (it is basically a return value, but it might 18365 use a local buffer it maintains instead of allocating a return value). 18366 18367 History: 18368 Added June 2, 2021. 18369 18370 See_Also: [getPropertiesList], [setPropertyFromString] 18371 +/ 18372 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 18373 /++ 18374 Sets the given property, if it exists, to the given value, if possible. If `strIsJson` is true, it will json decode (if the implementation wants to) then apply the value, otherwise it will treat it as a plain string. 18375 18376 History: 18377 Added June 2, 2021. 18378 18379 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 18380 +/ 18381 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 18382 18383 /// [setPropertyFromString] possible return values 18384 enum SetPropertyResult { 18385 success = 0, /// the property has been successfully set to the request value 18386 notPermitted = -1, /// the property exists but it cannot be changed at this time 18387 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 18388 noSuchProperty = -3, /// there is no property by that name 18389 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 18390 invalidValue = -5, /// the string is in the correct format, but the specific given value could not be used (for example, because it was out of bounds) 18391 } 18392 18393 /++ 18394 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 18395 18396 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 18397 18398 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 18399 rarely need to use these building blocks directly. 18400 +/ 18401 mixin template RegisterSetters() { 18402 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 18403 switch(name) { 18404 foreach(memberName; __traits(derivedMembers, typeof(this))) { 18405 case memberName: 18406 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 18407 if(value != "true" && value != "false") 18408 return SetPropertyResult.wrongFormat; 18409 __traits(getMember, this, memberName) = value == "true" ? true : false; 18410 return SetPropertyResult.success; 18411 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 18412 import core.stdc.stdlib; 18413 char[128] zero = 0; 18414 if(buffer.length + 1 >= zero.length) 18415 return SetPropertyResult.wrongFormat; 18416 zero[0 .. buffer.length] = buffer[]; 18417 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 18418 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 18419 import core.stdc.stdlib; 18420 char[128] zero = 0; 18421 if(buffer.length + 1 >= zero.length) 18422 return SetPropertyResult.wrongFormat; 18423 zero[0 .. buffer.length] = buffer[]; 18424 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 18425 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 18426 __traits(getMember, this, memberName) = value.idup; 18427 } else { 18428 return SetPropertyResult.notImplemented; 18429 } 18430 18431 } 18432 default: 18433 return super.setPropertyFromString(name, value, valueIsJson); 18434 } 18435 } 18436 } 18437 18438 /++ 18439 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 18440 18441 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 18442 18443 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 18444 rarely need to use these building blocks directly. 18445 +/ 18446 mixin template RegisterGetters() { 18447 override void getPropertiesList(scope void delegate(string name) sink) const { 18448 super.getPropertiesList(sink); 18449 18450 foreach(memberName; __traits(derivedMembers, typeof(this))) { 18451 sink(memberName); 18452 } 18453 } 18454 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 18455 switch(name) { 18456 foreach(memberName; __traits(derivedMembers, typeof(this))) { 18457 case memberName: 18458 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 18459 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 18460 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 18461 import core.stdc.stdio; 18462 char[32] buffer; 18463 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 18464 sink(name, buffer[0 .. len], true); 18465 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 18466 import core.stdc.stdio; 18467 char[32] buffer; 18468 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 18469 sink(name, buffer[0 .. len], true); 18470 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 18471 sink(name, __traits(getMember, this, memberName), false); 18472 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 18473 } else { 18474 sink(name, null, true); 18475 } 18476 18477 return; 18478 } 18479 default: 18480 return super.getPropertyAsString(name, sink); 18481 } 18482 } 18483 } 18484 } 18485 18486 private struct Stack(T) { 18487 this(int maxSize) { 18488 internalLength = 0; 18489 arr = initialBuffer[]; 18490 } 18491 18492 ///. 18493 void push(T t) { 18494 if(internalLength >= arr.length) { 18495 auto oldarr = arr; 18496 if(arr.length < 4096) 18497 arr = new T[arr.length * 2]; 18498 else 18499 arr = new T[arr.length + 4096]; 18500 arr[0 .. oldarr.length] = oldarr[]; 18501 } 18502 18503 arr[internalLength] = t; 18504 internalLength++; 18505 } 18506 18507 ///. 18508 T pop() { 18509 assert(internalLength); 18510 internalLength--; 18511 return arr[internalLength]; 18512 } 18513 18514 ///. 18515 T peek() { 18516 assert(internalLength); 18517 return arr[internalLength - 1]; 18518 } 18519 18520 ///. 18521 @property bool empty() { 18522 return internalLength ? false : true; 18523 } 18524 18525 ///. 18526 private T[] arr; 18527 private size_t internalLength; 18528 private T[64] initialBuffer; 18529 // the static array is allocated with this object, so if we have a small stack (which we prolly do; dom trees usually aren't insanely deep), 18530 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 18531 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 18532 } 18533 18534 /// This is the lazy range that walks the tree for you. It tries to go in the lexical order of the source: node, then children from first to last, each recursively. 18535 private struct WidgetStream { 18536 18537 ///. 18538 @property Widget front() { 18539 return current.widget; 18540 } 18541 18542 /// Use Widget.tree instead. 18543 this(Widget start) { 18544 current.widget = start; 18545 current.childPosition = -1; 18546 isEmpty = false; 18547 stack = typeof(stack)(0); 18548 } 18549 18550 /* 18551 Handle it 18552 handle its children 18553 18554 */ 18555 18556 ///. 18557 void popFront() { 18558 more: 18559 if(isEmpty) return; 18560 18561 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 18562 18563 current.childPosition++; 18564 if(current.childPosition >= current.widget.children.length) { 18565 if(stack.empty()) 18566 isEmpty = true; 18567 else { 18568 current = stack.pop(); 18569 goto more; 18570 } 18571 } else { 18572 stack.push(current); 18573 current.widget = current.widget.children[current.childPosition]; 18574 current.childPosition = -1; 18575 } 18576 } 18577 18578 ///. 18579 @property bool empty() { 18580 return isEmpty; 18581 } 18582 18583 private: 18584 18585 struct Current { 18586 Widget widget; 18587 int childPosition; 18588 } 18589 18590 Current current; 18591 18592 Stack!(Current) stack; 18593 18594 bool isEmpty; 18595 } 18596 18597 18598 /+ 18599 18600 I could fix up the hierarchy kinda like this 18601 18602 class Widget { 18603 Widget[] children() { return null; } 18604 } 18605 interface WidgetContainer { 18606 Widget asWidget(); 18607 void addChild(Widget w); 18608 18609 // alias asWidget this; // but meh 18610 } 18611 18612 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 18613 18614 class Layout : Widget, WidgetContainer {} 18615 18616 class Window : WidgetContainer {} 18617 18618 18619 All constructors that previously took Widgets should now take WidgetContainers instead 18620 18621 18622 18623 But I'm kinda meh toward it, im not sure this is a real problem even though there are some addChild things that throw "plz don't". 18624 +/ 18625 18626 /+ 18627 LAYOUTS 2.0 18628 18629 can just be assigned as a function. assigning a new one will cause it to be immediately called. 18630 18631 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 18632 18633 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 18634 18635 and even Paint can just use computedStyle... 18636 18637 background color 18638 font 18639 border color and style 18640 18641 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 18642 please note that many widgets and in some modes will completely ignore properties as they will. 18643 they are just hints you set, not promises. 18644 18645 18646 18647 18648 18649 So generally the existing virtual functions are just the default for the class. But individual objects 18650 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 18651 +/ 18652 18653 /++ 18654 Structure to represent a collection of background hints. New features can be added here, so make sure you use the provided constructors and factories for maximum compatibility. 18655 18656 History: 18657 Added May 24, 2021. 18658 +/ 18659 struct WidgetBackground { 18660 /++ 18661 A background with the given solid color. 18662 +/ 18663 this(Color color) { 18664 this.color = color; 18665 } 18666 18667 this(WidgetBackground bg) { 18668 this = bg; 18669 } 18670 18671 /++ 18672 Creates a widget from the string. 18673 18674 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 18675 +/ 18676 static WidgetBackground fromString(string s) { 18677 return WidgetBackground(Color.fromString(s)); 18678 } 18679 18680 /++ 18681 The background is not necessarily a solid color, but you can always specify a color as a fallback. 18682 18683 History: 18684 Made `public` on December 18, 2022 (dub v10.10). 18685 +/ 18686 Color color; 18687 } 18688 18689 /++ 18690 Interface to a custom visual theme which is able to access and use style hint properties, draw stylistic elements, and even completely override existing class' paint methods (though I'd note that can be a lot harder than it may seem due to the various little details of state you need to reflect visually, so that should be your last result!) 18691 18692 Please note that this is only guaranteed to be used by custom widgets, and custom widgets are generally inferior to system widgets. Layout properties may be used by sytstem widgets though. 18693 18694 You should not inherit from this directly, but instead use [VisualTheme]. 18695 18696 History: 18697 Added May 8, 2021 18698 +/ 18699 abstract class BaseVisualTheme { 18700 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 18701 abstract void doPaint(Widget widget, WidgetPainter painter); 18702 18703 /+ 18704 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 18705 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 18706 +/ 18707 18708 /++ 18709 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 18710 where the interpretation of the string varies for each property and may include things like measurement units. 18711 +/ 18712 abstract string getPropertyString(Widget widget, string propertyName); 18713 18714 /++ 18715 Default background color of the window. Widgets also use this to simulate transparency. 18716 18717 Probably some shade of grey. 18718 +/ 18719 abstract Color windowBackgroundColor(); 18720 abstract Color widgetBackgroundColor(); 18721 abstract Color foregroundColor(); 18722 abstract Color lightAccentColor(); 18723 abstract Color darkAccentColor(); 18724 18725 /++ 18726 Colors used to indicate active selections in lists and text boxes, etc. 18727 +/ 18728 abstract Color selectionForegroundColor(); 18729 /// ditto 18730 abstract Color selectionBackgroundColor(); 18731 18732 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 18733 18734 /++ 18735 If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally. 18736 +/ 18737 abstract OperatingSystemFont defaultFont(int dpi); 18738 18739 private OperatingSystemFont[int] defaultFontCache_; 18740 private OperatingSystemFont defaultFontCached(int dpi) { 18741 if(dpi !in defaultFontCache_) { 18742 // FIXME: set this to false if X disconnect or if visual theme changes 18743 defaultFontCache_[dpi] = defaultFont(dpi); 18744 } 18745 return defaultFontCache_[dpi]; 18746 } 18747 } 18748 18749 /+ 18750 A widget should have: 18751 classList 18752 dataset 18753 attributes 18754 computedStyles 18755 state (persistent) 18756 dynamic state (focused, hover, etc) 18757 +/ 18758 18759 // visualTheme.computedStyle(this).paddingLeft 18760 18761 18762 /++ 18763 This is your entry point to create your own visual theme for custom widgets. 18764 18765 You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods. 18766 18767 Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating. 18768 +/ 18769 abstract class VisualTheme(CRTP) : BaseVisualTheme { 18770 override string getPropertyString(Widget widget, string propertyName) { 18771 return null; 18772 } 18773 18774 /+ 18775 mixin StyleOverride!Widget 18776 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 18777 w.useStyleProperties(dg); 18778 } 18779 +/ 18780 18781 final override void doPaint(Widget widget, WidgetPainter painter) { 18782 auto derived = cast(CRTP) cast(void*) this; 18783 18784 scope void delegate(Widget, WidgetPainter) bestMatch; 18785 int bestMatchScore; 18786 18787 static if(__traits(hasMember, CRTP, "paint")) 18788 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 18789 static if(is(typeof(overload) Params == __parameters)) { 18790 static assert(Params.length == 2); 18791 static assert(is(Params[0] : Widget)); 18792 static assert(is(Params[1] == WidgetPainter)); 18793 static assert(is(typeof(&__traits(child, derived, overload)) == delegate), "Found a paint method that doesn't appear to be a delegate. One cause of this can be your dmd being too old, make sure it is version 2.094 or newer to use this feature."); // , __traits(getLocation, overload).stringof ~ " is not a delegate " ~ typeof(&__traits(child, derived, overload)).stringof); 18794 18795 alias type = Params[0]; 18796 if(cast(type) widget) { 18797 auto score = baseClassCount!type; 18798 18799 if(score > bestMatchScore) { 18800 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 18801 bestMatchScore = score; 18802 } 18803 } 18804 } else static assert(0, "paint should be a method."); 18805 } 18806 18807 if(bestMatch) 18808 bestMatch(widget, painter); 18809 else 18810 widget.paint(painter); 18811 } 18812 18813 deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; } 18814 18815 // I have to put these here even though I kinda don't want to since dmd regressed on detecting unimplemented interface functions through abstract classes 18816 // mixin Beautiful95Theme; 18817 mixin DefaultLightTheme; 18818 18819 private static struct Cached { 18820 // i prolly want to do this 18821 } 18822 } 18823 18824 /// ditto 18825 mixin template Beautiful95Theme() { 18826 override Color windowBackgroundColor() { return Color(212, 212, 212); } 18827 override Color widgetBackgroundColor() { return Color.white; } 18828 override Color foregroundColor() { return Color.black; } 18829 override Color darkAccentColor() { return Color(172, 172, 172); } 18830 override Color lightAccentColor() { return Color(223, 223, 223); } 18831 override Color selectionForegroundColor() { return Color.white; } 18832 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 18833 override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr 18834 } 18835 18836 /// ditto 18837 mixin template DefaultLightTheme() { 18838 override Color windowBackgroundColor() { return Color(232, 232, 232); } 18839 override Color widgetBackgroundColor() { return Color.white; } 18840 override Color foregroundColor() { return Color.black; } 18841 override Color darkAccentColor() { return Color(172, 172, 172); } 18842 override Color lightAccentColor() { return Color(223, 223, 223); } 18843 override Color selectionForegroundColor() { return Color.white; } 18844 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 18845 override OperatingSystemFont defaultFont(int dpi) { 18846 version(Windows) 18847 return new OperatingSystemFont("Segoe UI"); 18848 else static if(UsingSimpledisplayCocoa) { 18849 return (new OperatingSystemFont()).loadDefault; 18850 } else { 18851 // FIXME: undo xft's scaling so we don't end up double scaled 18852 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 18853 } 18854 } 18855 } 18856 18857 /// ditto 18858 mixin template DefaultDarkTheme() { 18859 override Color windowBackgroundColor() { return Color(64, 64, 64); } 18860 override Color widgetBackgroundColor() { return Color.black; } 18861 override Color foregroundColor() { return Color.white; } 18862 override Color darkAccentColor() { return Color(20, 20, 20); } 18863 override Color lightAccentColor() { return Color(80, 80, 80); } 18864 override Color selectionForegroundColor() { return Color.white; } 18865 override Color selectionBackgroundColor() { return Color(128, 0, 128); } 18866 override OperatingSystemFont defaultFont(int dpi) { 18867 version(Windows) 18868 return new OperatingSystemFont("Segoe UI", 12); 18869 else static if(UsingSimpledisplayCocoa) { 18870 return (new OperatingSystemFont()).loadDefault; 18871 } else { 18872 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 18873 } 18874 } 18875 } 18876 18877 /// ditto 18878 alias DefaultTheme = DefaultLightTheme; 18879 18880 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 18881 /+ 18882 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 18883 Color windowBackgroundColor() { return Color(242, 242, 242); } 18884 Color darkAccentColor() { return windowBackgroundColor; } 18885 Color lightAccentColor() { return windowBackgroundColor; } 18886 +/ 18887 } 18888 18889 /++ 18890 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 18891 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 18892 18893 History: 18894 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 18895 18896 Made `final` on January 3, 2025 18897 +/ 18898 final class StateChanged(alias field) : Event { 18899 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 18900 override bool cancelable() const { return false; } 18901 this(Widget target, typeof(field) newValue) { 18902 this.newValue = newValue; 18903 super(EventString, target); 18904 } 18905 18906 typeof(field) newValue; 18907 } 18908 18909 /++ 18910 Convenience function to add a `triggered` event listener. 18911 18912 Its implementation is simply `w.addEventListener("triggered", dg);` 18913 18914 History: 18915 Added November 27, 2021 (dub v10.4) 18916 +/ 18917 void addWhenTriggered(Widget w, void delegate() dg) { 18918 w.addEventListener("triggered", dg); 18919 } 18920 18921 /++ 18922 Observable variables can be added to widgets and when they are changed, it fires 18923 off a [StateChanged] event so you can react to it. 18924 18925 It is implemented as a getter and setter property, along with another helper you 18926 can use to subscribe with is `name_changed`. You can also subscribe to the [StateChanged] 18927 event through the usual means. Just give the name of the variable. See [StateChanged] for an 18928 example. 18929 18930 To get an `ObservableReference` to the observable, use `&yourname_changed`. 18931 18932 History: 18933 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 18934 18935 As of March 5, 2025, the changed function now returns an [EventListener] handle, which 18936 you can use to disconnect the observer. 18937 +/ 18938 mixin template Observable(T, string name) { 18939 private T backing; 18940 18941 mixin(q{ 18942 EventListener } ~ name ~ q{_changed (void delegate(T) dg) { 18943 return this.addEventListener((StateChanged!this_thing ev) { 18944 dg(ev.newValue); 18945 }); 18946 } 18947 18948 @property T } ~ name ~ q{ () { 18949 return backing; 18950 } 18951 18952 @property void } ~ name ~ q{ (T t) { 18953 backing = t; 18954 auto event = new StateChanged!this_thing(this, t); 18955 event.dispatch(); 18956 } 18957 }); 18958 18959 mixin("private alias this_thing = " ~ name ~ ";"); 18960 } 18961 18962 /// ditto 18963 alias ObservableReference(T) = EventListener delegate(void delegate(T)); 18964 18965 private bool startsWith(string test, string thing) { 18966 if(test.length < thing.length) 18967 return false; 18968 return test[0 .. thing.length] == thing; 18969 } 18970 18971 private bool endsWith(string test, string thing) { 18972 if(test.length < thing.length) 18973 return false; 18974 return test[$ - thing.length .. $] == thing; 18975 } 18976 18977 /++ 18978 Context menus can have `@hotkey`, `@label`, `@tip`, `@separator`, and `@icon` 18979 18980 Note they can NOT have accelerators or toolbars; those annotations will be ignored. 18981 18982 Mark the functions callable from it with `@context_menu { ... }` Presence of other `@menu(...)` annotations will exclude it from the context menu at this time. 18983 18984 See_Also: 18985 [Widget.setMenuAndToolbarFromAnnotatedCode] 18986 +/ 18987 Menu createContextMenuFromAnnotatedCode(TWidget)(TWidget w) if(is(TWidget : Widget)) { 18988 return createContextMenuFromAnnotatedCode(w, w); 18989 } 18990 18991 /// ditto 18992 Menu createContextMenuFromAnnotatedCode(T)(Widget w, ref T t) if(!is(T == class) && !is(T == interface)) { 18993 return createContextMenuFromAnnotatedCode_internal(w, t); 18994 } 18995 /// ditto 18996 Menu createContextMenuFromAnnotatedCode(T)(Widget w, T t) if(is(T == class) || is(T == interface)) { 18997 return createContextMenuFromAnnotatedCode_internal(w, t); 18998 } 18999 Menu createContextMenuFromAnnotatedCode_internal(T)(Widget w, ref T t) { 19000 Menu ret = new Menu("", w); 19001 19002 foreach(memberName; __traits(derivedMembers, T)) { 19003 static if(memberName != "this") 19004 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 19005 .menu menu; 19006 bool separator; 19007 .hotkey hotkey; 19008 .icon icon; 19009 string label; 19010 string tip; 19011 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 19012 static if(is(typeof(attr) == .menu)) 19013 menu = attr; 19014 else static if(is(attr == .separator)) 19015 separator = true; 19016 else static if(is(typeof(attr) == .hotkey)) 19017 hotkey = attr; 19018 else static if(is(typeof(attr) == .icon)) 19019 icon = attr; 19020 else static if(is(typeof(attr) == .label)) 19021 label = attr.label; 19022 else static if(is(typeof(attr) == .tip)) 19023 tip = attr.tip; 19024 } 19025 19026 if(menu is .menu.init) { 19027 ushort correctIcon = icon.id; // FIXME 19028 if(label.length == 0) 19029 label = memberName.toMenuLabel; 19030 19031 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(w.parentWindow, &__traits(getMember, t, memberName)); 19032 19033 auto action = new Action(label, correctIcon, handler); 19034 19035 if(separator) 19036 ret.addSeparator(); 19037 ret.addItem(new MenuItem(action)); 19038 } 19039 } 19040 } 19041 19042 return ret; 19043 } 19044 19045 // still do layout delegation 19046 // and... split off Window from Widget. 19047 19048 version(minigui_screenshots) 19049 struct Screenshot { 19050 string name; 19051 } 19052 19053 version(minigui_screenshots) 19054 static if(__VERSION__ > 2092) 19055 mixin(q{ 19056 shared static this() { 19057 import core.runtime; 19058 19059 static UnitTestResult screenshotMagic() { 19060 string name; 19061 19062 import arsd.png; 19063 19064 auto results = new Window(); 19065 auto button = new Button("do it", results); 19066 19067 Window.newWindowCreated = delegate(Window w) { 19068 Timer timer; 19069 timer = new Timer(250, { 19070 auto img = w.win.takeScreenshot(); 19071 timer.destroy(); 19072 19073 version(Windows) 19074 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 19075 else 19076 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 19077 19078 w.close(); 19079 }); 19080 }; 19081 19082 button.addWhenTriggered( { 19083 19084 foreach(test; __traits(getUnitTests, mixin("arsd.minigui"))) { 19085 name = null; 19086 static foreach(attr; __traits(getAttributes, test)) { 19087 static if(is(typeof(attr) == Screenshot)) 19088 name = attr.name; 19089 } 19090 if(name.length) { 19091 test(); 19092 } 19093 } 19094 19095 }); 19096 19097 results.loop(); 19098 19099 return UnitTestResult(0, 0, false, false); 19100 } 19101 19102 19103 Runtime.extendedModuleUnitTester = &screenshotMagic; 19104 } 19105 }); 19106 version(minigui_screenshots) { 19107 version(unittest) 19108 void main() {} 19109 else static assert(0, "dont forget the -unittest flag to dmd"); 19110 } 19111 19112 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 19113 // FIXME: make multiple accelerators disambiguate based ona rgs 19114 // FIXME: MainWindow ctor should have same arg order as Window 19115 // FIXME: mainwindow ctor w/ client area size instead of total size. 19116 // Push on/off button (basically an alternate display of a checkbox) -- BS_PUSHLIKE and maybe BS_TEXT (BS_TOP moves it). see also BS_FLAT. 19117 // FIXME: tri-state checkbox 19118 // FIXME: subordinate controls grouping...