1 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx 2 3 // 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 4 5 // me@arsd:~/.kde/share/config$ vim kdeglobals 6 7 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too. 8 9 // https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/ 10 11 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever. 12 13 // responsive minigui, menu search, and file open with a preview hook on the side. 14 15 // FIXME: add menu checkbox and menu icon eventually 16 17 /* 18 19 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 20 21 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 22 */ 23 24 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places 25 26 // FIXME: text label must be copyable to the clipboard, at least as a full chunk. 27 28 // FIXME: opt-in file picker widget with image support 29 30 // FIXME: number widget 31 32 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm 33 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles 34 35 // osx style menu search. 36 37 // would be cool for a scroll bar to have marking capabilities 38 // kinda like vim's marks just on clicks etc and visual representation 39 // generically. may be cool to add an up arrow to the bottom too 40 // 41 // leave a shadow of where you last were for going back easily 42 43 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 44 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 45 // the window. 46 47 // so what about context menus? 48 49 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 50 51 // FIXME: make the scroll thing go to bottom when the content changes. 52 53 // 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 54 55 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 56 57 58 // FIXME: add a command search thingy built in and implement tip. 59 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 60 61 // On Windows: 62 // FIXME: various labels look broken in high contrast mode 63 // FIXME: changing themes while the program is upen doesn't trigger a redraw 64 65 // add note about manifest to documentation. also icons. 66 67 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 68 // FIXME: clear the corner of scrollbars if they pop up 69 70 // minigui needs to have a stdout redirection for gui mode on windows writeln 71 72 // I kinda wanna do state reacting. sort of. idk tho 73 74 // need a viewer widget that works like a web page - arrows scroll down consistently 75 76 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 77 78 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 79 // and help info about menu items. 80 // and search in menus? 81 82 // FIXME: a scroll area event signaling when a thing comes into view might be good 83 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 84 85 // FIXME: unify Windows style line endings 86 87 /* 88 TODO: 89 90 pie menu 91 92 class Form with submit behavior -- see AutomaticDialog 93 94 disabled widgets and menu items 95 96 event cleanup 97 tooltips. 98 api improvements 99 100 margins are kinda broken, they don't collapse like they should. at least. 101 102 a table form btw would be a horizontal layout of vertical layouts holding each column 103 that would give the same width things 104 */ 105 106 /* 107 108 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 109 */ 110 111 /++ 112 minigui is a smallish GUI widget library, aiming to be on par with at least 113 HTML4 forms and a few other expected gui components. It uses native controls 114 on Windows and does its own thing on Linux (Mac is not currently supported but 115 may be later, and should use native controls) to keep size down. The Linux 116 appearance is similar to Windows 95 and avoids using images to maintain network 117 efficiency on remote X connections, though you can customize that. 118 119 120 minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color], 121 on which it is built. simpledisplay provides the low-level interfaces and minigui 122 builds the concept of widgets inside the windows on top of it. 123 124 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 125 It isn't hugely concerned with appearance - on Windows, it just uses the native 126 controls and native theme, and on Linux, it keeps it simple and I may change that 127 at any time, though after May 2021, you can customize some things with css-inspired 128 [Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`, 129 you can use the custom implementation there too, but... you shouldn't.) 130 131 The event model is similar to what you use in the browser with Javascript and the 132 layout engine tries to automatically fit things in, similar to a css flexbox. 133 134 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 135 `-L/SUBSYSTEM:WINDOWS` and -L/entry:mainCRTStartup`. If using ldc instead 136 of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w". 137 138 Otherwise you'll get a console and possibly other visual bugs. But if you do use 139 the subsystem:windows, note that Phobos' writeln will crash the program! 140 141 HTML_To_Classes: 142 $(SMALL_TABLE 143 HTML Code | Minigui Class 144 145 `<input type="text">` | [LineEdit] 146 `<textarea>` | [TextEdit] 147 `<select>` | [DropDownSelection] 148 `<input type="checkbox">` | [Checkbox] 149 `<input type="radio">` | [Radiobox] 150 `<button>` | [Button] 151 ) 152 153 154 Stretchiness: 155 The default is 4. You can use larger numbers for things that should 156 consume a lot of space, and lower numbers for ones that are better at 157 smaller sizes. 158 159 Overlapped_input: 160 COMING EVENTUALLY: 161 minigui will include a little bit of I/O functionality that just works 162 with the event loop. If you want to get fancy, I suggest spinning up 163 another thread and posting events back and forth. 164 165 $(H2 Add ons) 166 See the `minigui_addons` directory in the arsd repo for some add on widgets 167 you can import separately too. 168 169 $(H3 XML definitions) 170 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 171 172 $(H3 Scriptability) 173 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 174 in this documentation, it means you can call it from the script language. 175 176 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 177 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 178 179 --- 180 import arsd.minigui_xml; 181 import arsd.script; 182 183 var globals = var.emptyObject; 184 globals.makeWidgetFromString = &makeWidgetFromString; 185 186 // this now works 187 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 188 --- 189 190 More to come. 191 192 History: 193 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 194 195 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 196 tag this as version 2.0. 197 198 Among the changes: 199 $(LIST 200 * 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. 201 202 See [Event] for details. 203 204 * 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. 205 206 See [DoubleClickEvent] for details. 207 208 * 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. 209 210 See [Widget.Style] for details. 211 212 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 213 214 * 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. 215 216 * 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. 217 218 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 219 220 * 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. 221 222 * Various non-breaking additions. 223 ) 224 +/ 225 module arsd.minigui; 226 227 /++ 228 This hello world sample will have an oversized button, but that's ok, you see your first window! 229 +/ 230 version(Demo) 231 unittest { 232 import arsd.minigui; 233 234 void main() { 235 auto window = new MainWindow(); 236 237 // note the parent widget is almost always passed as the last argument to a constructor 238 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 239 auto button = new Button("Close", window); 240 button.addWhenTriggered({ 241 window.close(); 242 }); 243 244 window.loop(); 245 } 246 247 main(); // exclude from docs 248 } 249 250 /++ 251 This example shows one way you can partition your window into a header 252 and sidebar. Here, the header and sidebar have a fixed width, while the 253 rest of the content sizes with the window. 254 255 It might be a new way of thinking about window layout to do things this 256 way - perhaps [GridLayout] more matches your style of thought - but the 257 concept here is to partition the window into sub-boxes with a particular 258 size, then partition those boxes into further boxes. 259 260 $(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.) 261 262 So to make the header, start with a child layout that has a max height. 263 It will use that space from the top, then the remaining children will 264 split the remaining area, meaning you can think of is as just being another 265 box you can split again. Keep splitting until you have the look you desire. 266 +/ 267 // https://github.com/adamdruppe/arsd/issues/310 268 version(minigui_screenshots) 269 @Screenshot("layout") 270 unittest { 271 import arsd.minigui; 272 273 // This helper class is just to help make the layout boxes visible. 274 // think of it like a <div style="background-color: whatever;"></div> in HTML. 275 class ColorWidget : Widget { 276 this(Color color, Widget parent) { 277 this.color = color; 278 super(parent); 279 } 280 Color color; 281 class Style : Widget.Style { 282 override WidgetBackground background() { return WidgetBackground(color); } 283 } 284 mixin OverrideStyle!Style; 285 } 286 287 void main() { 288 auto window = new Window; 289 290 // the key is to give it a max height. This is one way to do it: 291 auto header = new class HorizontalLayout { 292 this() { super(window); } 293 override int maxHeight() { return 50; } 294 }; 295 // this next line is a shortcut way of doing it too, but it only works 296 // for HorizontalLayout and VerticalLayout, and is less explicit, so it 297 // is good to know how to make a new class like above anyway. 298 // auto header = new HorizontalLayout(50, window); 299 300 auto bar = new HorizontalLayout(window); 301 302 // or since this is so common, VerticalLayout and HorizontalLayout both 303 // can just take an argument in their constructor for max width/height respectively 304 305 // (could have tone this above too, but I wanted to demo both techniques) 306 auto left = new VerticalLayout(100, bar); 307 308 // and this is the main section's container. A plain Widget instance is good enough here. 309 auto container = new Widget(bar); 310 311 // and these just add color to the containers we made above for the screenshot. 312 // in a real application, you can just add your actual controls instead of these. 313 auto headerColorBox = new ColorWidget(Color.teal, header); 314 auto leftColorBox = new ColorWidget(Color.green, left); 315 auto rightColorBox = new ColorWidget(Color.purple, container); 316 317 window.loop(); 318 } 319 320 main(); // exclude from docs 321 } 322 323 324 import arsd.core; 325 alias Timer = arsd.simpledisplay.Timer; 326 public import arsd.simpledisplay; 327 /++ 328 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 329 330 History: 331 Was private until May 15, 2021. 332 +/ 333 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 334 335 version(Windows) { 336 import core.sys.windows.winnls; 337 import core.sys.windows.windef; 338 import core.sys.windows.basetyps; 339 import core.sys.windows.winbase; 340 import core.sys.windows.winuser; 341 import core.sys.windows.wingdi; 342 static import gdi = core.sys.windows.wingdi; 343 } 344 345 version(Windows) { 346 version(minigui_manifest) {} else version=minigui_no_manifest; 347 348 version(minigui_no_manifest) {} else 349 static if(__VERSION__ >= 2_083) 350 version(CRuntime_Microsoft) { // FIXME: mingw? 351 // assume we want commctrl6 whenever possible since there's really no reason not to 352 // and this avoids some of the manifest hassle 353 pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""); 354 } 355 } 356 357 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 358 private bool lastDefaultPrevented; 359 360 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 361 alias scriptable = arsd_jsvar_compatible; 362 363 version(Windows) { 364 // use native widgets when available unless specifically asked otherwise 365 version(custom_widgets) { 366 enum bool UsingCustomWidgets = true; 367 enum bool UsingWin32Widgets = false; 368 } else { 369 version = win32_widgets; 370 enum bool UsingCustomWidgets = false; 371 enum bool UsingWin32Widgets = true; 372 373 // give access to my text system for the rich text cross platform stuff 374 version = use_new_text_system; 375 import arsd.textlayouter; 376 } 377 // and native theming when needed 378 //version = win32_theming; 379 } else { 380 enum bool UsingCustomWidgets = true; 381 enum bool UsingWin32Widgets = false; 382 version=custom_widgets; 383 } 384 385 386 387 /* 388 389 The main goals of minigui.d are to: 390 1) Provide basic widgets that just work in a lightweight lib. 391 I basically want things comparable to a plain HTML form, 392 plus the easy and obvious things you expect from Windows 393 apps like a menu. 394 2) Use native things when possible for best functionality with 395 least library weight. 396 3) Give building blocks to provide easy extension for your 397 custom widgets, or hooking into additional native widgets 398 I didn't wrap. 399 4) Provide interfaces for easy interaction between third 400 party minigui extensions. (event model, perhaps 401 signals/slots, drop-in ease of use bits.) 402 5) Zero non-system dependencies, including Phobos as much as 403 I reasonably can. It must only import arsd.color and 404 my simpledisplay.d. If you need more, it will have to be 405 an extension module. 406 6) An easy layout system that generally works. 407 408 A stretch goal is to make it easy to make gui forms with code, 409 some kind of resource file (xml?) and even a wysiwyg designer. 410 411 Another stretch goal is to make it easy to hook data into the gui, 412 including from reflection. So like auto-generate a form from a 413 function signature or struct definition, or show a list from an 414 array that automatically updates as the array is changed. Then, 415 your program focuses on the data more than the gui interaction. 416 417 418 419 STILL NEEDED: 420 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 421 * slider 422 * listbox 423 * spinner 424 * label? 425 * rich text 426 */ 427 428 429 /+ 430 enum LayoutMethods { 431 verticalFlex, 432 horizontalFlex, 433 inlineBlock, // left to right, no stretch, goes to next line as needed 434 static, // just set to x, y 435 verticalNoStretch, // browser style default 436 437 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 438 439 grid, // magic 440 } 441 +/ 442 443 /++ 444 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. 445 446 447 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. 448 449 --- 450 class MinimalWidget : Widget { 451 this(Widget parent) { 452 super(parent); 453 } 454 } 455 --- 456 457 $(SIDEBAR 458 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. 459 ) 460 461 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. 462 463 Among the things you'll most likely want to change in your custom widget: 464 465 $(LIST 466 * 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.) 467 468 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. 469 470 Do this $(I after) calling the `super` constructor. 471 472 * 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. 473 474 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 475 476 * 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. 477 478 * 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. 479 ) 480 481 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. 482 483 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 484 485 Your own custom-drawn and native system controls can exist side-by-side. 486 487 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. 488 +/ 489 class Widget : ReflectableProperties { 490 491 private bool willDraw() { 492 return true; 493 } 494 495 /+ 496 /++ 497 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 498 499 History: 500 Added September 15, 2021 501 implemented.... ??? 502 +/ 503 void prepareReflection(this This)() { 504 505 } 506 +/ 507 508 private bool _enabled = true; 509 510 /++ 511 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. 512 513 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. 514 515 History: 516 Added November 23, 2021 (dub v10.4) 517 518 Warning: the specific behavior of disabling with parents may change in the future. 519 Bugs: 520 Currently only implemented for widgets backed by native Windows controls. 521 522 See_Also: [disabledReason], [disabledBy] 523 +/ 524 @property bool enabled() { 525 return disabledBy() is null; 526 } 527 528 /// ditto 529 @property void enabled(bool yes) { 530 _enabled = yes; 531 version(win32_widgets) { 532 if(hwnd) 533 EnableWindow(hwnd, yes); 534 } 535 setDynamicState(DynamicState.disabled, yes); 536 } 537 538 private string disabledReason_; 539 540 /++ 541 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. 542 543 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 544 545 History: 546 Added November 23, 2021 (dub v10.4) 547 See_Also: [enabled], [disabledBy] 548 +/ 549 @property string disabledReason() { 550 auto w = disabledBy(); 551 return (w is null) ? null : w.disabledReason_; 552 } 553 554 /// ditto 555 @property void disabledReason(string reason) { 556 disabledReason_ = reason; 557 } 558 559 /++ 560 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. 561 562 History: 563 Added November 25, 2021 (dub v10.4) 564 See_Also: [enabled], [disabledReason] 565 +/ 566 Widget disabledBy() { 567 Widget p = this; 568 while(p) { 569 if(!p._enabled) 570 return p; 571 p = p.parent; 572 } 573 return null; 574 } 575 576 /// Implementations of [ReflectableProperties] interface. See the interface for details. 577 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 578 if(valueIsJson) 579 return SetPropertyResult.wrongFormat; 580 switch(name) { 581 case "name": 582 this.name = value.idup; 583 return SetPropertyResult.success; 584 case "statusTip": 585 this.statusTip = value.idup; 586 return SetPropertyResult.success; 587 default: 588 return SetPropertyResult.noSuchProperty; 589 } 590 } 591 /// ditto 592 void getPropertiesList(scope void delegate(string name) sink) const { 593 sink("name"); 594 sink("statusTip"); 595 } 596 /// ditto 597 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 598 switch(name) { 599 case "name": 600 sink(name, this.name, false); 601 return; 602 case "statusTip": 603 sink(name, this.statusTip, false); 604 return; 605 default: 606 sink(name, null, true); 607 } 608 } 609 610 /++ 611 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 612 613 History: 614 Added November 25, 2021 (dub v10.5) 615 `Point` overload added January 12, 2022 (dub v10.6) 616 +/ 617 int scaleWithDpi(int value, int assumedDpi = 96) { 618 // avoid potential overflow with common special values 619 if(value == int.max) 620 return int.max; 621 if(value == int.min) 622 return int.min; 623 if(value == 0) 624 return 0; 625 return value * currentDpi(assumedDpi) / assumedDpi; 626 } 627 628 /// ditto 629 Point scaleWithDpi(Point value, int assumedDpi = 96) { 630 return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi)); 631 } 632 633 /++ 634 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. 635 636 Not entirely stable. 637 638 History: 639 Added August 25, 2023 (dub v11.1) 640 +/ 641 final int currentDpi(int assumedDpi = 96) { 642 // assert(parentWindow !is null); 643 // assert(parentWindow.win !is null); 644 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 645 //divide = 138; // to test 1.5x 646 // 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. 647 // this also covers the case when actualDpi returns 0. 648 if(divide < 96) 649 divide = 96; 650 return divide; 651 } 652 653 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 654 // I'll think up something better eventually 655 656 // FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight. 657 protected final int defaultLineHeight() { 658 auto cs = getComputedStyle(); 659 if(cs.font && !cs.font.isNull) 660 return cs.font.height() * 5 / 4; 661 else 662 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4); 663 } 664 665 /++ 666 667 History: 668 Added August 25, 2023 (dub v11.1) 669 +/ 670 protected final int defaultTextHeight(int numberOfLines = 1) { 671 auto cs = getComputedStyle(); 672 if(cs.font && !cs.font.isNull) 673 return cs.font.height() * numberOfLines; 674 else 675 return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines; 676 } 677 678 protected final int defaultTextWidth(const(char)[] text) { 679 auto cs = getComputedStyle(); 680 if(cs.font && !cs.font.isNull) 681 return cs.font.stringWidth(text); 682 else 683 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2); 684 } 685 686 /++ 687 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. 688 689 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. 690 691 History: 692 Added May 22, 2021 693 +/ 694 protected bool encapsulatedChildren() { 695 return false; 696 } 697 698 private void privateDpiChanged() { 699 dpiChanged(); 700 foreach(child; children) 701 child.privateDpiChanged(); 702 } 703 704 /++ 705 Virtual hook to update any caches or fonts you need on the event of a dpi scaling change. 706 707 History: 708 Added January 12, 2022 (dub v10.6) 709 +/ 710 protected void dpiChanged() { 711 712 } 713 714 // Default layout properties { 715 716 int minWidth() { return 0; } 717 int minHeight() { 718 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 719 int sum = this.paddingTop + this.paddingBottom; 720 foreach(child; children) { 721 if(child.hidden) 722 continue; 723 sum += child.minHeight(); 724 sum += child.marginTop(); 725 sum += child.marginBottom(); 726 } 727 728 return sum; 729 } 730 int maxWidth() { return int.max; } 731 int maxHeight() { return int.max; } 732 int widthStretchiness() { return 4; } 733 int heightStretchiness() { return 4; } 734 735 /++ 736 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 737 738 History: 739 Added June 15, 2021 (dub v10.1) 740 +/ 741 int widthShrinkiness() { return 0; } 742 /// ditto 743 int heightShrinkiness() { return 0; } 744 745 /++ 746 The initial size of the widget for layout calculations. Default is 0. 747 748 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 749 750 History: 751 Added June 15, 2021 (dub v10.1) 752 +/ 753 int flexBasisWidth() { return 0; } 754 /// ditto 755 int flexBasisHeight() { return 0; } 756 757 /++ 758 Not stable. 759 760 Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored. 761 762 So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6. 763 764 History: 765 Added January 5, 2023 766 +/ 767 Rectangle defaultMargin; 768 /// ditto 769 Rectangle defaultPadding; 770 771 int marginLeft() { return scaleWithDpi(defaultMargin.left); } 772 int marginRight() { return scaleWithDpi(defaultMargin.right); } 773 int marginTop() { return scaleWithDpi(defaultMargin.top); } 774 int marginBottom() { return scaleWithDpi(defaultMargin.bottom); } 775 int paddingLeft() { return scaleWithDpi(defaultPadding.left); } 776 int paddingRight() { return scaleWithDpi(defaultPadding.right); } 777 int paddingTop() { return scaleWithDpi(defaultPadding.top); } 778 int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); } 779 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 780 781 private bool recomputeChildLayoutRequired = true; 782 private static class RecomputeEvent {} 783 private __gshared rce = new RecomputeEvent(); 784 protected final void queueRecomputeChildLayout() { 785 recomputeChildLayoutRequired = true; 786 787 if(this.parentWindow) { 788 auto sw = this.parentWindow.win; 789 assert(sw !is null); 790 if(!sw.eventQueued!RecomputeEvent) { 791 sw.postEvent(rce); 792 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 793 } 794 } 795 796 } 797 798 protected final void recomputeChildLayoutEntry() { 799 if(recomputeChildLayoutRequired) { 800 recomputeChildLayout(); 801 recomputeChildLayoutRequired = false; 802 redraw(); 803 } else { 804 // I still need to check the tree just in case one of them was queued up 805 // and the event came up here instead of there. 806 foreach(child; children) 807 child.recomputeChildLayoutEntry(); 808 } 809 } 810 811 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 812 void recomputeChildLayout() { 813 .recomputeChildLayout!"height"(this); 814 } 815 816 // } 817 818 819 /++ 820 Returns the style's tag name string this object uses. 821 822 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. 823 824 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 825 826 History: 827 Added May 10, 2021 828 +/ 829 string styleTagName() const { 830 string n = typeid(this).name; 831 foreach_reverse(idx, ch; n) 832 if(ch == '.') { 833 n = n[idx + 1 .. $]; 834 break; 835 } 836 return n; 837 } 838 839 /// API for the [styleClassList] 840 static struct ClassList { 841 private Widget widget; 842 843 /// 844 void add(string s) { 845 widget.styleClassList_ ~= s; 846 } 847 848 /// 849 void remove(string s) { 850 foreach(idx, s1; widget.styleClassList_) 851 if(s1 == s) { 852 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 853 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 854 widget.styleClassList_.assumeSafeAppend(); 855 return; 856 } 857 } 858 859 /// Returns true if it was added, false if it was removed. 860 bool toggle(string s) { 861 if(contains(s)) { 862 remove(s); 863 return false; 864 } else { 865 add(s); 866 return true; 867 } 868 } 869 870 /// 871 bool contains(string s) const { 872 foreach(s1; widget.styleClassList_) 873 if(s1 == s) 874 return true; 875 return false; 876 877 } 878 } 879 880 private string[] styleClassList_; 881 882 /++ 883 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. 884 885 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 886 887 History: 888 Added May 10, 2021 889 +/ 890 inout(ClassList) styleClassList() inout { 891 return cast(inout(ClassList)) ClassList(cast() this); 892 } 893 894 /++ 895 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. 896 897 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. 898 899 The upper 32 bits are available for your own extensions. 900 901 History: 902 Added May 10, 2021 903 +/ 904 enum DynamicState : ulong { 905 focus = (1 << 0), /// the widget currently has the keyboard focus 906 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 907 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!) 908 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!) 909 checked = (1 << 4), /// the widget is toggleable and currently toggled on 910 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 911 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 912 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 913 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. 914 915 USER_BEGIN = (1UL << 32), 916 } 917 918 // I want to add the primary and cancel styles to buttons at least at some point somehow. 919 920 /// ditto 921 @property ulong dynamicState() { return dynamicState_; } 922 /// ditto 923 @property ulong dynamicState(ulong newValue) { 924 if(dynamicState != newValue) { 925 auto old = dynamicState_; 926 dynamicState_ = newValue; 927 928 useStyleProperties((scope Widget.Style s) { 929 if(s.variesWithState(old ^ newValue)) 930 redraw(); 931 }); 932 } 933 return dynamicState_; 934 } 935 936 /// ditto 937 void setDynamicState(ulong flags, bool state) { 938 auto ds = dynamicState_; 939 if(state) 940 ds |= flags; 941 else 942 ds &= ~flags; 943 944 dynamicState = ds; 945 } 946 947 private ulong dynamicState_; 948 949 deprecated("Use dynamic styles instead now") { 950 Color backgroundColor() { return backgroundColor_; } 951 void backgroundColor(Color c){ this.backgroundColor_ = c; } 952 953 MouseCursor cursor() { return GenericCursor.Default; } 954 } private Color backgroundColor_ = Color.transparent; 955 956 957 /++ 958 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). 959 960 It is here so there can be a specificity switch. 961 962 See [OverrideStyle] for a helper function to use your own. 963 964 History: 965 Added May 11, 2021 966 +/ 967 static class Style/* : StyleProperties*/ { 968 public Widget widget; // public because the mixin template needs access to it 969 970 /++ 971 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 972 973 History: 974 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. 975 +/ 976 bool variesWithState(ulong dynamicStateFlags) { 977 version(win32_widgets) { 978 if(widget.hwnd) 979 return false; 980 } 981 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 982 } 983 984 /// 985 Color foregroundColor() { 986 return WidgetPainter.visualTheme.foregroundColor; 987 } 988 989 /// 990 WidgetBackground background() { 991 // the default is a "transparent" background, which means 992 // it goes as far up as it can to get the color 993 if (widget.backgroundColor_ != Color.transparent) 994 return WidgetBackground(widget.backgroundColor_); 995 if (widget.parent) 996 return widget.parent.getComputedStyle.background; 997 return WidgetBackground(widget.backgroundColor_); 998 } 999 1000 private static OperatingSystemFont fontCached_; 1001 private OperatingSystemFont fontCached() { 1002 if(fontCached_ is null) 1003 fontCached_ = font(); 1004 return fontCached_; 1005 } 1006 1007 /++ 1008 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. 1009 +/ 1010 OperatingSystemFont font() { 1011 return null; 1012 } 1013 1014 /++ 1015 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. 1016 1017 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 1018 1019 History: 1020 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 1021 +/ 1022 MouseCursor cursor() { 1023 return GenericCursor.Default; 1024 } 1025 1026 FrameStyle borderStyle() { 1027 return FrameStyle.none; 1028 } 1029 1030 /++ 1031 +/ 1032 Color borderColor() { 1033 return Color.transparent; 1034 } 1035 1036 FrameStyle outlineStyle() { 1037 if(widget.dynamicState & DynamicState.focus) 1038 return FrameStyle.dotted; 1039 else 1040 return FrameStyle.none; 1041 } 1042 1043 Color outlineColor() { 1044 return foregroundColor; 1045 } 1046 } 1047 1048 /++ 1049 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 1050 The basic usage is simple: 1051 1052 --- 1053 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 1054 // override style hints as-needed here 1055 } 1056 OverrideStyle!Style; // add the method 1057 --- 1058 1059 $(TIP 1060 While the class is not forced to be `static`, for best results, it should be. A non-static class 1061 can not be inherited by other objects whereas the static one can. A property on the base class, 1062 called [Widget.Style.widget|widget], is available for you to access its properties. 1063 ) 1064 1065 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 1066 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 1067 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 1068 1069 1070 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 1071 You may also just override `variesWithState` when you use this flag. 1072 1073 --- 1074 mixin OverrideStyle!( 1075 DynamicState.focus, YourFocusedStyle, 1076 DynamicState.hover, YourHoverStyle, 1077 YourDefaultStyle 1078 ) 1079 --- 1080 1081 It checks if `dynamicState` matches the state and if so, returns the object given. 1082 1083 If there is no state mask given, the next one matches everything. The first match given is used. 1084 1085 However, since in most cases you'll want check state inside your individual methods, you probably won't 1086 find much use for this whole-class swap out. 1087 1088 History: 1089 Added May 16, 2021 1090 +/ 1091 static protected mixin template OverrideStyle(S...) { 1092 static import amg = arsd.minigui; 1093 override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) { 1094 ulong mask = 0; 1095 foreach(idx, thing; S) { 1096 static if(is(typeof(thing) : ulong)) { 1097 mask = thing; 1098 } else { 1099 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 1100 //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."); 1101 scope amg.Widget.Style s = new thing(); 1102 s.widget = this; 1103 dg(s); 1104 return; 1105 } 1106 } 1107 } 1108 } 1109 } 1110 /++ 1111 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 1112 +/ 1113 void useStyleProperties(scope void delegate(scope Style props) dg) { 1114 scope Style s = new Style(); 1115 s.widget = this; 1116 dg(s); 1117 } 1118 1119 1120 protected void sendResizeEvent() { 1121 this.emit!ResizeEvent(); 1122 } 1123 1124 Menu contextMenu(int x, int y) { return null; } 1125 1126 final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) { 1127 if(parentWindow is null || parentWindow.win is null) return false; 1128 1129 auto menu = this.contextMenu(x, y); 1130 if(menu is null) 1131 return false; 1132 1133 version(win32_widgets) { 1134 // FIXME: if it is -1, -1, do it at the current selection location instead 1135 // tho the corner of the window, whcih it does now, isn't the literal worst. 1136 1137 if(screenX < 0 && screenY < 0) { 1138 auto p = this.globalCoordinates(); 1139 if(screenX == -2) 1140 p.x += x; 1141 if(screenY == -2) 1142 p.y += y; 1143 1144 screenX = p.x; 1145 screenY = p.y; 1146 } 1147 1148 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1149 throw new Exception("TrackContextMenuEx"); 1150 } else version(custom_widgets) { 1151 menu.popup(this, x, y); 1152 } 1153 1154 return true; 1155 } 1156 1157 /++ 1158 Removes this widget from its parent. 1159 1160 History: 1161 `removeWidget` was made `final` on May 11, 2021. 1162 +/ 1163 @scriptable 1164 final void removeWidget() { 1165 auto p = this.parent; 1166 if(p) { 1167 int item; 1168 for(item = 0; item < p._children.length; item++) 1169 if(p._children[item] is this) 1170 break; 1171 auto idx = item; 1172 for(; item < p._children.length - 1; item++) 1173 p._children[item] = p._children[item + 1]; 1174 p._children = p._children[0 .. $-1]; 1175 1176 this.parent.widgetRemoved(idx, this); 1177 //this.parent = null; 1178 1179 p.queueRecomputeChildLayout(); 1180 } 1181 version(win32_widgets) { 1182 removeAllChildren(); 1183 if(hwnd) { 1184 DestroyWindow(hwnd); 1185 hwnd = null; 1186 } 1187 } 1188 } 1189 1190 /++ 1191 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. 1192 1193 History: 1194 Added September 19, 2021 1195 +/ 1196 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 1197 1198 /++ 1199 Removes all child widgets from `this`. You should not use the removed widgets again. 1200 1201 Note that on Windows, it also destroys the native handles for the removed children recursively. 1202 1203 History: 1204 Added July 1, 2021 (dub v10.2) 1205 +/ 1206 void removeAllChildren() { 1207 version(win32_widgets) 1208 foreach(child; _children) { 1209 child.removeAllChildren(); 1210 if(child.hwnd) { 1211 DestroyWindow(child.hwnd); 1212 child.hwnd = null; 1213 } 1214 } 1215 auto orig = this._children; 1216 this._children = null; 1217 foreach(idx, w; orig) 1218 this.widgetRemoved(idx, w); 1219 1220 queueRecomputeChildLayout(); 1221 } 1222 1223 /++ 1224 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1225 +/ 1226 @scriptable 1227 Widget getChildByName(string name) { 1228 return getByName(name); 1229 } 1230 /++ 1231 Finds the nearest descendant with the requested type and [name]. May return `this`. 1232 +/ 1233 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1234 if(this.name == name) 1235 if(auto c = cast(WidgetClass) this) 1236 return c; 1237 foreach(child; children) { 1238 auto w = child.getByName(name); 1239 if(auto c = cast(WidgetClass) w) 1240 return c; 1241 } 1242 return null; 1243 } 1244 1245 /++ 1246 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. 1247 Names should be unique in a window. 1248 1249 See_Also: [getByName], [getChildByName] 1250 +/ 1251 @scriptable string name; 1252 1253 private EventHandler[][string] bubblingEventHandlers; 1254 private EventHandler[][string] capturingEventHandlers; 1255 1256 /++ 1257 Default event handlers. These are called on the appropriate 1258 event unless [Event.preventDefault] is called on the event at 1259 some point through the bubbling process. 1260 1261 1262 If you are implementing your own widget and want to add custom 1263 events, you should follow the same pattern here: create a virtual 1264 function named `defaultEventHandler_eventname` with the implementation, 1265 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1266 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1267 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1268 This ensures virtual dispatch based on the correct subclass. 1269 1270 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1271 overridden version. 1272 1273 You only need to do that on parent classes adding NEW event types. If you 1274 just want to change the default behavior of an existing event type in a subclass, 1275 you override the function (and optionally call `super.method_name`) like normal. 1276 1277 +/ 1278 protected EventHandler[string] defaultEventHandlers; 1279 1280 /// ditto 1281 void setupDefaultEventHandlers() { 1282 defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); }; 1283 defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); }; 1284 defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); }; 1285 defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); }; 1286 defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); }; 1287 defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); }; 1288 defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); }; 1289 defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); }; 1290 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); }; 1291 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); }; 1292 defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); }; 1293 defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); }; 1294 defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); }; 1295 defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); }; 1296 defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); }; 1297 defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); }; 1298 defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); }; 1299 defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); }; 1300 } 1301 1302 /// ditto 1303 void defaultEventHandler_click(ClickEvent event) {} 1304 /// ditto 1305 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1306 /// ditto 1307 void defaultEventHandler_keydown(KeyDownEvent event) {} 1308 /// ditto 1309 void defaultEventHandler_keyup(KeyUpEvent event) {} 1310 /// ditto 1311 void defaultEventHandler_mousedown(MouseDownEvent event) { 1312 if(event.button == MouseButton.left) { 1313 if(this.tabStop) { 1314 this.focus(); 1315 } 1316 } 1317 } 1318 /// ditto 1319 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1320 /// ditto 1321 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1322 /// ditto 1323 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1324 /// ditto 1325 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1326 /// ditto 1327 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1328 /// ditto 1329 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1330 /// ditto 1331 void defaultEventHandler_char(CharEvent event) {} 1332 /// ditto 1333 void defaultEventHandler_triggered(Event event) {} 1334 /// ditto 1335 void defaultEventHandler_change(Event event) {} 1336 /// ditto 1337 void defaultEventHandler_focus(Event event) {} 1338 /// ditto 1339 void defaultEventHandler_blur(Event event) {} 1340 /// ditto 1341 void defaultEventHandler_focusin(Event event) {} 1342 /// ditto 1343 void defaultEventHandler_focusout(Event event) {} 1344 1345 /++ 1346 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1347 1348 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1349 1350 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1351 of participating in handler delegation. 1352 1353 $(TIP 1354 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. 1355 ) 1356 +/ 1357 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1358 return addEventListener(event, (Widget, scope Event e) { 1359 if(e.srcElement is this) 1360 handler(); 1361 }, useCapture); 1362 } 1363 1364 /// ditto 1365 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1366 return addEventListener(event, (Widget, Event e) { 1367 if(e.srcElement is this) 1368 handler(e); 1369 }, useCapture); 1370 } 1371 1372 /// ditto 1373 EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) { 1374 static if(is(Handler Fn == delegate)) { 1375 static if(is(Fn Params == __parameters)) { 1376 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1377 if(e.srcElement !is this) 1378 return; 1379 auto ty = cast(Params[0]) e; 1380 if(ty !is null) 1381 handler(ty); 1382 }, useCapture); 1383 } else static assert(0); 1384 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1385 } 1386 1387 /// ditto 1388 @scriptable 1389 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1390 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1391 } 1392 1393 /// ditto 1394 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1395 static if(is(Handler Fn == delegate)) { 1396 static if(is(Fn Params == __parameters)) { 1397 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1398 auto ty = cast(Params[0]) e; 1399 if(ty !is null) 1400 handler(ty); 1401 }, useCapture); 1402 } else static assert(0); 1403 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1404 } 1405 1406 /// ditto 1407 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1408 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1409 } 1410 1411 /// ditto 1412 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1413 if(event.length > 2 && event[0..2] == "on") 1414 event = event[2 .. $]; 1415 1416 if(useCapture) 1417 capturingEventHandlers[event] ~= handler; 1418 else 1419 bubblingEventHandlers[event] ~= handler; 1420 1421 return EventListener(this, event, handler, useCapture); 1422 } 1423 1424 /// ditto 1425 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1426 if(event.length > 2 && event[0..2] == "on") 1427 event = event[2 .. $]; 1428 1429 if(useCapture) { 1430 if(event in capturingEventHandlers) 1431 foreach(ref evt; capturingEventHandlers[event]) 1432 if(evt is handler) evt = null; 1433 } else { 1434 if(event in bubblingEventHandlers) 1435 foreach(ref evt; bubblingEventHandlers[event]) 1436 if(evt is handler) evt = null; 1437 } 1438 } 1439 1440 /// ditto 1441 void removeEventListener(EventListener listener) { 1442 removeEventListener(listener.event, listener.handler, listener.useCapture); 1443 } 1444 1445 static if(UsingSimpledisplayX11) { 1446 void discardXConnectionState() { 1447 foreach(child; children) 1448 child.discardXConnectionState(); 1449 } 1450 1451 void recreateXConnectionState() { 1452 foreach(child; children) 1453 child.recreateXConnectionState(); 1454 redraw(); 1455 } 1456 } 1457 1458 /++ 1459 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1460 1461 History: 1462 `globalCoordinates` was made `final` on May 11, 2021. 1463 +/ 1464 Point globalCoordinates() { 1465 int x = this.x; 1466 int y = this.y; 1467 auto p = this.parent; 1468 while(p) { 1469 x += p.x; 1470 y += p.y; 1471 p = p.parent; 1472 } 1473 1474 static if(UsingSimpledisplayX11) { 1475 auto dpy = XDisplayConnection.get; 1476 arsd.simpledisplay.Window dummyw; 1477 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1478 } else version(Windows) { 1479 POINT pt; 1480 pt.x = x; 1481 pt.y = y; 1482 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1483 x = pt.x; 1484 y = pt.y; 1485 } else { 1486 featureNotImplemented(); 1487 } 1488 1489 return Point(x, y); 1490 } 1491 1492 version(win32_widgets) 1493 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1494 1495 version(win32_widgets) 1496 /// Called when a WM_COMMAND is sent to the associated hwnd. 1497 void handleWmCommand(ushort cmd, ushort id) {} 1498 1499 version(win32_widgets) 1500 /++ 1501 Called when a WM_NOTIFY is sent to the associated hwnd. 1502 1503 History: 1504 +/ 1505 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1506 1507 version(win32_widgets) 1508 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); } 1509 1510 /++ 1511 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1512 1513 Updates to this variable will only be made visible on the next mouse enter event. 1514 +/ 1515 @scriptable string statusTip; 1516 // string toolTip; 1517 // string helpText; 1518 1519 /++ 1520 If true, this widget can be focused via keyboard control with the tab key. 1521 1522 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1523 +/ 1524 bool tabStop = true; 1525 /++ 1526 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.) 1527 +/ 1528 int tabOrder; 1529 1530 version(win32_widgets) { 1531 static Widget[HWND] nativeMapping; 1532 /// The native handle, if there is one. 1533 HWND hwnd; 1534 WNDPROC originalWindowProcedure; 1535 1536 SimpleWindow simpleWindowWrappingHwnd; 1537 1538 // please note it IGNORES your return value and does NOT forward it to Windows! 1539 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1540 return 0; 1541 } 1542 } 1543 private bool implicitlyCreated; 1544 1545 /// 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. 1546 int x; 1547 /// ditto 1548 int y; 1549 private int _width; 1550 private int _height; 1551 private Widget[] _children; 1552 private Widget _parent; 1553 private Window _parentWindow; 1554 1555 /++ 1556 Returns the window to which this widget is attached. 1557 1558 History: 1559 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. 1560 +/ 1561 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1562 private @property void parentWindow(Window parent) { 1563 auto old = _parentWindow; 1564 _parentWindow = parent; 1565 newParentWindow(old, _parentWindow); 1566 foreach(child; children) 1567 child.parentWindow = parent; // please note that this is recursive 1568 } 1569 1570 /++ 1571 Called when the widget has been added to or remove from a parent window. 1572 1573 Note that either oldParent and/or newParent may be null any time this is called. 1574 1575 History: 1576 Added September 13, 2024 1577 +/ 1578 protected void newParentWindow(Window oldParent, Window newParent) {} 1579 1580 /++ 1581 Returns the list of the widget's children. 1582 1583 History: 1584 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1585 1586 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1587 +/ 1588 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1589 1590 /++ 1591 Returns the widget's parent. 1592 1593 History: 1594 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1595 1596 The parent should only be managed by the [addChild] and [removeWidget] method. 1597 +/ 1598 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1599 1600 /// The widget's current size. 1601 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1602 /// ditto 1603 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1604 1605 /// Only the layout manager should be calling these. 1606 final protected @property int width(int a) @safe { return _width = a; } 1607 /// ditto 1608 final protected @property int height(int a) @safe { return _height = a; } 1609 1610 /++ 1611 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. 1612 1613 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 1614 +/ 1615 protected void registerMovement() { 1616 version(win32_widgets) { 1617 if(hwnd) { 1618 auto pos = getChildPositionRelativeToParentHwnd(this); 1619 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 1620 this.redraw(); 1621 } 1622 } 1623 sendResizeEvent(); 1624 } 1625 1626 /// Creates the widget and adds it to the parent. 1627 this(Widget parent) { 1628 if(parent !is null) 1629 parent.addChild(this); 1630 setupDefaultEventHandlers(); 1631 } 1632 1633 /// 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. 1634 @scriptable 1635 bool isFocused() { 1636 return parentWindow && parentWindow.focusedWidget is this; 1637 } 1638 1639 private bool showing_ = true; 1640 /// 1641 bool showing() { return showing_; } 1642 /// 1643 bool hidden() { return !showing_; } 1644 /++ 1645 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. 1646 +/ 1647 void showing(bool s, bool recalculate = true) { 1648 auto so = showing_; 1649 showing_ = s; 1650 if(s != so) { 1651 version(win32_widgets) 1652 if(hwnd) 1653 ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE); 1654 1655 if(parent && recalculate) { 1656 parent.queueRecomputeChildLayout(); 1657 parent.redraw(); 1658 } 1659 1660 foreach(child; children) 1661 child.showing(s, false); 1662 1663 } 1664 queueRecomputeChildLayout(); 1665 redraw(); 1666 } 1667 /// Convenience method for `showing = true` 1668 @scriptable 1669 void show() { 1670 showing = true; 1671 } 1672 /// Convenience method for `showing = false` 1673 @scriptable 1674 void hide() { 1675 showing = false; 1676 } 1677 1678 /// 1679 @scriptable 1680 void focus() { 1681 assert(parentWindow !is null); 1682 if(isFocused()) 1683 return; 1684 1685 if(parentWindow.focusedWidget) { 1686 // FIXME: more details here? like from and to 1687 auto from = parentWindow.focusedWidget; 1688 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 1689 parentWindow.focusedWidget = null; 1690 from.emit!BlurEvent(); 1691 this.emit!FocusOutEvent(); 1692 } 1693 1694 1695 version(win32_widgets) { 1696 if(this.hwnd !is null) 1697 SetFocus(this.hwnd); 1698 } 1699 //else static if(UsingSimpledisplayX11) 1700 //this.parentWindow.win.focus(); 1701 1702 parentWindow.focusedWidget = this; 1703 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 1704 this.emit!FocusEvent(); 1705 this.emit!FocusInEvent(); 1706 } 1707 1708 /+ 1709 /++ 1710 Unfocuses the widget. This may reset 1711 +/ 1712 @scriptable 1713 void blur() { 1714 1715 } 1716 +/ 1717 1718 1719 /++ 1720 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 1721 1722 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 1723 +/ 1724 void attachedToWindow(Window w) {} 1725 /++ 1726 Callback when the widget is added to another widget. 1727 1728 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 1729 +/ 1730 void addedTo(Widget w) {} 1731 1732 /++ 1733 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. 1734 1735 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 1736 +/ 1737 protected void addChild(Widget w, int position = int.max) { 1738 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 1739 assert(w !is this, "Child cannot be its own parent!"); 1740 w._parent = this; 1741 if(position == int.max || position == children.length) { 1742 _children ~= w; 1743 } else { 1744 assert(position < _children.length); 1745 _children.length = _children.length + 1; 1746 for(int i = cast(int) _children.length - 1; i > position; i--) 1747 _children[i] = _children[i - 1]; 1748 _children[position] = w; 1749 } 1750 1751 this.parentWindow = this._parentWindow; 1752 1753 w.addedTo(this); 1754 1755 if(this.hidden) 1756 w.showing = false; 1757 1758 if(parentWindow !is null) { 1759 w.attachedToWindow(parentWindow); 1760 parentWindow.queueRecomputeChildLayout(); 1761 parentWindow.redraw(); 1762 } 1763 } 1764 1765 /++ 1766 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. 1767 +/ 1768 Widget getChildAtPosition(int x, int y) { 1769 // it goes backward so the last one to show gets picked first 1770 // might use z-index later 1771 foreach_reverse(child; children) { 1772 if(child.hidden) 1773 continue; 1774 if(child.x <= x && child.y <= y 1775 && ((x - child.x) < child.width) 1776 && ((y - child.y) < child.height)) 1777 { 1778 return child; 1779 } 1780 } 1781 1782 return null; 1783 } 1784 1785 /++ 1786 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. 1787 1788 History: 1789 Added July 2, 2021 (v10.2) 1790 +/ 1791 protected void addScrollPosition(ref int x, ref int y) {}; 1792 1793 /++ 1794 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. 1795 1796 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. 1797 1798 [paint] is not called for system widgets as the OS library draws them instead. 1799 1800 1801 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. 1802 1803 You should also look at [WidgetPainter.visualTheme] to be theme aware. 1804 1805 History: 1806 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. 1807 +/ 1808 void paint(WidgetPainter painter) { 1809 version(win32_widgets) 1810 if(hwnd) { 1811 return; 1812 } 1813 painter.drawThemed(&paintContent); // note this refers to the following overload 1814 } 1815 1816 /++ 1817 Responsible for drawing the content as the theme engine is responsible for other elements. 1818 1819 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 1820 1821 Params: 1822 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. 1823 1824 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. 1825 1826 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. 1827 1828 Returns: 1829 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. 1830 1831 History: 1832 Added May 15, 2021 1833 +/ 1834 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 1835 return bounds; 1836 } 1837 1838 deprecated("Change ScreenPainter to WidgetPainter") 1839 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 1840 1841 /// I don't actually like the name of this 1842 /// this draws a background on it 1843 void erase(WidgetPainter painter) { 1844 version(win32_widgets) 1845 if(hwnd) return; // Windows will do it. I think. 1846 1847 auto c = getComputedStyle().background.color; 1848 painter.fillColor = c; 1849 painter.outlineColor = c; 1850 1851 version(win32_widgets) { 1852 HANDLE b, p; 1853 if(c.a == 0 && parent is parentWindow) { 1854 // I don't remember why I had this really... 1855 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 1856 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 1857 } 1858 } 1859 painter.drawRectangle(Point(0, 0), width, height); 1860 version(win32_widgets) { 1861 if(c.a == 0 && parent is parentWindow) { 1862 SelectObject(painter.impl.hdc, p); 1863 SelectObject(painter.impl.hdc, b); 1864 } 1865 } 1866 } 1867 1868 /// 1869 WidgetPainter draw() { 1870 int x = this.x, y = this.y; 1871 auto parent = this.parent; 1872 while(parent) { 1873 x += parent.x; 1874 y += parent.y; 1875 parent = parent.parent; 1876 } 1877 1878 auto painter = parentWindow.win.draw(true); 1879 painter.originX = x; 1880 painter.originY = y; 1881 painter.setClipRectangle(Point(0, 0), width, height); 1882 return WidgetPainter(painter, this); 1883 } 1884 1885 /// 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. 1886 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 1887 if(hidden) 1888 return; 1889 1890 int paintX = x; 1891 int paintY = y; 1892 if(this.useNativeDrawing()) { 1893 paintX = 0; 1894 paintY = 0; 1895 lox = 0; 1896 loy = 0; 1897 containment = Rectangle(0, 0, int.max, int.max); 1898 } 1899 1900 painter.originX = lox + paintX; 1901 painter.originY = loy + paintY; 1902 1903 bool actuallyPainted = false; 1904 1905 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 1906 if(clip == Rectangle.init) { 1907 // writeln(this, " clipped out"); 1908 return; 1909 } 1910 1911 bool invalidateChildren = invalidate; 1912 1913 if(redrawRequested || force) { 1914 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 1915 1916 painter.drawingUpon = this; 1917 1918 erase(painter); 1919 if(painter.visualTheme) 1920 painter.visualTheme.doPaint(this, painter); 1921 else 1922 paint(painter); 1923 1924 if(invalidate) { 1925 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 1926 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 1927 painter.invalidateRect(region); 1928 // children are contained inside this, so no need to do extra work 1929 invalidateChildren = false; 1930 } 1931 1932 redrawRequested = false; 1933 actuallyPainted = true; 1934 } 1935 1936 foreach(child; children) { 1937 version(win32_widgets) 1938 if(child.useNativeDrawing()) continue; 1939 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 1940 } 1941 1942 version(win32_widgets) 1943 foreach(child; children) { 1944 if(child.useNativeDrawing) { 1945 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 1946 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 1947 } 1948 } 1949 } 1950 1951 protected bool useNativeDrawing() nothrow { 1952 version(win32_widgets) 1953 return hwnd !is null; 1954 else 1955 return false; 1956 } 1957 1958 private static class RedrawEvent {} 1959 private __gshared re = new RedrawEvent(); 1960 1961 private bool redrawRequested; 1962 /// 1963 final void redraw(string file = __FILE__, size_t line = __LINE__) { 1964 redrawRequested = true; 1965 1966 if(this.parentWindow) { 1967 auto sw = this.parentWindow.win; 1968 assert(sw !is null); 1969 if(!sw.eventQueued!RedrawEvent) { 1970 sw.postEvent(re); 1971 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1972 } 1973 } 1974 } 1975 1976 private SimpleWindow drawableWindow; 1977 1978 /++ 1979 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. 1980 1981 Returns: 1982 `true` if you should do your default behavior. 1983 1984 History: 1985 Added May 5, 2021 1986 1987 Bugs: 1988 It does not do the static checks on gdc right now. 1989 +/ 1990 final protected bool emit(EventType, this This, Args...)(Args args) { 1991 version(GNU) {} else 1992 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1993 auto e = new EventType(this, args); 1994 e.dispatch(); 1995 return !e.defaultPrevented; 1996 } 1997 /// ditto 1998 final protected bool emit(string eventString, this This)() { 1999 auto e = new Event(eventString, this); 2000 e.dispatch(); 2001 return !e.defaultPrevented; 2002 } 2003 2004 /++ 2005 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. 2006 2007 History: 2008 Added May 5, 2021 2009 +/ 2010 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 2011 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 2012 return addEventListener(handler); 2013 } 2014 2015 /++ 2016 Gets the computed style properties from the visual theme. 2017 2018 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].) 2019 2020 History: 2021 Added May 8, 2021 2022 +/ 2023 final StyleInformation getComputedStyle() { 2024 return StyleInformation(this); 2025 } 2026 2027 int focusableWidgets(scope int delegate(Widget) dg) { 2028 foreach(widget; WidgetStream(this)) { 2029 if(widget.tabStop && !widget.hidden) { 2030 int result = dg(widget); 2031 if (result) 2032 return result; 2033 } 2034 } 2035 return 0; 2036 } 2037 2038 /++ 2039 Calculates the border box (that is, the full width/height of the widget, from border edge to border edge) 2040 for the given content box (the area between the padding) 2041 2042 History: 2043 Added January 4, 2023 (dub v11.0) 2044 +/ 2045 Rectangle borderBoxForContentBox(Rectangle contentBox) { 2046 auto cs = getComputedStyle(); 2047 2048 auto borderWidth = getBorderWidth(cs.borderStyle); 2049 2050 auto rect = contentBox; 2051 2052 rect.left -= borderWidth; 2053 rect.right += borderWidth; 2054 rect.top -= borderWidth; 2055 rect.bottom += borderWidth; 2056 2057 auto insideBorderRect = rect; 2058 2059 rect.left -= cs.paddingLeft; 2060 rect.right += cs.paddingRight; 2061 rect.top -= cs.paddingTop; 2062 rect.bottom += cs.paddingBottom; 2063 2064 return rect; 2065 } 2066 2067 2068 // FIXME: I kinda want to hide events from implementation widgets 2069 // so it just catches them all and stops propagation... 2070 // i guess i can do it with a event listener on star. 2071 2072 mixin Emits!KeyDownEvent; /// 2073 mixin Emits!KeyUpEvent; /// 2074 mixin Emits!CharEvent; /// 2075 2076 mixin Emits!MouseDownEvent; /// 2077 mixin Emits!MouseUpEvent; /// 2078 mixin Emits!ClickEvent; /// 2079 mixin Emits!DoubleClickEvent; /// 2080 mixin Emits!MouseMoveEvent; /// 2081 mixin Emits!MouseOverEvent; /// 2082 mixin Emits!MouseOutEvent; /// 2083 mixin Emits!MouseEnterEvent; /// 2084 mixin Emits!MouseLeaveEvent; /// 2085 2086 mixin Emits!ResizeEvent; /// 2087 2088 mixin Emits!BlurEvent; /// 2089 mixin Emits!FocusEvent; /// 2090 2091 mixin Emits!FocusInEvent; /// 2092 mixin Emits!FocusOutEvent; /// 2093 } 2094 2095 /+ 2096 /++ 2097 Interface to indicate that the widget has a simple value property. 2098 2099 History: 2100 Added August 26, 2021 2101 +/ 2102 interface HasValue!T { 2103 /// Getter 2104 @property T value(); 2105 /// Setter 2106 @property void value(T); 2107 } 2108 2109 /++ 2110 Interface to indicate that the widget has a range of possible values for its simple value property. 2111 This would be present on something like a slider or possibly a number picker. 2112 2113 History: 2114 Added September 11, 2021 2115 +/ 2116 interface HasRangeOfValues!T : HasValue!T { 2117 /// The minimum and maximum values in the range, inclusive. 2118 @property T minValue(); 2119 @property void minValue(T); /// ditto 2120 @property T maxValue(); /// ditto 2121 @property void maxValue(T); /// ditto 2122 2123 /// The smallest step the user interface allows. User may still type in values without this limitation. 2124 @property void step(T); 2125 @property T step(); /// ditto 2126 } 2127 2128 /++ 2129 Interface to indicate that the widget has a list of possible values the user can choose from. 2130 This would be present on something like a drop-down selector. 2131 2132 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2133 combobox. 2134 2135 History: 2136 Added September 11, 2021 2137 +/ 2138 interface HasListOfValues!T : HasValue!T { 2139 @property T[] values; 2140 @property void values(T[]); 2141 2142 @property int selectedIndex(); // note it may return -1! 2143 @property void selectedIndex(int); 2144 } 2145 +/ 2146 2147 /++ 2148 History: 2149 Added September 2021 (dub v10.4) 2150 +/ 2151 class GridLayout : Layout { 2152 2153 // 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. 2154 2155 /++ 2156 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2157 +/ 2158 enum Gravity { 2159 Center = 0, 2160 NorthWest = North | West, 2161 North = 0b10_00, 2162 NorthEast = North | East, 2163 West = 0b00_10, 2164 East = 0b00_01, 2165 SouthWest = South | West, 2166 South = 0b01_00, 2167 SouthEast = South | East, 2168 } 2169 2170 /++ 2171 The width and height are in some proportional units and can often just be 12. 2172 +/ 2173 this(int width, int height, Widget parent) { 2174 this.gridWidth = width; 2175 this.gridHeight = height; 2176 super(parent); 2177 } 2178 2179 /++ 2180 Sets the position of the given child. 2181 2182 The units of these arguments are in the proportional grid units you set in the constructor. 2183 +/ 2184 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2185 // ensure it is in bounds 2186 // then ensure no overlaps 2187 2188 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2189 2190 foreach(ref position; positions) { 2191 if(position.widget is child) { 2192 position = p; 2193 goto set; 2194 } 2195 } 2196 2197 positions ~= p; 2198 2199 set: 2200 2201 // FIXME: should this batch? 2202 queueRecomputeChildLayout(); 2203 2204 return child; 2205 } 2206 2207 override void addChild(Widget w, int position = int.max) { 2208 super.addChild(w, position); 2209 //positions ~= ChildPosition(w); 2210 if(position != int.max) { 2211 // FIXME: align it so they actually match. 2212 } 2213 } 2214 2215 override void widgetRemoved(size_t idx, Widget w) { 2216 // FIXME: keep the positions array aligned 2217 // positions[idx].widget = null; 2218 } 2219 2220 override void recomputeChildLayout() { 2221 registerMovement(); 2222 int onGrid = cast(int) positions.length; 2223 c: foreach(child; children) { 2224 // just snap it to the grid 2225 if(onGrid) 2226 foreach(position; positions) 2227 if(position.widget is child) { 2228 child.x = this.width * position.x / this.gridWidth; 2229 child.y = this.height * position.y / this.gridHeight; 2230 child.width = this.width * position.width / this.gridWidth; 2231 child.height = this.height * position.height / this.gridHeight; 2232 2233 auto diff = child.width - child.maxWidth(); 2234 // FIXME: gravity? 2235 if(diff > 0) { 2236 child.width = child.width - diff; 2237 2238 if(position.gravity & Gravity.West) { 2239 // nothing needed, already aligned 2240 } else if(position.gravity & Gravity.East) { 2241 child.x += diff; 2242 } else { 2243 child.x += diff / 2; 2244 } 2245 } 2246 2247 diff = child.height - child.maxHeight(); 2248 // FIXME: gravity? 2249 if(diff > 0) { 2250 child.height = child.height - diff; 2251 2252 if(position.gravity & Gravity.North) { 2253 // nothing needed, already aligned 2254 } else if(position.gravity & Gravity.South) { 2255 child.y += diff; 2256 } else { 2257 child.y += diff / 2; 2258 } 2259 } 2260 2261 2262 child.recomputeChildLayout(); 2263 onGrid--; 2264 continue c; 2265 } 2266 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2267 } 2268 } 2269 2270 private struct ChildPosition { 2271 Widget widget; 2272 int x; 2273 int y; 2274 int width; 2275 int height; 2276 Gravity gravity; 2277 } 2278 private ChildPosition[] positions; 2279 2280 int gridWidth = 12; 2281 int gridHeight = 12; 2282 } 2283 2284 /// 2285 abstract class ComboboxBase : Widget { 2286 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2287 // or to always show the list, we want CBS_SIMPLE == 1 2288 version(win32_widgets) 2289 this(uint style, Widget parent) { 2290 super(parent); 2291 createWin32Window(this, "ComboBox"w, null, style); 2292 } 2293 else version(custom_widgets) 2294 this(Widget parent) { 2295 super(parent); 2296 2297 addEventListener((KeyDownEvent event) { 2298 if(event.key == Key.Up) { 2299 if(selection_ > -1) { // -1 means select blank 2300 selection_--; 2301 fireChangeEvent(); 2302 } 2303 event.preventDefault(); 2304 } 2305 if(event.key == Key.Down) { 2306 if(selection_ + 1 < options.length) { 2307 selection_++; 2308 fireChangeEvent(); 2309 } 2310 event.preventDefault(); 2311 } 2312 2313 }); 2314 2315 } 2316 else static assert(false); 2317 2318 /++ 2319 Returns the current list of options in the selection. 2320 2321 History: 2322 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2323 +/ 2324 final @property string[] options() const { 2325 return cast(string[]) options_; 2326 } 2327 2328 private string[] options_; 2329 private int selection_ = -1; 2330 2331 /++ 2332 Adds an option to the end of options array. 2333 +/ 2334 void addOption(string s) { 2335 options_ ~= s; 2336 version(win32_widgets) 2337 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2338 } 2339 2340 /++ 2341 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2342 +/ 2343 int getSelection() { 2344 return selection_; 2345 } 2346 2347 /++ 2348 Returns the current selection as a string. 2349 2350 History: 2351 Added November 17, 2021 2352 +/ 2353 string getSelectionString() { 2354 return selection_ == -1 ? null : options[selection_]; 2355 } 2356 2357 /++ 2358 Sets the current selection to an index in the options array, or to the given option if present. 2359 Please note that the string version may do a linear lookup. 2360 2361 Returns: 2362 the index you passed in 2363 2364 History: 2365 The `string` based overload was added on March 1, 2022 (dub v10.7). 2366 2367 The return value was `void` prior to March 1, 2022. 2368 +/ 2369 int setSelection(int idx) { 2370 selection_ = idx; 2371 version(win32_widgets) 2372 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2373 2374 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2375 t.dispatch(); 2376 2377 return idx; 2378 } 2379 2380 /// ditto 2381 int setSelection(string s) { 2382 if(s !is null) 2383 foreach(idx, item; options) 2384 if(item == s) { 2385 return setSelection(cast(int) idx); 2386 } 2387 return setSelection(-1); 2388 } 2389 2390 /++ 2391 This event is fired when the selection changes. Note it inherits 2392 from ChangeEvent!string, meaning you can use that as well, and it also 2393 fills in [Event.intValue]. 2394 +/ 2395 static class SelectionChangedEvent : ChangeEvent!string { 2396 this(Widget target, int iv, string sv) { 2397 super(target, &stringValue); 2398 this.iv = iv; 2399 this.sv = sv; 2400 } 2401 immutable int iv; 2402 immutable string sv; 2403 2404 override @property string stringValue() { return sv; } 2405 override @property int intValue() { return iv; } 2406 } 2407 2408 version(win32_widgets) 2409 override void handleWmCommand(ushort cmd, ushort id) { 2410 if(cmd == CBN_SELCHANGE) { 2411 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2412 fireChangeEvent(); 2413 } 2414 } 2415 2416 private void fireChangeEvent() { 2417 if(selection_ >= options.length) 2418 selection_ = -1; 2419 2420 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2421 t.dispatch(); 2422 } 2423 2424 version(win32_widgets) { 2425 override int minHeight() { return defaultLineHeight + 6; } 2426 override int maxHeight() { return defaultLineHeight + 6; } 2427 } else { 2428 override int minHeight() { return defaultLineHeight + 4; } 2429 override int maxHeight() { return defaultLineHeight + 4; } 2430 } 2431 2432 version(custom_widgets) { 2433 2434 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2435 2436 SimpleWindow dropDown; 2437 void popup() { 2438 auto w = width; 2439 // FIXME: suggestedDropdownHeight see below 2440 auto h = cast(int) this.options.length * defaultLineHeight + 8; 2441 2442 auto coord = this.globalCoordinates(); 2443 auto dropDown = new SimpleWindow( 2444 w, h, 2445 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null); 2446 2447 dropDown.move(coord.x, coord.y + this.height); 2448 2449 { 2450 auto cs = getComputedStyle(); 2451 auto painter = dropDown.draw(); 2452 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2453 auto p = Point(4, 4); 2454 painter.outlineColor = cs.foregroundColor; 2455 foreach(option; options) { 2456 painter.drawText(p, option); 2457 p.y += defaultLineHeight; 2458 } 2459 } 2460 2461 dropDown.setEventHandlers( 2462 (MouseEvent event) { 2463 if(event.type == MouseEventType.buttonReleased) { 2464 dropDown.close(); 2465 auto element = (event.y - 4) / defaultLineHeight; 2466 if(element >= 0 && element <= options.length) { 2467 selection_ = element; 2468 2469 fireChangeEvent(); 2470 } 2471 } 2472 } 2473 ); 2474 2475 dropDown.visibilityChanged = (bool visible) { 2476 if(visible) { 2477 this.redraw(); 2478 dropDown.grabInput(); 2479 } else { 2480 dropDown.releaseInputGrab(); 2481 } 2482 }; 2483 2484 dropDown.show(); 2485 } 2486 2487 } 2488 } 2489 2490 /++ 2491 A drop-down list where the user must select one of the 2492 given options. Like `<select>` in HTML. 2493 +/ 2494 class DropDownSelection : ComboboxBase { 2495 this(Widget parent) { 2496 version(win32_widgets) 2497 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 2498 else version(custom_widgets) { 2499 super(parent); 2500 2501 addEventListener("focus", () { this.redraw; }); 2502 addEventListener("blur", () { this.redraw; }); 2503 addEventListener(EventType.change, () { this.redraw; }); 2504 addEventListener("mousedown", () { this.focus(); this.popup(); }); 2505 addEventListener((KeyDownEvent event) { 2506 if(event.key == Key.Space) 2507 popup(); 2508 }); 2509 } else static assert(false); 2510 } 2511 2512 mixin Padding!q{2}; 2513 static class Style : Widget.Style { 2514 override FrameStyle borderStyle() { return FrameStyle.risen; } 2515 } 2516 mixin OverrideStyle!Style; 2517 2518 version(custom_widgets) 2519 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2520 auto cs = getComputedStyle(); 2521 2522 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 2523 2524 painter.outlineColor = cs.foregroundColor; 2525 painter.fillColor = cs.foregroundColor; 2526 2527 /+ 2528 Point[4] triangle; 2529 enum padding = 6; 2530 enum paddingV = 7; 2531 enum triangleWidth = 10; 2532 triangle[0] = Point(width - padding - triangleWidth, paddingV); 2533 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 2534 triangle[2] = Point(width - padding - 0, paddingV); 2535 triangle[3] = triangle[0]; 2536 painter.drawPolygon(triangle[]); 2537 +/ 2538 2539 auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2); 2540 2541 painter.drawPolygon( 2542 scaleWithDpi(Point(2, 6) + offset), 2543 scaleWithDpi(Point(7, 11) + offset), 2544 scaleWithDpi(Point(12, 6) + offset), 2545 scaleWithDpi(Point(2, 6) + offset) 2546 ); 2547 2548 2549 return bounds; 2550 } 2551 2552 version(win32_widgets) 2553 override void registerMovement() { 2554 version(win32_widgets) { 2555 if(hwnd) { 2556 auto pos = getChildPositionRelativeToParentHwnd(this); 2557 // the height given to this from Windows' perspective is supposed 2558 // to include the drop down's height. so I add to it to give some 2559 // room for that. 2560 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 2561 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 2562 } 2563 } 2564 sendResizeEvent(); 2565 } 2566 } 2567 2568 /++ 2569 A text box with a drop down arrow listing selections. 2570 The user can choose from the list, or type their own. 2571 +/ 2572 class FreeEntrySelection : ComboboxBase { 2573 this(Widget parent) { 2574 version(win32_widgets) 2575 super(2 /* CBS_DROPDOWN */, parent); 2576 else version(custom_widgets) { 2577 super(parent); 2578 auto hl = new HorizontalLayout(this); 2579 lineEdit = new LineEdit(hl); 2580 2581 tabStop = false; 2582 2583 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2584 2585 auto btn = new class ArrowButton { 2586 this() { 2587 super(ArrowDirection.down, hl); 2588 } 2589 override int maxHeight() { 2590 return lineEdit.maxHeight; 2591 } 2592 }; 2593 //btn.addDirectEventListener("focus", &lineEdit.focus); 2594 btn.addEventListener("triggered", &this.popup); 2595 addEventListener(EventType.change, (Event event) { 2596 lineEdit.content = event.stringValue; 2597 lineEdit.focus(); 2598 redraw(); 2599 }); 2600 } 2601 else static assert(false); 2602 } 2603 2604 version(custom_widgets) { 2605 LineEdit lineEdit; 2606 } 2607 } 2608 2609 /++ 2610 A combination of free entry with a list below it. 2611 +/ 2612 class ComboBox : ComboboxBase { 2613 this(Widget parent) { 2614 version(win32_widgets) 2615 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 2616 else version(custom_widgets) { 2617 super(parent); 2618 lineEdit = new LineEdit(this); 2619 listWidget = new ListWidget(this); 2620 listWidget.multiSelect = false; 2621 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 2622 string c = null; 2623 foreach(option; listWidget.options) 2624 if(option.selected) { 2625 c = option.label; 2626 break; 2627 } 2628 lineEdit.content = c; 2629 }); 2630 2631 listWidget.tabStop = false; 2632 this.tabStop = false; 2633 listWidget.addEventListener("focus", &lineEdit.focus); 2634 this.addEventListener("focus", &lineEdit.focus); 2635 2636 addDirectEventListener(EventType.change, { 2637 listWidget.setSelection(selection_); 2638 if(selection_ != -1) 2639 lineEdit.content = options[selection_]; 2640 lineEdit.focus(); 2641 redraw(); 2642 }); 2643 2644 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2645 2646 listWidget.addDirectEventListener(EventType.change, { 2647 int set = -1; 2648 foreach(idx, opt; listWidget.options) 2649 if(opt.selected) { 2650 set = cast(int) idx; 2651 break; 2652 } 2653 if(set != selection_) 2654 this.setSelection(set); 2655 }); 2656 } else static assert(false); 2657 } 2658 2659 override int minHeight() { return defaultLineHeight * 3; } 2660 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 2661 override int heightStretchiness() { return 5; } 2662 2663 version(custom_widgets) { 2664 LineEdit lineEdit; 2665 ListWidget listWidget; 2666 2667 override void addOption(string s) { 2668 listWidget.options ~= ListWidget.Option(s); 2669 ComboboxBase.addOption(s); 2670 } 2671 } 2672 } 2673 2674 /+ 2675 class Spinner : Widget { 2676 version(win32_widgets) 2677 this(Widget parent) { 2678 super(parent); 2679 parentWindow = parent.parentWindow; 2680 auto hlayout = new HorizontalLayout(this); 2681 lineEdit = new LineEdit(hlayout); 2682 upDownControl = new UpDownControl(hlayout); 2683 } 2684 2685 LineEdit lineEdit; 2686 UpDownControl upDownControl; 2687 } 2688 2689 class UpDownControl : Widget { 2690 version(win32_widgets) 2691 this(Widget parent) { 2692 super(parent); 2693 parentWindow = parent.parentWindow; 2694 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 2695 } 2696 2697 override int minHeight() { return defaultLineHeight; } 2698 override int maxHeight() { return defaultLineHeight * 3/2; } 2699 2700 override int minWidth() { return defaultLineHeight * 3/2; } 2701 override int maxWidth() { return defaultLineHeight * 3/2; } 2702 } 2703 +/ 2704 2705 /+ 2706 class DataView : Widget { 2707 // this is the omnibus data viewer 2708 // the internal data layout is something like: 2709 // string[string][] but also each node can have parents 2710 } 2711 +/ 2712 2713 2714 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 2715 2716 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 2717 2718 // FIXME: menus should prolly capture the mouse. ugh i kno. 2719 /* 2720 TextEdit needs: 2721 2722 * caret manipulation 2723 * selection control 2724 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 2725 2726 For example: 2727 2728 connect(paste, &textEdit.insertTextAtCaret); 2729 2730 would be nice. 2731 2732 2733 2734 I kinda want an omnibus dataview that combines list, tree, 2735 and table - it can be switched dynamically between them. 2736 2737 Flattening policy: only show top level, show recursive, show grouped 2738 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 2739 2740 Single select, multi select, organization, drag+drop 2741 */ 2742 2743 //static if(UsingSimpledisplayX11) 2744 version(win32_widgets) {} 2745 else version(custom_widgets) { 2746 enum scrollClickRepeatInterval = 50; 2747 2748 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 2749 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 2750 enum activeTabColor = lightAccentColor; 2751 enum hoveringColor = Color(228, 228, 228); 2752 enum buttonColor = windowBackgroundColor; 2753 enum depressedButtonColor = darkAccentColor; 2754 enum activeListXorColor = Color(255, 255, 127); 2755 enum progressBarColor = Color(0, 0, 128); 2756 enum activeMenuItemColor = Color(0, 0, 128); 2757 2758 }} 2759 else static assert(false); 2760 deprecated("Get these properties off the `visualTheme` instead.") { 2761 // these are used by horizontal rule so not just custom_widgets. for now at least. 2762 enum darkAccentColor = Color(172, 172, 172); 2763 enum lightAccentColor = Color(223, 223, 223); // used to be 223 2764 } 2765 2766 private const(wchar)* toWstringzInternal(in char[] s) { 2767 wchar[] str; 2768 str.reserve(s.length + 1); 2769 foreach(dchar ch; s) 2770 str ~= ch; 2771 str ~= '\0'; 2772 return str.ptr; 2773 } 2774 2775 static if(SimpledisplayTimerAvailable) 2776 void setClickRepeat(Widget w, int interval, int delay = 250) { 2777 Timer timer; 2778 int delayRemaining = delay / interval; 2779 if(delayRemaining <= 1) 2780 delayRemaining = 2; 2781 2782 immutable originalDelayRemaining = delayRemaining; 2783 2784 w.addDirectEventListener((scope MouseDownEvent ev) { 2785 if(ev.srcElement !is w) 2786 return; 2787 if(timer !is null) { 2788 timer.destroy(); 2789 timer = null; 2790 } 2791 delayRemaining = originalDelayRemaining; 2792 timer = new Timer(interval, () { 2793 if(delayRemaining > 0) 2794 delayRemaining--; 2795 else { 2796 auto ev = new Event("triggered", w); 2797 ev.sendDirectly(); 2798 } 2799 }); 2800 }); 2801 2802 w.addDirectEventListener((scope MouseUpEvent ev) { 2803 if(ev.srcElement !is w) 2804 return; 2805 if(timer !is null) { 2806 timer.destroy(); 2807 timer = null; 2808 } 2809 }); 2810 2811 w.addDirectEventListener((scope MouseLeaveEvent ev) { 2812 if(ev.srcElement !is w) 2813 return; 2814 if(timer !is null) { 2815 timer.destroy(); 2816 timer = null; 2817 } 2818 }); 2819 2820 } 2821 else 2822 void setClickRepeat(Widget w, int interval, int delay = 250) {} 2823 2824 enum FrameStyle { 2825 none, /// 2826 risen, /// a 3d pop-out effect (think Windows 95 button) 2827 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 2828 solid, /// 2829 dotted, /// 2830 fantasy, /// a style based on a popular fantasy video game 2831 rounded, /// a rounded rectangle 2832 } 2833 2834 version(custom_widgets) 2835 deprecated 2836 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 2837 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2838 } 2839 2840 version(custom_widgets) 2841 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 2842 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 2843 } 2844 2845 version(custom_widgets) 2846 deprecated 2847 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 2848 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2849 } 2850 2851 int getBorderWidth(FrameStyle style) { 2852 final switch(style) { 2853 case FrameStyle.sunk, FrameStyle.risen: 2854 return 2; 2855 case FrameStyle.none: 2856 return 0; 2857 case FrameStyle.solid: 2858 return 1; 2859 case FrameStyle.dotted: 2860 return 1; 2861 case FrameStyle.fantasy: 2862 return 3; 2863 case FrameStyle.rounded: 2864 return 2; 2865 } 2866 } 2867 2868 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 2869 int borderWidth = getBorderWidth(style); 2870 final switch(style) { 2871 case FrameStyle.sunk, FrameStyle.risen: 2872 // outer layer 2873 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 2874 break; 2875 case FrameStyle.none: 2876 painter.outlineColor = background; 2877 break; 2878 case FrameStyle.solid: 2879 case FrameStyle.rounded: 2880 painter.pen = Pen(border, 1); 2881 break; 2882 case FrameStyle.dotted: 2883 painter.pen = Pen(border, 1, Pen.Style.Dotted); 2884 break; 2885 case FrameStyle.fantasy: 2886 painter.pen = Pen(border, 3); 2887 break; 2888 } 2889 2890 painter.fillColor = background; 2891 2892 if(style == FrameStyle.rounded) { 2893 painter.drawRectangleRounded(Point(x, y), Size(width, height), 6); 2894 } else { 2895 painter.drawRectangle(Point(x + 0, y + 0), width, height); 2896 2897 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 2898 // 3d effect 2899 auto vt = WidgetPainter.visualTheme; 2900 2901 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 2902 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 2903 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 2904 2905 // inner layer 2906 //right, bottom 2907 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 2908 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 2909 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 2910 // left, top 2911 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 2912 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 2913 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 2914 } else if(style == FrameStyle.fantasy) { 2915 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 2916 painter.fillColor = Color.transparent; 2917 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 2918 } 2919 } 2920 2921 return borderWidth; 2922 } 2923 2924 /++ 2925 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. 2926 2927 See_Also: 2928 [MenuItem] 2929 [ToolButton] 2930 [Menu.addItem] 2931 +/ 2932 class Action { 2933 version(win32_widgets) { 2934 private int id; 2935 private static int lastId = 9000; 2936 private static Action[int] mapping; 2937 } 2938 2939 KeyEvent accelerator; 2940 2941 // FIXME: disable message 2942 // and toggle thing? 2943 // ??? and trigger arguments too ??? 2944 2945 /++ 2946 Params: 2947 label = the textual label 2948 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 2949 triggered = initial handler, more can be added via the [triggered] member. 2950 +/ 2951 this(string label, ushort icon = 0, void delegate() triggered = null) { 2952 this.label = label; 2953 this.iconId = icon; 2954 if(triggered !is null) 2955 this.triggered ~= triggered; 2956 version(win32_widgets) { 2957 id = ++lastId; 2958 mapping[id] = this; 2959 } 2960 } 2961 2962 private string label; 2963 private ushort iconId; 2964 // icon 2965 2966 // when it is triggered, the triggered event is fired on the window 2967 /// The list of handlers when it is triggered. 2968 void delegate()[] triggered; 2969 } 2970 2971 /* 2972 plan: 2973 keyboard accelerators 2974 2975 * menus (and popups and tooltips) 2976 * status bar 2977 * toolbars and buttons 2978 2979 sortable table view 2980 2981 maybe notification area icons 2982 basic clipboard 2983 2984 * radio box 2985 splitter 2986 toggle buttons (optionally mutually exclusive, like in Paint) 2987 label, rich text display, multi line plain text (selectable) 2988 * fieldset 2989 * nestable grid layout 2990 single line text input 2991 * multi line text input 2992 slider 2993 spinner 2994 list box 2995 drop down 2996 combo box 2997 auto complete box 2998 * progress bar 2999 3000 terminal window/widget (on unix it might even be a pty but really idk) 3001 3002 ok button 3003 cancel button 3004 3005 keyboard hotkeys 3006 3007 scroll widget 3008 3009 event redirections and network transparency 3010 script integration 3011 */ 3012 3013 3014 /* 3015 MENUS 3016 3017 auto bar = new MenuBar(window); 3018 window.menuBar = bar; 3019 3020 auto fileMenu = bar.addItem(new Menu("&File")); 3021 fileMenu.addItem(new MenuItem("&Exit")); 3022 3023 3024 EVENTS 3025 3026 For controls, you should usually use "triggered" rather than "click", etc., because 3027 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 3028 This is the case on menus and pushbuttons. 3029 3030 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 3031 */ 3032 3033 3034 /* 3035 enum LinePreference { 3036 AlwaysOnOwnLine, // always on its own line 3037 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 3038 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 3039 } 3040 */ 3041 3042 /++ 3043 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. 3044 3045 --- 3046 class MyWidget : Widget { 3047 this(Widget parent) { super(parent); } 3048 3049 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 3050 mixin Padding!q{4}; 3051 3052 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 3053 mixin Margin!q{8}; 3054 3055 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 3056 // while Top/Bottom/Right remain 8 from the mixin above. 3057 override int marginLeft() { return 2; } 3058 } 3059 --- 3060 3061 3062 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]). 3063 3064 Padding is the area inside a widget where its background is drawn, but the content avoids. 3065 3066 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!). 3067 3068 * 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. 3069 +/ 3070 mixin template Padding(string code) { 3071 override int paddingLeft() { return mixin(code);} 3072 override int paddingRight() { return mixin(code);} 3073 override int paddingTop() { return mixin(code);} 3074 override int paddingBottom() { return mixin(code);} 3075 } 3076 3077 /// ditto 3078 mixin template Margin(string code) { 3079 override int marginLeft() { return mixin(code);} 3080 override int marginRight() { return mixin(code);} 3081 override int marginTop() { return mixin(code);} 3082 override int marginBottom() { return mixin(code);} 3083 } 3084 3085 private 3086 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3087 enum calcingV = relevantMeasure == "height"; 3088 3089 parent.registerMovement(); 3090 3091 if(parent.children.length == 0) 3092 return; 3093 3094 auto parentStyle = parent.getComputedStyle(); 3095 3096 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3097 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3098 3099 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3100 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3101 3102 // my own width and height should already be set by the caller of this function... 3103 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3104 mixin("parentStyle.padding"~firstThingy~"()") - 3105 mixin("parentStyle.padding"~secondThingy~"()"); 3106 3107 int stretchinessSum; 3108 int stretchyChildSum; 3109 int lastMargin = 0; 3110 3111 int shrinkinessSum; 3112 int shrinkyChildSum; 3113 3114 // set initial size 3115 foreach(child; parent.children) { 3116 3117 auto childStyle = child.getComputedStyle(); 3118 3119 if(cast(StaticPosition) child) 3120 continue; 3121 if(child.hidden) 3122 continue; 3123 3124 const iw = child.flexBasisWidth(); 3125 const ih = child.flexBasisHeight(); 3126 3127 static if(calcingV) { 3128 child.width = parent.width - 3129 mixin("childStyle.margin"~otherFirstThingy~"()") - 3130 mixin("childStyle.margin"~otherSecondThingy~"()") - 3131 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3132 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3133 3134 if(child.width < 0) 3135 child.width = 0; 3136 if(child.width > childStyle.maxWidth()) 3137 child.width = childStyle.maxWidth(); 3138 3139 if(iw > 0) { 3140 auto totalPossible = child.width; 3141 if(child.width > iw && child.widthStretchiness() == 0) 3142 child.width = iw; 3143 } 3144 3145 child.height = mymax(childStyle.minHeight(), ih); 3146 } else { 3147 // set to take all the space 3148 child.height = parent.height - 3149 mixin("childStyle.margin"~firstThingy~"()") - 3150 mixin("childStyle.margin"~secondThingy~"()") - 3151 mixin("parentStyle.padding"~firstThingy~"()") - 3152 mixin("parentStyle.padding"~secondThingy~"()"); 3153 3154 // then clamp it 3155 if(child.height < 0) 3156 child.height = 0; 3157 if(child.height > childStyle.maxHeight()) 3158 child.height = childStyle.maxHeight(); 3159 3160 // and if possible, respect the ideal target 3161 if(ih > 0) { 3162 auto totalPossible = child.height; 3163 if(child.height > ih && child.heightStretchiness() == 0) 3164 child.height = ih; 3165 } 3166 3167 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3168 child.width = mymax(childStyle.minWidth(), iw); 3169 } 3170 3171 spaceRemaining -= mixin("child." ~ relevantMeasure); 3172 3173 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3174 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3175 lastMargin = margin; 3176 spaceRemaining -= thisMargin + margin; 3177 3178 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3179 stretchinessSum += s; 3180 if(s > 0) 3181 stretchyChildSum++; 3182 3183 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3184 shrinkinessSum += s2; 3185 if(s2 > 0) 3186 shrinkyChildSum++; 3187 } 3188 3189 if(spaceRemaining < 0 && shrinkyChildSum) { 3190 // shrink to get into the space if it is possible 3191 auto toRemove = -spaceRemaining; 3192 auto removalPerItem = toRemove * shrinkinessSum / shrinkyChildSum; 3193 auto remainder = toRemove * shrinkinessSum % shrinkyChildSum; 3194 3195 // FIXME: wtf why am i shrinking things with no shrinkiness? 3196 3197 foreach(child; parent.children) { 3198 auto childStyle = child.getComputedStyle(); 3199 if(cast(StaticPosition) child) 3200 continue; 3201 if(child.hidden) 3202 continue; 3203 static if(calcingV) { 3204 auto maximum = childStyle.maxHeight(); 3205 } else { 3206 auto maximum = childStyle.maxWidth(); 3207 } 3208 3209 if(mixin("child._" ~ relevantMeasure) >= maximum) 3210 continue; 3211 3212 mixin("child._" ~ relevantMeasure) -= removalPerItem + remainder; // this is removing more than needed to trigger the next thing. ugh. 3213 3214 spaceRemaining += removalPerItem + remainder; 3215 } 3216 } 3217 3218 // stretch to fill space 3219 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3220 auto spacePerChild = spaceRemaining / stretchinessSum; 3221 bool spreadEvenly; 3222 bool giveToBiggest; 3223 if(spacePerChild <= 0) { 3224 spacePerChild = spaceRemaining / stretchyChildSum; 3225 spreadEvenly = true; 3226 } 3227 if(spacePerChild <= 0) { 3228 giveToBiggest = true; 3229 } 3230 int previousSpaceRemaining = spaceRemaining; 3231 stretchinessSum = 0; 3232 Widget mostStretchy; 3233 int mostStretchyS; 3234 foreach(child; parent.children) { 3235 auto childStyle = child.getComputedStyle(); 3236 if(cast(StaticPosition) child) 3237 continue; 3238 if(child.hidden) 3239 continue; 3240 static if(calcingV) { 3241 auto maximum = childStyle.maxHeight(); 3242 } else { 3243 auto maximum = childStyle.maxWidth(); 3244 } 3245 3246 if(mixin("child." ~ relevantMeasure) >= maximum) { 3247 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3248 mixin("child._" ~ relevantMeasure) -= adj; 3249 spaceRemaining += adj; 3250 continue; 3251 } 3252 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3253 if(s <= 0) 3254 continue; 3255 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3256 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3257 spaceRemaining -= spaceAdjustment; 3258 if(mixin("child." ~ relevantMeasure) > maximum) { 3259 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3260 mixin("child._" ~ relevantMeasure) -= diff; 3261 spaceRemaining += diff; 3262 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3263 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3264 if(mostStretchy is null || s >= mostStretchyS) { 3265 mostStretchy = child; 3266 mostStretchyS = s; 3267 } 3268 } 3269 } 3270 3271 if(giveToBiggest && mostStretchy !is null) { 3272 auto child = mostStretchy; 3273 auto childStyle = child.getComputedStyle(); 3274 int spaceAdjustment = spaceRemaining; 3275 3276 static if(calcingV) 3277 auto maximum = childStyle.maxHeight(); 3278 else 3279 auto maximum = childStyle.maxWidth(); 3280 3281 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3282 spaceRemaining -= spaceAdjustment; 3283 if(mixin("child._" ~ relevantMeasure) > maximum) { 3284 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3285 mixin("child._" ~ relevantMeasure) -= diff; 3286 spaceRemaining += diff; 3287 } 3288 } 3289 3290 if(spaceRemaining == previousSpaceRemaining) { 3291 if(mostStretchy !is null) { 3292 static if(calcingV) 3293 auto maximum = mostStretchy.maxHeight(); 3294 else 3295 auto maximum = mostStretchy.maxWidth(); 3296 3297 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3298 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3299 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3300 } 3301 break; // apparently nothing more we can do 3302 } 3303 } 3304 3305 foreach(child; parent.children) { 3306 auto childStyle = child.getComputedStyle(); 3307 if(cast(StaticPosition) child) 3308 continue; 3309 if(child.hidden) 3310 continue; 3311 3312 static if(calcingV) 3313 auto maximum = childStyle.maxHeight(); 3314 else 3315 auto maximum = childStyle.maxWidth(); 3316 if(mixin("child._" ~ relevantMeasure) > maximum) 3317 mixin("child._" ~ relevantMeasure) = maximum; 3318 } 3319 3320 // position 3321 lastMargin = 0; 3322 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3323 foreach(child; parent.children) { 3324 auto childStyle = child.getComputedStyle(); 3325 if(cast(StaticPosition) child) { 3326 child.recomputeChildLayout(); 3327 continue; 3328 } 3329 if(child.hidden) 3330 continue; 3331 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3332 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3333 currentPos += thisMargin; 3334 static if(calcingV) { 3335 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3336 child.y = currentPos; 3337 } else { 3338 child.x = currentPos; 3339 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3340 3341 } 3342 currentPos += mixin("child." ~ relevantMeasure); 3343 currentPos += margin; 3344 lastMargin = margin; 3345 3346 child.recomputeChildLayout(); 3347 } 3348 } 3349 3350 int mymax(int a, int b) { return a > b ? a : b; } 3351 int mymax(int a, int b, int c) { 3352 auto d = mymax(a, b); 3353 return c > d ? c : d; 3354 } 3355 3356 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3357 // and here, it must be integrable with the layout, the event system, and not be painted over. 3358 version(win32_widgets) { 3359 3360 // this function just does stuff that a parent window needs for redirection 3361 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3362 this_.hookedWndProc(msg, wParam, lParam); 3363 3364 switch(msg) { 3365 3366 case WM_VSCROLL, WM_HSCROLL: 3367 auto pos = HIWORD(wParam); 3368 auto m = LOWORD(wParam); 3369 3370 auto scrollbarHwnd = cast(HWND) lParam; 3371 3372 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3373 3374 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3375 3376 switch(m) { 3377 /+ 3378 // I don't think those messages are ever actually sent normally by the widget itself, 3379 // they are more used for the keyboard interface. methinks. 3380 case SB_BOTTOM: 3381 // writeln("end"); 3382 auto event = new Event("scrolltoend", *widgetp); 3383 event.dispatch(); 3384 //if(!event.defaultPrevented) 3385 break; 3386 case SB_TOP: 3387 // writeln("top"); 3388 auto event = new Event("scrolltobeginning", *widgetp); 3389 event.dispatch(); 3390 break; 3391 case SB_ENDSCROLL: 3392 // idk 3393 break; 3394 +/ 3395 case SB_LINEDOWN: 3396 (*widgetp).emitCommand!"scrolltonextline"(); 3397 return 0; 3398 case SB_LINEUP: 3399 (*widgetp).emitCommand!"scrolltopreviousline"(); 3400 return 0; 3401 case SB_PAGEDOWN: 3402 (*widgetp).emitCommand!"scrolltonextpage"(); 3403 return 0; 3404 case SB_PAGEUP: 3405 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3406 return 0; 3407 case SB_THUMBPOSITION: 3408 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3409 ev.dispatch(); 3410 return 0; 3411 case SB_THUMBTRACK: 3412 // eh kinda lying but i like the real time update display 3413 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3414 ev.dispatch(); 3415 3416 // the event loop doesn't seem to carry on with a requested redraw.. 3417 // so we request it to get our dirty bit set... 3418 // then we need to immediately actually redraw it too for instant feedback to user 3419 SimpleWindow.processAllCustomEvents(); 3420 SimpleWindow.processAllCustomEvents(); 3421 //if(this_.parentWindow) 3422 //this_.parentWindow.actualRedraw(); 3423 3424 // and this ensures the WM_PAINT message is sent fairly quickly 3425 // still seems to lag a little in large windows but meh it basically works. 3426 if(this_.parentWindow) { 3427 // FIXME: if painting is slow, this does still lag 3428 // we probably will want to expose some user hook to ScrollWindowEx 3429 // or something. 3430 UpdateWindow(this_.parentWindow.hwnd); 3431 } 3432 return 0; 3433 default: 3434 } 3435 } 3436 break; 3437 3438 case WM_CONTEXTMENU: 3439 auto hwndFrom = cast(HWND) wParam; 3440 3441 auto xPos = cast(short) LOWORD(lParam); 3442 auto yPos = cast(short) HIWORD(lParam); 3443 3444 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3445 POINT p; 3446 p.x = xPos; 3447 p.y = yPos; 3448 ScreenToClient(hwnd, &p); 3449 auto clientX = cast(ushort) p.x; 3450 auto clientY = cast(ushort) p.y; 3451 3452 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 3453 3454 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 3455 return 0; 3456 } 3457 } 3458 break; 3459 3460 case WM_DRAWITEM: 3461 auto dis = cast(DRAWITEMSTRUCT*) lParam; 3462 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 3463 return (*widgetp).handleWmDrawItem(dis); 3464 } 3465 break; 3466 3467 case WM_NOTIFY: 3468 auto hdr = cast(NMHDR*) lParam; 3469 auto hwndFrom = hdr.hwndFrom; 3470 auto code = hdr.code; 3471 3472 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3473 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 3474 } 3475 break; 3476 case WM_COMMAND: 3477 auto handle = cast(HWND) lParam; 3478 auto cmd = HIWORD(wParam); 3479 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 3480 3481 default: 3482 // pass it on 3483 } 3484 return 0; 3485 } 3486 3487 3488 3489 extern(Windows) 3490 private 3491 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 3492 // but can i merge them?! 3493 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3494 // try { writeln(iMessage); } catch(Exception e) {}; 3495 3496 if(auto te = hWnd in Widget.nativeMapping) { 3497 try { 3498 3499 te.hookedWndProc(iMessage, wParam, lParam); 3500 3501 int mustReturn; 3502 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 3503 if(mustReturn) 3504 return ret; 3505 3506 if(iMessage == WM_SETFOCUS) { 3507 auto lol = *te; 3508 while(lol !is null && lol.implicitlyCreated) 3509 lol = lol.parent; 3510 lol.focus(); 3511 //(*te).parentWindow.focusedWidget = lol; 3512 } 3513 3514 3515 if(iMessage == WM_CTLCOLOREDIT) { 3516 3517 } 3518 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 3519 SetBkMode(cast(HDC) wParam, TRANSPARENT); 3520 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 3521 //GetStockObject(NULL_BRUSH); 3522 } 3523 3524 auto pos = getChildPositionRelativeToParentOrigin(*te); 3525 lastDefaultPrevented = false; 3526 // try { writeln(typeid(*te)); } catch(Exception e) {} 3527 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 3528 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 3529 else { 3530 // it was something we recognized, should only call the window procedure if the default was not prevented 3531 } 3532 } catch(Exception e) { 3533 assert(0, e.toString()); 3534 } 3535 return 0; 3536 } 3537 assert(0, "shouldn't be receiving messages for this window...."); 3538 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 3539 } 3540 3541 extern(Windows) 3542 private 3543 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 3544 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3545 if(iMessage == WM_ERASEBKGND) { 3546 auto dc = GetDC(hWnd); 3547 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 3548 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 3549 RECT r; 3550 GetWindowRect(hWnd, &r); 3551 // since the pen is null, to fill the whole space, we need the +1 on both. 3552 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 3553 SelectObject(dc, p); 3554 SelectObject(dc, b); 3555 ReleaseDC(hWnd, dc); 3556 InvalidateRect(hWnd, null, false); // redraw the border 3557 return 1; 3558 } 3559 return HookedWndProc(hWnd, iMessage, wParam, lParam); 3560 } 3561 3562 /++ 3563 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 3564 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 3565 of minigui's expectations. 3566 3567 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 3568 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 3569 3570 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 3571 3572 To check if you can use this, use `static if(UsingWin32Widgets)`. 3573 +/ 3574 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 3575 assert(p.parentWindow !is null); 3576 assert(p.parentWindow.win.impl.hwnd !is null); 3577 3578 auto bsgroupbox = style == BS_GROUPBOX; 3579 3580 HWND phwnd; 3581 3582 auto wtf = p.parent; 3583 while(wtf) { 3584 if(wtf.hwnd !is null) { 3585 phwnd = wtf.hwnd; 3586 break; 3587 } 3588 wtf = wtf.parent; 3589 } 3590 3591 if(phwnd is null) 3592 phwnd = p.parentWindow.win.impl.hwnd; 3593 3594 assert(phwnd !is null); 3595 3596 WCharzBuffer wt = WCharzBuffer(windowText); 3597 3598 style |= WS_VISIBLE | WS_CHILD; 3599 //if(className != WC_TABCONTROL) 3600 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 3601 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 3602 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 3603 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 3604 3605 assert(p.hwnd !is null); 3606 3607 3608 static HFONT font; 3609 if(font is null) { 3610 NONCLIENTMETRICS params; 3611 params.cbSize = params.sizeof; 3612 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 3613 font = CreateFontIndirect(¶ms.lfMessageFont); 3614 } 3615 } 3616 3617 if(font) 3618 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 3619 3620 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 3621 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 3622 Widget.nativeMapping[p.hwnd] = p; 3623 3624 if(bsgroupbox) 3625 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 3626 else 3627 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3628 3629 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 3630 3631 p.registerMovement(); 3632 } 3633 } 3634 3635 version(win32_widgets) 3636 private 3637 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 3638 if(hwnd is null || hwnd in Widget.nativeMapping) 3639 return true; 3640 auto parent = cast(Widget) cast(void*) lparam; 3641 Widget p = new Widget(null); 3642 p._parent = parent; 3643 p.parentWindow = parent.parentWindow; 3644 p.hwnd = hwnd; 3645 p.implicitlyCreated = true; 3646 Widget.nativeMapping[p.hwnd] = p; 3647 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3648 return true; 3649 } 3650 3651 /++ 3652 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 3653 +/ 3654 struct WidgetPainter { 3655 this(ScreenPainter screenPainter, Widget drawingUpon) { 3656 this.drawingUpon = drawingUpon; 3657 this.screenPainter = screenPainter; 3658 if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 3659 this.screenPainter.setFont(font); 3660 } 3661 3662 /++ 3663 EXPERIMENTAL. subject to change. 3664 3665 When you draw a cursor, you can draw this to notify your window of where it is, 3666 for IME systems to use. 3667 +/ 3668 void notifyCursorPosition(int x, int y, int width, int height) { 3669 if(auto a = drawingUpon.parentWindow) 3670 if(auto w = a.inputProxy) { 3671 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 3672 } 3673 } 3674 3675 3676 /// 3677 ScreenPainter screenPainter; 3678 /// Forward to the screen painter for other methods 3679 alias screenPainter this; 3680 3681 private Widget drawingUpon; 3682 3683 /++ 3684 This is the list of rectangles that actually need to be redrawn. 3685 3686 Not actually implemented yet. 3687 +/ 3688 Rectangle[] invalidatedRectangles; 3689 3690 private static BaseVisualTheme _visualTheme; 3691 3692 /++ 3693 Functions to access the visual theme and helpers to easily use it. 3694 3695 These are aware of the current widget's computed style out of the theme. 3696 +/ 3697 static @property BaseVisualTheme visualTheme() { 3698 if(_visualTheme is null) 3699 _visualTheme = new DefaultVisualTheme(); 3700 return _visualTheme; 3701 } 3702 3703 /// ditto 3704 static @property void visualTheme(BaseVisualTheme theme) { 3705 _visualTheme = theme; 3706 3707 // FIXME: notify all windows about the new theme, they should recompute layout and redraw. 3708 } 3709 3710 /// ditto 3711 Color themeForeground() { 3712 return drawingUpon.getComputedStyle().foregroundColor(); 3713 } 3714 3715 /// ditto 3716 Color themeBackground() { 3717 return drawingUpon.getComputedStyle().background.color; 3718 } 3719 3720 int isDarkTheme() { 3721 return 0; // unspecified, yes, no as enum. FIXME 3722 } 3723 3724 /++ 3725 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. 3726 3727 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 3728 3729 If you change teh clip rectangle, you should change it back before you return. 3730 3731 3732 The sequence it uses is: 3733 background 3734 content (delegated to you) 3735 border 3736 focused outline 3737 selected overlay 3738 3739 Example code: 3740 3741 --- 3742 void paint(WidgetPainter painter) { 3743 painter.drawThemed((bounds) { 3744 return bounds; // if the selection overlay should be contained, you can return it here. 3745 }); 3746 } 3747 --- 3748 +/ 3749 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 3750 drawThemed((WidgetPainter painter, const Rectangle bounds) { 3751 return drawBody(bounds); 3752 }); 3753 } 3754 // this overload is actually mroe for setting the delegate to a virtual function 3755 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 3756 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 3757 3758 auto cs = drawingUpon.getComputedStyle(); 3759 3760 auto bg = cs.background.color; 3761 3762 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 3763 3764 rect.left += borderWidth; 3765 rect.right -= borderWidth; 3766 rect.top += borderWidth; 3767 rect.bottom -= borderWidth; 3768 3769 auto insideBorderRect = rect; 3770 3771 rect.left += cs.paddingLeft; 3772 rect.right -= cs.paddingRight; 3773 rect.top += cs.paddingTop; 3774 rect.bottom -= cs.paddingBottom; 3775 3776 this.outlineColor = this.themeForeground; 3777 this.fillColor = bg; 3778 3779 auto widgetFont = cs.fontCached; 3780 if(widgetFont !is null) 3781 this.setFont(widgetFont); 3782 3783 rect = drawBody(this, rect); 3784 3785 if(widgetFont !is null) { 3786 if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 3787 this.setFont(vtFont); 3788 else 3789 this.setFont(null); 3790 } 3791 3792 if(auto os = cs.outlineStyle()) { 3793 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 3794 this.fillColor = Color.transparent; 3795 this.drawRectangle(insideBorderRect); 3796 } 3797 } 3798 3799 /++ 3800 First, draw the background. 3801 Then draw your content. 3802 Next, draw the border. 3803 And the focused indicator. 3804 And the is-selected box. 3805 3806 If it is focused i can draw the outline too... 3807 3808 If selected i can even do the xor action but that's at the end. 3809 +/ 3810 void drawThemeBackground() { 3811 3812 } 3813 3814 void drawThemeBorder() { 3815 3816 } 3817 3818 // all this stuff is a dangerous experiment.... 3819 static class ScriptableVersion { 3820 ScreenPainterImplementation* p; 3821 int originX, originY; 3822 3823 @scriptable: 3824 void drawRectangle(int x, int y, int width, int height) { 3825 p.drawRectangle(x + originX, y + originY, width, height); 3826 } 3827 void drawLine(int x1, int y1, int x2, int y2) { 3828 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 3829 } 3830 void drawText(int x, int y, string text) { 3831 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 3832 } 3833 void setOutlineColor(int r, int g, int b) { 3834 p.pen = Pen(Color(r,g,b), 1); 3835 } 3836 void setFillColor(int r, int g, int b) { 3837 p.fillColor = Color(r,g,b); 3838 } 3839 } 3840 3841 ScriptableVersion toArsdJsvar() { 3842 auto sv = new ScriptableVersion; 3843 sv.p = this.screenPainter.impl; 3844 sv.originX = this.screenPainter.originX; 3845 sv.originY = this.screenPainter.originY; 3846 return sv; 3847 } 3848 3849 static WidgetPainter fromJsVar(T)(T t) { 3850 return WidgetPainter.init; 3851 } 3852 // done.......... 3853 } 3854 3855 3856 struct Style { 3857 static struct helper(string m, T) { 3858 enum method = m; 3859 T v; 3860 3861 mixin template MethodOverride(typeof(this) v) { 3862 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 3863 } 3864 } 3865 3866 static auto opDispatch(string method, T)(T value) { 3867 return helper!(method, T)(value); 3868 } 3869 } 3870 3871 /++ 3872 Implementation detail of the [ControlledBy] UDA. 3873 3874 History: 3875 Added Oct 28, 2020 3876 +/ 3877 struct ControlledBy_(T, Args...) { 3878 Args args; 3879 3880 static if(Args.length) 3881 this(Args args) { 3882 this.args = args; 3883 } 3884 3885 private T construct(Widget parent) { 3886 return new T(args, parent); 3887 } 3888 } 3889 3890 /++ 3891 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 3892 3893 History: 3894 Added Oct 28, 2020 3895 +/ 3896 auto ControlledBy(T, Args...)(Args args) { 3897 return ControlledBy_!(T, Args)(args); 3898 } 3899 3900 struct ContainerMeta { 3901 string name; 3902 ContainerMeta[] children; 3903 Widget function(Widget parent) factory; 3904 3905 Widget instantiate(Widget parent) { 3906 auto n = factory(parent); 3907 n.name = name; 3908 foreach(child; children) 3909 child.instantiate(n); 3910 return n; 3911 } 3912 } 3913 3914 /++ 3915 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 3916 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 3917 3918 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 3919 structures. It works fine on structs declared inside functions though. 3920 3921 See: https://issues.dlang.org/show_bug.cgi?id=21984 3922 +/ 3923 template Container(CArgs...) { 3924 static if(CArgs.length && is(CArgs[0] : Widget)) { 3925 private alias Super = CArgs[0]; 3926 private alias CArgs2 = CArgs[1 .. $]; 3927 } else { 3928 private alias Super = Layout; 3929 private alias CArgs2 = CArgs; 3930 } 3931 3932 class Container : Super { 3933 this(Widget parent) { super(parent); } 3934 3935 // just to partially support old gdc versions 3936 version(GNU) { 3937 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 3938 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 3939 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 3940 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 3941 } else mixin(q{ 3942 static foreach(Arg; CArgs2) { 3943 mixin Arg.MethodOverride!(Arg); 3944 } 3945 }); 3946 3947 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 3948 return ContainerMeta( 3949 name, 3950 children.dup, 3951 function (Widget parent) { return new typeof(this)(parent); } 3952 ); 3953 } 3954 3955 static ContainerMeta opCall(ContainerMeta[] children...) { 3956 return opCall(null, children); 3957 } 3958 } 3959 } 3960 3961 /++ 3962 The data controller widget is created by reflecting over the given 3963 data type. You can use [ControlledBy] as a UDA on a struct or 3964 just let it create things automatically. 3965 3966 Unlike [dialog], this uses real-time updating of the data and 3967 you add it to another window yourself. 3968 3969 --- 3970 struct Test { 3971 int x; 3972 int y; 3973 } 3974 3975 auto window = new Window(); 3976 auto dcw = new DataControllerWidget!Test(new Test, window); 3977 --- 3978 3979 The way it works is any public members are given a widget based 3980 on their data type, and public methods trigger an action button 3981 if no relevant parameters or a dialog action if it does have 3982 parameters, similar to the [menu] facility. 3983 3984 If you change data programmatically, without going through the 3985 DataControllerWidget methods, you will have to tell it something 3986 has changed and it needs to redraw. This is done with the `invalidate` 3987 method. 3988 3989 History: 3990 Added Oct 28, 2020 3991 +/ 3992 /// Group: generating_from_code 3993 class DataControllerWidget(T) : WidgetContainer { 3994 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 3995 private alias Tref = T; 3996 else 3997 private alias Tref = T*; 3998 3999 Tref datum; 4000 4001 /++ 4002 See_also: [addDataControllerWidget] 4003 +/ 4004 this(Tref datum, Widget parent) { 4005 this.datum = datum; 4006 4007 Widget cp = this; 4008 4009 super(parent); 4010 4011 foreach(attr; __traits(getAttributes, T)) 4012 static if(is(typeof(attr) == ContainerMeta)) { 4013 cp = attr.instantiate(this); 4014 } 4015 4016 auto def = this.getByName("default"); 4017 if(def !is null) 4018 cp = def; 4019 4020 Widget helper(string name) { 4021 auto maybe = this.getByName(name); 4022 if(maybe is null) 4023 return cp; 4024 return maybe; 4025 4026 } 4027 4028 foreach(member; __traits(allMembers, T)) 4029 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 4030 static if(is(typeof(__traits(getMember, this.datum, member)))) 4031 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 4032 void delegate() update; 4033 4034 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 4035 4036 if(update) 4037 updaters ~= update; 4038 4039 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 4040 w.addEventListener("triggered", delegate() { 4041 makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))(); 4042 notifyDataUpdated(); 4043 }); 4044 } else static if(is(typeof(w.isChecked) == bool)) { 4045 w.addEventListener(EventType.change, (Event ev) { 4046 __traits(getMember, this.datum, member) = w.isChecked; 4047 }); 4048 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 4049 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 4050 } else static if(is(typeof(w.value) == int)) { 4051 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4052 } else static if(is(typeof(w) == DropDownSelection)) { 4053 // special case for this to kinda support enums and such. coudl be better though 4054 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4055 } else { 4056 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 4057 } 4058 } 4059 } 4060 4061 /++ 4062 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 4063 4064 History: 4065 Added May 28, 2021 4066 +/ 4067 void notifyDataUpdated() { 4068 foreach(updater; updaters) 4069 updater(); 4070 4071 this.emit!(ChangeEvent!void)(delegate{}); 4072 } 4073 4074 private Widget[string] memberWidgets; 4075 private void delegate()[] updaters; 4076 4077 mixin Emits!(ChangeEvent!void); 4078 } 4079 4080 private int saturatedSum(int[] values...) { 4081 int sum; 4082 foreach(value; values) { 4083 if(value == int.max) 4084 return int.max; 4085 sum += value; 4086 } 4087 return sum; 4088 } 4089 4090 void genericSetValue(T, W)(T* where, W what) { 4091 import std.conv; 4092 *where = to!T(what); 4093 //*where = cast(T) stringToLong(what); 4094 } 4095 4096 /++ 4097 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4098 4099 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4100 4101 Note that this creates the widget but does not attach any event handlers to it. 4102 +/ 4103 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 4104 4105 string displayName = __traits(identifier, tt).beautify; 4106 4107 static if(controlledByCount!tt == 1) { 4108 foreach(i, attr; __traits(getAttributes, tt)) { 4109 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4110 auto w = attr.construct(parent); 4111 static if(__traits(compiles, w.setPosition(*valptr))) 4112 update = () { w.setPosition(*valptr); }; 4113 else static if(__traits(compiles, w.setValue(*valptr))) 4114 update = () { w.setValue(*valptr); }; 4115 4116 if(update) 4117 update(); 4118 return w; 4119 } 4120 } 4121 } else static if(controlledByCount!tt == 0) { 4122 static if(is(typeof(tt) == enum)) { 4123 // FIXME: update 4124 auto dds = new DropDownSelection(parent); 4125 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4126 dds.addOption(option); 4127 if(__traits(getMember, typeof(tt), option) == *valptr) 4128 dds.setSelection(cast(int) idx); 4129 } 4130 return dds; 4131 } else static if(is(typeof(tt) == bool)) { 4132 auto box = new Checkbox(displayName, parent); 4133 update = () { box.isChecked = *valptr; }; 4134 update(); 4135 return box; 4136 } else static if(is(typeof(tt) : const long)) { 4137 auto le = new LabeledLineEdit(displayName, parent); 4138 update = () { le.content = toInternal!string(*valptr); }; 4139 update(); 4140 return le; 4141 } else static if(is(typeof(tt) : const double)) { 4142 auto le = new LabeledLineEdit(displayName, parent); 4143 import std.conv; 4144 update = () { le.content = to!string(*valptr); }; 4145 update(); 4146 return le; 4147 } else static if(is(typeof(tt) : const string)) { 4148 auto le = new LabeledLineEdit(displayName, parent); 4149 update = () { le.content = *valptr; }; 4150 update(); 4151 return le; 4152 } else static if(is(typeof(tt) == function)) { 4153 auto w = new Button(displayName, parent); 4154 return w; 4155 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4156 return parent.addDataControllerWidget(tt); 4157 } else static assert(0, typeof(tt).stringof); 4158 } else static assert(0, "multiple controllers not yet supported"); 4159 } 4160 4161 private template controlledByCount(alias tt) { 4162 static int helper() { 4163 int count; 4164 foreach(i, attr; __traits(getAttributes, tt)) 4165 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 4166 count++; 4167 return count; 4168 } 4169 4170 enum controlledByCount = helper; 4171 } 4172 4173 /++ 4174 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 4175 4176 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 4177 4178 History: 4179 The `redrawOnChange` parameter was added on May 28, 2021. 4180 +/ 4181 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 4182 auto dcw = new DataControllerWidget!T(t, parent); 4183 initializeDataControllerWidget(dcw, redrawOnChange); 4184 return dcw; 4185 } 4186 4187 /// ditto 4188 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 4189 auto dcw = new DataControllerWidget!T(t, parent); 4190 initializeDataControllerWidget(dcw, redrawOnChange); 4191 return dcw; 4192 } 4193 4194 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 4195 if(redrawOnChange !is null) 4196 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 4197 } 4198 4199 /++ 4200 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. 4201 4202 History: 4203 Finalized on June 3, 2021 for the dub v10.0 release 4204 +/ 4205 struct StyleInformation { 4206 private Widget w; 4207 private BaseVisualTheme visualTheme; 4208 4209 private this(Widget w) { 4210 this.w = w; 4211 this.visualTheme = WidgetPainter.visualTheme; 4212 } 4213 4214 /++ 4215 Forwards to [Widget.Style] 4216 4217 Bugs: 4218 It is supposed to fall back to the [VisualTheme] if 4219 the style doesn't override the default, but that is 4220 not generally implemented. Many of them may end up 4221 being explicit overloads instead of the generic 4222 opDispatch fallback, like [font] is now. 4223 +/ 4224 public @property opDispatch(string name)() { 4225 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4226 w.useStyleProperties((scope Widget.Style props) { 4227 //visualTheme.useStyleProperties(w, (props) { 4228 prop = __traits(getMember, props, name); 4229 }); 4230 return prop; 4231 } 4232 4233 /++ 4234 Returns the cached font object associated with the widget, 4235 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 4236 4237 History: 4238 Prior to March 21, 2022 (dub v10.7), `font` went through 4239 [opDispatch], which did not use the cache. You can now call it 4240 repeatedly without guilt. 4241 +/ 4242 public @property OperatingSystemFont font() { 4243 OperatingSystemFont prop; 4244 w.useStyleProperties((scope Widget.Style props) { 4245 prop = props.fontCached; 4246 }); 4247 if(prop is null) { 4248 prop = visualTheme.defaultFontCached(w.currentDpi); 4249 } 4250 return prop; 4251 } 4252 4253 @property { 4254 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 4255 /** */ int paddingLeft() { return w.paddingLeft(); } 4256 /** */ int paddingRight() { return w.paddingRight(); } 4257 /** */ int paddingTop() { return w.paddingTop(); } 4258 /** */ int paddingBottom() { return w.paddingBottom(); } 4259 4260 /** */ int marginLeft() { return w.marginLeft(); } 4261 /** */ int marginRight() { return w.marginRight(); } 4262 /** */ int marginTop() { return w.marginTop(); } 4263 /** */ int marginBottom() { return w.marginBottom(); } 4264 4265 /** */ int maxHeight() { return w.maxHeight(); } 4266 /** */ int minHeight() { return w.minHeight(); } 4267 4268 /** */ int maxWidth() { return w.maxWidth(); } 4269 /** */ int minWidth() { return w.minWidth(); } 4270 4271 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 4272 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 4273 4274 /** */ int heightStretchiness() { return w.heightStretchiness(); } 4275 /** */ int widthStretchiness() { return w.widthStretchiness(); } 4276 4277 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 4278 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 4279 4280 // Global helpers some of these are unstable. 4281 static: 4282 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4283 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4284 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4285 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4286 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 4287 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4288 4289 /** */ Color activeTabColor() { return lightAccentColor; } 4290 /** */ Color buttonColor() { return windowBackgroundColor; } 4291 /** */ Color depressedButtonColor() { return darkAccentColor; } 4292 /** */ Color hoveringColor() { return lightAccentColor; } 4293 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 4294 auto c = WidgetPainter.visualTheme.selectionColor(); 4295 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4296 } 4297 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4298 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4299 } 4300 4301 4302 4303 /+ 4304 4305 private static auto extractStyleProperty(string name)(Widget w) { 4306 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4307 w.useStyleProperties((props) { 4308 prop = __traits(getMember, props, name); 4309 }); 4310 return prop; 4311 } 4312 4313 // FIXME: clear this upon a X server disconnect 4314 private static OperatingSystemFont[string] fontCache; 4315 4316 T getProperty(T)(string name, lazy T default_) { 4317 if(visualTheme !is null) { 4318 auto str = visualTheme.getPropertyString(w, name); 4319 if(str is null) 4320 return default_; 4321 static if(is(T == Color)) 4322 return Color.fromString(str); 4323 else static if(is(T == Measurement)) 4324 return Measurement(cast(int) toInternal!int(str)); 4325 else static if(is(T == WidgetBackground)) 4326 return WidgetBackground.fromString(str); 4327 else static if(is(T == OperatingSystemFont)) { 4328 if(auto f = str in fontCache) 4329 return *f; 4330 else 4331 return fontCache[str] = new OperatingSystemFont(str); 4332 } else static if(is(T == FrameStyle)) { 4333 switch(str) { 4334 default: 4335 return FrameStyle.none; 4336 foreach(style; __traits(allMembers, FrameStyle)) 4337 case style: 4338 return __traits(getMember, FrameStyle, style); 4339 } 4340 } else static assert(0); 4341 } else 4342 return default_; 4343 } 4344 4345 static struct Measurement { 4346 int value; 4347 alias value this; 4348 } 4349 4350 @property: 4351 4352 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 4353 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 4354 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 4355 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 4356 4357 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 4358 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 4359 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 4360 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 4361 4362 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 4363 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 4364 4365 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 4366 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 4367 4368 4369 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 4370 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 4371 4372 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 4373 4374 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 4375 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 4376 4377 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 4378 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 4379 4380 4381 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4382 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4383 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4384 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4385 4386 Color activeTabColor() { return lightAccentColor; } 4387 Color buttonColor() { return windowBackgroundColor; } 4388 Color depressedButtonColor() { return darkAccentColor; } 4389 Color hoveringColor() { return Color(228, 228, 228); } 4390 Color activeListXorColor() { 4391 auto c = WidgetPainter.visualTheme.selectionColor(); 4392 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4393 } 4394 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4395 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4396 +/ 4397 } 4398 4399 4400 4401 // pragma(msg, __traits(classInstanceSize, Widget)); 4402 4403 /*private*/ template EventString(E) { 4404 static if(is(typeof(E.EventString))) 4405 enum EventString = E.EventString; 4406 else 4407 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 4408 } 4409 4410 /*private*/ template EventStringIdentifier(E) { 4411 string helper() { 4412 auto es = EventString!E; 4413 char[] id = new char[](es.length * 2); 4414 size_t idx; 4415 foreach(char ch; es) { 4416 id[idx++] = cast(char)('a' + (ch >> 4)); 4417 id[idx++] = cast(char)('a' + (ch & 0x0f)); 4418 } 4419 return cast(string) id; 4420 } 4421 4422 enum EventStringIdentifier = helper(); 4423 } 4424 4425 4426 template classStaticallyEmits(This, EventType) { 4427 static if(is(This Base == super)) 4428 static if(is(Base : Widget)) 4429 enum baseEmits = classStaticallyEmits!(Base, EventType); 4430 else 4431 enum baseEmits = false; 4432 else 4433 enum baseEmits = false; 4434 4435 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 4436 4437 enum classStaticallyEmits = thisEmits || baseEmits; 4438 } 4439 4440 /++ 4441 A helper to make widgets out of other native windows. 4442 4443 History: 4444 Factored out of OpenGlWidget on November 5, 2021 4445 +/ 4446 class NestedChildWindowWidget : Widget { 4447 SimpleWindow win; 4448 4449 /++ 4450 Used on X to send focus to the appropriate child window when requested by the window manager. 4451 4452 Normally returns its own nested window. Can also return another child or null to revert to the parent 4453 if you override it in a child class. 4454 4455 History: 4456 Added April 2, 2022 (dub v10.8) 4457 +/ 4458 SimpleWindow focusableWindow() { 4459 return win; 4460 } 4461 4462 /// 4463 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4464 this(SimpleWindow win, Widget parent) { 4465 this.parentWindow = parent.parentWindow; 4466 this.win = win; 4467 4468 super(parent); 4469 windowsetup(win); 4470 } 4471 4472 static protected SimpleWindow getParentWindow(Widget parent) { 4473 assert(parent !is null); 4474 SimpleWindow pwin = parent.parentWindow.win; 4475 4476 version(win32_widgets) { 4477 HWND phwnd; 4478 auto wtf = parent; 4479 while(wtf) { 4480 if(wtf.hwnd) { 4481 phwnd = wtf.hwnd; 4482 break; 4483 } 4484 wtf = wtf.parent; 4485 } 4486 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 4487 if(phwnd) 4488 pwin = new SimpleWindow(phwnd); 4489 } 4490 4491 return pwin; 4492 } 4493 4494 /++ 4495 Called upon the nested window being destroyed. 4496 Remember the window has already been destroyed at 4497 this point, so don't use the native handle for anything. 4498 4499 History: 4500 Added April 3, 2022 (dub v10.8) 4501 +/ 4502 protected void dispose() { 4503 4504 } 4505 4506 protected void windowsetup(SimpleWindow w) { 4507 /* 4508 win.onFocusChange = (bool getting) { 4509 if(getting) 4510 this.focus(); 4511 }; 4512 */ 4513 4514 /+ 4515 win.onFocusChange = (bool getting) { 4516 if(getting) { 4517 this.parentWindow.focusedWidget = this; 4518 this.emit!FocusEvent(); 4519 this.emit!FocusInEvent(); 4520 } else { 4521 this.emit!BlurEvent(); 4522 this.emit!FocusOutEvent(); 4523 } 4524 }; 4525 +/ 4526 4527 win.onDestroyed = () { 4528 this.dispose(); 4529 }; 4530 4531 version(win32_widgets) { 4532 Widget.nativeMapping[win.hwnd] = this; 4533 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4534 } else { 4535 win.setEventHandlers( 4536 (MouseEvent e) { 4537 Widget p = this; 4538 while(p ! is parentWindow) { 4539 e.x += p.x; 4540 e.y += p.y; 4541 p = p.parent; 4542 } 4543 parentWindow.dispatchMouseEvent(e); 4544 }, 4545 (KeyEvent e) { 4546 //writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 4547 parentWindow.dispatchKeyEvent(e); 4548 }, 4549 (dchar e) { 4550 parentWindow.dispatchCharEvent(e); 4551 }, 4552 ); 4553 } 4554 4555 } 4556 4557 override void showing(bool s, bool recalc) { 4558 auto cur = hidden; 4559 win.hidden = !s; 4560 if(cur != s && s) 4561 redraw(); 4562 } 4563 4564 /// OpenGL widgets cannot have child widgets. Do not call this. 4565 /* @disable */ final override void addChild(Widget, int) { 4566 throw new Error("cannot add children to OpenGL widgets"); 4567 } 4568 4569 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 4570 /// Keep in mind that events like mouse coordinates are still relative to your size. 4571 override void registerMovement() { 4572 // writefln("%d %d %d %d", x,y,width,height); 4573 version(win32_widgets) 4574 auto pos = getChildPositionRelativeToParentHwnd(this); 4575 else 4576 auto pos = getChildPositionRelativeToParentOrigin(this); 4577 win.moveResize(pos[0], pos[1], width, height); 4578 4579 registerMovementAdditionalWork(); 4580 sendResizeEvent(); 4581 } 4582 4583 abstract void registerMovementAdditionalWork(); 4584 } 4585 4586 /++ 4587 Nests an opengl capable window inside this window as a widget. 4588 4589 You may also just want to create an additional [SimpleWindow] with 4590 [OpenGlOptions.yes] yourself. 4591 4592 An OpenGL widget cannot have child widgets. It will throw if you try. 4593 +/ 4594 static if(OpenGlEnabled) 4595 class OpenGlWidget : NestedChildWindowWidget { 4596 4597 override void registerMovementAdditionalWork() { 4598 win.setAsCurrentOpenGlContext(); 4599 } 4600 4601 /// 4602 this(Widget parent) { 4603 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4604 super(win, parent); 4605 } 4606 4607 override void paint(WidgetPainter painter) { 4608 win.setAsCurrentOpenGlContext(); 4609 glViewport(0, 0, this.width, this.height); 4610 win.redrawOpenGlSceneNow(); 4611 } 4612 4613 void redrawOpenGlScene(void delegate() dg) { 4614 win.redrawOpenGlScene = dg; 4615 } 4616 } 4617 4618 /++ 4619 This demo shows how to draw text in an opengl scene. 4620 +/ 4621 unittest { 4622 import arsd.minigui; 4623 import arsd.ttf; 4624 4625 void main() { 4626 auto window = new Window(); 4627 4628 auto widget = new OpenGlWidget(window); 4629 4630 // old means non-shader code so compatible with glBegin etc. 4631 // tbh I haven't implemented new one in font yet... 4632 // anyway, declaring here, will construct soon. 4633 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 4634 4635 // this is a little bit awkward, calling some methods through 4636 // the underlying SimpleWindow `win` method, and you can't do this 4637 // on a nanovega widget due to conflicts so I should probably fix 4638 // the api to be a bit easier. But here it will work. 4639 // 4640 // Alternatively, you could load the font on the first draw, inside 4641 // the redrawOpenGlScene, and keep a flag so you don't do it every 4642 // time. That'd be a bit easier since the lib sets up the context 4643 // by then guaranteed. 4644 // 4645 // But still, I wanna show this. 4646 widget.win.visibleForTheFirstTime = delegate { 4647 // must set the opengl context 4648 widget.win.setAsCurrentOpenGlContext(); 4649 4650 // if you were doing a OpenGL 3+ shader, this 4651 // gets especially important to do in order. With 4652 // old-style opengl, I think you can even do it 4653 // in main(), but meh, let's show it more correctly. 4654 4655 // Anyway, now it is time to load the font from the 4656 // OS (you can alternatively load one from a .ttf file 4657 // you bundle with the application), then load the 4658 // font into texture for drawing. 4659 4660 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 4661 4662 assert(!osfont.isNull()); // make sure it actually loaded 4663 4664 // using typeof to avoid repeating the long name lol 4665 glfont = new typeof(glfont)( 4666 // get the raw data from the font for loading in here 4667 // since it doesn't use the OS function to draw the 4668 // text, we gotta treat it more as a file than as 4669 // a drawing api. 4670 osfont.getTtfBytes(), 4671 18, // need to respecify size since opengl world is different coordinate system 4672 4673 // these last two numbers are why it is called 4674 // "Limited" font. It only loads the characters 4675 // in the given range, since the texture atlas 4676 // it references is all a big image generated ahead 4677 // of time. You could maybe do the whole thing but 4678 // idk how much memory that is. 4679 // 4680 // But here, 0-128 represents the ASCII range, so 4681 // good enough for most English things, numeric labels, 4682 // etc. 4683 0, 4684 128 4685 ); 4686 }; 4687 4688 widget.redrawOpenGlScene = () { 4689 // now we can use the glfont's drawString function 4690 4691 // first some opengl setup. You can do this in one place 4692 // on window first visible too in many cases, just showing 4693 // here cuz it is easier for me. 4694 4695 // gonna need some alpha blending or it just looks awful 4696 glEnable(GL_BLEND); 4697 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 4698 glClearColor(0,0,0,0); 4699 glDepthFunc(GL_LEQUAL); 4700 4701 // Also need to enable 2d textures, since it draws the 4702 // font characters as images baked in 4703 glMatrixMode(GL_MODELVIEW); 4704 glLoadIdentity(); 4705 glDisable(GL_DEPTH_TEST); 4706 glEnable(GL_TEXTURE_2D); 4707 4708 // the orthographic matrix is best for 2d things like text 4709 // so let's set that up. This matrix makes the coordinates 4710 // in the opengl scene be one-to-one with the actual pixels 4711 // on screen. (Not necessarily best, you may wish to scale 4712 // things, but it does help keep fonts looking normal.) 4713 glMatrixMode(GL_PROJECTION); 4714 glLoadIdentity(); 4715 glOrtho(0, widget.width, widget.height, 0, 0, 1); 4716 4717 // you can do other glScale, glRotate, glTranslate, etc 4718 // to the matrix here of course if you want. 4719 4720 // note the x,y coordinates here are for the text baseline 4721 // NOT the upper-left corner. The baseline is like the line 4722 // in the notebook you write on. Most the letters are actually 4723 // above it, but some, like p and q, dip a bit below it. 4724 // 4725 // So if you're used to the upper left coordinate like the 4726 // rest of simpledisplay/minigui usually do, do the 4727 // y + glfont.ascent to bring it down a little. So this 4728 // example puts the string in the upper left of the window. 4729 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 4730 4731 // re color btw: the function sets a solid color internally, 4732 // but you actually COULD do your own thing for rainbow effects 4733 // and the sort if you wanted too, by pulling its guts out. 4734 // Just view its source for an idea of how it actually draws: 4735 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 4736 4737 // it gets a bit complicated with the character positioning, 4738 // but the opengl parts are fairly simple: bind a texture, 4739 // set the color, draw a quad for each letter. 4740 4741 4742 // the last optional argument there btw is a bounding box 4743 // it will/ use to word wrap and return an object you can 4744 // use to implement scrolling or pagination; it tells how 4745 // much of the string didn't fit in the box. But for simple 4746 // labels we can just ignore that. 4747 4748 4749 // I'd suggest drawing text as the last step, after you 4750 // do your other drawing. You might use the push/pop matrix 4751 // stuff to keep your place. You, in theory, should be able 4752 // to do text in a 3d space but I've never actually tried 4753 // that.... 4754 }; 4755 4756 window.loop(); 4757 } 4758 } 4759 4760 version(custom_widgets) 4761 private alias ListWidgetBase = ScrollableWidget; 4762 else 4763 private alias ListWidgetBase = Widget; 4764 4765 /++ 4766 A list widget contains a list of strings that the user can examine and select. 4767 4768 4769 In the future, items in the list may be possible to be more than just strings. 4770 4771 See_Also: 4772 [TableView] 4773 +/ 4774 class ListWidget : ListWidgetBase { 4775 /// 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. 4776 mixin Emits!(ChangeEvent!void); 4777 4778 static struct Option { 4779 string label; 4780 bool selected; 4781 void* tag; 4782 } 4783 4784 /++ 4785 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 4786 +/ 4787 void setSelection(int y) { 4788 if(!multiSelect) 4789 foreach(ref opt; options) 4790 opt.selected = false; 4791 if(y >= 0 && y < options.length) 4792 options[y].selected = !options[y].selected; 4793 4794 this.emit!(ChangeEvent!void)(delegate {}); 4795 4796 version(custom_widgets) 4797 redraw(); 4798 } 4799 4800 /++ 4801 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 4802 Returns -1 if nothing is selected. 4803 +/ 4804 int getSelection() 4805 { 4806 foreach(i, opt; options) { 4807 if (opt.selected) 4808 return cast(int) i; 4809 } 4810 return -1; 4811 } 4812 4813 version(custom_widgets) 4814 override void defaultEventHandler_click(ClickEvent event) { 4815 this.focus(); 4816 if(event.button == MouseButton.left) { 4817 auto y = (event.clientY - 4) / defaultLineHeight; 4818 if(y >= 0 && y < options.length) { 4819 setSelection(y); 4820 } 4821 } 4822 super.defaultEventHandler_click(event); 4823 } 4824 4825 this(Widget parent) { 4826 tabStop = false; 4827 super(parent); 4828 version(win32_widgets) 4829 createWin32Window(this, WC_LISTBOX, "", 4830 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 4831 } 4832 4833 version(win32_widgets) 4834 override void handleWmCommand(ushort code, ushort id) { 4835 switch(code) { 4836 case LBN_SELCHANGE: 4837 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 4838 setSelection(cast(int) sel); 4839 break; 4840 default: 4841 } 4842 } 4843 4844 4845 version(custom_widgets) 4846 override void paintFrameAndBackground(WidgetPainter painter) { 4847 draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor); 4848 } 4849 4850 version(custom_widgets) 4851 override void paint(WidgetPainter painter) { 4852 auto cs = getComputedStyle(); 4853 auto pos = Point(4, 4); 4854 foreach(idx, option; options) { 4855 painter.fillColor = painter.visualTheme.widgetBackgroundColor; 4856 painter.outlineColor = painter.visualTheme.widgetBackgroundColor; 4857 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4858 if(option.selected) { 4859 //painter.rasterOp = RasterOp.xor; 4860 painter.outlineColor = cs.selectionForegroundColor; 4861 painter.fillColor = cs.selectionBackgroundColor; 4862 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4863 //painter.rasterOp = RasterOp.normal; 4864 } 4865 painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor; 4866 painter.drawText(pos, option.label); 4867 pos.y += defaultLineHeight; 4868 } 4869 } 4870 4871 static class Style : Widget.Style { 4872 override WidgetBackground background() { 4873 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 4874 } 4875 } 4876 mixin OverrideStyle!Style; 4877 //mixin Padding!q{2}; 4878 4879 void addOption(string text, void* tag = null) { 4880 options ~= Option(text, false, tag); 4881 version(win32_widgets) { 4882 WCharzBuffer buffer = WCharzBuffer(text); 4883 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 4884 } 4885 version(custom_widgets) { 4886 setContentSize(width, cast(int) (options.length * defaultLineHeight)); 4887 redraw(); 4888 } 4889 } 4890 4891 void clear() { 4892 options = null; 4893 version(win32_widgets) { 4894 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 4895 {} 4896 4897 } else version(custom_widgets) { 4898 scrollTo(Point(0, 0)); 4899 redraw(); 4900 } 4901 } 4902 4903 Option[] options; 4904 version(win32_widgets) 4905 enum multiSelect = false; /// not implemented yet 4906 else 4907 bool multiSelect; 4908 4909 override int heightStretchiness() { return 6; } 4910 } 4911 4912 4913 4914 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 4915 enum ScrollBarShowPolicy { 4916 automatic, /// automatically show the scroll bar if it is necessary 4917 never, /// never show the scroll bar (scrolling must be done programmatically) 4918 always /// always show the scroll bar, even if it is disabled 4919 } 4920 4921 /++ 4922 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 4923 4924 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 4925 +/ 4926 // FIXME ScrollBarShowPolicy 4927 // FIXME: use the ScrollMessageWidget in here now that it exists 4928 class ScrollableWidget : Widget { 4929 // FIXME: make line size configurable 4930 // FIXME: add keyboard controls 4931 version(win32_widgets) { 4932 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 4933 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 4934 auto pos = HIWORD(wParam); 4935 auto m = LOWORD(wParam); 4936 4937 // FIXME: I can reintroduce the 4938 // scroll bars now by using this 4939 // in the top-level window handler 4940 // to forward comamnds 4941 auto scrollbarHwnd = lParam; 4942 switch(m) { 4943 case SB_BOTTOM: 4944 if(msg == WM_HSCROLL) 4945 horizontalScrollTo(contentWidth_); 4946 else 4947 verticalScrollTo(contentHeight_); 4948 break; 4949 case SB_TOP: 4950 if(msg == WM_HSCROLL) 4951 horizontalScrollTo(0); 4952 else 4953 verticalScrollTo(0); 4954 break; 4955 case SB_ENDSCROLL: 4956 // idk 4957 break; 4958 case SB_LINEDOWN: 4959 if(msg == WM_HSCROLL) 4960 horizontalScroll(scaleWithDpi(16)); 4961 else 4962 verticalScroll(scaleWithDpi(16)); 4963 break; 4964 case SB_LINEUP: 4965 if(msg == WM_HSCROLL) 4966 horizontalScroll(scaleWithDpi(-16)); 4967 else 4968 verticalScroll(scaleWithDpi(-16)); 4969 break; 4970 case SB_PAGEDOWN: 4971 if(msg == WM_HSCROLL) 4972 horizontalScroll(scaleWithDpi(100)); 4973 else 4974 verticalScroll(scaleWithDpi(100)); 4975 break; 4976 case SB_PAGEUP: 4977 if(msg == WM_HSCROLL) 4978 horizontalScroll(scaleWithDpi(-100)); 4979 else 4980 verticalScroll(scaleWithDpi(-100)); 4981 break; 4982 case SB_THUMBPOSITION: 4983 case SB_THUMBTRACK: 4984 if(msg == WM_HSCROLL) 4985 horizontalScrollTo(pos); 4986 else 4987 verticalScrollTo(pos); 4988 4989 if(m == SB_THUMBTRACK) { 4990 // the event loop doesn't seem to carry on with a requested redraw.. 4991 // so we request it to get our dirty bit set... 4992 redraw(); 4993 4994 // then we need to immediately actually redraw it too for instant feedback to user 4995 4996 SimpleWindow.processAllCustomEvents(); 4997 //if(parentWindow) 4998 //parentWindow.actualRedraw(); 4999 } 5000 break; 5001 default: 5002 } 5003 } 5004 return super.hookedWndProc(msg, wParam, lParam); 5005 } 5006 } 5007 /// 5008 this(Widget parent) { 5009 this.parentWindow = parent.parentWindow; 5010 5011 version(win32_widgets) { 5012 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 5013 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 5014 super(parent); 5015 } else version(custom_widgets) { 5016 outerContainer = new InternalScrollableContainerWidget(this, parent); 5017 super(outerContainer); 5018 } else static assert(0); 5019 } 5020 5021 version(custom_widgets) 5022 InternalScrollableContainerWidget outerContainer; 5023 5024 override void defaultEventHandler_click(ClickEvent event) { 5025 if(event.button == MouseButton.wheelUp) 5026 verticalScroll(scaleWithDpi(-16)); 5027 if(event.button == MouseButton.wheelDown) 5028 verticalScroll(scaleWithDpi(16)); 5029 super.defaultEventHandler_click(event); 5030 } 5031 5032 override void defaultEventHandler_keydown(KeyDownEvent event) { 5033 switch(event.key) { 5034 case Key.Left: 5035 horizontalScroll(scaleWithDpi(-16)); 5036 break; 5037 case Key.Right: 5038 horizontalScroll(scaleWithDpi(16)); 5039 break; 5040 case Key.Up: 5041 verticalScroll(scaleWithDpi(-16)); 5042 break; 5043 case Key.Down: 5044 verticalScroll(scaleWithDpi(16)); 5045 break; 5046 case Key.Home: 5047 verticalScrollTo(0); 5048 break; 5049 case Key.End: 5050 verticalScrollTo(contentHeight); 5051 break; 5052 case Key.PageUp: 5053 verticalScroll(scaleWithDpi(-160)); 5054 break; 5055 case Key.PageDown: 5056 verticalScroll(scaleWithDpi(160)); 5057 break; 5058 default: 5059 } 5060 super.defaultEventHandler_keydown(event); 5061 } 5062 5063 5064 version(win32_widgets) 5065 override void recomputeChildLayout() { 5066 super.recomputeChildLayout(); 5067 SCROLLINFO info; 5068 info.cbSize = info.sizeof; 5069 info.nPage = viewportHeight; 5070 info.fMask = SIF_PAGE | SIF_RANGE; 5071 info.nMin = 0; 5072 info.nMax = contentHeight_; 5073 SetScrollInfo(hwnd, SB_VERT, &info, true); 5074 5075 info.cbSize = info.sizeof; 5076 info.nPage = viewportWidth; 5077 info.fMask = SIF_PAGE | SIF_RANGE; 5078 info.nMin = 0; 5079 info.nMax = contentWidth_; 5080 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5081 } 5082 5083 /* 5084 Scrolling 5085 ------------ 5086 5087 You are assigned a width and a height by the layout engine, which 5088 is your viewport box. However, you may draw more than that by setting 5089 a contentWidth and contentHeight. 5090 5091 If these can be contained by the viewport, no scrollbar is displayed. 5092 If they cannot fit though, it will automatically show scroll as necessary. 5093 5094 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 5095 is zero, no vertical scrolling is performed. 5096 5097 If scrolling is necessary, the lib will automatically work with the bars. 5098 When you redraw, the origin and clipping info in the painter is set so if 5099 you just draw everything, it will work, but you can be more efficient by checking 5100 the viewportWidth, viewportHeight, and scrollOrigin members. 5101 */ 5102 5103 /// 5104 final @property int viewportWidth() { 5105 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 5106 } 5107 /// 5108 final @property int viewportHeight() { 5109 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 5110 } 5111 5112 // FIXME property 5113 Point scrollOrigin_; 5114 5115 /// 5116 final const(Point) scrollOrigin() { 5117 return scrollOrigin_; 5118 } 5119 5120 // the user sets these two 5121 private int contentWidth_ = 0; 5122 private int contentHeight_ = 0; 5123 5124 /// 5125 int contentWidth() { return contentWidth_; } 5126 /// 5127 int contentHeight() { return contentHeight_; } 5128 5129 /// 5130 void setContentSize(int width, int height) { 5131 contentWidth_ = width; 5132 contentHeight_ = height; 5133 5134 version(custom_widgets) { 5135 if(showingVerticalScroll || showingHorizontalScroll) { 5136 outerContainer.queueRecomputeChildLayout(); 5137 } 5138 5139 if(showingVerticalScroll()) 5140 outerContainer.verticalScrollBar.redraw(); 5141 if(showingHorizontalScroll()) 5142 outerContainer.horizontalScrollBar.redraw(); 5143 } else version(win32_widgets) { 5144 queueRecomputeChildLayout(); 5145 } else static assert(0); 5146 } 5147 5148 /// 5149 void verticalScroll(int delta) { 5150 verticalScrollTo(scrollOrigin.y + delta); 5151 } 5152 /// 5153 void verticalScrollTo(int pos) { 5154 scrollOrigin_.y = pos; 5155 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 5156 scrollOrigin_.y = contentHeight - viewportHeight; 5157 5158 if(scrollOrigin_.y < 0) 5159 scrollOrigin_.y = 0; 5160 5161 version(win32_widgets) { 5162 SCROLLINFO info; 5163 info.cbSize = info.sizeof; 5164 info.fMask = SIF_POS; 5165 info.nPos = scrollOrigin_.y; 5166 SetScrollInfo(hwnd, SB_VERT, &info, true); 5167 } else version(custom_widgets) { 5168 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 5169 } else static assert(0); 5170 5171 redraw(); 5172 } 5173 5174 /// 5175 void horizontalScroll(int delta) { 5176 horizontalScrollTo(scrollOrigin.x + delta); 5177 } 5178 /// 5179 void horizontalScrollTo(int pos) { 5180 scrollOrigin_.x = pos; 5181 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 5182 scrollOrigin_.x = contentWidth - viewportWidth; 5183 5184 if(scrollOrigin_.x < 0) 5185 scrollOrigin_.x = 0; 5186 5187 version(win32_widgets) { 5188 SCROLLINFO info; 5189 info.cbSize = info.sizeof; 5190 info.fMask = SIF_POS; 5191 info.nPos = scrollOrigin_.x; 5192 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5193 } else version(custom_widgets) { 5194 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 5195 } else static assert(0); 5196 5197 redraw(); 5198 } 5199 /// 5200 void scrollTo(Point p) { 5201 verticalScrollTo(p.y); 5202 horizontalScrollTo(p.x); 5203 } 5204 5205 /// 5206 void ensureVisibleInScroll(Point p) { 5207 auto rect = viewportRectangle(); 5208 if(rect.contains(p)) 5209 return; 5210 if(p.x < rect.left) 5211 horizontalScroll(p.x - rect.left); 5212 else if(p.x > rect.right) 5213 horizontalScroll(p.x - rect.right); 5214 5215 if(p.y < rect.top) 5216 verticalScroll(p.y - rect.top); 5217 else if(p.y > rect.bottom) 5218 verticalScroll(p.y - rect.bottom); 5219 } 5220 5221 /// 5222 void ensureVisibleInScroll(Rectangle rect) { 5223 ensureVisibleInScroll(rect.upperLeft); 5224 ensureVisibleInScroll(rect.lowerRight); 5225 } 5226 5227 /// 5228 Rectangle viewportRectangle() { 5229 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 5230 } 5231 5232 /// 5233 bool showingHorizontalScroll() { 5234 return contentWidth > width; 5235 } 5236 /// 5237 bool showingVerticalScroll() { 5238 return contentHeight > height; 5239 } 5240 5241 /// This is called before the ordinary paint delegate, 5242 /// giving you a chance to draw the window frame, etc, 5243 /// before the scroll clip takes effect 5244 void paintFrameAndBackground(WidgetPainter painter) { 5245 version(win32_widgets) { 5246 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5247 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5248 // since the pen is null, to fill the whole space, we need the +1 on both. 5249 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5250 SelectObject(painter.impl.hdc, p); 5251 SelectObject(painter.impl.hdc, b); 5252 } 5253 5254 } 5255 5256 // make space for the scroll bar, and that's it. 5257 final override int paddingRight() { return scaleWithDpi(16); } 5258 final override int paddingBottom() { return scaleWithDpi(16); } 5259 5260 /* 5261 END SCROLLING 5262 */ 5263 5264 override WidgetPainter draw() { 5265 int x = this.x, y = this.y; 5266 auto parent = this.parent; 5267 while(parent) { 5268 x += parent.x; 5269 y += parent.y; 5270 parent = parent.parent; 5271 } 5272 5273 //version(win32_widgets) { 5274 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5275 //} else { 5276 auto painter = parentWindow.win.draw(true); 5277 //} 5278 painter.originX = x; 5279 painter.originY = y; 5280 5281 painter.originX = painter.originX - scrollOrigin.x; 5282 painter.originY = painter.originY - scrollOrigin.y; 5283 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 5284 5285 return WidgetPainter(painter, this); 5286 } 5287 5288 mixin ScrollableChildren; 5289 } 5290 5291 // you need to have a Point scrollOrigin in the class somewhere 5292 // and a paintFrameAndBackground 5293 private mixin template ScrollableChildren() { 5294 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5295 if(hidden) 5296 return; 5297 5298 //version(win32_widgets) 5299 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5300 5301 painter.originX = lox + x; 5302 painter.originY = loy + y; 5303 5304 bool actuallyPainted = false; 5305 5306 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 5307 if(clip == Rectangle.init) 5308 return; 5309 5310 if(force || redrawRequested) { 5311 //painter.setClipRectangle(scrollOrigin, width, height); 5312 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5313 paintFrameAndBackground(painter); 5314 } 5315 5316 /+ 5317 version(win32_widgets) { 5318 if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW); 5319 } 5320 +/ 5321 5322 painter.originX = painter.originX - scrollOrigin.x; 5323 painter.originY = painter.originY - scrollOrigin.y; 5324 if(force || redrawRequested) { 5325 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 5326 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 5327 5328 //erase(painter); // we paintFrameAndBackground above so no need 5329 if(painter.visualTheme) 5330 painter.visualTheme.doPaint(this, painter); 5331 else 5332 paint(painter); 5333 5334 if(invalidate) { 5335 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5336 // children are contained inside this, so no need to do extra work 5337 invalidate = false; 5338 } 5339 5340 5341 actuallyPainted = true; 5342 redrawRequested = false; 5343 } 5344 5345 foreach(child; children) { 5346 if(cast(FixedPosition) child) 5347 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5348 else 5349 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5350 } 5351 } 5352 } 5353 5354 private class InternalScrollableContainerInsideWidget : ContainerWidget { 5355 ScrollableContainerWidget scw; 5356 5357 this(ScrollableContainerWidget parent) { 5358 scw = parent; 5359 super(parent); 5360 } 5361 5362 version(custom_widgets) 5363 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5364 if(hidden) 5365 return; 5366 5367 bool actuallyPainted = false; 5368 5369 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 5370 5371 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 5372 if(clip == Rectangle.init) 5373 return; 5374 5375 painter.originX = lox + x - scrollOrigin.x; 5376 painter.originY = loy + y - scrollOrigin.y; 5377 if(force || redrawRequested) { 5378 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5379 5380 erase(painter); 5381 if(painter.visualTheme) 5382 painter.visualTheme.doPaint(this, painter); 5383 else 5384 paint(painter); 5385 5386 if(invalidate) { 5387 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5388 // children are contained inside this, so no need to do extra work 5389 invalidate = false; 5390 } 5391 5392 actuallyPainted = true; 5393 redrawRequested = false; 5394 } 5395 foreach(child; children) { 5396 if(cast(FixedPosition) child) 5397 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5398 else 5399 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5400 } 5401 } 5402 5403 version(custom_widgets) 5404 override protected void addScrollPosition(ref int x, ref int y) { 5405 x += scw.scrollX_; 5406 y += scw.scrollY_; 5407 } 5408 } 5409 5410 /++ 5411 A widget meant to contain other widgets that may need to scroll. 5412 5413 Currently buggy. 5414 5415 History: 5416 Added July 1, 2021 (dub v10.2) 5417 5418 On January 3, 2022, I tried to use it in a few other cases 5419 and found it only worked well in the original test case. Since 5420 it still sucks, I think I'm going to rewrite it again. 5421 +/ 5422 class ScrollableContainerWidget : ContainerWidget { 5423 /// 5424 this(Widget parent) { 5425 super(parent); 5426 5427 container = new InternalScrollableContainerInsideWidget(this); 5428 hsb = new HorizontalScrollbar(this); 5429 vsb = new VerticalScrollbar(this); 5430 5431 tabStop = false; 5432 container.tabStop = false; 5433 magic = true; 5434 5435 5436 vsb.addEventListener("scrolltonextline", () { 5437 scrollBy(0, scaleWithDpi(16)); 5438 }); 5439 vsb.addEventListener("scrolltopreviousline", () { 5440 scrollBy(0,scaleWithDpi( -16)); 5441 }); 5442 vsb.addEventListener("scrolltonextpage", () { 5443 scrollBy(0, container.height); 5444 }); 5445 vsb.addEventListener("scrolltopreviouspage", () { 5446 scrollBy(0, -container.height); 5447 }); 5448 vsb.addEventListener((scope ScrollToPositionEvent spe) { 5449 scrollTo(scrollX_, spe.value); 5450 }); 5451 5452 this.addEventListener(delegate (scope ClickEvent e) { 5453 if(e.button == MouseButton.wheelUp) { 5454 if(!e.defaultPrevented) 5455 scrollBy(0, scaleWithDpi(-16)); 5456 e.stopPropagation(); 5457 } else if(e.button == MouseButton.wheelDown) { 5458 if(!e.defaultPrevented) 5459 scrollBy(0, scaleWithDpi(16)); 5460 e.stopPropagation(); 5461 } 5462 }); 5463 } 5464 5465 /+ 5466 override void defaultEventHandler_click(ClickEvent e) { 5467 } 5468 +/ 5469 5470 override void removeAllChildren() { 5471 container.removeAllChildren(); 5472 } 5473 5474 void scrollTo(int x, int y) { 5475 scrollBy(x - scrollX_, y - scrollY_); 5476 } 5477 5478 void scrollBy(int x, int y) { 5479 auto ox = scrollX_; 5480 auto oy = scrollY_; 5481 5482 auto nx = ox + x; 5483 auto ny = oy + y; 5484 5485 if(nx < 0) 5486 nx = 0; 5487 if(ny < 0) 5488 ny = 0; 5489 5490 auto maxX = hsb.max - container.width; 5491 if(maxX < 0) maxX = 0; 5492 auto maxY = vsb.max - container.height; 5493 if(maxY < 0) maxY = 0; 5494 5495 if(nx > maxX) 5496 nx = maxX; 5497 if(ny > maxY) 5498 ny = maxY; 5499 5500 auto dx = nx - ox; 5501 auto dy = ny - oy; 5502 5503 if(dx || dy) { 5504 version(win32_widgets) 5505 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 5506 else { 5507 redraw(); 5508 } 5509 5510 hsb.setPosition = nx; 5511 vsb.setPosition = ny; 5512 5513 scrollX_ = nx; 5514 scrollY_ = ny; 5515 } 5516 } 5517 5518 private int scrollX_; 5519 private int scrollY_; 5520 5521 void setTotalArea(int width, int height) { 5522 hsb.setMax(width); 5523 vsb.setMax(height); 5524 } 5525 5526 /// 5527 void setViewableArea(int width, int height) { 5528 hsb.setViewableArea(width); 5529 vsb.setViewableArea(height); 5530 } 5531 5532 private bool magic; 5533 override void addChild(Widget w, int position = int.max) { 5534 if(magic) 5535 container.addChild(w, position); 5536 else 5537 super.addChild(w, position); 5538 } 5539 5540 override void recomputeChildLayout() { 5541 if(hsb is null || vsb is null || container is null) return; 5542 5543 /+ 5544 writeln(x, " ", y , " ", width, " ", height); 5545 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 5546 +/ 5547 5548 registerMovement(); 5549 5550 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 5551 hsb.x = 0; 5552 hsb.y = this.height - hsb.height; 5553 hsb.width = this.width - scaleWithDpi(16); 5554 hsb.recomputeChildLayout(); 5555 5556 vsb.width = scaleWithDpi(16); // FIXME? 5557 vsb.x = this.width - vsb.width; 5558 vsb.y = 0; 5559 vsb.height = this.height - scaleWithDpi(16); 5560 vsb.recomputeChildLayout(); 5561 5562 container.x = 0; 5563 container.y = 0; 5564 container.width = this.width - vsb.width; 5565 container.height = this.height - hsb.height; 5566 container.recomputeChildLayout(); 5567 5568 scrollX_ = 0; 5569 scrollY_ = 0; 5570 5571 hsb.setPosition(0); 5572 vsb.setPosition(0); 5573 5574 int mw, mh; 5575 Widget c = container; 5576 // FIXME: hack here to handle a layout inside... 5577 if(c.children.length == 1 && cast(Layout) c.children[0]) 5578 c = c.children[0]; 5579 foreach(child; c.children) { 5580 auto w = child.x + child.width; 5581 auto h = child.y + child.height; 5582 5583 if(w > mw) mw = w; 5584 if(h > mh) mh = h; 5585 } 5586 5587 setTotalArea(mw, mh); 5588 setViewableArea(width, height); 5589 } 5590 5591 override int minHeight() { return scaleWithDpi(64); } 5592 5593 HorizontalScrollbar hsb; 5594 VerticalScrollbar vsb; 5595 ContainerWidget container; 5596 } 5597 5598 5599 version(custom_widgets) 5600 private class InternalScrollableContainerWidget : Widget { 5601 5602 ScrollableWidget sw; 5603 5604 VerticalScrollbar verticalScrollBar; 5605 HorizontalScrollbar horizontalScrollBar; 5606 5607 this(ScrollableWidget sw, Widget parent) { 5608 this.sw = sw; 5609 5610 this.tabStop = false; 5611 5612 super(parent); 5613 5614 horizontalScrollBar = new HorizontalScrollbar(this); 5615 verticalScrollBar = new VerticalScrollbar(this); 5616 5617 horizontalScrollBar.showing_ = false; 5618 verticalScrollBar.showing_ = false; 5619 5620 horizontalScrollBar.addEventListener("scrolltonextline", { 5621 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 5622 sw.horizontalScrollTo(horizontalScrollBar.position); 5623 }); 5624 horizontalScrollBar.addEventListener("scrolltopreviousline", { 5625 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 5626 sw.horizontalScrollTo(horizontalScrollBar.position); 5627 }); 5628 verticalScrollBar.addEventListener("scrolltonextline", { 5629 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 5630 sw.verticalScrollTo(verticalScrollBar.position); 5631 }); 5632 verticalScrollBar.addEventListener("scrolltopreviousline", { 5633 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 5634 sw.verticalScrollTo(verticalScrollBar.position); 5635 }); 5636 horizontalScrollBar.addEventListener("scrolltonextpage", { 5637 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 5638 sw.horizontalScrollTo(horizontalScrollBar.position); 5639 }); 5640 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 5641 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 5642 sw.horizontalScrollTo(horizontalScrollBar.position); 5643 }); 5644 verticalScrollBar.addEventListener("scrolltonextpage", { 5645 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 5646 sw.verticalScrollTo(verticalScrollBar.position); 5647 }); 5648 verticalScrollBar.addEventListener("scrolltopreviouspage", { 5649 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 5650 sw.verticalScrollTo(verticalScrollBar.position); 5651 }); 5652 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 5653 horizontalScrollBar.setPosition(event.intValue); 5654 sw.horizontalScrollTo(horizontalScrollBar.position); 5655 }); 5656 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 5657 verticalScrollBar.setPosition(event.intValue); 5658 sw.verticalScrollTo(verticalScrollBar.position); 5659 }); 5660 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 5661 horizontalScrollBar.setPosition(event.intValue); 5662 sw.horizontalScrollTo(horizontalScrollBar.position); 5663 }); 5664 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 5665 verticalScrollBar.setPosition(event.intValue); 5666 }); 5667 } 5668 5669 // this is supposed to be basically invisible... 5670 override int minWidth() { return sw.minWidth; } 5671 override int minHeight() { return sw.minHeight; } 5672 override int maxWidth() { return sw.maxWidth; } 5673 override int maxHeight() { return sw.maxHeight; } 5674 override int widthStretchiness() { return sw.widthStretchiness; } 5675 override int heightStretchiness() { return sw.heightStretchiness; } 5676 override int marginLeft() { return sw.marginLeft; } 5677 override int marginRight() { return sw.marginRight; } 5678 override int marginTop() { return sw.marginTop; } 5679 override int marginBottom() { return sw.marginBottom; } 5680 override int paddingLeft() { return sw.paddingLeft; } 5681 override int paddingRight() { return sw.paddingRight; } 5682 override int paddingTop() { return sw.paddingTop; } 5683 override int paddingBottom() { return sw.paddingBottom; } 5684 override void focus() { sw.focus(); } 5685 5686 5687 override void recomputeChildLayout() { 5688 // The stupid thing needs to calculate if a scroll bar is needed... 5689 recomputeChildLayoutHelper(); 5690 // then running it again will position things correctly if the bar is NOT needed 5691 recomputeChildLayoutHelper(); 5692 5693 // this sucks but meh it barely works 5694 } 5695 5696 private void recomputeChildLayoutHelper() { 5697 if(sw is null) return; 5698 5699 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 5700 if(horizontalScrollBar && verticalScrollBar) { 5701 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 5702 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 5703 horizontalScrollBar.x = 0; 5704 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 5705 5706 verticalScrollBar.width = verticalScrollBar.minWidth(); 5707 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 5708 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 5709 verticalScrollBar.y = 0 + 2; 5710 5711 sw.x = 0; 5712 sw.y = 0; 5713 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 5714 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 5715 5716 if(sw.contentWidth_ <= this.width) 5717 sw.scrollOrigin_.x = 0; 5718 if(sw.contentHeight_ <= this.height) 5719 sw.scrollOrigin_.y = 0; 5720 5721 horizontalScrollBar.recomputeChildLayout(); 5722 verticalScrollBar.recomputeChildLayout(); 5723 sw.recomputeChildLayout(); 5724 } 5725 5726 if(sw.contentWidth_ <= this.width) 5727 sw.scrollOrigin_.x = 0; 5728 if(sw.contentHeight_ <= this.height) 5729 sw.scrollOrigin_.y = 0; 5730 5731 if(sw.showingHorizontalScroll()) 5732 horizontalScrollBar.showing(true, false); 5733 else 5734 horizontalScrollBar.showing(false, false); 5735 if(sw.showingVerticalScroll()) 5736 verticalScrollBar.showing(true, false); 5737 else 5738 verticalScrollBar.showing(false, false); 5739 5740 verticalScrollBar.setViewableArea(sw.viewportHeight()); 5741 verticalScrollBar.setMax(sw.contentHeight); 5742 verticalScrollBar.setPosition(sw.scrollOrigin.y); 5743 5744 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 5745 horizontalScrollBar.setMax(sw.contentWidth); 5746 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 5747 } 5748 } 5749 5750 /* 5751 class ScrollableClientWidget : Widget { 5752 this(Widget parent) { 5753 super(parent); 5754 } 5755 override void paint(WidgetPainter p) { 5756 parent.paint(p); 5757 } 5758 } 5759 */ 5760 5761 /++ 5762 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. 5763 +/ 5764 abstract class Slider : Widget { 5765 this(int min, int max, int step, Widget parent) { 5766 min_ = min; 5767 max_ = max; 5768 step_ = step; 5769 page_ = step; 5770 super(parent); 5771 } 5772 5773 private int min_; 5774 private int max_; 5775 private int step_; 5776 private int position_; 5777 private int page_; 5778 5779 // selection start and selection end 5780 // tics 5781 // tooltip? 5782 // some way to see and just type the value 5783 // win32 buddy controls are labels 5784 5785 /// 5786 void setMin(int a) { 5787 min_ = a; 5788 version(custom_widgets) 5789 redraw(); 5790 version(win32_widgets) 5791 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 5792 } 5793 /// 5794 int min() { 5795 return min_; 5796 } 5797 /// 5798 void setMax(int a) { 5799 max_ = a; 5800 version(custom_widgets) 5801 redraw(); 5802 version(win32_widgets) 5803 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 5804 } 5805 /// 5806 int max() { 5807 return max_; 5808 } 5809 /// 5810 void setPosition(int a) { 5811 if(a > max) 5812 a = max; 5813 if(a < min) 5814 a = min; 5815 position_ = a; 5816 version(custom_widgets) 5817 setPositionCustom(a); 5818 5819 version(win32_widgets) 5820 setPositionWindows(a); 5821 } 5822 version(win32_widgets) { 5823 protected abstract void setPositionWindows(int a); 5824 } 5825 5826 protected abstract int win32direction(); 5827 5828 /++ 5829 Alias for [position] for better compatibility with generic code. 5830 5831 History: 5832 Added October 5, 2021 5833 +/ 5834 @property int value() { 5835 return position; 5836 } 5837 5838 /// 5839 int position() { 5840 return position_; 5841 } 5842 /// 5843 void setStep(int a) { 5844 step_ = a; 5845 version(win32_widgets) 5846 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 5847 } 5848 /// 5849 int step() { 5850 return step_; 5851 } 5852 /// 5853 void setPageSize(int a) { 5854 page_ = a; 5855 version(win32_widgets) 5856 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 5857 } 5858 /// 5859 int pageSize() { 5860 return page_; 5861 } 5862 5863 private void notify() { 5864 auto event = new ChangeEvent!int(this, &this.position); 5865 event.dispatch(); 5866 } 5867 5868 version(win32_widgets) 5869 void win32Setup(int style) { 5870 createWin32Window(this, TRACKBAR_CLASS, "", 5871 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 5872 5873 // the trackbar sends the same messages as scroll, which 5874 // our other layer sends as these... just gonna translate 5875 // here 5876 this.addDirectEventListener("scrolltoposition", (Event event) { 5877 event.stopPropagation(); 5878 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 5879 notify(); 5880 }); 5881 this.addDirectEventListener("scrolltonextline", (Event event) { 5882 event.stopPropagation(); 5883 this.setPosition(this.position + this.step_ * this.win32direction); 5884 notify(); 5885 }); 5886 this.addDirectEventListener("scrolltopreviousline", (Event event) { 5887 event.stopPropagation(); 5888 this.setPosition(this.position - this.step_ * this.win32direction); 5889 notify(); 5890 }); 5891 this.addDirectEventListener("scrolltonextpage", (Event event) { 5892 event.stopPropagation(); 5893 this.setPosition(this.position + this.page_ * this.win32direction); 5894 notify(); 5895 }); 5896 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 5897 event.stopPropagation(); 5898 this.setPosition(this.position - this.page_ * this.win32direction); 5899 notify(); 5900 }); 5901 5902 setMin(min_); 5903 setMax(max_); 5904 setStep(step_); 5905 setPageSize(page_); 5906 } 5907 5908 version(custom_widgets) { 5909 protected MouseTrackingWidget thumb; 5910 5911 protected abstract void setPositionCustom(int a); 5912 5913 override void defaultEventHandler_keydown(KeyDownEvent event) { 5914 switch(event.key) { 5915 case Key.Up: 5916 case Key.Right: 5917 setPosition(position() - step() * win32direction); 5918 changed(); 5919 break; 5920 case Key.Down: 5921 case Key.Left: 5922 setPosition(position() + step() * win32direction); 5923 changed(); 5924 break; 5925 case Key.Home: 5926 setPosition(win32direction > 0 ? min() : max()); 5927 changed(); 5928 break; 5929 case Key.End: 5930 setPosition(win32direction > 0 ? max() : min()); 5931 changed(); 5932 break; 5933 case Key.PageUp: 5934 setPosition(position() - pageSize() * win32direction); 5935 changed(); 5936 break; 5937 case Key.PageDown: 5938 setPosition(position() + pageSize() * win32direction); 5939 changed(); 5940 break; 5941 default: 5942 } 5943 super.defaultEventHandler_keydown(event); 5944 } 5945 5946 protected void changed() { 5947 auto ev = new ChangeEvent!int(this, &position); 5948 ev.dispatch(); 5949 } 5950 } 5951 } 5952 5953 /++ 5954 5955 +/ 5956 class VerticalSlider : Slider { 5957 this(int min, int max, int step, Widget parent) { 5958 version(custom_widgets) 5959 initialize(); 5960 5961 super(min, max, step, parent); 5962 5963 version(win32_widgets) 5964 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 5965 } 5966 5967 protected override int win32direction() { 5968 return -1; 5969 } 5970 5971 version(win32_widgets) 5972 protected override void setPositionWindows(int a) { 5973 // the windows thing makes the top 0 and i don't like that. 5974 SendMessage(hwnd, TBM_SETPOS, true, max - a); 5975 } 5976 5977 version(custom_widgets) 5978 private void initialize() { 5979 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 5980 5981 thumb.tabStop = false; 5982 5983 thumb.thumbWidth = width; 5984 thumb.thumbHeight = scaleWithDpi(16); 5985 5986 thumb.addEventListener(EventType.change, () { 5987 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 5988 sx = max - sx; 5989 //informProgramThatUserChangedPosition(sx); 5990 5991 position_ = sx; 5992 5993 changed(); 5994 }); 5995 } 5996 5997 version(custom_widgets) 5998 override void recomputeChildLayout() { 5999 thumb.thumbWidth = this.width; 6000 super.recomputeChildLayout(); 6001 setPositionCustom(position_); 6002 } 6003 6004 version(custom_widgets) 6005 protected override void setPositionCustom(int a) { 6006 if(max()) 6007 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 6008 redraw(); 6009 } 6010 } 6011 6012 /++ 6013 6014 +/ 6015 class HorizontalSlider : Slider { 6016 this(int min, int max, int step, Widget parent) { 6017 version(custom_widgets) 6018 initialize(); 6019 6020 super(min, max, step, parent); 6021 6022 version(win32_widgets) 6023 win32Setup(TBS_HORZ); 6024 } 6025 6026 version(win32_widgets) 6027 protected override void setPositionWindows(int a) { 6028 SendMessage(hwnd, TBM_SETPOS, true, a); 6029 } 6030 6031 protected override int win32direction() { 6032 return 1; 6033 } 6034 6035 version(custom_widgets) 6036 private void initialize() { 6037 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 6038 6039 thumb.tabStop = false; 6040 6041 thumb.thumbWidth = scaleWithDpi(16); 6042 thumb.thumbHeight = height; 6043 6044 thumb.addEventListener(EventType.change, () { 6045 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 6046 //informProgramThatUserChangedPosition(sx); 6047 6048 position_ = sx; 6049 6050 changed(); 6051 }); 6052 } 6053 6054 version(custom_widgets) 6055 override void recomputeChildLayout() { 6056 thumb.thumbHeight = this.height; 6057 super.recomputeChildLayout(); 6058 setPositionCustom(position_); 6059 } 6060 6061 version(custom_widgets) 6062 protected override void setPositionCustom(int a) { 6063 if(max()) 6064 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 6065 redraw(); 6066 } 6067 } 6068 6069 6070 /// 6071 abstract class ScrollbarBase : Widget { 6072 /// 6073 this(Widget parent) { 6074 super(parent); 6075 tabStop = false; 6076 step_ = scaleWithDpi(16); 6077 } 6078 6079 private int viewableArea_; 6080 private int max_; 6081 private int step_;// = 16; 6082 private int position_; 6083 6084 /// 6085 bool atEnd() { 6086 return position_ + viewableArea_ >= max_; 6087 } 6088 6089 /// 6090 bool atStart() { 6091 return position_ == 0; 6092 } 6093 6094 /// 6095 void setViewableArea(int a) { 6096 viewableArea_ = a; 6097 version(custom_widgets) 6098 redraw(); 6099 } 6100 /// 6101 void setMax(int a) { 6102 max_ = a; 6103 version(custom_widgets) 6104 redraw(); 6105 } 6106 /// 6107 int max() { 6108 return max_; 6109 } 6110 /// 6111 void setPosition(int a) { 6112 auto logicalMax = max_ - viewableArea_; 6113 if(a == int.max) 6114 a = logicalMax; 6115 6116 if(a > logicalMax) 6117 a = logicalMax; 6118 if(a < 0) 6119 a = 0; 6120 6121 position_ = a; 6122 6123 version(custom_widgets) 6124 redraw(); 6125 } 6126 /// 6127 int position() { 6128 return position_; 6129 } 6130 /// 6131 void setStep(int a) { 6132 step_ = a; 6133 } 6134 /// 6135 int step() { 6136 return step_; 6137 } 6138 6139 // FIXME: remove this.... maybe 6140 /+ 6141 protected void informProgramThatUserChangedPosition(int n) { 6142 position_ = n; 6143 auto evt = new Event(EventType.change, this); 6144 evt.intValue = n; 6145 evt.dispatch(); 6146 } 6147 +/ 6148 6149 version(custom_widgets) { 6150 enum MIN_THUMB_SIZE = 8; 6151 6152 abstract protected int getBarDim(); 6153 int thumbSize() { 6154 if(viewableArea_ >= max_ || max_ == 0) 6155 return getBarDim(); 6156 6157 int res = viewableArea_ * getBarDim() / max_; 6158 6159 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 6160 res = scaleWithDpi(MIN_THUMB_SIZE); 6161 6162 return res; 6163 } 6164 6165 int thumbPosition() { 6166 /* 6167 viewableArea_ is the viewport height/width 6168 position_ is where we are 6169 */ 6170 //if(position_ + viewableArea_ >= max_) 6171 //return getBarDim - thumbSize; 6172 6173 auto maximumPossibleValue = getBarDim() - thumbSize; 6174 auto maximiumLogicalValue = max_ - viewableArea_; 6175 6176 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 6177 6178 return p; 6179 } 6180 } 6181 } 6182 6183 //public import mgt; 6184 6185 /++ 6186 A mouse tracking widget is one that follows the mouse when dragged inside it. 6187 6188 Concrete subclasses may include a scrollbar thumb and a volume control. 6189 +/ 6190 //version(custom_widgets) 6191 class MouseTrackingWidget : Widget { 6192 6193 /// 6194 int positionX() { return positionX_; } 6195 /// 6196 int positionY() { return positionY_; } 6197 6198 /// 6199 void positionX(int p) { positionX_ = p; } 6200 /// 6201 void positionY(int p) { positionY_ = p; } 6202 6203 private int positionX_; 6204 private int positionY_; 6205 6206 /// 6207 enum Orientation { 6208 horizontal, /// 6209 vertical, /// 6210 twoDimensional, /// 6211 } 6212 6213 private int thumbWidth_; 6214 private int thumbHeight_; 6215 6216 /// 6217 int thumbWidth() { return thumbWidth_; } 6218 /// 6219 int thumbHeight() { return thumbHeight_; } 6220 /// 6221 int thumbWidth(int a) { return thumbWidth_ = a; } 6222 /// 6223 int thumbHeight(int a) { return thumbHeight_ = a; } 6224 6225 private bool dragging; 6226 private bool hovering; 6227 private int startMouseX, startMouseY; 6228 6229 /// 6230 this(Orientation orientation, Widget parent) { 6231 super(parent); 6232 6233 //assert(parentWindow !is null); 6234 6235 addEventListener((MouseDownEvent event) { 6236 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6237 dragging = true; 6238 startMouseX = event.clientX - positionX; 6239 startMouseY = event.clientY - positionY; 6240 parentWindow.captureMouse(this); 6241 } else { 6242 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6243 positionX = event.clientX - thumbWidth / 2; 6244 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6245 positionY = event.clientY - thumbHeight / 2; 6246 6247 if(positionX + thumbWidth > this.width) 6248 positionX = this.width - thumbWidth; 6249 if(positionY + thumbHeight > this.height) 6250 positionY = this.height - thumbHeight; 6251 6252 if(positionX < 0) 6253 positionX = 0; 6254 if(positionY < 0) 6255 positionY = 0; 6256 6257 6258 // this.emit!(ChangeEvent!void)(); 6259 auto evt = new Event(EventType.change, this); 6260 evt.sendDirectly(); 6261 6262 redraw(); 6263 6264 } 6265 }); 6266 6267 addEventListener(EventType.mouseup, (Event event) { 6268 dragging = false; 6269 parentWindow.releaseMouseCapture(); 6270 }); 6271 6272 addEventListener(EventType.mouseout, (Event event) { 6273 if(!hovering) 6274 return; 6275 hovering = false; 6276 redraw(); 6277 }); 6278 6279 int lpx, lpy; 6280 6281 addEventListener((MouseMoveEvent event) { 6282 auto oh = hovering; 6283 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6284 hovering = true; 6285 } else { 6286 hovering = false; 6287 } 6288 if(!dragging) { 6289 if(hovering != oh) 6290 redraw(); 6291 return; 6292 } 6293 6294 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6295 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 6296 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6297 positionY = event.clientY - startMouseY; 6298 6299 if(positionX + thumbWidth > this.width) 6300 positionX = this.width - thumbWidth; 6301 if(positionY + thumbHeight > this.height) 6302 positionY = this.height - thumbHeight; 6303 6304 if(positionX < 0) 6305 positionX = 0; 6306 if(positionY < 0) 6307 positionY = 0; 6308 6309 if(positionX != lpx || positionY != lpy) { 6310 lpx = positionX; 6311 lpy = positionY; 6312 6313 auto evt = new Event(EventType.change, this); 6314 evt.sendDirectly(); 6315 } 6316 6317 redraw(); 6318 }); 6319 } 6320 6321 version(custom_widgets) 6322 override void paint(WidgetPainter painter) { 6323 auto cs = getComputedStyle(); 6324 auto c = darken(cs.windowBackgroundColor, 0.2); 6325 painter.outlineColor = c; 6326 painter.fillColor = c; 6327 painter.drawRectangle(Point(0, 0), this.width, this.height); 6328 6329 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 6330 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 6331 } 6332 } 6333 6334 //version(custom_widgets) 6335 //private 6336 class HorizontalScrollbar : ScrollbarBase { 6337 6338 version(custom_widgets) { 6339 private MouseTrackingWidget thumb; 6340 6341 override int getBarDim() { 6342 return thumb.width; 6343 } 6344 } 6345 6346 override void setViewableArea(int a) { 6347 super.setViewableArea(a); 6348 6349 version(win32_widgets) { 6350 SCROLLINFO info; 6351 info.cbSize = info.sizeof; 6352 info.nPage = a + 1; 6353 info.fMask = SIF_PAGE; 6354 SetScrollInfo(hwnd, SB_CTL, &info, true); 6355 } else version(custom_widgets) { 6356 thumb.positionX = thumbPosition; 6357 thumb.thumbWidth = thumbSize; 6358 thumb.redraw(); 6359 } else static assert(0); 6360 6361 } 6362 6363 override void setMax(int a) { 6364 super.setMax(a); 6365 version(win32_widgets) { 6366 SCROLLINFO info; 6367 info.cbSize = info.sizeof; 6368 info.nMin = 0; 6369 info.nMax = max; 6370 info.fMask = SIF_RANGE; 6371 SetScrollInfo(hwnd, SB_CTL, &info, true); 6372 } else version(custom_widgets) { 6373 thumb.positionX = thumbPosition; 6374 thumb.thumbWidth = thumbSize; 6375 thumb.redraw(); 6376 } 6377 } 6378 6379 override void setPosition(int a) { 6380 super.setPosition(a); 6381 version(win32_widgets) { 6382 SCROLLINFO info; 6383 info.cbSize = info.sizeof; 6384 info.fMask = SIF_POS; 6385 info.nPos = position; 6386 SetScrollInfo(hwnd, SB_CTL, &info, true); 6387 } else version(custom_widgets) { 6388 thumb.positionX = thumbPosition(); 6389 thumb.thumbWidth = thumbSize; 6390 thumb.redraw(); 6391 } else static assert(0); 6392 } 6393 6394 this(Widget parent) { 6395 super(parent); 6396 6397 version(win32_widgets) { 6398 createWin32Window(this, "Scrollbar"w, "", 6399 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 6400 } else version(custom_widgets) { 6401 auto vl = new HorizontalLayout(this); 6402 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 6403 leftButton.setClickRepeat(scrollClickRepeatInterval); 6404 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 6405 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 6406 rightButton.setClickRepeat(scrollClickRepeatInterval); 6407 6408 leftButton.tabStop = false; 6409 rightButton.tabStop = false; 6410 thumb.tabStop = false; 6411 6412 leftButton.addEventListener(EventType.triggered, () { 6413 this.emitCommand!"scrolltopreviousline"(); 6414 //informProgramThatUserChangedPosition(position - step()); 6415 }); 6416 rightButton.addEventListener(EventType.triggered, () { 6417 this.emitCommand!"scrolltonextline"(); 6418 //informProgramThatUserChangedPosition(position + step()); 6419 }); 6420 6421 thumb.thumbWidth = this.minWidth; 6422 thumb.thumbHeight = scaleWithDpi(16); 6423 6424 thumb.addEventListener(EventType.change, () { 6425 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 6426 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 6427 6428 //informProgramThatUserChangedPosition(sx); 6429 6430 auto ev = new ScrollToPositionEvent(this, sx); 6431 ev.dispatch(); 6432 }); 6433 } 6434 } 6435 6436 override int minHeight() { return scaleWithDpi(16); } 6437 override int maxHeight() { return scaleWithDpi(16); } 6438 override int minWidth() { return scaleWithDpi(48); } 6439 } 6440 6441 class ScrollToPositionEvent : Event { 6442 enum EventString = "scrolltoposition"; 6443 6444 this(Widget target, int value) { 6445 this.value = value; 6446 super(EventString, target); 6447 } 6448 6449 immutable int value; 6450 6451 override @property int intValue() { 6452 return value; 6453 } 6454 } 6455 6456 //version(custom_widgets) 6457 //private 6458 class VerticalScrollbar : ScrollbarBase { 6459 6460 version(custom_widgets) { 6461 override int getBarDim() { 6462 return thumb.height; 6463 } 6464 6465 private MouseTrackingWidget thumb; 6466 } 6467 6468 override void setViewableArea(int a) { 6469 super.setViewableArea(a); 6470 6471 version(win32_widgets) { 6472 SCROLLINFO info; 6473 info.cbSize = info.sizeof; 6474 info.nPage = a + 1; 6475 info.fMask = SIF_PAGE; 6476 SetScrollInfo(hwnd, SB_CTL, &info, true); 6477 } else version(custom_widgets) { 6478 thumb.positionY = thumbPosition; 6479 thumb.thumbHeight = thumbSize; 6480 thumb.redraw(); 6481 } else static assert(0); 6482 6483 } 6484 6485 override void setMax(int a) { 6486 super.setMax(a); 6487 version(win32_widgets) { 6488 SCROLLINFO info; 6489 info.cbSize = info.sizeof; 6490 info.nMin = 0; 6491 info.nMax = max; 6492 info.fMask = SIF_RANGE; 6493 SetScrollInfo(hwnd, SB_CTL, &info, true); 6494 } else version(custom_widgets) { 6495 thumb.positionY = thumbPosition; 6496 thumb.thumbHeight = thumbSize; 6497 thumb.redraw(); 6498 } 6499 } 6500 6501 override void setPosition(int a) { 6502 super.setPosition(a); 6503 version(win32_widgets) { 6504 SCROLLINFO info; 6505 info.cbSize = info.sizeof; 6506 info.fMask = SIF_POS; 6507 info.nPos = position; 6508 SetScrollInfo(hwnd, SB_CTL, &info, true); 6509 } else version(custom_widgets) { 6510 thumb.positionY = thumbPosition; 6511 thumb.thumbHeight = thumbSize; 6512 thumb.redraw(); 6513 } else static assert(0); 6514 } 6515 6516 this(Widget parent) { 6517 super(parent); 6518 6519 version(win32_widgets) { 6520 createWin32Window(this, "Scrollbar"w, "", 6521 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 6522 } else version(custom_widgets) { 6523 auto vl = new VerticalLayout(this); 6524 auto upButton = new ArrowButton(ArrowDirection.up, vl); 6525 upButton.setClickRepeat(scrollClickRepeatInterval); 6526 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 6527 auto downButton = new ArrowButton(ArrowDirection.down, vl); 6528 downButton.setClickRepeat(scrollClickRepeatInterval); 6529 6530 upButton.addEventListener(EventType.triggered, () { 6531 this.emitCommand!"scrolltopreviousline"(); 6532 //informProgramThatUserChangedPosition(position - step()); 6533 }); 6534 downButton.addEventListener(EventType.triggered, () { 6535 this.emitCommand!"scrolltonextline"(); 6536 //informProgramThatUserChangedPosition(position + step()); 6537 }); 6538 6539 thumb.thumbWidth = this.minWidth; 6540 thumb.thumbHeight = scaleWithDpi(16); 6541 6542 thumb.addEventListener(EventType.change, () { 6543 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 6544 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 6545 6546 auto ev = new ScrollToPositionEvent(this, sy); 6547 ev.dispatch(); 6548 6549 //informProgramThatUserChangedPosition(sy); 6550 }); 6551 6552 upButton.tabStop = false; 6553 downButton.tabStop = false; 6554 thumb.tabStop = false; 6555 } 6556 } 6557 6558 override int minWidth() { return scaleWithDpi(16); } 6559 override int maxWidth() { return scaleWithDpi(16); } 6560 override int minHeight() { return scaleWithDpi(48); } 6561 } 6562 6563 6564 /++ 6565 EXPERIMENTAL 6566 6567 A widget specialized for being a container for other widgets. 6568 6569 History: 6570 Added May 29, 2021. Not stabilized at this time. 6571 +/ 6572 class WidgetContainer : Widget { 6573 this(Widget parent) { 6574 tabStop = false; 6575 super(parent); 6576 } 6577 6578 override int maxHeight() { 6579 if(this.children.length == 1) { 6580 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 6581 } else { 6582 return int.max; 6583 } 6584 } 6585 6586 override int maxWidth() { 6587 if(this.children.length == 1) { 6588 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 6589 } else { 6590 return int.max; 6591 } 6592 } 6593 6594 /+ 6595 6596 override int minHeight() { 6597 int largest = 0; 6598 int margins = 0; 6599 int lastMargin = 0; 6600 foreach(child; children) { 6601 auto mh = child.minHeight(); 6602 if(mh > largest) 6603 largest = mh; 6604 margins += mymax(lastMargin, child.marginTop()); 6605 lastMargin = child.marginBottom(); 6606 } 6607 return largest + margins; 6608 } 6609 6610 override int maxHeight() { 6611 int largest = 0; 6612 int margins = 0; 6613 int lastMargin = 0; 6614 foreach(child; children) { 6615 auto mh = child.maxHeight(); 6616 if(mh == int.max) 6617 return int.max; 6618 if(mh > largest) 6619 largest = mh; 6620 margins += mymax(lastMargin, child.marginTop()); 6621 lastMargin = child.marginBottom(); 6622 } 6623 return largest + margins; 6624 } 6625 6626 override int minWidth() { 6627 int min; 6628 foreach(child; children) { 6629 auto cm = child.minWidth; 6630 if(cm > min) 6631 min = cm; 6632 } 6633 return min + paddingLeft + paddingRight; 6634 } 6635 6636 override int minHeight() { 6637 int min; 6638 foreach(child; children) { 6639 auto cm = child.minHeight; 6640 if(cm > min) 6641 min = cm; 6642 } 6643 return min + paddingTop + paddingBottom; 6644 } 6645 6646 override int maxHeight() { 6647 int largest = 0; 6648 int margins = 0; 6649 int lastMargin = 0; 6650 foreach(child; children) { 6651 auto mh = child.maxHeight(); 6652 if(mh == int.max) 6653 return int.max; 6654 if(mh > largest) 6655 largest = mh; 6656 margins += mymax(lastMargin, child.marginTop()); 6657 lastMargin = child.marginBottom(); 6658 } 6659 return largest + margins; 6660 } 6661 6662 override int heightStretchiness() { 6663 int max; 6664 foreach(child; children) { 6665 auto c = child.heightStretchiness; 6666 if(c > max) 6667 max = c; 6668 } 6669 return max; 6670 } 6671 6672 override int marginTop() { 6673 if(this.children.length) 6674 return this.children[0].marginTop; 6675 return 0; 6676 } 6677 +/ 6678 } 6679 6680 /// 6681 abstract class Layout : Widget { 6682 this(Widget parent) { 6683 tabStop = false; 6684 super(parent); 6685 } 6686 } 6687 6688 /++ 6689 Makes all children minimum width and height, placing them down 6690 left to right, top to bottom. 6691 6692 Useful if you want to make a list of buttons that automatically 6693 wrap to a new line when necessary. 6694 +/ 6695 class InlineBlockLayout : Layout { 6696 /// 6697 this(Widget parent) { super(parent); } 6698 6699 override void recomputeChildLayout() { 6700 registerMovement(); 6701 6702 int x = this.paddingLeft, y = this.paddingTop; 6703 6704 int lineHeight; 6705 int previousMargin = 0; 6706 int previousMarginBottom = 0; 6707 6708 foreach(child; children) { 6709 if(child.hidden) 6710 continue; 6711 if(cast(FixedPosition) child) { 6712 child.recomputeChildLayout(); 6713 continue; 6714 } 6715 child.width = child.flexBasisWidth(); 6716 if(child.width == 0) 6717 child.width = child.minWidth(); 6718 if(child.width == 0) 6719 child.width = 32; 6720 6721 child.height = child.flexBasisHeight(); 6722 if(child.height == 0) 6723 child.height = child.minHeight(); 6724 if(child.height == 0) 6725 child.height = 32; 6726 6727 if(x + child.width + paddingRight > this.width) { 6728 x = this.paddingLeft; 6729 y += lineHeight; 6730 lineHeight = 0; 6731 previousMargin = 0; 6732 previousMarginBottom = 0; 6733 } 6734 6735 auto margin = child.marginLeft; 6736 if(previousMargin > margin) 6737 margin = previousMargin; 6738 6739 x += margin; 6740 6741 child.x = x; 6742 child.y = y; 6743 6744 int marginTopApplied; 6745 if(child.marginTop > previousMarginBottom) { 6746 child.y += child.marginTop; 6747 marginTopApplied = child.marginTop; 6748 } 6749 6750 x += child.width; 6751 previousMargin = child.marginRight; 6752 6753 if(child.marginBottom > previousMarginBottom) 6754 previousMarginBottom = child.marginBottom; 6755 6756 auto h = child.height + previousMarginBottom + marginTopApplied; 6757 if(h > lineHeight) 6758 lineHeight = h; 6759 6760 child.recomputeChildLayout(); 6761 } 6762 6763 } 6764 6765 override int minWidth() { 6766 int min; 6767 foreach(child; children) { 6768 auto cm = child.minWidth; 6769 if(cm > min) 6770 min = cm; 6771 } 6772 return min + paddingLeft + paddingRight; 6773 } 6774 6775 override int minHeight() { 6776 int min; 6777 foreach(child; children) { 6778 auto cm = child.minHeight; 6779 if(cm > min) 6780 min = cm; 6781 } 6782 return min + paddingTop + paddingBottom; 6783 } 6784 } 6785 6786 /++ 6787 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 6788 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 6789 the [TabWidget] will automatically change pages of child widgets. 6790 6791 This allows you to react to it however you see fit rather than having to 6792 be tied to just the new sets of child widgets. 6793 6794 It sends the message in the form of `this.emitCommand!"changetab"();`. 6795 6796 History: 6797 Added December 24, 2021 (dub v10.5) 6798 +/ 6799 class TabMessageWidget : Widget { 6800 6801 protected void tabIndexClicked(int item) { 6802 this.emitCommand!"changetab"(); 6803 } 6804 6805 /++ 6806 Adds the a new tab to the control with the given title. 6807 6808 Returns: 6809 The index of the newly added tab. You will need to know 6810 this index to refer to it later and to know which tab to 6811 change to when you get a changetab message. 6812 +/ 6813 int addTab(string title, int pos = int.max) { 6814 version(win32_widgets) { 6815 TCITEM item; 6816 item.mask = TCIF_TEXT; 6817 WCharzBuffer buf = WCharzBuffer(title); 6818 item.pszText = buf.ptr; 6819 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 6820 } else version(custom_widgets) { 6821 if(pos >= tabs.length) { 6822 tabs ~= title; 6823 redraw(); 6824 return cast(int) tabs.length - 1; 6825 } else if(pos <= 0) { 6826 tabs = title ~ tabs; 6827 redraw(); 6828 return 0; 6829 } else { 6830 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 6831 redraw(); 6832 return pos; 6833 } 6834 } 6835 } 6836 6837 override void addChild(Widget child, int pos = int.max) { 6838 if(container) 6839 container.addChild(child, pos); 6840 else 6841 super.addChild(child, pos); 6842 } 6843 6844 protected Widget makeContainer() { 6845 return new Widget(this); 6846 } 6847 6848 private Widget container; 6849 6850 override void recomputeChildLayout() { 6851 version(win32_widgets) { 6852 this.registerMovement(); 6853 6854 RECT rect; 6855 GetWindowRect(hwnd, &rect); 6856 6857 auto left = rect.left; 6858 auto top = rect.top; 6859 6860 TabCtrl_AdjustRect(hwnd, false, &rect); 6861 foreach(child; children) { 6862 if(!child.showing) continue; 6863 child.x = rect.left - left; 6864 child.y = rect.top - top; 6865 child.width = rect.right - rect.left; 6866 child.height = rect.bottom - rect.top; 6867 child.recomputeChildLayout(); 6868 } 6869 } else version(custom_widgets) { 6870 this.registerMovement(); 6871 foreach(child; children) { 6872 if(!child.showing) continue; 6873 child.x = 2; 6874 child.y = tabBarHeight + 2; // for the border 6875 child.width = width - 4; // for the border 6876 child.height = height - tabBarHeight - 2 - 2; // for the border 6877 child.recomputeChildLayout(); 6878 } 6879 } else static assert(0); 6880 } 6881 6882 version(custom_widgets) 6883 string[] tabs; 6884 6885 this(Widget parent) { 6886 super(parent); 6887 6888 tabStop = false; 6889 6890 version(win32_widgets) { 6891 createWin32Window(this, WC_TABCONTROL, "", 0); 6892 } else version(custom_widgets) { 6893 addEventListener((ClickEvent event) { 6894 if(event.target !is this) 6895 return; 6896 if(event.clientY >= 0 && event.clientY < tabBarHeight) { 6897 auto t = (event.clientX / tabWidth); 6898 if(t >= 0 && t < tabs.length) { 6899 currentTab_ = t; 6900 tabIndexClicked(t); 6901 redraw(); 6902 } 6903 } 6904 }); 6905 } else static assert(0); 6906 6907 this.container = makeContainer(); 6908 } 6909 6910 override int marginTop() { return 4; } 6911 override int paddingBottom() { return 4; } 6912 6913 override int minHeight() { 6914 int max = 0; 6915 foreach(child; children) 6916 max = mymax(child.minHeight, max); 6917 6918 6919 version(win32_widgets) { 6920 RECT rect; 6921 rect.right = this.width; 6922 rect.bottom = max; 6923 TabCtrl_AdjustRect(hwnd, true, &rect); 6924 6925 max = rect.bottom; 6926 } else { 6927 max += defaultLineHeight + 4; 6928 } 6929 6930 6931 return max; 6932 } 6933 6934 version(win32_widgets) 6935 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 6936 switch(code) { 6937 case TCN_SELCHANGE: 6938 auto sel = TabCtrl_GetCurSel(hwnd); 6939 tabIndexClicked(sel); 6940 break; 6941 default: 6942 } 6943 return 0; 6944 } 6945 6946 version(custom_widgets) { 6947 private int currentTab_; 6948 private int tabBarHeight() { return defaultLineHeight; } 6949 int tabWidth() { return scaleWithDpi(80); } 6950 } 6951 6952 version(win32_widgets) 6953 override void paint(WidgetPainter painter) {} 6954 6955 version(custom_widgets) 6956 override void paint(WidgetPainter painter) { 6957 auto cs = getComputedStyle(); 6958 6959 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 6960 6961 int posX = 0; 6962 foreach(idx, title; tabs) { 6963 auto isCurrent = idx == getCurrentTab(); 6964 6965 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 6966 6967 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 6968 painter.outlineColor = cs.foregroundColor; 6969 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 6970 6971 if(isCurrent) { 6972 painter.outlineColor = cs.windowBackgroundColor; 6973 painter.fillColor = Color.transparent; 6974 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 6975 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 6976 6977 painter.outlineColor = Color.white; 6978 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 6979 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 6980 painter.outlineColor = cs.activeTabColor; 6981 painter.drawPixel(Point(posX, tabBarHeight - 1)); 6982 } 6983 6984 posX += tabWidth - 2; 6985 } 6986 } 6987 6988 /// 6989 @scriptable 6990 void setCurrentTab(int item) { 6991 version(win32_widgets) 6992 TabCtrl_SetCurSel(hwnd, item); 6993 else version(custom_widgets) 6994 currentTab_ = item; 6995 else static assert(0); 6996 6997 tabIndexClicked(item); 6998 } 6999 7000 /// 7001 @scriptable 7002 int getCurrentTab() { 7003 version(win32_widgets) 7004 return TabCtrl_GetCurSel(hwnd); 7005 else version(custom_widgets) 7006 return currentTab_; // FIXME 7007 else static assert(0); 7008 } 7009 7010 /// 7011 @scriptable 7012 void removeTab(int item) { 7013 if(item && item == getCurrentTab()) 7014 setCurrentTab(item - 1); 7015 7016 version(win32_widgets) { 7017 TabCtrl_DeleteItem(hwnd, item); 7018 } 7019 7020 for(int a = item; a < children.length - 1; a++) 7021 this._children[a] = this._children[a + 1]; 7022 this._children = this._children[0 .. $-1]; 7023 } 7024 7025 } 7026 7027 7028 /++ 7029 A tab widget is a set of clickable tab buttons followed by a content area. 7030 7031 7032 Tabs can change existing content or can be new pages. 7033 7034 When the user picks a different tab, a `change` message is generated. 7035 +/ 7036 class TabWidget : TabMessageWidget { 7037 this(Widget parent) { 7038 super(parent); 7039 } 7040 7041 override protected Widget makeContainer() { 7042 return null; 7043 } 7044 7045 override void addChild(Widget child, int pos = int.max) { 7046 if(auto twp = cast(TabWidgetPage) child) { 7047 Widget.addChild(child, pos); 7048 if(pos == int.max) 7049 pos = cast(int) this.children.length - 1; 7050 7051 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 7052 7053 if(pos != getCurrentTab) { 7054 child.showing = false; 7055 } 7056 } else { 7057 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 7058 } 7059 } 7060 7061 // FIXME: add tab icons at some point, Windows supports them 7062 /++ 7063 Adds a page and its associated tab with the given label to the widget. 7064 7065 Returns: 7066 The added page object, to which you can add other widgets. 7067 +/ 7068 @scriptable 7069 TabWidgetPage addPage(string title) { 7070 return new TabWidgetPage(title, this); 7071 } 7072 7073 /++ 7074 Gets the page at the given tab index, or `null` if the index is bad. 7075 7076 History: 7077 Added December 24, 2021. 7078 +/ 7079 TabWidgetPage getPage(int index) { 7080 if(index < this.children.length) 7081 return null; 7082 return cast(TabWidgetPage) this.children[index]; 7083 } 7084 7085 /++ 7086 While you can still use the addTab from the parent class, 7087 *strongly* recommend you use [addPage] insteaad. 7088 7089 History: 7090 Added December 24, 2021 to fulful the interface 7091 requirement that came from adding [TabMessageWidget]. 7092 7093 You should not use it though since the [addPage] function 7094 is much easier to use here. 7095 +/ 7096 override int addTab(string title, int pos = int.max) { 7097 auto p = addPage(title); 7098 foreach(idx, child; this.children) 7099 if(child is p) 7100 return cast(int) idx; 7101 return -1; 7102 } 7103 7104 protected override void tabIndexClicked(int item) { 7105 foreach(idx, child; children) { 7106 child.showing(false, false); // batch the recalculates for the end 7107 } 7108 7109 foreach(idx, child; children) { 7110 if(idx == item) { 7111 child.showing(true, false); 7112 if(parentWindow) { 7113 auto f = parentWindow.getFirstFocusable(child); 7114 if(f) 7115 f.focus(); 7116 } 7117 recomputeChildLayout(); 7118 } 7119 } 7120 7121 version(win32_widgets) { 7122 InvalidateRect(hwnd, null, true); 7123 } else version(custom_widgets) { 7124 this.redraw(); 7125 } 7126 } 7127 7128 } 7129 7130 /++ 7131 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 7132 7133 You add [TabWidgetPage]s to it. 7134 +/ 7135 class PageWidget : Widget { 7136 this(Widget parent) { 7137 super(parent); 7138 } 7139 7140 override int minHeight() { 7141 int max = 0; 7142 foreach(child; children) 7143 max = mymax(child.minHeight, max); 7144 7145 return max; 7146 } 7147 7148 7149 override void addChild(Widget child, int pos = int.max) { 7150 if(auto twp = cast(TabWidgetPage) child) { 7151 super.addChild(child, pos); 7152 if(pos == int.max) 7153 pos = cast(int) this.children.length - 1; 7154 7155 if(pos != getCurrentTab) { 7156 child.showing = false; 7157 } 7158 } else { 7159 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 7160 } 7161 } 7162 7163 override void recomputeChildLayout() { 7164 this.registerMovement(); 7165 foreach(child; children) { 7166 child.x = 0; 7167 child.y = 0; 7168 child.width = width; 7169 child.height = height; 7170 child.recomputeChildLayout(); 7171 } 7172 } 7173 7174 private int currentTab_; 7175 7176 /// 7177 @scriptable 7178 void setCurrentTab(int item) { 7179 currentTab_ = item; 7180 7181 showOnly(item); 7182 } 7183 7184 /// 7185 @scriptable 7186 int getCurrentTab() { 7187 return currentTab_; 7188 } 7189 7190 /// 7191 @scriptable 7192 void removeTab(int item) { 7193 if(item && item == getCurrentTab()) 7194 setCurrentTab(item - 1); 7195 7196 for(int a = item; a < children.length - 1; a++) 7197 this._children[a] = this._children[a + 1]; 7198 this._children = this._children[0 .. $-1]; 7199 } 7200 7201 /// 7202 @scriptable 7203 TabWidgetPage addPage(string title) { 7204 return new TabWidgetPage(title, this); 7205 } 7206 7207 private void showOnly(int item) { 7208 foreach(idx, child; children) 7209 if(idx == item) { 7210 child.show(); 7211 child.queueRecomputeChildLayout(); 7212 } else { 7213 child.hide(); 7214 } 7215 } 7216 7217 } 7218 7219 /++ 7220 7221 +/ 7222 class TabWidgetPage : Widget { 7223 string title; 7224 this(string title, Widget parent) { 7225 this.title = title; 7226 this.tabStop = false; 7227 super(parent); 7228 7229 ///* 7230 version(win32_widgets) { 7231 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 7232 } 7233 //*/ 7234 } 7235 7236 override int minHeight() { 7237 int sum = 0; 7238 foreach(child; children) 7239 sum += child.minHeight(); 7240 return sum; 7241 } 7242 } 7243 7244 version(none) 7245 /++ 7246 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 7247 7248 I think I need to modify the layout algorithms to support this. 7249 +/ 7250 class CollapsableSidebar : Widget { 7251 7252 } 7253 7254 /// Stacks the widgets vertically, taking all the available width for each child. 7255 class VerticalLayout : Layout { 7256 // most of this is intentionally blank - widget's default is vertical layout right now 7257 /// 7258 this(Widget parent) { super(parent); } 7259 7260 /++ 7261 Sets a max width for the layout so you don't have to subclass. The max width 7262 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7263 7264 History: 7265 Added November 29, 2021 (dub v10.5) 7266 +/ 7267 this(int maxWidth, Widget parent) { 7268 this.mw = maxWidth; 7269 super(parent); 7270 } 7271 7272 private int mw = int.max; 7273 7274 override int maxWidth() { return scaleWithDpi(mw); } 7275 } 7276 7277 /// Stacks the widgets horizontally, taking all the available height for each child. 7278 class HorizontalLayout : Layout { 7279 /// 7280 this(Widget parent) { super(parent); } 7281 7282 /++ 7283 Sets a max height for the layout so you don't have to subclass. The max height 7284 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7285 7286 History: 7287 Added November 29, 2021 (dub v10.5) 7288 +/ 7289 this(int maxHeight, Widget parent) { 7290 this.mh = maxHeight; 7291 super(parent); 7292 } 7293 7294 private int mh = 0; 7295 7296 7297 7298 override void recomputeChildLayout() { 7299 .recomputeChildLayout!"width"(this); 7300 } 7301 7302 override int minHeight() { 7303 int largest = 0; 7304 int margins = 0; 7305 int lastMargin = 0; 7306 foreach(child; children) { 7307 auto mh = child.minHeight(); 7308 if(mh > largest) 7309 largest = mh; 7310 margins += mymax(lastMargin, child.marginTop()); 7311 lastMargin = child.marginBottom(); 7312 } 7313 return largest + margins; 7314 } 7315 7316 override int maxHeight() { 7317 if(mh != 0) 7318 return mymax(minHeight, scaleWithDpi(mh)); 7319 7320 int largest = 0; 7321 int margins = 0; 7322 int lastMargin = 0; 7323 foreach(child; children) { 7324 auto mh = child.maxHeight(); 7325 if(mh == int.max) 7326 return int.max; 7327 if(mh > largest) 7328 largest = mh; 7329 margins += mymax(lastMargin, child.marginTop()); 7330 lastMargin = child.marginBottom(); 7331 } 7332 return largest + margins; 7333 } 7334 7335 override int heightStretchiness() { 7336 int max; 7337 foreach(child; children) { 7338 auto c = child.heightStretchiness; 7339 if(c > max) 7340 max = c; 7341 } 7342 return max; 7343 } 7344 7345 } 7346 7347 version(win32_widgets) 7348 private 7349 extern(Windows) 7350 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 7351 Widget* pwin = hwnd in Widget.nativeMapping; 7352 if(pwin is null) 7353 return DefWindowProc(hwnd, message, wparam, lparam); 7354 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 7355 if(win is null) 7356 return DefWindowProc(hwnd, message, wparam, lparam); 7357 7358 switch(message) { 7359 case WM_SIZE: 7360 auto width = LOWORD(lparam); 7361 auto height = HIWORD(lparam); 7362 7363 auto hdc = GetDC(hwnd); 7364 auto hdcBmp = CreateCompatibleDC(hdc); 7365 7366 // FIXME: could this be more efficient? it never relinquishes a large bitmap 7367 if(width > win.bmpWidth || height > win.bmpHeight) { 7368 auto oldBuffer = win.buffer; 7369 win.buffer = CreateCompatibleBitmap(hdc, width, height); 7370 7371 if(oldBuffer) 7372 DeleteObject(oldBuffer); 7373 7374 win.bmpWidth = width; 7375 win.bmpHeight = height; 7376 } 7377 7378 // just always erase it upon resizing so minigui can draw over with a clean slate 7379 auto oldBmp = SelectObject(hdcBmp, win.buffer); 7380 7381 auto brush = GetSysColorBrush(COLOR_3DFACE); 7382 RECT r; 7383 r.left = 0; 7384 r.top = 0; 7385 r.right = width; 7386 r.bottom = height; 7387 FillRect(hdcBmp, &r, brush); 7388 7389 SelectObject(hdcBmp, oldBmp); 7390 DeleteDC(hdcBmp); 7391 ReleaseDC(hwnd, hdc); 7392 break; 7393 case WM_PAINT: 7394 if(win.buffer is null) 7395 goto default; 7396 7397 BITMAP bm; 7398 PAINTSTRUCT ps; 7399 7400 HDC hdc = BeginPaint(hwnd, &ps); 7401 7402 HDC hdcMem = CreateCompatibleDC(hdc); 7403 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 7404 7405 GetObject(win.buffer, bm.sizeof, &bm); 7406 7407 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 7408 7409 SelectObject(hdcMem, hbmOld); 7410 DeleteDC(hdcMem); 7411 EndPaint(hwnd, &ps); 7412 break; 7413 default: 7414 return DefWindowProc(hwnd, message, wparam, lparam); 7415 } 7416 7417 return 0; 7418 } 7419 7420 private wstring Win32Class(wstring name)() { 7421 static bool classRegistered; 7422 if(!classRegistered) { 7423 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 7424 WNDCLASSEX wc; 7425 wc.cbSize = wc.sizeof; 7426 wc.hInstance = hInstance; 7427 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 7428 wc.lpfnWndProc = &DoubleBufferWndProc; 7429 wc.lpszClassName = name.ptr; 7430 if(!RegisterClassExW(&wc)) 7431 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 7432 classRegistered = true; 7433 } 7434 7435 return name; 7436 } 7437 7438 /+ 7439 version(win32_widgets) 7440 extern(Windows) 7441 private 7442 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 7443 switch(iMessage) { 7444 case WM_PAINT: 7445 if(auto te = hWnd in Widget.nativeMapping) { 7446 try { 7447 //te.redraw(); 7448 writeln(te, " drawing"); 7449 } catch(Exception) {} 7450 } 7451 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7452 default: 7453 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7454 } 7455 } 7456 +/ 7457 7458 7459 /++ 7460 A widget specifically designed to hold other widgets. 7461 7462 History: 7463 Added July 1, 2021 7464 +/ 7465 class ContainerWidget : Widget { 7466 this(Widget parent) { 7467 super(parent); 7468 this.tabStop = false; 7469 7470 version(win32_widgets) { 7471 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 7472 } 7473 } 7474 } 7475 7476 /++ 7477 A widget that takes your widget, puts scroll bars around it, and sends 7478 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 7479 no effort to automatically scroll or clip its child widgets - it just sends 7480 the messages. 7481 7482 7483 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 7484 The scroll coordinates are all given in a unit you interpret as you wish. One 7485 of these units is moved on each press of the arrow buttons and represents the 7486 smallest amount the user can scroll. The intention is for this to be one line, 7487 one item in a list, one row in a table, etc. Whatever makes sense for your widget 7488 in each direction that the user might be interested in. 7489 7490 You can set a "page size" with the [step] property. (Yes, I regret the name...) 7491 This is the amount it jumps when the user pressed page up and page down, or clicks 7492 in the exposed part of the scroll bar. 7493 7494 You should add child content to the ScrollMessageWidget. However, it is important to 7495 note that the coordinates are always independent of the scroll position! It is YOUR 7496 responsibility to do any necessary transforms, clipping, etc., while drawing the 7497 content and interpreting mouse events if they are supposed to change with the scroll. 7498 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 7499 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 7500 you more control (which can be considerably more efficient and adapted to your actual data) 7501 at the expense of you also needing to be aware of its reality. 7502 7503 Please note that it does NOT react to mouse wheel events or various keyboard events as of 7504 version 10.3. Maybe this will change in the future.... but for now you must call 7505 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 7506 +/ 7507 class ScrollMessageWidget : Widget { 7508 this(Widget parent) { 7509 super(parent); 7510 7511 container = new Widget(this); 7512 hsb = new HorizontalScrollbar(this); 7513 vsb = new VerticalScrollbar(this); 7514 7515 hsb.addEventListener("scrolltonextline", { 7516 hsb.setPosition(hsb.position + movementPerButtonClickH_); 7517 notify(); 7518 }); 7519 hsb.addEventListener("scrolltopreviousline", { 7520 hsb.setPosition(hsb.position - movementPerButtonClickH_); 7521 notify(); 7522 }); 7523 vsb.addEventListener("scrolltonextline", { 7524 vsb.setPosition(vsb.position + movementPerButtonClickV_); 7525 notify(); 7526 }); 7527 vsb.addEventListener("scrolltopreviousline", { 7528 vsb.setPosition(vsb.position - movementPerButtonClickV_); 7529 notify(); 7530 }); 7531 hsb.addEventListener("scrolltonextpage", { 7532 hsb.setPosition(hsb.position + hsb.step_); 7533 notify(); 7534 }); 7535 hsb.addEventListener("scrolltopreviouspage", { 7536 hsb.setPosition(hsb.position - hsb.step_); 7537 notify(); 7538 }); 7539 vsb.addEventListener("scrolltonextpage", { 7540 vsb.setPosition(vsb.position + vsb.step_); 7541 notify(); 7542 }); 7543 vsb.addEventListener("scrolltopreviouspage", { 7544 vsb.setPosition(vsb.position - vsb.step_); 7545 notify(); 7546 }); 7547 hsb.addEventListener("scrolltoposition", (Event event) { 7548 hsb.setPosition(event.intValue); 7549 notify(); 7550 }); 7551 vsb.addEventListener("scrolltoposition", (Event event) { 7552 vsb.setPosition(event.intValue); 7553 notify(); 7554 }); 7555 7556 7557 tabStop = false; 7558 container.tabStop = false; 7559 magic = true; 7560 } 7561 7562 private int movementPerButtonClickH_ = 1; 7563 private int movementPerButtonClickV_ = 1; 7564 public void movementPerButtonClick(int h, int v) { 7565 movementPerButtonClickH_ = h; 7566 movementPerButtonClickV_ = v; 7567 } 7568 7569 /++ 7570 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 7571 7572 7573 The defaults for [addDefaultWheelListeners] are: 7574 7575 $(LIST 7576 * Mouse wheel scrolls vertically 7577 * Alt key + mouse wheel scrolls horiontally 7578 * Shift + mouse wheel scrolls faster. 7579 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 7580 ) 7581 7582 The defaults for [addDefaultKeyboardListeners] are: 7583 7584 $(LIST 7585 * Arrow keys scroll by the given amounts 7586 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 7587 * Page up and down scroll by the vertical viewable area 7588 * Home and end scroll to the start and end of the verticle viewable area. 7589 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 7590 ) 7591 7592 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 7593 7594 Params: 7595 horizontalArrowScrollAmount = 7596 verticalArrowScrollAmount = 7597 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 7598 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 7599 shiftMultiplier = multiplies the scroll amount by this when shift is held 7600 +/ 7601 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 7602 auto _this = this; 7603 7604 container.addEventListener((scope KeyDownEvent ke) { 7605 switch(ke.key) { 7606 case Key.Left: 7607 _this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7608 break; 7609 case Key.Right: 7610 _this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7611 break; 7612 case Key.Up: 7613 _this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7614 break; 7615 case Key.Down: 7616 _this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7617 break; 7618 case Key.PageUp: 7619 if(ke.altKey) 7620 _this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7621 else 7622 _this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7623 break; 7624 case Key.PageDown: 7625 if(ke.altKey) 7626 _this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7627 else 7628 _this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7629 break; 7630 case Key.Home: 7631 if(ke.altKey) 7632 _this.scrollLeft(short.max * 16); 7633 else 7634 _this.scrollUp(short.max * 16); 7635 break; 7636 case Key.End: 7637 if(ke.altKey) 7638 _this.scrollRight(short.max * 16); 7639 else 7640 _this.scrollDown(short.max * 16); 7641 break; 7642 7643 default: 7644 // ignore, not for us. 7645 } 7646 7647 }); 7648 } 7649 7650 /// ditto 7651 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 7652 auto _this = this; 7653 container.addEventListener((scope ClickEvent ce) { 7654 7655 //if(ce.target && ce.target.tabStop) 7656 //ce.target.focus(); 7657 7658 // ctrl is reserved for the application 7659 if(ce.ctrlKey) 7660 return; 7661 7662 if(horizontalWheelScrollAmount == 0 && ce.altKey) 7663 return; 7664 7665 if(shiftMultiplier == 0 && ce.shiftKey) 7666 return; 7667 7668 if(ce.button == MouseButton.wheelDown) { 7669 if(ce.altKey) 7670 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7671 else 7672 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7673 } else if(ce.button == MouseButton.wheelUp) { 7674 if(ce.altKey) 7675 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7676 else 7677 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7678 } 7679 }); 7680 } 7681 7682 /++ 7683 Scrolls the given amount. 7684 7685 History: 7686 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. 7687 +/ 7688 void scrollUp(int amount = 1) { 7689 vsb.setPosition(vsb.position - amount); 7690 notify(); 7691 } 7692 /// ditto 7693 void scrollDown(int amount = 1) { 7694 vsb.setPosition(vsb.position + amount); 7695 notify(); 7696 } 7697 /// ditto 7698 void scrollLeft(int amount = 1) { 7699 hsb.setPosition(hsb.position - amount); 7700 notify(); 7701 } 7702 /// ditto 7703 void scrollRight(int amount = 1) { 7704 hsb.setPosition(hsb.position + amount); 7705 notify(); 7706 } 7707 7708 /// 7709 VerticalScrollbar verticalScrollBar() { return vsb; } 7710 /// 7711 HorizontalScrollbar horizontalScrollBar() { return hsb; } 7712 7713 void notify() { 7714 static bool insideNotify; 7715 7716 if(insideNotify) 7717 return; // avoid the recursive call, even if it isn't strictly correct 7718 7719 insideNotify = true; 7720 scope(exit) insideNotify = false; 7721 7722 this.emit!ScrollEvent(); 7723 } 7724 7725 mixin Emits!ScrollEvent; 7726 7727 /// 7728 Point position() { 7729 return Point(hsb.position, vsb.position); 7730 } 7731 7732 /// 7733 void setPosition(int x, int y) { 7734 hsb.setPosition(x); 7735 vsb.setPosition(y); 7736 } 7737 7738 /// 7739 void setPageSize(int unitsX, int unitsY) { 7740 hsb.setStep(unitsX); 7741 vsb.setStep(unitsY); 7742 } 7743 7744 /// Always call this BEFORE setViewableArea 7745 void setTotalArea(int width, int height) { 7746 hsb.setMax(width); 7747 vsb.setMax(height); 7748 } 7749 7750 /++ 7751 Always set the viewable area AFTER setitng the total area if you are going to change both. 7752 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 7753 If you need to do that, use [queueRecomputeChildLayout]. 7754 +/ 7755 void setViewableArea(int width, int height) { 7756 7757 // actually there IS A need to dothis cuz the max might have changed since then 7758 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 7759 //return; // no need to do what is already done 7760 hsb.setViewableArea(width); 7761 vsb.setViewableArea(height); 7762 7763 bool needsNotify = false; 7764 7765 // FIXME: if at any point the rhs is outside the scrollbar, we need 7766 // to reset to 0. but it should remember the old position in case the 7767 // window resizes again, so it can kinda return ot where it was. 7768 // 7769 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 7770 if(width >= hsb.max) { 7771 // there's plenty of room to display it all so we need to reset to zero 7772 // FIXME: adjust so it matches the note above 7773 hsb.setPosition(0); 7774 needsNotify = true; 7775 } 7776 if(height >= vsb.max) { 7777 // there's plenty of room to display it all so we need to reset to zero 7778 // FIXME: adjust so it matches the note above 7779 vsb.setPosition(0); 7780 needsNotify = true; 7781 } 7782 if(needsNotify) 7783 notify(); 7784 } 7785 7786 private bool magic; 7787 override void addChild(Widget w, int position = int.max) { 7788 if(magic) 7789 container.addChild(w, position); 7790 else 7791 super.addChild(w, position); 7792 } 7793 7794 override void recomputeChildLayout() { 7795 if(hsb is null || vsb is null || container is null) return; 7796 7797 registerMovement(); 7798 7799 enum BUTTON_SIZE = 16; 7800 7801 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 7802 hsb.x = 0; 7803 hsb.y = this.height - hsb.height; 7804 7805 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 7806 vsb.x = this.width - vsb.width; 7807 vsb.y = 0; 7808 7809 auto vsb_width = vsb.showing ? vsb.width : 0; 7810 auto hsb_height = hsb.showing ? hsb.height : 0; 7811 7812 hsb.width = this.width - vsb_width; 7813 vsb.height = this.height - hsb_height; 7814 7815 hsb.recomputeChildLayout(); 7816 vsb.recomputeChildLayout(); 7817 7818 if(this.header is null) { 7819 container.x = 0; 7820 container.y = 0; 7821 container.width = this.width - vsb_width; 7822 container.height = this.height - hsb_height; 7823 container.recomputeChildLayout(); 7824 } else { 7825 header.x = 0; 7826 header.y = 0; 7827 header.width = this.width - vsb_width; 7828 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 7829 header.recomputeChildLayout(); 7830 7831 container.x = 0; 7832 container.y = scaleWithDpi(BUTTON_SIZE); 7833 container.width = this.width - vsb_width; 7834 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 7835 container.recomputeChildLayout(); 7836 } 7837 } 7838 7839 private HorizontalScrollbar hsb; 7840 private VerticalScrollbar vsb; 7841 Widget container; 7842 private Widget header; 7843 7844 /++ 7845 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 7846 7847 History: 7848 Added September 27, 2021 (dub v10.3) 7849 +/ 7850 Widget getHeader() { 7851 if(this.header is null) { 7852 magic = false; 7853 scope(exit) magic = true; 7854 this.header = new Widget(this); 7855 queueRecomputeChildLayout(); 7856 } 7857 return this.header; 7858 } 7859 7860 /++ 7861 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 7862 7863 History: 7864 Added January 3, 2023 (dub v11.0) 7865 +/ 7866 void scrollIntoView(Rectangle rect) { 7867 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 7868 7869 // import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1))); 7870 7871 // the lower right is exclusive normally 7872 auto test = rect.lowerRight; 7873 if(test.x > 0) test.x--; 7874 if(test.y > 0) test.y--; 7875 7876 if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) { 7877 // try to scroll only one dimension at a time if we can 7878 if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y))) 7879 setPosition(rect.upperLeft.x, position.y); 7880 if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y))) 7881 setPosition(position.x, rect.upperLeft.y); 7882 } 7883 7884 } 7885 7886 override int minHeight() { 7887 int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0)); 7888 if(header !is null) 7889 min += header.minHeight; 7890 if(horizontalScrollBar.showing) 7891 min += horizontalScrollBar.minHeight; 7892 return min; 7893 } 7894 7895 override int maxHeight() { 7896 int max = container ? container.maxHeight : int.max; 7897 if(max == int.max) 7898 return max; 7899 if(horizontalScrollBar.showing) 7900 max += horizontalScrollBar.minHeight; 7901 return max; 7902 } 7903 } 7904 7905 /++ 7906 $(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") 7907 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 7908 +/ 7909 version(minigui_screenshots) 7910 @Screenshot("ScrollMessageWidget") 7911 unittest { 7912 auto window = new Window("ScrollMessageWidget"); 7913 7914 auto smw = new ScrollMessageWidget(window); 7915 smw.addDefaultKeyboardListeners(); 7916 smw.addDefaultWheelListeners(); 7917 7918 window.loop(); 7919 } 7920 7921 /++ 7922 Bypasses automatic layout for its children, using manual positioning and sizing only. 7923 While you need to manually position them, you must ensure they are inside the StaticLayout's 7924 bounding box to avoid undefined behavior. 7925 7926 You should almost never use this. 7927 +/ 7928 class StaticLayout : Layout { 7929 /// 7930 this(Widget parent) { super(parent); } 7931 override void recomputeChildLayout() { 7932 registerMovement(); 7933 foreach(child; children) 7934 child.recomputeChildLayout(); 7935 } 7936 } 7937 7938 /++ 7939 Bypasses automatic positioning when being laid out. It is your responsibility to make 7940 room for this widget in the parent layout. 7941 7942 Its children are laid out normally, unless there is exactly one, in which case it takes 7943 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 7944 can do that with `padding`). 7945 +/ 7946 class StaticPosition : Layout { 7947 /// 7948 this(Widget parent) { super(parent); } 7949 7950 override void recomputeChildLayout() { 7951 registerMovement(); 7952 if(this.children.length == 1) { 7953 auto child = children[0]; 7954 child.x = 0; 7955 child.y = 0; 7956 child.width = this.width; 7957 child.height = this.height; 7958 child.recomputeChildLayout(); 7959 } else 7960 foreach(child; children) 7961 child.recomputeChildLayout(); 7962 } 7963 7964 alias width = typeof(super).width; 7965 alias height = typeof(super).height; 7966 7967 @property int width(int w) @nogc pure @safe nothrow { 7968 return this._width = w; 7969 } 7970 7971 @property int height(int w) @nogc pure @safe nothrow { 7972 return this._height = w; 7973 } 7974 7975 } 7976 7977 /++ 7978 FixedPosition is like [StaticPosition], but its coordinates 7979 are always relative to the viewport, meaning they do not scroll with 7980 the parent content. 7981 +/ 7982 class FixedPosition : StaticPosition { 7983 /// 7984 this(Widget parent) { super(parent); } 7985 } 7986 7987 version(win32_widgets) 7988 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 7989 if(true) { 7990 // cmd == 0 = menu, cmd == 1 = accelerator 7991 if(auto item = idm in Action.mapping) { 7992 foreach(handler; (*item).triggered) 7993 handler(); 7994 /* 7995 auto event = new Event("triggered", *item); 7996 event.button = idm; 7997 event.dispatch(); 7998 */ 7999 return 0; 8000 } 8001 } 8002 if(handle) 8003 if(auto widgetp = handle in Widget.nativeMapping) { 8004 (*widgetp).handleWmCommand(cmd, idm); 8005 return 0; 8006 } 8007 return 1; 8008 } 8009 8010 8011 /// 8012 class Window : Widget { 8013 int mouseCaptureCount = 0; 8014 Widget mouseCapturedBy; 8015 void captureMouse(Widget byWhom) { 8016 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 8017 mouseCaptureCount++; 8018 mouseCapturedBy = byWhom; 8019 win.grabInput(); 8020 } 8021 void releaseMouseCapture() { 8022 mouseCaptureCount--; 8023 mouseCapturedBy = null; 8024 win.releaseInputGrab(); 8025 } 8026 8027 /++ 8028 Sets the window icon which is often seen in title bars and taskbars. 8029 8030 History: 8031 Added April 5, 2022 (dub v10.8) 8032 +/ 8033 @property void icon(MemoryImage icon) { 8034 if(win && icon) 8035 win.icon = icon; 8036 } 8037 8038 // 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 8039 // this does NOT change the icon on the window! That's what the other overload is for 8040 static @property .icon icon(GenericIcons i) { 8041 return .icon(i); 8042 } 8043 8044 /// 8045 @scriptable 8046 @property bool focused() { 8047 return win.focused; 8048 } 8049 8050 static class Style : Widget.Style { 8051 override WidgetBackground background() { 8052 version(custom_widgets) 8053 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 8054 else version(win32_widgets) 8055 return WidgetBackground(Color.transparent); 8056 else static assert(0); 8057 } 8058 } 8059 mixin OverrideStyle!Style; 8060 8061 /++ 8062 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. 8063 +/ 8064 deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() { 8065 return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback(); 8066 } 8067 8068 private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() { 8069 OperatingSystemFont font; 8070 if(auto vt = WidgetPainter.visualTheme) { 8071 font = vt.defaultFontCached(96); // FIXME 8072 } 8073 8074 if(font is null) { 8075 static int defaultHeightCache; 8076 if(defaultHeightCache == 0) { 8077 font = new OperatingSystemFont; 8078 font.loadDefault; 8079 defaultHeightCache = font.height();// * 5 / 4; 8080 } 8081 return defaultHeightCache; 8082 } 8083 8084 return font.height();// * 5 / 4; 8085 } 8086 8087 Widget focusedWidget; 8088 8089 private SimpleWindow win_; 8090 8091 @property { 8092 /++ 8093 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 8094 8095 History: 8096 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 8097 +/ 8098 public SimpleWindow win() { 8099 return win_; 8100 } 8101 /// 8102 protected void win(SimpleWindow w) { 8103 win_ = w; 8104 } 8105 } 8106 8107 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 8108 this(Widget p) { 8109 tabStop = false; 8110 super(p); 8111 } 8112 8113 private void actualRedraw() { 8114 if(recomputeChildLayoutRequired) 8115 recomputeChildLayoutEntry(); 8116 if(!showing) return; 8117 8118 assert(parentWindow !is null); 8119 8120 auto w = drawableWindow; 8121 if(w is null) 8122 w = parentWindow.win; 8123 8124 if(w.closed()) 8125 return; 8126 8127 auto ugh = this.parent; 8128 int lox, loy; 8129 while(ugh) { 8130 lox += ugh.x; 8131 loy += ugh.y; 8132 ugh = ugh.parent; 8133 } 8134 auto painter = w.draw(true); 8135 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 8136 } 8137 8138 8139 private bool skipNextChar = false; 8140 8141 /++ 8142 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 8143 8144 This constructor is intended primarily for internal use and may be changed to `protected` later. 8145 +/ 8146 this(SimpleWindow win) { 8147 8148 static if(UsingSimpledisplayX11) { 8149 win.discardAdditionalConnectionState = &discardXConnectionState; 8150 win.recreateAdditionalConnectionState = &recreateXConnectionState; 8151 } 8152 8153 tabStop = false; 8154 super(null); 8155 this.win = win; 8156 8157 win.addEventListener((Widget.RedrawEvent) { 8158 if(win.eventQueued!RecomputeEvent) { 8159 // writeln("skipping"); 8160 return; // let the recompute event do the actual redraw 8161 } 8162 this.actualRedraw(); 8163 }); 8164 8165 win.addEventListener((Widget.RecomputeEvent) { 8166 recomputeChildLayoutEntry(); 8167 if(win.eventQueued!RedrawEvent) 8168 return; // let the queued one do it 8169 else { 8170 // writeln("drawing"); 8171 this.actualRedraw(); // if not queued, it needs to be done now anyway 8172 } 8173 }); 8174 8175 this.width = win.width; 8176 this.height = win.height; 8177 this.parentWindow = this; 8178 8179 win.closeQuery = () { 8180 if(this.emit!ClosingEvent()) 8181 win.close(); 8182 }; 8183 win.onClosing = () { 8184 this.emit!ClosedEvent(); 8185 }; 8186 8187 win.windowResized = (int w, int h) { 8188 this.width = w; 8189 this.height = h; 8190 queueRecomputeChildLayout(); 8191 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 8192 //version(win32_widgets) 8193 //InvalidateRect(hwnd, null, true); 8194 redraw(); 8195 }; 8196 8197 win.onFocusChange = (bool getting) { 8198 if(this.focusedWidget) { 8199 if(getting) { 8200 this.focusedWidget.emit!FocusEvent(); 8201 this.focusedWidget.emit!FocusInEvent(); 8202 } else { 8203 this.focusedWidget.emit!BlurEvent(); 8204 this.focusedWidget.emit!FocusOutEvent(); 8205 } 8206 } 8207 8208 if(getting) { 8209 this.emit!FocusEvent(); 8210 this.emit!FocusInEvent(); 8211 } else { 8212 this.emit!BlurEvent(); 8213 this.emit!FocusOutEvent(); 8214 } 8215 }; 8216 8217 win.onDpiChanged = { 8218 this.queueRecomputeChildLayout(); 8219 auto event = new DpiChangedEvent(this); 8220 event.sendDirectly(); 8221 8222 privateDpiChanged(); 8223 }; 8224 8225 win.setEventHandlers( 8226 (MouseEvent e) { 8227 dispatchMouseEvent(e); 8228 }, 8229 (KeyEvent e) { 8230 //writefln("%x %s", cast(uint) e.key, e.key); 8231 dispatchKeyEvent(e); 8232 }, 8233 (dchar e) { 8234 if(e == 13) e = 10; // hack? 8235 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8236 dispatchCharEvent(e); 8237 }, 8238 ); 8239 8240 addEventListener("char", (Widget, Event ev) { 8241 if(skipNextChar) { 8242 ev.preventDefault(); 8243 skipNextChar = false; 8244 } 8245 }); 8246 8247 version(win32_widgets) 8248 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 8249 if(hwnd !is this.win.impl.hwnd) 8250 return 1; // we don't care... pass it on 8251 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 8252 if(mustReturn) 8253 return ret; 8254 return 1; // pass it on 8255 }; 8256 8257 if(Window.newWindowCreated) 8258 Window.newWindowCreated(this); 8259 } 8260 8261 version(custom_widgets) 8262 override void defaultEventHandler_click(ClickEvent event) { 8263 if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) { 8264 if(event.target && event.target.tabStop) 8265 event.target.focus(); 8266 } 8267 } 8268 8269 private static void delegate(Window) newWindowCreated; 8270 8271 version(win32_widgets) 8272 override void paint(WidgetPainter painter) { 8273 /* 8274 RECT rect; 8275 rect.right = this.width; 8276 rect.bottom = this.height; 8277 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 8278 */ 8279 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 8280 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 8281 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 8282 // since the pen is null, to fill the whole space, we need the +1 on both. 8283 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 8284 SelectObject(painter.impl.hdc, p); 8285 SelectObject(painter.impl.hdc, b); 8286 } 8287 version(custom_widgets) 8288 override void paint(WidgetPainter painter) { 8289 auto cs = getComputedStyle(); 8290 painter.fillColor = cs.windowBackgroundColor; 8291 painter.outlineColor = cs.windowBackgroundColor; 8292 painter.drawRectangle(Point(0, 0), this.width, this.height); 8293 } 8294 8295 8296 override void defaultEventHandler_keydown(KeyDownEvent event) { 8297 Widget _this = event.target; 8298 8299 if(event.key == Key.Tab) { 8300 /* Window tab ordering is a recursive thingy with each group */ 8301 8302 // FIXME inefficient 8303 Widget[] helper(Widget p) { 8304 if(p.hidden) 8305 return null; 8306 Widget[] childOrdering; 8307 8308 auto children = p.children.dup; 8309 8310 while(true) { 8311 // UIs should be generally small, so gonna brute force it a little 8312 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 8313 8314 Widget smallestTab; 8315 foreach(ref c; children) { 8316 if(c is null) continue; 8317 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 8318 smallestTab = c; 8319 c = null; 8320 } 8321 } 8322 if(smallestTab !is null) { 8323 if(smallestTab.tabStop && !smallestTab.hidden) 8324 childOrdering ~= smallestTab; 8325 if(!smallestTab.hidden) 8326 childOrdering ~= helper(smallestTab); 8327 } else 8328 break; 8329 8330 } 8331 8332 return childOrdering; 8333 } 8334 8335 Widget[] tabOrdering = helper(this); 8336 8337 Widget recipient; 8338 8339 if(tabOrdering.length) { 8340 bool seenThis = false; 8341 Widget previous; 8342 foreach(idx, child; tabOrdering) { 8343 if(child is focusedWidget) { 8344 8345 if(event.shiftKey) { 8346 if(idx == 0) 8347 recipient = tabOrdering[$-1]; 8348 else 8349 recipient = tabOrdering[idx - 1]; 8350 break; 8351 } 8352 8353 seenThis = true; 8354 if(idx + 1 == tabOrdering.length) { 8355 // we're at the end, either move to the next group 8356 // or start back over 8357 recipient = tabOrdering[0]; 8358 } 8359 continue; 8360 } 8361 if(seenThis) { 8362 recipient = child; 8363 break; 8364 } 8365 previous = child; 8366 } 8367 } 8368 8369 if(recipient !is null) { 8370 // writeln(typeid(recipient)); 8371 recipient.focus(); 8372 8373 skipNextChar = true; 8374 } 8375 } 8376 8377 debug if(event.key == Key.F12) { 8378 if(devTools) { 8379 devTools.close(); 8380 devTools = null; 8381 } else { 8382 devTools = new DevToolWindow(this); 8383 devTools.show(); 8384 } 8385 } 8386 } 8387 8388 debug DevToolWindow devTools; 8389 8390 8391 /++ 8392 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 8393 8394 History: 8395 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 8396 8397 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 8398 +/ 8399 this(int width = 500, int height = 500, string title = null) { 8400 if(title is null) { 8401 import core.runtime; 8402 if(Runtime.args.length) 8403 title = Runtime.args[0]; 8404 } 8405 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus); 8406 8407 static if(UsingSimpledisplayX11) { 8408 ///+ 8409 // for input proxy 8410 auto display = XDisplayConnection.get; 8411 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 8412 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 8413 XMapWindow(display, inputProxy); 8414 // writefln("input proxy: 0x%0x", inputProxy); 8415 this.inputProxy = new SimpleWindow(inputProxy); 8416 8417 XEvent lastEvent; 8418 this.inputProxy.handleNativeEvent = (XEvent ev) { 8419 lastEvent = ev; 8420 return 1; 8421 }; 8422 this.inputProxy.setEventHandlers( 8423 (MouseEvent e) { 8424 dispatchMouseEvent(e); 8425 }, 8426 (KeyEvent e) { 8427 //writefln("%x %s", cast(uint) e.key, e.key); 8428 if(dispatchKeyEvent(e)) { 8429 // FIXME: i should trap error 8430 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 8431 auto thing = nw.focusableWindow(); 8432 if(thing && thing.window) { 8433 lastEvent.xkey.window = thing.window; 8434 // writeln("sending event ", lastEvent.xkey); 8435 trapXErrors( { 8436 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 8437 }); 8438 } 8439 } 8440 } 8441 }, 8442 (dchar e) { 8443 if(e == 13) e = 10; // hack? 8444 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8445 dispatchCharEvent(e); 8446 }, 8447 ); 8448 8449 this.inputProxy.populateXic(); 8450 // done 8451 //+/ 8452 } 8453 8454 8455 8456 win.setRequestedInputFocus = &this.setRequestedInputFocus; 8457 8458 this(win); 8459 } 8460 8461 SimpleWindow inputProxy; 8462 8463 private SimpleWindow setRequestedInputFocus() { 8464 return inputProxy; 8465 } 8466 8467 /// ditto 8468 this(string title, int width = 500, int height = 500) { 8469 this(width, height, title); 8470 } 8471 8472 /// 8473 @property string title() { return parentWindow.win.title; } 8474 /// 8475 @property void title(string title) { parentWindow.win.title = title; } 8476 8477 /// 8478 @scriptable 8479 void close() { 8480 win.close(); 8481 // I synchronize here upon window closing to ensure all child windows 8482 // get updated too before the event loop. This avoids some random X errors. 8483 static if(UsingSimpledisplayX11) { 8484 runInGuiThread( { 8485 XSync(XDisplayConnection.get, false); 8486 }); 8487 } 8488 } 8489 8490 bool dispatchKeyEvent(KeyEvent ev) { 8491 auto wid = focusedWidget; 8492 if(wid is null) 8493 wid = this; 8494 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 8495 event.originalKeyEvent = ev; 8496 event.key = ev.key; 8497 event.state = ev.modifierState; 8498 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8499 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8500 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8501 event.dispatch(); 8502 8503 return !event.propagationStopped; 8504 } 8505 8506 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 8507 bool dispatchCharEvent(dchar ch) { 8508 if(focusedWidget) { 8509 auto event = new CharEvent(focusedWidget, ch); 8510 event.dispatch(); 8511 return !event.propagationStopped; 8512 } 8513 return true; 8514 } 8515 8516 Widget mouseLastOver; 8517 Widget mouseLastDownOn; 8518 bool lastWasDoubleClick; 8519 bool dispatchMouseEvent(MouseEvent ev) { 8520 auto eleR = widgetAtPoint(this, ev.x, ev.y); 8521 auto ele = eleR.widget; 8522 8523 auto captureEle = ele; 8524 8525 if(mouseCapturedBy !is null) { 8526 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 8527 captureEle = mouseCapturedBy; 8528 } 8529 8530 // a hack to get it relative to the widget. 8531 eleR.x = ev.x; 8532 eleR.y = ev.y; 8533 auto pain = captureEle; 8534 while(pain) { 8535 eleR.x -= pain.x; 8536 eleR.y -= pain.y; 8537 pain.addScrollPosition(eleR.x, eleR.y); 8538 pain = pain.parent; 8539 } 8540 8541 void populateMouseEventBase(MouseEventBase event) { 8542 event.button = ev.button; 8543 event.buttonLinear = ev.buttonLinear; 8544 event.state = ev.modifierState; 8545 event.clientX = eleR.x; 8546 event.clientY = eleR.y; 8547 8548 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8549 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8550 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8551 } 8552 8553 if(ev.type == MouseEventType.buttonPressed) { 8554 { 8555 auto event = new MouseDownEvent(captureEle); 8556 populateMouseEventBase(event); 8557 event.dispatch(); 8558 } 8559 8560 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 8561 auto event = new DoubleClickEvent(captureEle); 8562 populateMouseEventBase(event); 8563 event.dispatch(); 8564 lastWasDoubleClick = ev.doubleClick; 8565 } else { 8566 lastWasDoubleClick = false; 8567 } 8568 8569 mouseLastDownOn = ele; 8570 } else if(ev.type == MouseEventType.buttonReleased) { 8571 { 8572 auto event = new MouseUpEvent(captureEle); 8573 populateMouseEventBase(event); 8574 event.dispatch(); 8575 } 8576 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 8577 auto event = new ClickEvent(captureEle); 8578 populateMouseEventBase(event); 8579 event.dispatch(); 8580 } 8581 } else if(ev.type == MouseEventType.motion) { 8582 // motion 8583 { 8584 auto event = new MouseMoveEvent(captureEle); 8585 populateMouseEventBase(event); // fills in button which is meaningless but meh 8586 event.dispatch(); 8587 } 8588 8589 if(mouseLastOver !is ele) { 8590 if(ele !is null) { 8591 if(!isAParentOf(ele, mouseLastOver)) { 8592 ele.setDynamicState(DynamicState.hover, true); 8593 auto event = new MouseEnterEvent(ele); 8594 event.relatedTarget = mouseLastOver; 8595 event.sendDirectly(); 8596 8597 ele.useStyleProperties((scope Widget.Style s) { 8598 ele.parentWindow.win.cursor = s.cursor; 8599 }); 8600 } 8601 } 8602 8603 if(mouseLastOver !is null) { 8604 if(!isAParentOf(mouseLastOver, ele)) { 8605 mouseLastOver.setDynamicState(DynamicState.hover, false); 8606 auto event = new MouseLeaveEvent(mouseLastOver); 8607 event.relatedTarget = ele; 8608 event.sendDirectly(); 8609 } 8610 } 8611 8612 if(ele !is null) { 8613 auto event = new MouseOverEvent(ele); 8614 event.relatedTarget = mouseLastOver; 8615 event.dispatch(); 8616 } 8617 8618 if(mouseLastOver !is null) { 8619 auto event = new MouseOutEvent(mouseLastOver); 8620 event.relatedTarget = ele; 8621 event.dispatch(); 8622 } 8623 8624 mouseLastOver = ele; 8625 } 8626 } 8627 8628 return true; // FIXME: the event default prevented? 8629 } 8630 8631 /++ 8632 Shows the window and runs the application event loop. 8633 8634 Blocks until this window is closed. 8635 8636 Bugs: 8637 8638 $(PITFALL 8639 You should always have one event loop live for your application. 8640 If you make two windows in sequence, the second call to loop (or 8641 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 8642 might fail: 8643 8644 --- 8645 // don't do this! 8646 auto window = new Window(); 8647 window.loop(); 8648 8649 // or new Window or new MainWindow, all the same 8650 auto window2 = new SimpleWindow(); 8651 window2.eventLoop(0); // problematic! might crash 8652 --- 8653 8654 simpledisplay's current implementation assumes that final cleanup is 8655 done when the event loop refcount reaches zero. So after the first 8656 eventLoop returns, when there isn't already another one active, it assumes 8657 the program will exit soon and cleans up. 8658 8659 This is arguably a bug that it doesn't reinitialize, and I'll probably change 8660 it eventually, but in the mean time, there's an easy solution: 8661 8662 --- 8663 // do this 8664 EventLoop mainEventLoop = EventLoop.get; // just add this line 8665 8666 auto window = new Window(); 8667 window.loop(); 8668 8669 // or any other type of Window etc. 8670 auto window2 = new Window(); 8671 window2.loop(); // perfectly fine since mainEventLoop still alive 8672 --- 8673 8674 By adding a top-level reference to the event loop, it ensures the final cleanup 8675 is not performed until it goes out of scope too, letting the individual window loops 8676 work without trouble despite the bug. 8677 ) 8678 8679 History: 8680 The [BlockingMode] parameter was added on December 8, 2021. 8681 The default behavior is to block until the application quits 8682 (so all windows have been closed), unless another minigui or 8683 simpledisplay event loop is already running, in which case it 8684 will block until this window closes specifically. 8685 +/ 8686 @scriptable 8687 void loop(BlockingMode bm = BlockingMode.automatic) { 8688 if(win.closed) 8689 return; // otherwise show will throw 8690 show(); 8691 win.eventLoopWithBlockingMode(bm, 0); 8692 } 8693 8694 private bool firstShow = true; 8695 8696 @scriptable 8697 override void show() { 8698 bool rd = false; 8699 if(firstShow) { 8700 firstShow = false; 8701 queueRecomputeChildLayout(); 8702 auto f = getFirstFocusable(this); // FIXME: autofocus? 8703 if(f) 8704 f.focus(); 8705 redraw(); 8706 } 8707 win.show(); 8708 super.show(); 8709 } 8710 @scriptable 8711 override void hide() { 8712 win.hide(); 8713 super.hide(); 8714 } 8715 8716 static Widget getFirstFocusable(Widget start) { 8717 if(start is null) 8718 return null; 8719 8720 foreach(widget; &start.focusableWidgets) { 8721 return widget; 8722 } 8723 8724 return null; 8725 } 8726 8727 static Widget getLastFocusable(Widget start) { 8728 if(start is null) 8729 return null; 8730 8731 Widget last; 8732 foreach(widget; &start.focusableWidgets) { 8733 last = widget; 8734 } 8735 8736 return last; 8737 } 8738 8739 8740 mixin Emits!ClosingEvent; 8741 mixin Emits!ClosedEvent; 8742 } 8743 8744 /++ 8745 History: 8746 Added January 12, 2022 8747 +/ 8748 class DpiChangedEvent : Event { 8749 enum EventString = "dpichanged"; 8750 8751 this(Widget target) { 8752 super(EventString, target); 8753 } 8754 } 8755 8756 debug private class DevToolWindow : Window { 8757 Window p; 8758 8759 TextEdit parentList; 8760 TextEdit logWindow; 8761 TextLabel clickX, clickY; 8762 8763 this(Window p) { 8764 this.p = p; 8765 super(400, 300, "Developer Toolbox"); 8766 8767 logWindow = new TextEdit(this); 8768 parentList = new TextEdit(this); 8769 8770 auto hl = new HorizontalLayout(this); 8771 clickX = new TextLabel("", TextAlignment.Right, hl); 8772 clickY = new TextLabel("", TextAlignment.Right, hl); 8773 8774 parentListeners ~= p.addEventListener("*", (Event ev) { 8775 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 8776 }); 8777 8778 parentListeners ~= p.addEventListener((ClickEvent ev) { 8779 auto s = ev.srcElement; 8780 8781 string list; 8782 8783 void addInfo(Widget s) { 8784 list ~= s.toString(); 8785 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 8786 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 8787 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 8788 list ~= "\n\theight: " ~ toInternal!string(s.height); 8789 list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth); 8790 list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth); 8791 list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness); 8792 list ~= "\n\twidth: " ~ toInternal!string(s.width); 8793 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 8794 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 8795 } 8796 8797 addInfo(s); 8798 8799 s = s.parent; 8800 while(s) { 8801 list ~= "\n"; 8802 addInfo(s); 8803 s = s.parent; 8804 } 8805 parentList.content = list; 8806 8807 clickX.label = toInternal!string(ev.clientX); 8808 clickY.label = toInternal!string(ev.clientY); 8809 }); 8810 } 8811 8812 EventListener[] parentListeners; 8813 8814 override void close() { 8815 assert(p !is null); 8816 foreach(p; parentListeners) 8817 p.disconnect(); 8818 parentListeners = null; 8819 p.devTools = null; 8820 p = null; 8821 super.close(); 8822 } 8823 8824 override void defaultEventHandler_keydown(KeyDownEvent ev) { 8825 if(ev.key == Key.F12) { 8826 this.close(); 8827 if(p) 8828 p.devTools = null; 8829 } else { 8830 super.defaultEventHandler_keydown(ev); 8831 } 8832 } 8833 8834 void log(T...)(T t) { 8835 string str; 8836 import std.conv; 8837 foreach(i; t) 8838 str ~= to!string(i); 8839 str ~= "\n"; 8840 logWindow.addText(str); 8841 8842 //version(custom_widgets) 8843 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 8844 } 8845 } 8846 8847 /++ 8848 A dialog is a transient window that intends to get information from 8849 the user before being dismissed. 8850 +/ 8851 abstract class Dialog : Window { 8852 /// 8853 this(int width, int height, string title = null) { 8854 super(width, height, title); 8855 } 8856 8857 /// 8858 abstract void OK(); 8859 8860 /// 8861 void Cancel() { 8862 this.close(); 8863 } 8864 } 8865 8866 /++ 8867 A custom widget similar to the HTML5 <details> tag. 8868 +/ 8869 version(none) 8870 class DetailsView : Widget { 8871 8872 } 8873 8874 // FIXME: maybe i should expose the other list views Windows offers too 8875 8876 /++ 8877 A TableView is a widget made to display a table of data strings. 8878 8879 8880 Future_Directions: 8881 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 8882 8883 I will add a selection changed event at some point, as well as item clicked events. 8884 History: 8885 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 8886 See_Also: 8887 [ListWidget] which displays a list of strings without additional columns. 8888 +/ 8889 class TableView : Widget { 8890 /++ 8891 8892 +/ 8893 this(Widget parent) { 8894 super(parent); 8895 8896 version(win32_widgets) { 8897 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 8898 } else version(custom_widgets) { 8899 auto smw = new ScrollMessageWidget(this); 8900 smw.addDefaultKeyboardListeners(); 8901 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 8902 tvwi = new TableViewWidgetInner(this, smw); 8903 } 8904 } 8905 8906 // FIXME: auto-size columns on double click of header thing like in Windows 8907 // it need only make the currently displayed things fit well. 8908 8909 8910 private ColumnInfo[] columns; 8911 private int itemCount; 8912 8913 version(custom_widgets) private { 8914 TableViewWidgetInner tvwi; 8915 } 8916 8917 /// Passed to [setColumnInfo] 8918 static struct ColumnInfo { 8919 const(char)[] name; /// the name displayed in the header 8920 /++ 8921 The default width, in pixels. As a special case, you can set this to -1 8922 if you want the system to try to automatically size the width to fit visible 8923 content. If it can't, it will try to pick a sensible default size. 8924 8925 Any other negative value is not allowed and may lead to unpredictable results. 8926 8927 History: 8928 The -1 behavior was specified on December 3, 2021. It actually worked before 8929 anyway on Win32 but now it is a formal feature with partial Linux support. 8930 8931 Bugs: 8932 It doesn't actually attempt to calculate a best-fit width on Linux as of 8933 December 3, 2021. I do plan to fix this in the future, but Windows is the 8934 priority right now. At least it doesn't break things when you use it now. 8935 +/ 8936 int width; 8937 8938 /++ 8939 Alignment of the text in the cell. Applies to the header as well as all data in this 8940 column. 8941 8942 Bugs: 8943 On Windows, the first column ignores this member and is always left aligned. 8944 You can work around this by inserting a dummy first column with width = 0 8945 then putting your actual data in the second column, which does respect the 8946 alignment. 8947 8948 This is a quirk of the operating system's implementation going back a very 8949 long time and is unlikely to ever be fixed. 8950 +/ 8951 TextAlignment alignment; 8952 8953 /++ 8954 After all the pixel widths have been assigned, any left over 8955 space is divided up among all columns and distributed to according 8956 to the widthPercent field. 8957 8958 8959 For example, if you have two fields, both with width 50 and one with 8960 widthPercent of 25 and the other with widthPercent of 75, and the 8961 container is 200 pixels wide, first both get their width of 50. 8962 then the 100 remaining pixels are split up, so the one gets a total 8963 of 75 pixels and the other gets a total of 125. 8964 8965 This is automatically applied as the window is resized. 8966 8967 If there is not enough space - that is, when a horizontal scrollbar 8968 needs to appear - there are 0 pixels divided up, and thus everyone 8969 gets 0. This can cause a column to shrink out of proportion when 8970 passing the scroll threshold. 8971 8972 It is important to still set a fixed width (that is, to populate the 8973 `width` field) even if you use the percents because that will be the 8974 default minimum in the event of a scroll bar appearing. 8975 8976 The percents total in the column can never exceed 100 or be less than 0. 8977 Doing this will trigger an assert error. 8978 8979 Implementation note: 8980 8981 Please note that percentages are only recalculated 1) upon original 8982 construction and 2) upon resizing the control. If the user adjusts the 8983 width of a column, the percentage items will not be updated. 8984 8985 On the other hand, if the user adjusts the width of a percentage column 8986 then resizes the window, it is recalculated, meaning their hand adjustment 8987 is discarded. This specific behavior may change in the future as it is 8988 arguably a bug, but I'm not certain yet. 8989 8990 History: 8991 Added November 10, 2021 (dub v10.4) 8992 +/ 8993 int widthPercent; 8994 8995 8996 private int calculatedWidth; 8997 } 8998 /++ 8999 Sets the number of columns along with information about the headers. 9000 9001 Please note: on Windows, the first column ignores your alignment preference 9002 and is always left aligned. 9003 +/ 9004 void setColumnInfo(ColumnInfo[] columns...) { 9005 9006 foreach(ref c; columns) { 9007 c.name = c.name.idup; 9008 } 9009 this.columns = columns.dup; 9010 9011 updateCalculatedWidth(false); 9012 9013 version(custom_widgets) { 9014 tvwi.header.updateHeaders(); 9015 tvwi.updateScrolls(); 9016 } else version(win32_widgets) 9017 foreach(i, column; this.columns) { 9018 LVCOLUMN lvColumn; 9019 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 9020 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 9021 9022 auto bfr = WCharzBuffer(column.name); 9023 lvColumn.pszText = bfr.ptr; 9024 9025 if(column.alignment & TextAlignment.Center) 9026 lvColumn.fmt = LVCFMT_CENTER; 9027 else if(column.alignment & TextAlignment.Right) 9028 lvColumn.fmt = LVCFMT_RIGHT; 9029 else 9030 lvColumn.fmt = LVCFMT_LEFT; 9031 9032 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 9033 throw new WindowsApiException("Insert Column Fail", GetLastError()); 9034 } 9035 } 9036 9037 private int getActualSetSize(size_t i, bool askWindows) { 9038 version(win32_widgets) 9039 if(askWindows) 9040 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 9041 auto w = columns[i].width; 9042 if(w == -1) 9043 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 9044 return w; 9045 } 9046 9047 private void updateCalculatedWidth(bool informWindows) { 9048 int padding; 9049 version(win32_widgets) 9050 padding = 4; 9051 int remaining = this.width; 9052 foreach(i, column; columns) 9053 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 9054 remaining -= padding; 9055 if(remaining < 0) 9056 remaining = 0; 9057 9058 int percentTotal; 9059 foreach(i, ref column; columns) { 9060 percentTotal += column.widthPercent; 9061 9062 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 9063 9064 column.calculatedWidth = c; 9065 9066 version(win32_widgets) 9067 if(informWindows) 9068 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 9069 } 9070 9071 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 9072 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)."); 9073 9074 9075 } 9076 9077 override void registerMovement() { 9078 super.registerMovement(); 9079 9080 updateCalculatedWidth(true); 9081 } 9082 9083 /++ 9084 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. 9085 +/ 9086 void setItemCount(int count) { 9087 this.itemCount = count; 9088 version(custom_widgets) { 9089 tvwi.updateScrolls(); 9090 redraw(); 9091 } else version(win32_widgets) { 9092 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 9093 } 9094 } 9095 9096 /++ 9097 Clears all items; 9098 +/ 9099 void clear() { 9100 this.itemCount = 0; 9101 this.columns = null; 9102 version(custom_widgets) { 9103 tvwi.header.updateHeaders(); 9104 tvwi.updateScrolls(); 9105 redraw(); 9106 } else version(win32_widgets) { 9107 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 9108 } 9109 } 9110 9111 /+ 9112 version(win32_widgets) 9113 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 9114 auto itemId = dis.itemID; 9115 auto hdc = dis.hDC; 9116 auto rect = dis.rcItem; 9117 switch(dis.itemAction) { 9118 case ODA_DRAWENTIRE: 9119 9120 // FIXME: do other items 9121 // FIXME: do the focus rectangle i guess 9122 // FIXME: alignment 9123 // FIXME: column width 9124 // FIXME: padding left 9125 // FIXME: check dpi scaling 9126 // FIXME: don't owner draw unless it is necessary. 9127 9128 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 9129 RECT itemRect; 9130 itemRect.top = 1; // subitem idx, 1-based 9131 itemRect.left = LVIR_BOUNDS; 9132 9133 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 9134 itemRect.left += padding; 9135 9136 getData(itemId, 0, (in char[] data) { 9137 auto wdata = WCharzBuffer(data); 9138 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 9139 9140 }); 9141 goto case; 9142 case ODA_FOCUS: 9143 if(dis.itemState & ODS_FOCUS) 9144 DrawFocusRect(hdc, &rect); 9145 break; 9146 case ODA_SELECT: 9147 // itemState & ODS_SELECTED 9148 break; 9149 default: 9150 } 9151 return 1; 9152 } 9153 +/ 9154 9155 version(win32_widgets) { 9156 CellStyle last; 9157 COLORREF defaultColor; 9158 COLORREF defaultBackground; 9159 } 9160 9161 version(win32_widgets) 9162 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 9163 switch(code) { 9164 case NM_CUSTOMDRAW: 9165 auto s = cast(NMLVCUSTOMDRAW*) hdr; 9166 switch(s.nmcd.dwDrawStage) { 9167 case CDDS_PREPAINT: 9168 if(getCellStyle is null) 9169 return 0; 9170 9171 mustReturn = true; 9172 return CDRF_NOTIFYITEMDRAW; 9173 case CDDS_ITEMPREPAINT: 9174 mustReturn = true; 9175 return CDRF_NOTIFYSUBITEMDRAW; 9176 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 9177 mustReturn = true; 9178 9179 if(getCellStyle is null) // this SHOULD never happen... 9180 return 0; 9181 9182 if(s.iSubItem == 0) { 9183 // Windows resets it per row so we'll use item 0 as a chance 9184 // to capture these for later 9185 defaultColor = s.clrText; 9186 defaultBackground = s.clrTextBk; 9187 } 9188 9189 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 9190 // if no special style and no reset needed... 9191 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 9192 return 0; // allow default processing to continue 9193 9194 last = style; 9195 9196 // might still need to reset or use the preference. 9197 9198 if(style.flags & CellStyle.Flags.textColorSet) 9199 s.clrText = style.textColor.asWindowsColorRef; 9200 else 9201 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 9202 if(style.flags & CellStyle.Flags.backgroundColorSet) 9203 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 9204 else 9205 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 9206 9207 return CDRF_NEWFONT; 9208 default: 9209 return 0; 9210 9211 } 9212 case NM_RETURN: // no need since i subclass keydown 9213 break; 9214 case LVN_COLUMNCLICK: 9215 auto info = cast(LPNMLISTVIEW) hdr; 9216 this.emit!HeaderClickedEvent(info.iSubItem); 9217 break; 9218 case NM_CLICK: 9219 case NM_DBLCLK: 9220 case NM_RCLICK: 9221 case NM_RDBLCLK: 9222 // the item/subitem is set here and that can be a useful notification 9223 // even beyond the normal click notification 9224 break; 9225 case LVN_GETDISPINFO: 9226 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 9227 if(info.item.mask & LVIF_TEXT) { 9228 if(getData) { 9229 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 9230 auto bfr = WCharzBuffer(dataReceived); 9231 auto len = info.item.cchTextMax; 9232 if(bfr.length < len) 9233 len = cast(typeof(len)) bfr.length; 9234 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 9235 info.item.pszText[len] = 0; 9236 }); 9237 } else { 9238 info.item.pszText[0] = 0; 9239 } 9240 //info.item.iItem 9241 //if(info.item.iSubItem) 9242 } 9243 break; 9244 default: 9245 } 9246 return 0; 9247 } 9248 9249 override bool encapsulatedChildren() { 9250 return true; 9251 } 9252 9253 /++ 9254 Informs the control that content has changed. 9255 9256 History: 9257 Added November 10, 2021 (dub v10.4) 9258 +/ 9259 void update() { 9260 version(custom_widgets) 9261 redraw(); 9262 else { 9263 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 9264 UpdateWindow(hwnd); 9265 } 9266 9267 9268 } 9269 9270 /++ 9271 Called by the system to request the text content of an individual cell. You 9272 should pass the text into the provided `sink` delegate. This function will be 9273 called for each visible cell as-needed when drawing. 9274 +/ 9275 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 9276 9277 /++ 9278 Available per-cell style customization options. Use one of the constructors 9279 provided to set the values conveniently, or default construct it and set individual 9280 values yourself. Just remember to set the `flags` so your values are actually used. 9281 If the flag isn't set, the field is ignored and the system default is used instead. 9282 9283 This is returned by the [getCellStyle] delegate. 9284 9285 Examples: 9286 --- 9287 // assumes you have a variables called `my_data` which is an array of arrays of numbers 9288 auto table = new TableView(window); 9289 // snip: you would set up columns here 9290 9291 // this is how you provide data to the table view class 9292 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 9293 import std.conv; 9294 sink(to!string(my_data[row][column])); 9295 }; 9296 9297 // and this is how you customize the colors 9298 table.getCellStyle = delegate(int row, int column) { 9299 return (my_data[row][column] < 0) ? 9300 TableView.CellStyle(Color.red); // make negative numbers red 9301 : TableView.CellStyle.init; // leave the rest alone 9302 }; 9303 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 9304 --- 9305 9306 History: 9307 Added November 27, 2021 (dub v10.4) 9308 +/ 9309 struct CellStyle { 9310 /// 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. 9311 this(Color textColor) { 9312 this.textColor = textColor; 9313 this.flags |= Flags.textColorSet; 9314 } 9315 /// Sets a custom text and background color. 9316 this(Color textColor, Color backgroundColor) { 9317 this.textColor = textColor; 9318 this.backgroundColor = backgroundColor; 9319 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 9320 } 9321 9322 Color textColor; 9323 Color backgroundColor; 9324 int flags; /// bitmask of [Flags] 9325 /// available options to combine into [flags] 9326 enum Flags { 9327 textColorSet = 1 << 0, 9328 backgroundColorSet = 1 << 1, 9329 } 9330 } 9331 /++ 9332 Companion delegate to [getData] that allows you to custom style each 9333 cell of the table. 9334 9335 Returns: 9336 A [CellStyle] structure that describes the desired style for the 9337 given cell. `return CellStyle.init` if you want the default style. 9338 9339 History: 9340 Added November 27, 2021 (dub v10.4) 9341 +/ 9342 CellStyle delegate(int row, int column) getCellStyle; 9343 9344 // i want to be able to do things like draw little colored things to show red for negative numbers 9345 // or background color indicators or even in-cell charts 9346 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 9347 9348 /++ 9349 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 9350 +/ 9351 mixin Emits!HeaderClickedEvent; 9352 } 9353 9354 /++ 9355 This is emitted by the [TableView] when a user clicks on a column header. 9356 9357 Its member `columnIndex` has the zero-based index of the column that was clicked. 9358 9359 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 9360 9361 History: 9362 Added November 27, 2021 (dub v10.4) 9363 +/ 9364 class HeaderClickedEvent : Event { 9365 enum EventString = "HeaderClicked"; 9366 this(Widget target, int columnIndex) { 9367 this.columnIndex = columnIndex; 9368 super(EventString, target); 9369 } 9370 9371 /// The index of the column 9372 int columnIndex; 9373 9374 /// 9375 override @property int intValue() { 9376 return columnIndex; 9377 } 9378 } 9379 9380 version(custom_widgets) 9381 private class TableViewWidgetInner : Widget { 9382 9383 // wrap this thing in a ScrollMessageWidget 9384 9385 TableView tvw; 9386 ScrollMessageWidget smw; 9387 HeaderWidget header; 9388 9389 this(TableView tvw, ScrollMessageWidget smw) { 9390 this.tvw = tvw; 9391 this.smw = smw; 9392 super(smw); 9393 9394 this.tabStop = true; 9395 9396 header = new HeaderWidget(this, smw.getHeader()); 9397 9398 smw.addEventListener("scroll", () { 9399 this.redraw(); 9400 header.redraw(); 9401 }); 9402 9403 9404 // I need headers outside the scroll area but rendered on the same line as the up arrow 9405 // FIXME: add a fixed header to the SMW 9406 } 9407 9408 enum padding = 3; 9409 9410 void updateScrolls() { 9411 int w; 9412 foreach(idx, column; tvw.columns) { 9413 if(column.width == 0) continue; 9414 w += tvw.getActualSetSize(idx, false);// + padding; 9415 } 9416 smw.setTotalArea(w, tvw.itemCount); 9417 columnsWidth = w; 9418 } 9419 9420 private int columnsWidth; 9421 9422 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 9423 9424 override void registerMovement() { 9425 super.registerMovement(); 9426 // FIXME: actual column width. it might need to be done per-pixel instead of per-column 9427 smw.setViewableArea(this.width, this.height / lh); 9428 } 9429 9430 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 9431 int x; 9432 int y; 9433 9434 int row = smw.position.y; 9435 9436 foreach(lol; 0 .. this.height / lh) { 9437 if(row >= tvw.itemCount) 9438 break; 9439 x = 0; 9440 foreach(columnNumber, column; tvw.columns) { 9441 auto x2 = x + column.calculatedWidth; 9442 auto smwx = smw.position.x; 9443 9444 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 9445 auto startX = x; 9446 auto endX = x + column.calculatedWidth; 9447 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 9448 case TextAlignment.Left: startX += padding; break; 9449 case TextAlignment.Center: startX += padding; endX -= padding; break; 9450 case TextAlignment.Right: endX -= padding; break; 9451 default: /* broken */ break; 9452 } 9453 if(column.width != 0) // no point drawing an invisible column 9454 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 9455 auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endX - smw.position.x, y + lh))); 9456 9457 void dotext(WidgetPainter painter) { 9458 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 9459 } 9460 9461 if(tvw.getCellStyle !is null) { 9462 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 9463 9464 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 9465 auto tempPainter = painter; 9466 tempPainter.fillColor = style.backgroundColor; 9467 tempPainter.outlineColor = style.backgroundColor; 9468 9469 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 9470 Point(endX - smw.position.x, y + lh)); 9471 } 9472 auto tempPainter = painter; 9473 if(style.flags & TableView.CellStyle.Flags.textColorSet) 9474 tempPainter.outlineColor = style.textColor; 9475 9476 dotext(tempPainter); 9477 } else { 9478 dotext(painter); 9479 } 9480 }); 9481 } 9482 9483 x += column.calculatedWidth; 9484 } 9485 row++; 9486 y += lh; 9487 } 9488 return bounds; 9489 } 9490 9491 static class Style : Widget.Style { 9492 override WidgetBackground background() { 9493 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 9494 } 9495 } 9496 mixin OverrideStyle!Style; 9497 9498 private static class HeaderWidget : Widget { 9499 /+ 9500 maybe i should do a splitter thing on top of the other widgets 9501 so the splitter itself isn't really drawn but still replies to mouse events? 9502 +/ 9503 this(TableViewWidgetInner tvw, Widget parent) { 9504 super(parent); 9505 this.tvw = tvw; 9506 9507 this.remainder = new Button("", this); 9508 9509 this.addEventListener((scope ClickEvent ev) { 9510 int header = -1; 9511 foreach(idx, child; this.children[1 .. $]) { 9512 if(child is ev.target) { 9513 header = cast(int) idx; 9514 break; 9515 } 9516 } 9517 9518 if(header != -1) { 9519 auto hce = new HeaderClickedEvent(tvw.tvw, header); 9520 hce.dispatch(); 9521 } 9522 9523 }); 9524 } 9525 9526 void updateHeaders() { 9527 foreach(child; children[1 .. $]) 9528 child.removeWidget(); 9529 9530 foreach(column; tvw.tvw.columns) { 9531 // the cast is ok because I dup it above, just the type is never changed. 9532 // all this is private so it should never get messed up. 9533 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 9534 } 9535 } 9536 9537 Button remainder; 9538 TableViewWidgetInner tvw; 9539 9540 override void recomputeChildLayout() { 9541 registerMovement(); 9542 int pos; 9543 foreach(idx, child; children[1 .. $]) { 9544 if(idx >= tvw.tvw.columns.length) 9545 continue; 9546 child.x = pos; 9547 child.y = 0; 9548 child.width = tvw.tvw.columns[idx].calculatedWidth; 9549 child.height = scaleWithDpi(16);// this.height; 9550 pos += child.width; 9551 9552 child.recomputeChildLayout(); 9553 } 9554 9555 if(remainder is null) 9556 return; 9557 9558 remainder.x = pos; 9559 remainder.y = 0; 9560 if(pos < this.width) 9561 remainder.width = this.width - pos;// + 4; 9562 else 9563 remainder.width = 0; 9564 remainder.height = scaleWithDpi(16); 9565 9566 remainder.recomputeChildLayout(); 9567 } 9568 9569 // for the scrollable children mixin 9570 Point scrollOrigin() { 9571 return Point(tvw.smw.position.x, 0); 9572 } 9573 void paintFrameAndBackground(WidgetPainter painter) { } 9574 9575 mixin ScrollableChildren; 9576 } 9577 } 9578 9579 /+ 9580 9581 // given struct / array / number / string / etc, make it viewable and editable 9582 class DataViewerWidget : Widget { 9583 9584 } 9585 +/ 9586 9587 /++ 9588 A line edit box with an associated label. 9589 9590 History: 9591 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 9592 9593 ``` 9594 Old: ________ 9595 9596 New: 9597 ____________ 9598 ``` 9599 9600 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 9601 9602 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 9603 horizontal label but left aligned. You may also consider a [GridLayout]. 9604 +/ 9605 alias LabeledLineEdit = Labeled!LineEdit; 9606 9607 private int widthThatWouldFitChildLabels(Widget w) { 9608 if(w is null) 9609 return 0; 9610 9611 int max; 9612 9613 if(auto label = cast(TextLabel) w) { 9614 return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight(); 9615 } else { 9616 foreach(child; w.children) { 9617 max = mymax(max, widthThatWouldFitChildLabels(child)); 9618 } 9619 } 9620 9621 return max; 9622 } 9623 9624 /++ 9625 History: 9626 Added May 19, 2021 9627 +/ 9628 class Labeled(T) : Widget { 9629 /// 9630 this(string label, Widget parent) { 9631 super(parent); 9632 initialize!VerticalLayout(label, TextAlignment.Left, parent); 9633 } 9634 9635 /++ 9636 History: 9637 The alignment parameter was added May 17, 2021 9638 +/ 9639 this(string label, TextAlignment alignment, Widget parent) { 9640 super(parent); 9641 initialize!HorizontalLayout(label, alignment, parent); 9642 } 9643 9644 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 9645 tabStop = false; 9646 horizontal = is(L == HorizontalLayout); 9647 auto hl = new L(this); 9648 if(horizontal) { 9649 static class SpecialTextLabel : TextLabel { 9650 Widget outerParent; 9651 9652 this(string label, TextAlignment alignment, Widget outerParent, Widget parent) { 9653 this.outerParent = outerParent; 9654 super(label, alignment, parent); 9655 } 9656 9657 override int flexBasisWidth() { 9658 return widthThatWouldFitChildLabels(outerParent); 9659 } 9660 /+ 9661 override int widthShrinkiness() { return 0; } 9662 override int widthStretchiness() { return 1; } 9663 +/ 9664 9665 override int paddingRight() { return 6; } 9666 override int paddingLeft() { return 9; } 9667 9668 override int paddingTop() { return 3; } 9669 } 9670 this.label = new SpecialTextLabel(label, alignment, parent, hl); 9671 } else 9672 this.label = new TextLabel(label, alignment, hl); 9673 this.lineEdit = new T(hl); 9674 9675 this.label.labelFor = this.lineEdit; 9676 } 9677 9678 private bool horizontal; 9679 9680 TextLabel label; /// 9681 T lineEdit; /// 9682 9683 override int flexBasisWidth() { return 250; } 9684 override int widthShrinkiness() { return 1; } 9685 9686 override int minHeight() { 9687 return this.children[0].minHeight; 9688 } 9689 override int maxHeight() { return minHeight(); } 9690 override int marginTop() { return 4; } 9691 override int marginBottom() { return 4; } 9692 9693 // FIXME: i should prolly call it value as well as content tbh 9694 9695 /// 9696 @property string content() { 9697 return lineEdit.content; 9698 } 9699 /// 9700 @property void content(string c) { 9701 return lineEdit.content(c); 9702 } 9703 9704 /// 9705 void selectAll() { 9706 lineEdit.selectAll(); 9707 } 9708 9709 override void focus() { 9710 lineEdit.focus(); 9711 } 9712 } 9713 9714 /++ 9715 A labeled password edit. 9716 9717 History: 9718 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 9719 9720 The default parameters for the constructors were also removed on May 19, 2021 9721 +/ 9722 alias LabeledPasswordEdit = Labeled!PasswordEdit; 9723 9724 private string toMenuLabel(string s) { 9725 string n; 9726 n.reserve(s.length); 9727 foreach(c; s) 9728 if(c == '_') 9729 n ~= ' '; 9730 else 9731 n ~= c; 9732 return n; 9733 } 9734 9735 private void autoExceptionHandler(Exception e) { 9736 messageBox(e.msg); 9737 } 9738 9739 private void delegate() makeAutomaticHandler(alias fn, T)(T t) { 9740 static if(is(T : void delegate())) { 9741 return () { 9742 try 9743 t(); 9744 catch(Exception e) 9745 autoExceptionHandler(e); 9746 }; 9747 } else static if(is(typeof(fn) Params == __parameters)) { 9748 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 9749 return () { 9750 void onOK(string s) { 9751 member = s; 9752 try 9753 t(Params[0](s)); 9754 catch(Exception e) 9755 autoExceptionHandler(e); 9756 } 9757 9758 if( 9759 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 9760 || type == FileDialogType.Save) 9761 { 9762 getSaveFileName(&onOK, member, filters, null); 9763 } else 9764 getOpenFileName(&onOK, member, filters, null); 9765 }; 9766 } else { 9767 struct S { 9768 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 9769 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 9770 } else mixin(q{ 9771 static foreach(idx, ignore; Params) { 9772 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 9773 } 9774 }); 9775 } 9776 return () { 9777 dialog((S s) { 9778 try { 9779 static if(is(typeof(t) Ret == return)) { 9780 static if(is(Ret == void)) { 9781 t(s.tupleof); 9782 } else { 9783 auto ret = t(s.tupleof); 9784 import std.conv; 9785 messageBox(to!string(ret), "Returned Value"); 9786 } 9787 } 9788 } catch(Exception e) 9789 autoExceptionHandler(e); 9790 }, null, __traits(identifier, fn)); 9791 }; 9792 } 9793 } 9794 } 9795 9796 private template hasAnyRelevantAnnotations(a...) { 9797 bool helper() { 9798 bool any; 9799 foreach(attr; a) { 9800 static if(is(typeof(attr) == .menu)) 9801 any = true; 9802 else static if(is(typeof(attr) == .toolbar)) 9803 any = true; 9804 else static if(is(attr == .separator)) 9805 any = true; 9806 else static if(is(typeof(attr) == .accelerator)) 9807 any = true; 9808 else static if(is(typeof(attr) == .hotkey)) 9809 any = true; 9810 else static if(is(typeof(attr) == .icon)) 9811 any = true; 9812 else static if(is(typeof(attr) == .label)) 9813 any = true; 9814 else static if(is(typeof(attr) == .tip)) 9815 any = true; 9816 } 9817 return any; 9818 } 9819 9820 enum bool hasAnyRelevantAnnotations = helper(); 9821 } 9822 9823 /++ 9824 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. 9825 +/ 9826 class MainWindow : Window { 9827 /// 9828 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 9829 super(initialWidth, initialHeight, title); 9830 9831 _clientArea = new ClientAreaWidget(); 9832 _clientArea.x = 0; 9833 _clientArea.y = 0; 9834 _clientArea.width = this.width; 9835 _clientArea.height = this.height; 9836 _clientArea.tabStop = false; 9837 9838 super.addChild(_clientArea); 9839 9840 statusBar = new StatusBar(this); 9841 } 9842 9843 /++ 9844 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). 9845 9846 --- 9847 struct Commands { 9848 @menu("File") { 9849 @toolbar("") // adds it to a generic toolbar 9850 void New() {} 9851 void Open() {} 9852 void Save() {} 9853 @separator 9854 void Exit() @accelerator("Alt+F4") @hotkey('x') { 9855 window.close(); 9856 } 9857 } 9858 9859 @menu("Edit") { 9860 @icon(GenericIcons.Undo) 9861 void Undo() { 9862 undo(); 9863 } 9864 @separator 9865 void Cut() {} 9866 void Copy() {} 9867 void Paste() {} 9868 } 9869 9870 @menu("Help") { 9871 void About() {} 9872 } 9873 } 9874 9875 Commands commands; 9876 9877 window.setMenuAndToolbarFromAnnotatedCode(commands); 9878 --- 9879 9880 Note that you can call this function multiple times and it will add the items in order to the given items. 9881 9882 +/ 9883 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 9884 setMenuAndToolbarFromAnnotatedCode_internal(t); 9885 } 9886 /// ditto 9887 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 9888 setMenuAndToolbarFromAnnotatedCode_internal(t); 9889 } 9890 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 9891 Action[] toolbarActions; 9892 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 9893 Menu[string] mcs; 9894 9895 foreach(menu; menuBar.subMenus) { 9896 mcs[menu.label] = menu; 9897 } 9898 9899 foreach(memberName; __traits(derivedMembers, T)) { 9900 static if(memberName != "this") 9901 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 9902 .menu menu; 9903 .toolbar toolbar; 9904 bool separator; 9905 .accelerator accelerator; 9906 .hotkey hotkey; 9907 .icon icon; 9908 string label; 9909 string tip; 9910 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 9911 static if(is(typeof(attr) == .menu)) 9912 menu = attr; 9913 else static if(is(typeof(attr) == .toolbar)) 9914 toolbar = attr; 9915 else static if(is(attr == .separator)) 9916 separator = true; 9917 else static if(is(typeof(attr) == .accelerator)) 9918 accelerator = attr; 9919 else static if(is(typeof(attr) == .hotkey)) 9920 hotkey = attr; 9921 else static if(is(typeof(attr) == .icon)) 9922 icon = attr; 9923 else static if(is(typeof(attr) == .label)) 9924 label = attr.label; 9925 else static if(is(typeof(attr) == .tip)) 9926 tip = attr.tip; 9927 } 9928 9929 if(menu !is .menu.init || toolbar !is .toolbar.init) { 9930 ushort correctIcon = icon.id; // FIXME 9931 if(label.length == 0) 9932 label = memberName.toMenuLabel; 9933 9934 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName)); 9935 9936 auto action = new Action(label, correctIcon, handler); 9937 9938 if(accelerator.keyString.length) { 9939 auto ke = KeyEvent.parse(accelerator.keyString); 9940 action.accelerator = ke; 9941 accelerators[ke.toStr] = handler; 9942 } 9943 9944 if(toolbar !is .toolbar.init) 9945 toolbarActions ~= action; 9946 if(menu !is .menu.init) { 9947 Menu mc; 9948 if(menu.name in mcs) { 9949 mc = mcs[menu.name]; 9950 } else { 9951 mc = new Menu(menu.name, this); 9952 menuBar.addItem(mc); 9953 mcs[menu.name] = mc; 9954 } 9955 9956 if(separator) 9957 mc.addSeparator(); 9958 mc.addItem(new MenuItem(action)); 9959 } 9960 } 9961 } 9962 } 9963 9964 this.menuBar = menuBar; 9965 9966 if(toolbarActions.length) { 9967 auto tb = new ToolBar(toolbarActions, this); 9968 } 9969 } 9970 9971 void delegate()[string] accelerators; 9972 9973 override void defaultEventHandler_keydown(KeyDownEvent event) { 9974 auto str = event.originalKeyEvent.toStr; 9975 if(auto acl = str in accelerators) 9976 (*acl)(); 9977 super.defaultEventHandler_keydown(event); 9978 } 9979 9980 override void defaultEventHandler_mouseover(MouseOverEvent event) { 9981 super.defaultEventHandler_mouseover(event); 9982 if(this.statusBar !is null && event.target.statusTip.length) 9983 this.statusBar.parts[0].content = event.target.statusTip; 9984 else if(this.statusBar !is null && this.statusTip.length) 9985 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 9986 } 9987 9988 override void addChild(Widget c, int position = int.max) { 9989 if(auto tb = cast(ToolBar) c) 9990 version(win32_widgets) 9991 super.addChild(c, 0); 9992 else version(custom_widgets) 9993 super.addChild(c, menuBar ? 1 : 0); 9994 else static assert(0); 9995 else 9996 clientArea.addChild(c, position); 9997 } 9998 9999 ToolBar _toolBar; 10000 /// 10001 ToolBar toolBar() { return _toolBar; } 10002 /// 10003 ToolBar toolBar(ToolBar t) { 10004 _toolBar = t; 10005 foreach(child; this.children) 10006 if(child is t) 10007 return t; 10008 version(win32_widgets) 10009 super.addChild(t, 0); 10010 else version(custom_widgets) 10011 super.addChild(t, menuBar ? 1 : 0); 10012 else static assert(0); 10013 return t; 10014 } 10015 10016 MenuBar _menu; 10017 /// 10018 MenuBar menuBar() { return _menu; } 10019 /// 10020 MenuBar menuBar(MenuBar m) { 10021 if(m is _menu) { 10022 version(custom_widgets) 10023 queueRecomputeChildLayout(); 10024 return m; 10025 } 10026 10027 if(_menu !is null) { 10028 // make sure it is sanely removed 10029 // FIXME 10030 } 10031 10032 _menu = m; 10033 10034 version(win32_widgets) { 10035 SetMenu(parentWindow.win.impl.hwnd, m.handle); 10036 } else version(custom_widgets) { 10037 super.addChild(m, 0); 10038 10039 // clientArea.y = menu.height; 10040 // clientArea.height = this.height - menu.height; 10041 10042 queueRecomputeChildLayout(); 10043 } else static assert(false); 10044 10045 return _menu; 10046 } 10047 private Widget _clientArea; 10048 /// 10049 @property Widget clientArea() { return _clientArea; } 10050 protected @property void clientArea(Widget wid) { 10051 _clientArea = wid; 10052 } 10053 10054 private StatusBar _statusBar; 10055 /++ 10056 Returns the window's [StatusBar]. Be warned it may be `null`. 10057 +/ 10058 @property StatusBar statusBar() { return _statusBar; } 10059 /// ditto 10060 @property void statusBar(StatusBar bar) { 10061 if(_statusBar !is null) 10062 _statusBar.removeWidget(); 10063 _statusBar = bar; 10064 if(bar !is null) 10065 super.addChild(_statusBar); 10066 } 10067 } 10068 10069 /+ 10070 This is really an implementation detail of [MainWindow] 10071 +/ 10072 private class ClientAreaWidget : Widget { 10073 this() { 10074 this.tabStop = false; 10075 super(null); 10076 //sa = new ScrollableWidget(this); 10077 } 10078 /* 10079 ScrollableWidget sa; 10080 override void addChild(Widget w, int position) { 10081 if(sa is null) 10082 super.addChild(w, position); 10083 else { 10084 sa.addChild(w, position); 10085 sa.setContentSize(this.minWidth + 1, this.minHeight); 10086 writeln(sa.contentWidth, "x", sa.contentHeight); 10087 } 10088 } 10089 */ 10090 } 10091 10092 /** 10093 Toolbars are lists of buttons (typically icons) that appear under the menu. 10094 Each button ought to correspond to a menu item, represented by [Action] objects. 10095 */ 10096 class ToolBar : Widget { 10097 version(win32_widgets) { 10098 private int idealHeight; 10099 override int minHeight() { return idealHeight; } 10100 override int maxHeight() { return idealHeight; } 10101 } else version(custom_widgets) { 10102 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 10103 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 10104 } else static assert(false); 10105 override int heightStretchiness() { return 0; } 10106 10107 version(win32_widgets) { 10108 HIMAGELIST imageListSmall; 10109 HIMAGELIST imageListLarge; 10110 } 10111 10112 this(Widget parent) { 10113 this(null, parent); 10114 } 10115 10116 version(win32_widgets) 10117 void changeIconSize(bool useLarge) { 10118 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall)); 10119 10120 /+ 10121 SIZE size; 10122 import core.sys.windows.commctrl; 10123 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 10124 idealHeight = size.cy + 4; // the plus 4 is a hack 10125 +/ 10126 10127 idealHeight = useLarge ? 34 : 26; 10128 10129 if(parent) { 10130 parent.queueRecomputeChildLayout(); 10131 parent.redraw(); 10132 } 10133 10134 SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4)); 10135 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 10136 } 10137 10138 /// 10139 this(Action[] actions, Widget parent) { 10140 super(parent); 10141 10142 tabStop = false; 10143 10144 version(win32_widgets) { 10145 // so i like how the flat thing looks on windows, but not on wine 10146 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 10147 // leave it commented 10148 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 10149 10150 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 10151 10152 imageListSmall = ImageList_Create( 10153 // width, height 10154 16, 16, 10155 ILC_COLOR16 | ILC_MASK, 10156 16 /*numberOfButtons*/, 0); 10157 10158 imageListLarge = ImageList_Create( 10159 // width, height 10160 24, 24, 10161 ILC_COLOR16 | ILC_MASK, 10162 16 /*numberOfButtons*/, 0); 10163 10164 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall); 10165 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 10166 10167 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge); 10168 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL); 10169 10170 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 10171 10172 TBBUTTON[] buttons; 10173 10174 // FIXME: I_IMAGENONE is if here is no icon 10175 foreach(action; actions) 10176 buttons ~= TBBUTTON( 10177 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 10178 action.id, 10179 TBSTATE_ENABLED, // state 10180 0, // style 10181 0, // reserved array, just zero it out 10182 0, // dwData 10183 cast(size_t) toWstringzInternal(action.label) // INT_PTR 10184 ); 10185 10186 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 10187 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 10188 10189 /* 10190 RECT rect; 10191 GetWindowRect(hwnd, &rect); 10192 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 10193 */ 10194 10195 dpiChanged(); // to load the things calling changeIconSize the first time 10196 10197 assert(idealHeight); 10198 } else version(custom_widgets) { 10199 foreach(action; actions) 10200 new ToolButton(action, this); 10201 } else static assert(false); 10202 } 10203 10204 override void recomputeChildLayout() { 10205 .recomputeChildLayout!"width"(this); 10206 } 10207 10208 10209 version(win32_widgets) 10210 override protected void dpiChanged() { 10211 auto sz = scaleWithDpi(16); 10212 if(sz >= 20) 10213 changeIconSize(true); 10214 else 10215 changeIconSize(false); 10216 } 10217 } 10218 10219 enum toolbarIconSize = 24; 10220 10221 /// 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. 10222 class ToolButton : Button { 10223 /// 10224 this(string label, Widget parent) { 10225 super(label, parent); 10226 tabStop = false; 10227 } 10228 /// 10229 this(Action action, Widget parent) { 10230 super(action.label, parent); 10231 tabStop = false; 10232 this.action = action; 10233 } 10234 10235 version(custom_widgets) 10236 override void defaultEventHandler_click(ClickEvent event) { 10237 foreach(handler; action.triggered) 10238 handler(); 10239 } 10240 10241 Action action; 10242 10243 override int maxWidth() { return toolbarIconSize; } 10244 override int minWidth() { return toolbarIconSize; } 10245 override int maxHeight() { return toolbarIconSize; } 10246 override int minHeight() { return toolbarIconSize; } 10247 10248 version(custom_widgets) 10249 override void paint(WidgetPainter painter) { 10250 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 10251 painter.outlineColor = Color.black; 10252 10253 // I want to get from 16 to 24. that's * 3 / 2 10254 static assert(toolbarIconSize >= 16); 10255 enum multiplier = toolbarIconSize / 8; 10256 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 10257 switch(action.iconId) { 10258 case GenericIcons.New: 10259 painter.fillColor = Color.white; 10260 painter.drawPolygon( 10261 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 10262 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 10263 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 10264 ); 10265 break; 10266 case GenericIcons.Save: 10267 painter.fillColor = Color.white; 10268 painter.outlineColor = Color.black; 10269 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10270 10271 // the label 10272 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 10273 10274 // the slider 10275 painter.fillColor = Color.black; 10276 painter.outlineColor = Color.black; 10277 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 10278 10279 painter.fillColor = Color.white; 10280 painter.outlineColor = Color.white; 10281 // the disc window 10282 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 10283 break; 10284 case GenericIcons.Open: 10285 painter.fillColor = Color.white; 10286 painter.drawPolygon( 10287 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 10288 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 10289 painter.drawPolygon( 10290 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 10291 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 10292 Point(2, 6) * multiplier / divisor); 10293 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 10294 break; 10295 case GenericIcons.Copy: 10296 painter.fillColor = Color.white; 10297 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 10298 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 10299 break; 10300 case GenericIcons.Cut: 10301 painter.fillColor = Color.transparent; 10302 painter.outlineColor = getComputedStyle.foregroundColor(); 10303 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 10304 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 10305 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 10306 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 10307 break; 10308 case GenericIcons.Paste: 10309 painter.fillColor = Color.white; 10310 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 10311 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10312 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 10313 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 10314 painter.fillColor = Color.black; 10315 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 10316 break; 10317 case GenericIcons.Help: 10318 painter.outlineColor = getComputedStyle.foregroundColor(); 10319 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10320 break; 10321 case GenericIcons.Undo: 10322 painter.fillColor = Color.transparent; 10323 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10324 painter.outlineColor = Color.black; 10325 painter.fillColor = Color.black; 10326 painter.drawPolygon( 10327 Point(4, 4) * multiplier / divisor, 10328 Point(8, 2) * multiplier / divisor, 10329 Point(8, 6) * multiplier / divisor, 10330 Point(4, 4) * multiplier / divisor, 10331 ); 10332 break; 10333 case GenericIcons.Redo: 10334 painter.fillColor = Color.transparent; 10335 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10336 painter.outlineColor = Color.black; 10337 painter.fillColor = Color.black; 10338 painter.drawPolygon( 10339 Point(10, 4) * multiplier / divisor, 10340 Point(6, 2) * multiplier / divisor, 10341 Point(6, 6) * multiplier / divisor, 10342 Point(10, 4) * multiplier / divisor, 10343 ); 10344 break; 10345 default: 10346 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10347 } 10348 return bounds; 10349 }); 10350 } 10351 10352 } 10353 10354 10355 /++ 10356 You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode]. 10357 +/ 10358 class MenuBar : Widget { 10359 MenuItem[] items; 10360 Menu[] subMenus; 10361 10362 version(win32_widgets) { 10363 HMENU handle; 10364 /// 10365 this(Widget parent = null) { 10366 super(parent); 10367 10368 handle = CreateMenu(); 10369 tabStop = false; 10370 } 10371 } else version(custom_widgets) { 10372 /// 10373 this(Widget parent = null) { 10374 tabStop = false; // these are selected some other way 10375 super(parent); 10376 } 10377 10378 mixin Padding!q{2}; 10379 } else static assert(false); 10380 10381 version(custom_widgets) 10382 override void paint(WidgetPainter painter) { 10383 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 10384 } 10385 10386 /// 10387 MenuItem addItem(MenuItem item) { 10388 this.addChild(item); 10389 items ~= item; 10390 version(win32_widgets) { 10391 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10392 } 10393 return item; 10394 } 10395 10396 10397 /// 10398 Menu addItem(Menu item) { 10399 10400 subMenus ~= item; 10401 10402 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 10403 10404 addChild(mbItem); 10405 items ~= mbItem; 10406 10407 version(win32_widgets) { 10408 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 10409 } else version(custom_widgets) { 10410 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 10411 item.popup(mbItem); 10412 }; 10413 } else static assert(false); 10414 10415 return item; 10416 } 10417 10418 override void recomputeChildLayout() { 10419 .recomputeChildLayout!"width"(this); 10420 } 10421 10422 override int maxHeight() { return defaultLineHeight + 4; } 10423 override int minHeight() { return defaultLineHeight + 4; } 10424 } 10425 10426 10427 /** 10428 Status bars appear at the bottom of a MainWindow. 10429 They are made out of Parts, with a width and content. 10430 10431 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 10432 10433 10434 sb.parts[0].content = "Status bar text!"; 10435 */ 10436 class StatusBar : Widget { 10437 private Part[] partsArray; 10438 /// 10439 struct Parts { 10440 @disable this(); 10441 this(StatusBar owner) { this.owner = owner; } 10442 //@disable this(this); 10443 /// 10444 @property int length() { return cast(int) owner.partsArray.length; } 10445 private StatusBar owner; 10446 private this(StatusBar owner, Part[] parts) { 10447 this.owner.partsArray = parts; 10448 this.owner = owner; 10449 } 10450 /// 10451 Part opIndex(int p) { 10452 if(owner.partsArray.length == 0) 10453 this ~= new StatusBar.Part(0); 10454 return owner.partsArray[p]; 10455 } 10456 10457 /// 10458 Part opOpAssign(string op : "~" )(Part p) { 10459 assert(owner.partsArray.length < 255); 10460 p.owner = this.owner; 10461 p.idx = cast(int) owner.partsArray.length; 10462 owner.partsArray ~= p; 10463 10464 owner.queueRecomputeChildLayout(); 10465 10466 version(win32_widgets) { 10467 int[256] pos; 10468 int cpos; 10469 foreach(idx, part; owner.partsArray) { 10470 if(idx + 1 == owner.partsArray.length) 10471 pos[idx] = -1; 10472 else { 10473 cpos += part.currentlyAssignedWidth; 10474 pos[idx] = cpos; 10475 } 10476 } 10477 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 10478 } else version(custom_widgets) { 10479 owner.redraw(); 10480 } else static assert(false); 10481 10482 return p; 10483 } 10484 } 10485 10486 private Parts _parts; 10487 /// 10488 final @property Parts parts() { 10489 return _parts; 10490 } 10491 10492 /++ 10493 10494 +/ 10495 static class Part { 10496 /++ 10497 History: 10498 Added September 1, 2023 (dub v11.1) 10499 +/ 10500 enum WidthUnits { 10501 /++ 10502 Unscaled pixels as they appear on screen. 10503 10504 If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui. 10505 +/ 10506 DeviceDependentPixels, 10507 /++ 10508 Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui. 10509 +/ 10510 DeviceIndependentPixels, 10511 /++ 10512 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`). 10513 +/ 10514 ApproximateCharacters, 10515 /++ 10516 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. 10517 10518 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. 10519 +/ 10520 Proportional 10521 } 10522 private WidthUnits units; 10523 private int width; 10524 private StatusBar owner; 10525 10526 private int currentlyAssignedWidth; 10527 10528 /++ 10529 History: 10530 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. 10531 10532 It now allows you to provide your own value for [WidthUnits]. 10533 10534 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`. 10535 +/ 10536 this(int w, WidthUnits units = WidthUnits.Proportional) { 10537 this.units = units; 10538 this.width = w; 10539 } 10540 10541 /// ditto 10542 this(int w = 0) { 10543 if(w == 0) 10544 this(w, WidthUnits.Proportional); 10545 else 10546 this(w, WidthUnits.DeviceDependentPixels); 10547 } 10548 10549 private int idx; 10550 private string _content; 10551 /// 10552 @property string content() { return _content; } 10553 /// 10554 @property void content(string s) { 10555 version(win32_widgets) { 10556 _content = s; 10557 WCharzBuffer bfr = WCharzBuffer(s); 10558 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 10559 } else version(custom_widgets) { 10560 if(_content != s) { 10561 _content = s; 10562 owner.redraw(); 10563 } 10564 } else static assert(false); 10565 } 10566 } 10567 string simpleModeContent; 10568 bool inSimpleMode; 10569 10570 10571 /// 10572 this(Widget parent) { 10573 super(null); // FIXME 10574 _parts = Parts(this); 10575 tabStop = false; 10576 version(win32_widgets) { 10577 parentWindow = parent.parentWindow; 10578 createWin32Window(this, "msctls_statusbar32"w, "", 0); 10579 10580 RECT rect; 10581 GetWindowRect(hwnd, &rect); 10582 idealHeight = rect.bottom - rect.top; 10583 assert(idealHeight); 10584 } else version(custom_widgets) { 10585 } else static assert(false); 10586 } 10587 10588 override void recomputeChildLayout() { 10589 int remainingLength = this.width; 10590 10591 int proportionalSum; 10592 int proportionalCount; 10593 foreach(idx, part; this.partsArray) { 10594 with(Part.WidthUnits) 10595 final switch(part.units) { 10596 case DeviceDependentPixels: 10597 part.currentlyAssignedWidth = part.width; 10598 remainingLength -= part.currentlyAssignedWidth; 10599 break; 10600 case DeviceIndependentPixels: 10601 part.currentlyAssignedWidth = scaleWithDpi(part.width); 10602 remainingLength -= part.currentlyAssignedWidth; 10603 break; 10604 case ApproximateCharacters: 10605 auto cs = getComputedStyle(); 10606 auto font = cs.font; 10607 10608 part.currentlyAssignedWidth = font.averageWidth * this.width; 10609 remainingLength -= part.currentlyAssignedWidth; 10610 break; 10611 case Proportional: 10612 proportionalSum += part.width; 10613 proportionalCount ++; 10614 break; 10615 } 10616 } 10617 10618 foreach(part; this.partsArray) { 10619 if(part.units == Part.WidthUnits.Proportional) { 10620 auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width; 10621 if(proportion == 0) 10622 proportion = 1; 10623 10624 if(proportionalSum == 0) 10625 proportionalSum = proportionalCount; 10626 10627 part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum; 10628 } 10629 } 10630 10631 super.recomputeChildLayout(); 10632 } 10633 10634 version(win32_widgets) 10635 override protected void dpiChanged() { 10636 RECT rect; 10637 GetWindowRect(hwnd, &rect); 10638 idealHeight = rect.bottom - rect.top; 10639 assert(idealHeight); 10640 } 10641 10642 version(custom_widgets) 10643 override void paint(WidgetPainter painter) { 10644 auto cs = getComputedStyle(); 10645 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10646 int cpos = 0; 10647 foreach(idx, part; this.partsArray) { 10648 auto partWidth = part.currentlyAssignedWidth; 10649 // part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 10650 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 10651 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 10652 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 10653 10654 painter.outlineColor = cs.foregroundColor(); 10655 painter.fillColor = cs.foregroundColor(); 10656 10657 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 10658 cpos += partWidth; 10659 } 10660 } 10661 10662 10663 version(win32_widgets) { 10664 private int idealHeight; 10665 override int maxHeight() { return idealHeight; } 10666 override int minHeight() { return idealHeight; } 10667 } else version(custom_widgets) { 10668 override int maxHeight() { return defaultLineHeight + 4; } 10669 override int minHeight() { return defaultLineHeight + 4; } 10670 } else static assert(false); 10671 } 10672 10673 /// Displays an in-progress indicator without known values 10674 version(none) 10675 class IndefiniteProgressBar : Widget { 10676 version(win32_widgets) 10677 this(Widget parent) { 10678 super(parent); 10679 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 10680 tabStop = false; 10681 } 10682 override int minHeight() { return 10; } 10683 } 10684 10685 /// A progress bar with a known endpoint and completion amount 10686 class ProgressBar : Widget { 10687 /++ 10688 History: 10689 Added March 16, 2022 (dub v10.7) 10690 +/ 10691 this(int min, int max, Widget parent) { 10692 this(parent); 10693 setRange(cast(ushort) min, cast(ushort) max); // FIXME 10694 } 10695 this(Widget parent) { 10696 version(win32_widgets) { 10697 super(parent); 10698 createWin32Window(this, "msctls_progress32"w, "", 0); 10699 tabStop = false; 10700 } else version(custom_widgets) { 10701 super(parent); 10702 max = 100; 10703 step = 10; 10704 tabStop = false; 10705 } else static assert(0); 10706 } 10707 10708 version(custom_widgets) 10709 override void paint(WidgetPainter painter) { 10710 auto cs = getComputedStyle(); 10711 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10712 painter.fillColor = cs.progressBarColor; 10713 painter.drawRectangle(Point(0, 0), width * current / max, height); 10714 } 10715 10716 10717 version(custom_widgets) { 10718 int current; 10719 int max; 10720 int step; 10721 } 10722 10723 /// 10724 void advanceOneStep() { 10725 version(win32_widgets) 10726 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 10727 else version(custom_widgets) 10728 addToPosition(step); 10729 else static assert(false); 10730 } 10731 10732 /// 10733 void setStepIncrement(int increment) { 10734 version(win32_widgets) 10735 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 10736 else version(custom_widgets) 10737 step = increment; 10738 else static assert(false); 10739 } 10740 10741 /// 10742 void addToPosition(int amount) { 10743 version(win32_widgets) 10744 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 10745 else version(custom_widgets) 10746 setPosition(current + amount); 10747 else static assert(false); 10748 } 10749 10750 /// 10751 void setPosition(int pos) { 10752 version(win32_widgets) 10753 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 10754 else version(custom_widgets) { 10755 current = pos; 10756 if(current > max) 10757 current = max; 10758 redraw(); 10759 } 10760 else static assert(false); 10761 } 10762 10763 /// 10764 void setRange(ushort min, ushort max) { 10765 version(win32_widgets) 10766 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 10767 else version(custom_widgets) { 10768 this.max = max; 10769 } 10770 else static assert(false); 10771 } 10772 10773 override int minHeight() { return 10; } 10774 } 10775 10776 version(custom_widgets) 10777 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 10778 thisLabel.reserve(label.length); 10779 bool justSawAmpersand; 10780 foreach(ch; label) { 10781 if(justSawAmpersand) { 10782 justSawAmpersand = false; 10783 if(ch == '&') { 10784 goto plain; 10785 } 10786 thisAccelerator = ch; 10787 } else { 10788 if(ch == '&') { 10789 justSawAmpersand = true; 10790 continue; 10791 } 10792 plain: 10793 thisLabel ~= ch; 10794 } 10795 } 10796 } 10797 10798 /++ 10799 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. 10800 10801 10802 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 10803 10804 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10805 10806 History: 10807 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. 10808 +/ 10809 class Fieldset : Widget { 10810 // FIXME: on Windows,it doesn't draw the background on the label 10811 // on X, it doesn't fix the clipping rectangle for it 10812 version(win32_widgets) 10813 override int paddingTop() { return defaultLineHeight; } 10814 else version(custom_widgets) 10815 override int paddingTop() { return defaultLineHeight + 2; } 10816 else static assert(false); 10817 override int paddingBottom() { return 6; } 10818 override int paddingLeft() { return 6; } 10819 override int paddingRight() { return 6; } 10820 10821 override int marginLeft() { return 6; } 10822 override int marginRight() { return 6; } 10823 override int marginTop() { return 2; } 10824 override int marginBottom() { return 2; } 10825 10826 string legend; 10827 10828 version(custom_widgets) private dchar accelerator; 10829 10830 this(string legend, Widget parent) { 10831 version(win32_widgets) { 10832 super(parent); 10833 this.legend = legend; 10834 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 10835 tabStop = false; 10836 } else version(custom_widgets) { 10837 super(parent); 10838 tabStop = false; 10839 10840 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 10841 } else static assert(0); 10842 } 10843 10844 version(custom_widgets) 10845 override void paint(WidgetPainter painter) { 10846 auto dlh = defaultLineHeight; 10847 10848 painter.fillColor = Color.transparent; 10849 auto cs = getComputedStyle(); 10850 painter.pen = Pen(cs.foregroundColor, 1); 10851 painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2); 10852 10853 auto tx = painter.textSize(legend); 10854 painter.outlineColor = Color.transparent; 10855 10856 version(Windows) { 10857 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 10858 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 10859 SelectObject(painter.impl.hdc, b); 10860 } else static if(UsingSimpledisplayX11) { 10861 painter.fillColor = getComputedStyle().windowBackgroundColor; 10862 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 10863 } 10864 painter.outlineColor = cs.foregroundColor; 10865 painter.drawText(Point(8, 0), legend); 10866 } 10867 10868 override int maxHeight() { 10869 auto m = paddingTop() + paddingBottom(); 10870 foreach(child; children) { 10871 auto mh = child.maxHeight(); 10872 if(mh == int.max) 10873 return int.max; 10874 m += mh; 10875 m += child.marginBottom(); 10876 m += child.marginTop(); 10877 } 10878 m += 6; 10879 if(m < minHeight) 10880 return minHeight; 10881 return m; 10882 } 10883 10884 override int minHeight() { 10885 auto m = paddingTop() + paddingBottom(); 10886 foreach(child; children) { 10887 m += child.minHeight(); 10888 m += child.marginBottom(); 10889 m += child.marginTop(); 10890 } 10891 return m + 6; 10892 } 10893 10894 override int minWidth() { 10895 return 6 + cast(int) this.legend.length * 7; 10896 } 10897 } 10898 10899 /++ 10900 $(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") 10901 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 10902 +/ 10903 version(minigui_screenshots) 10904 @Screenshot("Fieldset") 10905 unittest { 10906 auto window = new Window(200, 100); 10907 auto set = new Fieldset("Baby will", window); 10908 auto option1 = new Radiobox("Eat", set); 10909 auto option2 = new Radiobox("Cry", set); 10910 auto option3 = new Radiobox("Sleep", set); 10911 window.loop(); 10912 } 10913 10914 /// Draws a line 10915 class HorizontalRule : Widget { 10916 mixin Margin!q{ 2 }; 10917 override int minHeight() { return 2; } 10918 override int maxHeight() { return 2; } 10919 10920 /// 10921 this(Widget parent) { 10922 super(parent); 10923 } 10924 10925 override void paint(WidgetPainter painter) { 10926 auto cs = getComputedStyle(); 10927 painter.outlineColor = cs.darkAccentColor; 10928 painter.drawLine(Point(0, 0), Point(width, 0)); 10929 painter.outlineColor = cs.lightAccentColor; 10930 painter.drawLine(Point(0, 1), Point(width, 1)); 10931 } 10932 } 10933 10934 version(minigui_screenshots) 10935 @Screenshot("HorizontalRule") 10936 /++ 10937 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 10938 10939 +/ 10940 unittest { 10941 auto window = new Window(200, 100); 10942 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 10943 new HorizontalRule(window); 10944 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 10945 window.loop(); 10946 } 10947 10948 /// ditto 10949 class VerticalRule : Widget { 10950 mixin Margin!q{ 2 }; 10951 override int minWidth() { return 2; } 10952 override int maxWidth() { return 2; } 10953 10954 /// 10955 this(Widget parent) { 10956 super(parent); 10957 } 10958 10959 override void paint(WidgetPainter painter) { 10960 auto cs = getComputedStyle(); 10961 painter.outlineColor = cs.darkAccentColor; 10962 painter.drawLine(Point(0, 0), Point(0, height)); 10963 painter.outlineColor = cs.lightAccentColor; 10964 painter.drawLine(Point(1, 0), Point(1, height)); 10965 } 10966 } 10967 10968 10969 /// 10970 class Menu : Window { 10971 void remove() { 10972 foreach(i, child; parentWindow.children) 10973 if(child is this) { 10974 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 10975 break; 10976 } 10977 parentWindow.redraw(); 10978 10979 parentWindow.releaseMouseCapture(); 10980 } 10981 10982 /// 10983 void addSeparator() { 10984 version(win32_widgets) 10985 AppendMenu(handle, MF_SEPARATOR, 0, null); 10986 else version(custom_widgets) 10987 auto hr = new HorizontalRule(this); 10988 else static assert(0); 10989 } 10990 10991 override int paddingTop() { return 4; } 10992 override int paddingBottom() { return 4; } 10993 override int paddingLeft() { return 2; } 10994 override int paddingRight() { return 2; } 10995 10996 version(win32_widgets) {} 10997 else version(custom_widgets) { 10998 SimpleWindow dropDown; 10999 Widget menuParent; 11000 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 11001 this.menuParent = parent; 11002 11003 int w = 150; 11004 int h = paddingTop + paddingBottom; 11005 if(this.children.length) { 11006 // hacking it to get the ideal height out of recomputeChildLayout 11007 this.width = w; 11008 this.height = h; 11009 this.recomputeChildLayoutEntry(); 11010 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 11011 h += paddingBottom; 11012 11013 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 11014 } 11015 11016 if(offsetY == int.min) 11017 offsetY = parent.defaultLineHeight; 11018 11019 auto coord = parent.globalCoordinates(); 11020 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 11021 this.x = 0; 11022 this.y = 0; 11023 this.width = dropDown.width; 11024 this.height = dropDown.height; 11025 this.drawableWindow = dropDown; 11026 this.recomputeChildLayoutEntry(); 11027 11028 static if(UsingSimpledisplayX11) 11029 XSync(XDisplayConnection.get, 0); 11030 11031 dropDown.visibilityChanged = (bool visible) { 11032 if(visible) { 11033 this.redraw(); 11034 dropDown.grabInput(); 11035 } else { 11036 dropDown.releaseInputGrab(); 11037 } 11038 }; 11039 11040 dropDown.show(); 11041 11042 clickListener = this.addEventListener((scope ClickEvent ev) { 11043 unpopup(); 11044 // need to unlock asap just in case other user handlers block... 11045 static if(UsingSimpledisplayX11) 11046 flushGui(); 11047 }, true /* again for asap action */); 11048 } 11049 11050 EventListener clickListener; 11051 } 11052 else static assert(false); 11053 11054 version(custom_widgets) 11055 void unpopup() { 11056 mouseLastOver = mouseLastDownOn = null; 11057 dropDown.hide(); 11058 if(!menuParent.parentWindow.win.closed) { 11059 if(auto maw = cast(MouseActivatedWidget) menuParent) { 11060 maw.setDynamicState(DynamicState.depressed, false); 11061 maw.setDynamicState(DynamicState.hover, false); 11062 maw.redraw(); 11063 } 11064 // menuParent.parentWindow.win.focus(); 11065 } 11066 clickListener.disconnect(); 11067 } 11068 11069 MenuItem[] items; 11070 11071 /// 11072 MenuItem addItem(MenuItem item) { 11073 addChild(item); 11074 items ~= item; 11075 version(win32_widgets) { 11076 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 11077 } 11078 return item; 11079 } 11080 11081 string label; 11082 11083 version(win32_widgets) { 11084 HMENU handle; 11085 /// 11086 this(string label, Widget parent) { 11087 // not actually passing the parent since it effs up the drawing 11088 super(cast(Widget) null);// parent); 11089 this.label = label; 11090 handle = CreatePopupMenu(); 11091 } 11092 } else version(custom_widgets) { 11093 /// 11094 this(string label, Widget parent) { 11095 11096 if(dropDown) { 11097 dropDown.close(); 11098 } 11099 dropDown = new SimpleWindow( 11100 150, 4, 11101 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 11102 11103 this.label = label; 11104 11105 super(dropDown); 11106 } 11107 } else static assert(false); 11108 11109 override int maxHeight() { return defaultLineHeight; } 11110 override int minHeight() { return defaultLineHeight; } 11111 11112 version(custom_widgets) 11113 override void paint(WidgetPainter painter) { 11114 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 11115 } 11116 } 11117 11118 /++ 11119 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 11120 +/ 11121 class MenuItem : MouseActivatedWidget { 11122 Menu submenu; 11123 11124 Action action; 11125 string label; 11126 11127 override int paddingLeft() { return 4; } 11128 11129 override int maxHeight() { return defaultLineHeight + 4; } 11130 override int minHeight() { return defaultLineHeight + 4; } 11131 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 11132 override int maxWidth() { 11133 if(cast(MenuBar) parent) { 11134 return minWidth(); 11135 } 11136 return int.max; 11137 } 11138 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 11139 this(string lbl, Widget parent = null) { 11140 super(parent); 11141 //label = lbl; // FIXME 11142 foreach(char ch; lbl) // FIXME 11143 if(ch != '&') // FIXME 11144 label ~= ch; // FIXME 11145 tabStop = false; // these are selected some other way 11146 } 11147 11148 /// 11149 this(Action action, Widget parent = null) { 11150 assert(action !is null); 11151 this(action.label, parent); 11152 this.action = action; 11153 tabStop = false; // these are selected some other way 11154 } 11155 11156 version(custom_widgets) 11157 override void paint(WidgetPainter painter) { 11158 auto cs = getComputedStyle(); 11159 if(dynamicState & DynamicState.depressed) 11160 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 11161 if(dynamicState & DynamicState.hover) 11162 painter.outlineColor = cs.activeMenuItemColor; 11163 else 11164 painter.outlineColor = cs.foregroundColor; 11165 painter.fillColor = Color.transparent; 11166 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11167 if(action && action.accelerator !is KeyEvent.init) { 11168 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 11169 11170 } 11171 } 11172 11173 static class Style : Widget.Style { 11174 override bool variesWithState(ulong dynamicStateFlags) { 11175 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11176 } 11177 } 11178 mixin OverrideStyle!Style; 11179 11180 override void defaultEventHandler_triggered(Event event) { 11181 if(action) 11182 foreach(handler; action.triggered) 11183 handler(); 11184 11185 if(auto pmenu = cast(Menu) this.parent) 11186 pmenu.remove(); 11187 11188 super.defaultEventHandler_triggered(event); 11189 } 11190 } 11191 11192 version(win32_widgets) 11193 /// A "mouse activiated widget" is really just an abstract variant of button. 11194 class MouseActivatedWidget : Widget { 11195 @property bool isChecked() { 11196 assert(hwnd); 11197 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 11198 11199 } 11200 @property void isChecked(bool state) { 11201 assert(hwnd); 11202 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 11203 11204 } 11205 11206 override void handleWmCommand(ushort cmd, ushort id) { 11207 if(cmd == 0) { 11208 auto event = new Event(EventType.triggered, this); 11209 event.dispatch(); 11210 } 11211 } 11212 11213 this(Widget parent) { 11214 super(parent); 11215 } 11216 } 11217 else version(custom_widgets) 11218 /// ditto 11219 class MouseActivatedWidget : Widget { 11220 @property bool isChecked() { return isChecked_; } 11221 @property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;} 11222 11223 private bool isChecked_; 11224 11225 this(Widget parent) { 11226 super(parent); 11227 11228 addEventListener((MouseDownEvent ev) { 11229 if(ev.button == MouseButton.left) { 11230 setDynamicState(DynamicState.depressed, true); 11231 setDynamicState(DynamicState.hover, true); 11232 redraw(); 11233 } 11234 }); 11235 11236 addEventListener((MouseUpEvent ev) { 11237 if(ev.button == MouseButton.left) { 11238 setDynamicState(DynamicState.depressed, false); 11239 setDynamicState(DynamicState.hover, false); 11240 redraw(); 11241 } 11242 }); 11243 11244 addEventListener((MouseMoveEvent mme) { 11245 if(!(mme.state & ModifierState.leftButtonDown)) { 11246 if(dynamicState_ & DynamicState.depressed) { 11247 setDynamicState(DynamicState.depressed, false); 11248 redraw(); 11249 } 11250 } 11251 }); 11252 } 11253 11254 override void defaultEventHandler_focus(Event ev) { 11255 super.defaultEventHandler_focus(ev); 11256 this.redraw(); 11257 } 11258 override void defaultEventHandler_blur(Event ev) { 11259 super.defaultEventHandler_blur(ev); 11260 setDynamicState(DynamicState.depressed, false); 11261 this.redraw(); 11262 } 11263 override void defaultEventHandler_keydown(KeyDownEvent ev) { 11264 super.defaultEventHandler_keydown(ev); 11265 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 11266 setDynamicState(DynamicState.depressed, true); 11267 setDynamicState(DynamicState.hover, true); 11268 this.redraw(); 11269 } 11270 } 11271 override void defaultEventHandler_keyup(KeyUpEvent ev) { 11272 super.defaultEventHandler_keyup(ev); 11273 if(!(dynamicState & DynamicState.depressed)) 11274 return; 11275 setDynamicState(DynamicState.depressed, false); 11276 setDynamicState(DynamicState.hover, false); 11277 this.redraw(); 11278 11279 auto event = new Event(EventType.triggered, this); 11280 event.sendDirectly(); 11281 } 11282 override void defaultEventHandler_click(ClickEvent ev) { 11283 super.defaultEventHandler_click(ev); 11284 if(ev.button == MouseButton.left) { 11285 auto event = new Event(EventType.triggered, this); 11286 event.sendDirectly(); 11287 } 11288 } 11289 11290 } 11291 else static assert(false); 11292 11293 /* 11294 /++ 11295 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 11296 11297 Basically the same as a checkbox. 11298 +/ 11299 class OnOffSwitch : MouseActivatedWidget { 11300 11301 } 11302 */ 11303 11304 /++ 11305 History: 11306 Added June 15, 2021 (dub v10.1) 11307 +/ 11308 struct ImageLabel { 11309 /++ 11310 Defines a label+image combo used by some widgets. 11311 11312 If you provide just a text label, that is all the widget will try to 11313 display. Or just an image will display just that. If you provide both, 11314 it may display both text and image side by side or display the image 11315 and offer text on an input event depending on the widget. 11316 11317 History: 11318 The `alignment` parameter was added on September 27, 2021 11319 +/ 11320 this(string label, TextAlignment alignment = TextAlignment.Center) { 11321 this.label = label; 11322 this.displayFlags = DisplayFlags.displayText; 11323 this.alignment = alignment; 11324 } 11325 11326 /// ditto 11327 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11328 this.label = label; 11329 this.image = image; 11330 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11331 this.alignment = alignment; 11332 } 11333 11334 /// ditto 11335 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11336 this.image = image; 11337 this.displayFlags = DisplayFlags.displayImage; 11338 this.alignment = alignment; 11339 } 11340 11341 /// ditto 11342 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 11343 this.label = label; 11344 this.image = image; 11345 this.alignment = alignment; 11346 this.displayFlags = displayFlags; 11347 } 11348 11349 string label; 11350 MemoryImage image; 11351 11352 enum DisplayFlags { 11353 displayText = 1 << 0, 11354 displayImage = 1 << 1, 11355 } 11356 11357 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11358 11359 TextAlignment alignment; 11360 } 11361 11362 /++ 11363 A basic checked or not checked box with an attached label. 11364 11365 11366 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 11367 11368 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11369 11370 History: 11371 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. 11372 +/ 11373 class Checkbox : MouseActivatedWidget { 11374 version(win32_widgets) { 11375 override int maxHeight() { return scaleWithDpi(16); } 11376 override int minHeight() { return scaleWithDpi(16); } 11377 } else version(custom_widgets) { 11378 private enum buttonSize = 16; 11379 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 11380 override int minHeight() { return maxHeight(); } 11381 } else static assert(0); 11382 11383 override int marginLeft() { return 4; } 11384 11385 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 11386 11387 /++ 11388 Just an alias because I keep typing checked out of web habit. 11389 11390 History: 11391 Added May 31, 2021 11392 +/ 11393 alias checked = isChecked; 11394 11395 private string label; 11396 private dchar accelerator; 11397 11398 /++ 11399 +/ 11400 this(string label, Widget parent) { 11401 this(ImageLabel(label), Appearance.checkbox, parent); 11402 } 11403 11404 /// ditto 11405 this(string label, Appearance appearance, Widget parent) { 11406 this(ImageLabel(label), appearance, parent); 11407 } 11408 11409 /++ 11410 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 11411 11412 History: 11413 Added June 29, 2021 (dub v10.2) 11414 +/ 11415 enum Appearance { 11416 checkbox, /// a normal checkbox 11417 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 11418 //sliderswitch, 11419 } 11420 private Appearance appearance; 11421 11422 /// ditto 11423 private this(ImageLabel label, Appearance appearance, Widget parent) { 11424 super(parent); 11425 version(win32_widgets) { 11426 this.label = label.label; 11427 11428 uint extraStyle; 11429 final switch(appearance) { 11430 case Appearance.checkbox: 11431 break; 11432 case Appearance.pushbutton: 11433 extraStyle |= BS_PUSHLIKE; 11434 break; 11435 } 11436 11437 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 11438 } else version(custom_widgets) { 11439 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 11440 } else static assert(0); 11441 } 11442 11443 version(custom_widgets) 11444 override void paint(WidgetPainter painter) { 11445 auto cs = getComputedStyle(); 11446 if(isFocused()) { 11447 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11448 painter.fillColor = cs.windowBackgroundColor; 11449 painter.drawRectangle(Point(0, 0), width, height); 11450 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11451 } else { 11452 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 11453 painter.fillColor = cs.windowBackgroundColor; 11454 painter.drawRectangle(Point(0, 0), width, height); 11455 } 11456 11457 11458 painter.outlineColor = Color.black; 11459 painter.fillColor = Color.white; 11460 enum rectOffset = 2; 11461 painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset)); 11462 11463 if(isChecked) { 11464 auto size = scaleWithDpi(2); 11465 painter.pen = Pen(Color.black, size); 11466 // I'm using height so the checkbox is square 11467 enum padding = 3; 11468 painter.drawLine( 11469 scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)), 11470 scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2) 11471 ); 11472 painter.drawLine( 11473 scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0), 11474 scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 - size % 2) 11475 ); 11476 11477 painter.pen = Pen(Color.black, 1); 11478 } 11479 11480 if(label !is null) { 11481 painter.outlineColor = cs.foregroundColor(); 11482 painter.fillColor = cs.foregroundColor(); 11483 11484 // i want the centerline of the text to be aligned with the centerline of the checkbox 11485 /+ 11486 auto font = cs.font(); 11487 auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2; 11488 painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label); 11489 +/ 11490 painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter); 11491 } 11492 } 11493 11494 override void defaultEventHandler_triggered(Event ev) { 11495 isChecked = !isChecked; 11496 11497 this.emit!(ChangeEvent!bool)(&isChecked); 11498 11499 redraw(); 11500 } 11501 11502 /// Emits a change event with the checked state 11503 mixin Emits!(ChangeEvent!bool); 11504 } 11505 11506 /// Adds empty space to a layout. 11507 class VerticalSpacer : Widget { 11508 /// 11509 this(Widget parent) { 11510 super(parent); 11511 } 11512 } 11513 11514 /// ditto 11515 class HorizontalSpacer : Widget { 11516 /// 11517 this(Widget parent) { 11518 super(parent); 11519 this.tabStop = false; 11520 } 11521 } 11522 11523 11524 /++ 11525 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 11526 11527 11528 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 11529 11530 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11531 11532 History: 11533 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. 11534 +/ 11535 class Radiobox : MouseActivatedWidget { 11536 11537 version(win32_widgets) { 11538 override int maxHeight() { return scaleWithDpi(16); } 11539 override int minHeight() { return scaleWithDpi(16); } 11540 } else version(custom_widgets) { 11541 private enum buttonSize = 16; 11542 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 11543 override int minHeight() { return maxHeight(); } 11544 } else static assert(0); 11545 11546 override int marginLeft() { return 4; } 11547 11548 // FIXME: make a label getter 11549 private string label; 11550 private dchar accelerator; 11551 11552 /++ 11553 11554 +/ 11555 this(string label, Widget parent) { 11556 super(parent); 11557 version(win32_widgets) { 11558 this.label = label; 11559 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 11560 } else version(custom_widgets) { 11561 label.extractWindowsStyleLabel(this.label, this.accelerator); 11562 height = 16; 11563 width = height + 4 + cast(int) label.length * 16; 11564 } 11565 } 11566 11567 version(custom_widgets) 11568 override void paint(WidgetPainter painter) { 11569 auto cs = getComputedStyle(); 11570 11571 if(isFocused) { 11572 painter.fillColor = cs.windowBackgroundColor; 11573 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11574 } else { 11575 painter.fillColor = cs.windowBackgroundColor; 11576 painter.outlineColor = cs.windowBackgroundColor; 11577 } 11578 painter.drawRectangle(Point(0, 0), width, height); 11579 11580 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11581 11582 painter.outlineColor = Color.black; 11583 painter.fillColor = Color.white; 11584 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 11585 if(isChecked) { 11586 painter.outlineColor = Color.black; 11587 painter.fillColor = Color.black; 11588 // I'm using height so the checkbox is square 11589 auto size = scaleWithDpi(2); 11590 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2)); 11591 } 11592 11593 painter.outlineColor = cs.foregroundColor(); 11594 painter.fillColor = cs.foregroundColor(); 11595 11596 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11597 } 11598 11599 11600 override void defaultEventHandler_triggered(Event ev) { 11601 isChecked = true; 11602 11603 if(this.parent) { 11604 foreach(child; this.parent.children) { 11605 if(child is this) continue; 11606 if(auto rb = cast(Radiobox) child) { 11607 rb.isChecked = false; 11608 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 11609 rb.redraw(); 11610 } 11611 } 11612 } 11613 11614 this.emit!(ChangeEvent!bool)(&this.isChecked); 11615 11616 redraw(); 11617 } 11618 11619 /// 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. 11620 mixin Emits!(ChangeEvent!bool); 11621 } 11622 11623 11624 /++ 11625 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 11626 11627 11628 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 11629 11630 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11631 11632 History: 11633 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. 11634 +/ 11635 class Button : MouseActivatedWidget { 11636 override int heightStretchiness() { return 3; } 11637 override int widthStretchiness() { return 3; } 11638 11639 /++ 11640 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. 11641 11642 History: 11643 Added July 2, 2021 11644 +/ 11645 public bool triggersOnMultiClick; 11646 11647 private string label_; 11648 private TextAlignment alignment; 11649 private dchar accelerator; 11650 11651 /// 11652 string label() { return label_; } 11653 /// 11654 void label(string l) { 11655 label_ = l; 11656 version(win32_widgets) { 11657 WCharzBuffer bfr = WCharzBuffer(l); 11658 SetWindowTextW(hwnd, bfr.ptr); 11659 } else version(custom_widgets) { 11660 redraw(); 11661 } 11662 } 11663 11664 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 11665 super.defaultEventHandler_dblclick(ev); 11666 if(triggersOnMultiClick) { 11667 if(ev.button == MouseButton.left) { 11668 auto event = new Event(EventType.triggered, this); 11669 event.sendDirectly(); 11670 } 11671 } 11672 } 11673 11674 private Sprite sprite; 11675 private int displayFlags; 11676 11677 /++ 11678 Creates a push button with the given label, which may be an image or some text. 11679 11680 Bugs: 11681 If the image is bigger than the button, it may not be displayed in the right position on Linux. 11682 11683 History: 11684 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 11685 11686 The button with label and image will respect requests to show both on Windows as 11687 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 11688 +/ 11689 this(ImageLabel label, Widget parent) { 11690 version(win32_widgets) { 11691 // FIXME: use ideal button size instead 11692 width = 50; 11693 height = 30; 11694 super(parent); 11695 11696 // BS_BITMAP is set when we want image only, so checking for exactly that combination 11697 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 11698 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 11699 11700 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 11701 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 11702 11703 if(label.image) { 11704 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 11705 11706 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 11707 } 11708 11709 this.label = label.label; 11710 } else version(custom_widgets) { 11711 width = 50; 11712 height = 30; 11713 super(parent); 11714 11715 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 11716 11717 if(label.image) { 11718 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 11719 this.displayFlags = label.displayFlags; 11720 } 11721 11722 this.alignment = label.alignment; 11723 } 11724 } 11725 11726 /// 11727 this(string label, Widget parent) { 11728 this(ImageLabel(label), parent); 11729 } 11730 11731 override int minHeight() { return defaultLineHeight + 4; } 11732 11733 static class Style : Widget.Style { 11734 override WidgetBackground background() { 11735 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 11736 11737 auto pressed = DynamicState.depressed | DynamicState.hover; 11738 if((widget.dynamicState & pressed) == pressed) { 11739 return WidgetBackground(cs.depressedButtonColor()); 11740 } else if(widget.dynamicState & DynamicState.hover) { 11741 return WidgetBackground(cs.hoveringColor()); 11742 } else { 11743 return WidgetBackground(cs.buttonColor()); 11744 } 11745 } 11746 11747 override FrameStyle borderStyle() { 11748 auto pressed = DynamicState.depressed | DynamicState.hover; 11749 if((widget.dynamicState & pressed) == pressed) { 11750 return FrameStyle.sunk; 11751 } else { 11752 return FrameStyle.risen; 11753 } 11754 11755 } 11756 11757 override bool variesWithState(ulong dynamicStateFlags) { 11758 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11759 } 11760 } 11761 mixin OverrideStyle!Style; 11762 11763 version(custom_widgets) 11764 override void paint(WidgetPainter painter) { 11765 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 11766 if(sprite) { 11767 sprite.drawAt( 11768 painter, 11769 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 11770 Point(0, 0) 11771 ); 11772 } else { 11773 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 11774 } 11775 return bounds; 11776 }); 11777 } 11778 11779 override int flexBasisWidth() { 11780 version(win32_widgets) { 11781 SIZE size; 11782 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11783 if(size.cx == 0) 11784 goto fallback; 11785 return size.cx + scaleWithDpi(16); 11786 } 11787 fallback: 11788 return scaleWithDpi(cast(int) label.length * 8 + 16); 11789 } 11790 11791 override int flexBasisHeight() { 11792 version(win32_widgets) { 11793 SIZE size; 11794 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11795 if(size.cy == 0) 11796 goto fallback; 11797 return size.cy + scaleWithDpi(6); 11798 } 11799 fallback: 11800 return defaultLineHeight + 4; 11801 } 11802 } 11803 11804 /++ 11805 A button with a consistent size, suitable for user commands like OK and CANCEL. 11806 +/ 11807 class CommandButton : Button { 11808 this(string label, Widget parent) { 11809 super(label, parent); 11810 } 11811 11812 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 11813 11814 override int maxHeight() { 11815 return defaultLineHeight + 4; 11816 } 11817 11818 override int maxWidth() { 11819 return defaultLineHeight * 4; 11820 } 11821 11822 override int marginLeft() { return 12; } 11823 override int marginRight() { return 12; } 11824 override int marginTop() { return 12; } 11825 override int marginBottom() { return 12; } 11826 } 11827 11828 /// 11829 enum ArrowDirection { 11830 left, /// 11831 right, /// 11832 up, /// 11833 down /// 11834 } 11835 11836 /// 11837 version(custom_widgets) 11838 class ArrowButton : Button { 11839 /// 11840 this(ArrowDirection direction, Widget parent) { 11841 super("", parent); 11842 this.direction = direction; 11843 triggersOnMultiClick = true; 11844 } 11845 11846 private ArrowDirection direction; 11847 11848 override int minHeight() { return scaleWithDpi(16); } 11849 override int maxHeight() { return scaleWithDpi(16); } 11850 override int minWidth() { return scaleWithDpi(16); } 11851 override int maxWidth() { return scaleWithDpi(16); } 11852 11853 override void paint(WidgetPainter painter) { 11854 super.paint(painter); 11855 11856 auto cs = getComputedStyle(); 11857 11858 painter.outlineColor = cs.foregroundColor; 11859 painter.fillColor = cs.foregroundColor; 11860 11861 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 11862 11863 final switch(direction) { 11864 case ArrowDirection.up: 11865 painter.drawPolygon( 11866 scaleWithDpi(Point(2, 10) + offset), 11867 scaleWithDpi(Point(7, 5) + offset), 11868 scaleWithDpi(Point(12, 10) + offset), 11869 scaleWithDpi(Point(2, 10) + offset) 11870 ); 11871 break; 11872 case ArrowDirection.down: 11873 painter.drawPolygon( 11874 scaleWithDpi(Point(2, 6) + offset), 11875 scaleWithDpi(Point(7, 11) + offset), 11876 scaleWithDpi(Point(12, 6) + offset), 11877 scaleWithDpi(Point(2, 6) + offset) 11878 ); 11879 break; 11880 case ArrowDirection.left: 11881 painter.drawPolygon( 11882 scaleWithDpi(Point(10, 2) + offset), 11883 scaleWithDpi(Point(5, 7) + offset), 11884 scaleWithDpi(Point(10, 12) + offset), 11885 scaleWithDpi(Point(10, 2) + offset) 11886 ); 11887 break; 11888 case ArrowDirection.right: 11889 painter.drawPolygon( 11890 scaleWithDpi(Point(6, 2) + offset), 11891 scaleWithDpi(Point(11, 7) + offset), 11892 scaleWithDpi(Point(6, 12) + offset), 11893 scaleWithDpi(Point(6, 2) + offset) 11894 ); 11895 break; 11896 } 11897 } 11898 } 11899 11900 private 11901 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 11902 int x, y; 11903 Widget par = c; 11904 while(par) { 11905 x += par.x; 11906 y += par.y; 11907 par = par.parent; 11908 } 11909 return [x, y]; 11910 } 11911 11912 version(win32_widgets) 11913 private 11914 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 11915 // MapWindowPoints? 11916 int x, y; 11917 Widget par = c; 11918 while(par) { 11919 x += par.x; 11920 y += par.y; 11921 par = par.parent; 11922 if(par !is null && par.useNativeDrawing()) 11923 break; 11924 } 11925 return [x, y]; 11926 } 11927 11928 /// 11929 class ImageBox : Widget { 11930 private MemoryImage image_; 11931 11932 override int widthStretchiness() { return 1; } 11933 override int heightStretchiness() { return 1; } 11934 override int widthShrinkiness() { return 1; } 11935 override int heightShrinkiness() { return 1; } 11936 11937 override int flexBasisHeight() { 11938 return image_.height; 11939 } 11940 11941 override int flexBasisWidth() { 11942 return image_.width; 11943 } 11944 11945 /// 11946 public void setImage(MemoryImage image){ 11947 this.image_ = image; 11948 if(this.parentWindow && this.parentWindow.win) { 11949 if(sprite) 11950 sprite.dispose(); 11951 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11952 } 11953 redraw(); 11954 } 11955 11956 /// How to fit the image in the box if they aren't an exact match in size? 11957 enum HowToFit { 11958 center, /// centers the image, cropping around all the edges as needed 11959 crop, /// always draws the image in the upper left, cropping the lower right if needed 11960 // stretch, /// not implemented 11961 } 11962 11963 private Sprite sprite; 11964 private HowToFit howToFit_; 11965 11966 private Color backgroundColor_; 11967 11968 /// 11969 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 11970 this.image_ = image; 11971 this.tabStop = false; 11972 this.howToFit_ = howToFit; 11973 this.backgroundColor_ = backgroundColor; 11974 super(parent); 11975 updateSprite(); 11976 } 11977 11978 /// ditto 11979 this(MemoryImage image, HowToFit howToFit, Widget parent) { 11980 this(image, howToFit, Color.transparent, parent); 11981 } 11982 11983 private void updateSprite() { 11984 if(sprite is null && this.parentWindow && this.parentWindow.win) { 11985 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11986 } 11987 } 11988 11989 override void paint(WidgetPainter painter) { 11990 updateSprite(); 11991 if(backgroundColor_.a) { 11992 painter.fillColor = backgroundColor_; 11993 painter.drawRectangle(Point(0, 0), width, height); 11994 } 11995 if(howToFit_ == HowToFit.crop) 11996 sprite.drawAt(painter, Point(0, 0)); 11997 else if(howToFit_ == HowToFit.center) { 11998 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 11999 } 12000 } 12001 } 12002 12003 /// 12004 class TextLabel : Widget { 12005 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; } 12006 override int maxHeight() { return minHeight; } 12007 override int minWidth() { return 32; } 12008 12009 override int flexBasisHeight() { return minHeight(); } 12010 override int flexBasisWidth() { return defaultTextWidth(label); } 12011 12012 string label_; 12013 12014 /++ 12015 Indicates which other control this label is here for. Similar to HTML `for` attribute. 12016 12017 In practice this means a click on the label will focus the `labelFor`. In future versions 12018 it will also set screen reader hints but that is not yet implemented. 12019 12020 History: 12021 Added October 3, 2021 (dub v10.4) 12022 +/ 12023 Widget labelFor; 12024 12025 /// 12026 @scriptable 12027 string label() { return label_; } 12028 12029 /// 12030 @scriptable 12031 void label(string l) { 12032 label_ = l; 12033 version(win32_widgets) { 12034 WCharzBuffer bfr = WCharzBuffer(l); 12035 SetWindowTextW(hwnd, bfr.ptr); 12036 } else version(custom_widgets) 12037 redraw(); 12038 } 12039 12040 override void defaultEventHandler_click(scope ClickEvent ce) { 12041 if(this.labelFor !is null) 12042 this.labelFor.focus(); 12043 } 12044 12045 /++ 12046 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 12047 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 12048 +/ 12049 this(string label, TextAlignment alignment, Widget parent) { 12050 this.label_ = label; 12051 this.alignment = alignment; 12052 this.tabStop = false; 12053 super(parent); 12054 12055 version(win32_widgets) 12056 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 12057 } 12058 12059 /// ditto 12060 this(string label, Widget parent) { 12061 this(label, TextAlignment.Right, parent); 12062 } 12063 12064 TextAlignment alignment; 12065 12066 version(custom_widgets) 12067 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 12068 painter.outlineColor = getComputedStyle().foregroundColor; 12069 painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment); 12070 return bounds; 12071 } 12072 12073 } 12074 12075 version(custom_widgets) 12076 private struct etc { 12077 mixin ExperimentalTextComponent; 12078 } 12079 12080 version(win32_widgets) { 12081 alias EditableTextWidgetParent = Widget; /// 12082 version=use_new_text_system; 12083 import arsd.textlayouter; 12084 } else version(custom_widgets) { 12085 version(trash_text) { 12086 alias EditableTextWidgetParent = ScrollableWidget; /// 12087 } else { 12088 alias EditableTextWidgetParent = Widget; 12089 version=use_new_text_system; 12090 import arsd.textlayouter; 12091 } 12092 } else static assert(0); 12093 12094 version(use_new_text_system) 12095 class TextDisplayHelper : Widget { 12096 protected TextLayouter l; 12097 protected ScrollMessageWidget smw; 12098 12099 private const(TextLayouter.State)*[] undoStack; 12100 private const(TextLayouter.State)*[] redoStack; 12101 12102 private string preservedPrimaryText; 12103 protected void selectionChanged() { 12104 static if(UsingSimpledisplayX11) 12105 with(l.selection()) { 12106 if(!isEmpty()) { 12107 getPrimarySelection(parentWindow.win, (in char[] txt) { 12108 // import std.stdio; writeln("txt: ", txt, " sel: ", getContentString); 12109 if(txt.length) { 12110 preservedPrimaryText = txt.idup; 12111 // writeln(preservedPrimaryText); 12112 } 12113 12114 setPrimarySelection(parentWindow.win, getContentString()); 12115 }); 12116 } 12117 } 12118 } 12119 12120 12121 bool readonly; 12122 bool caretNavigation; // scroll lock can flip this 12123 bool singleLine; 12124 bool acceptsTabInput; 12125 12126 private Menu ctx; 12127 override Menu contextMenu(int x, int y) { 12128 if(ctx is null) { 12129 ctx = new Menu("Actions", this); 12130 if(!readonly) { 12131 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 12132 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 12133 ctx.addSeparator(); 12134 } 12135 if(!readonly) 12136 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 12137 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 12138 if(!readonly) 12139 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 12140 if(!readonly) 12141 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 12142 ctx.addSeparator(); 12143 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 12144 } 12145 return ctx; 12146 } 12147 12148 override void defaultEventHandler_blur(Event ev) { 12149 super.defaultEventHandler_blur(ev); 12150 if(l.wasMutated()) { 12151 auto evt = new ChangeEvent!string(this, &this.content); 12152 evt.dispatch(); 12153 l.clearWasMutatedFlag(); 12154 } 12155 } 12156 12157 private string content() { 12158 return l.getTextString(); 12159 } 12160 12161 void undo() { 12162 if(readonly) return; 12163 if(undoStack.length) { 12164 auto state = undoStack[$-1]; 12165 undoStack = undoStack[0 .. $-1]; 12166 undoStack.assumeSafeAppend(); 12167 redoStack ~= l.saveState(); 12168 l.restoreState(state); 12169 adjustScrollbarSizes(); 12170 scrollForCaret(); 12171 redraw(); 12172 stateCheckpoint = true; 12173 } 12174 } 12175 12176 void redo() { 12177 if(readonly) return; 12178 if(redoStack.length) { 12179 doStateCheckpoint(); 12180 auto state = redoStack[$-1]; 12181 redoStack = redoStack[0 .. $-1]; 12182 redoStack.assumeSafeAppend(); 12183 l.restoreState(state); 12184 adjustScrollbarSizes(); 12185 scrollForCaret(); 12186 redraw(); 12187 stateCheckpoint = true; 12188 } 12189 } 12190 12191 void cut() { 12192 if(readonly) return; 12193 with(l.selection()) { 12194 if(!isEmpty()) { 12195 setClipboardText(parentWindow.win, getContentString()); 12196 doStateCheckpoint(); 12197 replaceContent(""); 12198 adjustScrollbarSizes(); 12199 scrollForCaret(); 12200 this.redraw(); 12201 } 12202 } 12203 12204 } 12205 12206 void copy() { 12207 with(l.selection()) { 12208 if(!isEmpty()) { 12209 setClipboardText(parentWindow.win, getContentString()); 12210 this.redraw(); 12211 } 12212 } 12213 } 12214 12215 void paste() { 12216 if(readonly) return; 12217 getClipboardText(parentWindow.win, (txt) { 12218 doStateCheckpoint(); 12219 l.selection.replaceContent(txt); 12220 adjustScrollbarSizes(); 12221 scrollForCaret(); 12222 this.redraw(); 12223 }); 12224 } 12225 12226 void deleteContentOfSelection() { 12227 if(readonly) return; 12228 doStateCheckpoint(); 12229 l.selection.replaceContent(""); 12230 l.selection.setUserXCoordinate(); 12231 adjustScrollbarSizes(); 12232 scrollForCaret(); 12233 redraw(); 12234 } 12235 12236 void selectAll() { 12237 with(l.selection) { 12238 moveToStartOfDocument(); 12239 setAnchor(); 12240 moveToEndOfDocument(); 12241 setFocus(); 12242 12243 selectionChanged(); 12244 } 12245 redraw(); 12246 } 12247 12248 protected bool stateCheckpoint = true; 12249 12250 protected void doStateCheckpoint() { 12251 if(stateCheckpoint) { 12252 undoStack ~= l.saveState(); 12253 stateCheckpoint = false; 12254 } 12255 } 12256 12257 protected void adjustScrollbarSizes() { 12258 // FIXME: will want a content area helper function instead of doing all these subtractions myself 12259 auto borderWidth = 2; 12260 this.smw.setTotalArea(l.width, l.height); 12261 this.smw.setViewableArea( 12262 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 12263 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 12264 } 12265 12266 protected void scrollForCaret() { 12267 // writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 12268 smw.scrollIntoView(l.selection.focusBoundingBox()); 12269 } 12270 12271 // FIXME: this should be a theme changed event listener instead 12272 private BaseVisualTheme currentTheme; 12273 override void recomputeChildLayout() { 12274 if(currentTheme is null) 12275 currentTheme = WidgetPainter.visualTheme; 12276 if(WidgetPainter.visualTheme !is currentTheme) { 12277 currentTheme = WidgetPainter.visualTheme; 12278 auto ds = this.l.defaultStyle; 12279 if(auto ms = cast(MyTextStyle) ds) { 12280 auto cs = getComputedStyle(); 12281 auto font = cs.font(); 12282 if(font !is null) 12283 ms.font_ = font; 12284 else { 12285 auto osc = new OperatingSystemFont(); 12286 osc.loadDefault; 12287 ms.font_ = osc; 12288 } 12289 } 12290 } 12291 super.recomputeChildLayout(); 12292 } 12293 12294 private Point adjustForSingleLine(Point p) { 12295 if(singleLine) 12296 return Point(p.x, this.height / 2); 12297 else 12298 return p; 12299 } 12300 12301 private bool wordWrapEnabled_; 12302 12303 this(TextLayouter l, ScrollMessageWidget parent) { 12304 this.smw = parent; 12305 12306 smw.addDefaultWheelListeners(16, 16, 8); 12307 smw.movementPerButtonClick(16, 16); 12308 12309 this.defaultPadding = Rectangle(2, 2, 2, 2); 12310 12311 this.l = l; 12312 super(parent); 12313 12314 smw.addEventListener((scope ScrollEvent se) { 12315 this.redraw(); 12316 }); 12317 12318 bool mouseDown; 12319 bool mouseActuallyMoved; 12320 12321 this.addEventListener((scope ResizeEvent re) { 12322 // FIXME: I should add a method to give this client area width thing 12323 if(wordWrapEnabled_) 12324 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 12325 12326 adjustScrollbarSizes(); 12327 scrollForCaret(); 12328 12329 this.redraw(); 12330 }); 12331 12332 this.addEventListener((scope KeyDownEvent kde) { 12333 switch(kde.key) { 12334 case Key.Up, Key.Down, Key.Left, Key.Right: 12335 case Key.Home, Key.End: 12336 stateCheckpoint = true; 12337 bool setPosition = false; 12338 switch(kde.key) { 12339 case Key.Up: l.selection.moveUp(); break; 12340 case Key.Down: l.selection.moveDown(); break; 12341 case Key.Left: l.selection.moveLeft(); setPosition = true; break; 12342 case Key.Right: l.selection.moveRight(); setPosition = true; break; 12343 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 12344 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 12345 default: assert(0); 12346 } 12347 12348 if(kde.shiftKey) 12349 l.selection.setFocus(); 12350 else 12351 l.selection.setAnchor(); 12352 12353 selectionChanged(); 12354 12355 if(setPosition) 12356 l.selection.setUserXCoordinate(); 12357 scrollForCaret(); 12358 redraw(); 12359 break; 12360 case Key.PageUp, Key.PageDown: 12361 // FIXME 12362 scrollForCaret(); 12363 break; 12364 case Key.Delete: 12365 if(l.selection.isEmpty()) { 12366 l.selection.setAnchor(); 12367 l.selection.moveRight(); 12368 l.selection.setFocus(); 12369 } 12370 deleteContentOfSelection(); 12371 adjustScrollbarSizes(); 12372 scrollForCaret(); 12373 break; 12374 case Key.Insert: 12375 break; 12376 case Key.A: 12377 if(kde.ctrlKey) 12378 selectAll(); 12379 break; 12380 case Key.F: 12381 // find 12382 break; 12383 case Key.Z: 12384 if(kde.ctrlKey) 12385 undo(); 12386 break; 12387 case Key.R: 12388 if(kde.ctrlKey) 12389 redo(); 12390 break; 12391 case Key.X: 12392 if(kde.ctrlKey) 12393 cut(); 12394 break; 12395 case Key.C: 12396 if(kde.ctrlKey) 12397 copy(); 12398 break; 12399 case Key.V: 12400 if(kde.ctrlKey) 12401 paste(); 12402 break; 12403 case Key.F1: 12404 with(l.selection()) { 12405 moveToStartOfLine(); 12406 setAnchor(); 12407 moveToEndOfLine(); 12408 moveToIncludeAdjacentEndOfLineMarker(); 12409 setFocus(); 12410 replaceContent(""); 12411 } 12412 12413 redraw(); 12414 break; 12415 /* 12416 case Key.F2: 12417 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 12418 //(cast(MyTextStyle) old).font, 12419 font2, 12420 Color.red))); 12421 redraw(); 12422 break; 12423 */ 12424 case Key.Tab: 12425 // we process the char event, so don't want to change focus on it 12426 if(acceptsTabInput) 12427 kde.preventDefault(); 12428 break; 12429 default: 12430 } 12431 }); 12432 12433 Point downAt; 12434 12435 static if(UsingSimpledisplayX11) 12436 this.addEventListener((scope ClickEvent ce) { 12437 if(ce.button == MouseButton.middle) { 12438 parentWindow.win.getPrimarySelection((txt) { 12439 doStateCheckpoint(); 12440 12441 // import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText); 12442 12443 if(txt == l.selection.getContentString && preservedPrimaryText.length) 12444 l.selection.replaceContent(preservedPrimaryText); 12445 else 12446 l.selection.replaceContent(txt); 12447 redraw(); 12448 }); 12449 } 12450 }); 12451 12452 this.addEventListener((scope DoubleClickEvent dce) { 12453 if(dce.button == MouseButton.left) { 12454 with(l.selection()) { 12455 scope dg = delegate const(char)[] (scope return const(char)[] ch) { 12456 if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r") 12457 return ch; 12458 return null; 12459 }; 12460 find(dg, 1, true).moveToEnd.setAnchor; 12461 find(dg, 1, false).moveTo.setFocus; 12462 selectionChanged(); 12463 redraw(); 12464 } 12465 } 12466 }); 12467 12468 this.addEventListener((scope MouseDownEvent ce) { 12469 if(ce.button == MouseButton.left) { 12470 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12471 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 12472 l.selection.setAnchor(); 12473 mouseDown = true; 12474 mouseActuallyMoved = false; 12475 parentWindow.captureMouse(this); 12476 this.redraw(); 12477 } else if(ce.button == MouseButton.right) { 12478 this.showContextMenu(ce.clientX, ce.clientY); 12479 } 12480 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12481 }); 12482 12483 Timer autoscrollTimer; 12484 int autoscrollDirection; 12485 int autoscrollAmount; 12486 12487 void autoscroll() { 12488 switch(autoscrollDirection) { 12489 case 0: smw.scrollUp(autoscrollAmount); break; 12490 case 1: smw.scrollDown(autoscrollAmount); break; 12491 case 2: smw.scrollLeft(autoscrollAmount); break; 12492 case 3: smw.scrollRight(autoscrollAmount); break; 12493 default: assert(0); 12494 } 12495 12496 this.redraw(); 12497 } 12498 12499 void setAutoscrollTimer(int direction, int amount) { 12500 if(autoscrollTimer is null) { 12501 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 12502 } 12503 12504 autoscrollDirection = direction; 12505 autoscrollAmount = amount; 12506 } 12507 12508 void stopAutoscrollTimer() { 12509 if(autoscrollTimer !is null) { 12510 autoscrollTimer.dispose(); 12511 autoscrollTimer = null; 12512 } 12513 autoscrollAmount = 0; 12514 autoscrollDirection = 0; 12515 } 12516 12517 this.addEventListener((scope MouseMoveEvent ce) { 12518 if(mouseDown) { 12519 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12520 12521 // FIXME: when scrolling i actually do want a timer. 12522 // i also want a zone near the sides of the window where i can auto scroll 12523 12524 auto scrollMultiplier = scaleWithDpi(16); 12525 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 12526 12527 if(!singleLine && movedTo.y < 4) { 12528 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 12529 } else 12530 if(!singleLine && (movedTo.y + 6) > this.height) { 12531 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 12532 } else 12533 if(movedTo.x < 4) { 12534 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 12535 } else 12536 if((movedTo.x + 6) > this.width) { 12537 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 12538 } else 12539 stopAutoscrollTimer(); 12540 12541 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 12542 l.selection.setFocus(); 12543 mouseActuallyMoved = true; 12544 this.redraw(); 12545 } 12546 }); 12547 12548 this.addEventListener((scope MouseUpEvent ce) { 12549 // FIXME: assert primary selection 12550 if(mouseDown && ce.button == MouseButton.left) { 12551 stateCheckpoint = true; 12552 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 12553 //l.selection.setFocus(); 12554 mouseDown = false; 12555 parentWindow.releaseMouseCapture(); 12556 stopAutoscrollTimer(); 12557 this.redraw(); 12558 12559 if(mouseActuallyMoved) 12560 selectionChanged(); 12561 } 12562 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12563 }); 12564 12565 this.addEventListener((scope CharEvent ce) { 12566 if(readonly) 12567 return; 12568 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 12569 return; // skip the ctrl+x characters we don't care about as plain text 12570 12571 if(singleLine && ce.character == '\n') 12572 return; 12573 if(!acceptsTabInput && ce.character == '\t') 12574 return; 12575 12576 doStateCheckpoint(); 12577 12578 char[4] buffer; 12579 import std.utf; // FIXME: i should remove this. compile time not significant but the logs get spammed with phobos' import web 12580 auto stride = encode(buffer, ce.character); 12581 l.selection.replaceContent(buffer[0 .. stride]); 12582 l.selection.setUserXCoordinate(); 12583 adjustScrollbarSizes(); 12584 scrollForCaret(); 12585 redraw(); 12586 }); 12587 } 12588 12589 // we want to delegate all the Widget.Style stuff up to the other class that the user can see 12590 override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) { 12591 // this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidgetParent 12592 if(parent && parent.parent && parent.parent.parent) 12593 parent.parent.parent.useStyleProperties(dg); 12594 else 12595 super.useStyleProperties(dg); 12596 } 12597 12598 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; } 12599 override int maxHeight() { 12600 if(singleLine) 12601 return minHeight; 12602 else 12603 return super.maxHeight(); 12604 } 12605 12606 void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 12607 painter.drawText(upperLeft, text); 12608 } 12609 12610 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 12611 //painter.setFont(font); 12612 12613 auto cs = getComputedStyle(); 12614 auto defaultColor = cs.foregroundColor; 12615 12616 auto old = painter.setClipRectangle(bounds); 12617 scope(exit) painter.setClipRectangle(old); 12618 12619 l.getDrawableText(delegate bool(txt, style, info, carets...) { 12620 //writeln("Segment: ", txt); 12621 assert(style !is null); 12622 12623 auto myStyle = cast(MyTextStyle) style; 12624 assert(myStyle !is null); 12625 12626 painter.setFont(myStyle.font); 12627 // defaultColor = myStyle.color; // FIXME: so wrong 12628 12629 if(info.selections && info.boundingBox.width > 0) { 12630 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 12631 painter.fillColor = color; 12632 painter.outlineColor = color; 12633 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 12634 painter.outlineColor = cs.selectionForegroundColor; 12635 //painter.fillColor = Color.white; 12636 } else { 12637 painter.outlineColor = defaultColor; 12638 } 12639 12640 if(this.isFocused) 12641 foreach(idx, caret; carets) { 12642 if(idx == 0) 12643 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 12644 painter.drawLine( 12645 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 12646 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 12647 ); 12648 } 12649 12650 if(txt.stripInternal.length) { 12651 drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 12652 } 12653 12654 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) { 12655 return false; 12656 } else { 12657 return true; 12658 } 12659 }, Rectangle(smw.position(), bounds.size)); 12660 12661 /+ 12662 int place = 0; 12663 int y = 75; 12664 foreach(width; widths) { 12665 painter.fillColor = Color.red; 12666 painter.drawRectangle(Point(place, y), Size(width, 75)); 12667 //y += 15; 12668 place += width; 12669 } 12670 +/ 12671 12672 return bounds; 12673 } 12674 12675 static class MyTextStyle : TextStyle { 12676 OperatingSystemFont font_; 12677 this(OperatingSystemFont font, bool passwordMode = false) { 12678 this.font_ = font; 12679 } 12680 12681 override OperatingSystemFont font() { 12682 return font_; 12683 } 12684 } 12685 } 12686 12687 /+ 12688 version(use_new_text_system) 12689 class TextWidget : Widget { 12690 TextLayouter l; 12691 ScrollMessageWidget smw; 12692 TextDisplayHelper helper; 12693 this(TextLayouter l, Widget parent) { 12694 this.l = l; 12695 super(parent); 12696 12697 smw = new ScrollMessageWidget(this); 12698 //smw.horizontalScrollBar.hide; 12699 //smw.verticalScrollBar.hide; 12700 smw.addDefaultWheelListeners(16, 16, 8); 12701 smw.movementPerButtonClick(16, 16); 12702 helper = new TextDisplayHelper(l, smw); 12703 12704 // no need to do this here since there's gonna be a resize 12705 // event immediately before any drawing 12706 // smw.setTotalArea(l.width, l.height); 12707 smw.setViewableArea( 12708 this.width - this.paddingLeft - this.paddingRight, 12709 this.height - this.paddingTop - this.paddingBottom); 12710 12711 /+ 12712 writeln(l.width, "x", l.height); 12713 +/ 12714 } 12715 } 12716 +/ 12717 12718 12719 12720 12721 /+ 12722 This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too 12723 +/ 12724 12725 /// Contains the implementation of text editing 12726 abstract class EditableTextWidget : EditableTextWidgetParent { 12727 this(Widget parent) { 12728 version(custom_widgets) 12729 this(true, parent); 12730 else 12731 this(false, parent); 12732 } 12733 12734 private bool useCustomWidget; 12735 12736 this(bool useCustomWidget, Widget parent) { 12737 this.useCustomWidget = useCustomWidget; 12738 12739 super(parent); 12740 12741 if(useCustomWidget) 12742 setupCustomTextEditing(); 12743 } 12744 12745 private bool wordWrapEnabled_; 12746 void wordWrapEnabled(bool enabled) { 12747 if(useCustomWidget) { 12748 wordWrapEnabled_ = enabled; 12749 version(use_new_text_system) 12750 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 12751 } else version(win32_widgets) { 12752 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 12753 } 12754 } 12755 12756 override int minWidth() { return scaleWithDpi(16); } 12757 override int widthStretchiness() { return 7; } 12758 override int widthShrinkiness() { return 1; } 12759 12760 version(use_new_text_system) 12761 override int maxHeight() { 12762 if(useCustomWidget) 12763 return tdh.maxHeight; 12764 else 12765 return super.maxHeight(); 12766 } 12767 12768 version(use_new_text_system) 12769 override void focus() { 12770 if(useCustomWidget && tdh) 12771 tdh.focus(); 12772 else 12773 super.focus(); 12774 } 12775 12776 void selectAll() { 12777 if(useCustomWidget) { 12778 version(use_new_text_system) 12779 tdh.selectAll(); 12780 else version(trash_text) 12781 textLayout.selectAll(); 12782 redraw(); 12783 } else version(win32_widgets) { 12784 SendMessage(hwnd, EM_SETSEL, 0, -1); 12785 } 12786 } 12787 12788 version(use_new_text_system) 12789 TextDisplayHelper tdh; 12790 12791 @property string content() { 12792 if(useCustomWidget) { 12793 version(use_new_text_system) { 12794 return textLayout.getTextString(); 12795 } else version(trash_text) { 12796 return textLayout.getPlainText(); 12797 } 12798 } else version(win32_widgets) { 12799 wchar[4096] bufferstack; 12800 wchar[] buffer; 12801 auto len = GetWindowTextLength(hwnd); 12802 if(len < bufferstack.length) 12803 buffer = bufferstack[0 .. len + 1]; 12804 else 12805 buffer = new wchar[](len + 1); 12806 12807 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 12808 if(l >= 0) 12809 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 12810 else 12811 return null; 12812 } 12813 12814 assert(0); 12815 } 12816 @property void content(string s) { 12817 if(useCustomWidget) { 12818 version(use_new_text_system) { 12819 selectAll(); 12820 textLayout.selection.replaceContent(s); 12821 12822 tdh.adjustScrollbarSizes(); 12823 // these don't seem to help 12824 // tdh.smw.setPosition(0, 0); 12825 // tdh.scrollForCaret(); 12826 12827 redraw(); 12828 } else version(trash_text) { 12829 textLayout.clear(); 12830 textLayout.addText(s); 12831 12832 { 12833 // FIXME: it should be able to get this info easier 12834 auto painter = draw(); 12835 textLayout.redoLayout(painter); 12836 } 12837 auto cbb = textLayout.contentBoundingBox(); 12838 setContentSize(cbb.width, cbb.height); 12839 /* 12840 textLayout.addText(ForegroundColor.red, s); 12841 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 12842 textLayout.addText(" is the best!"); 12843 */ 12844 redraw(); 12845 } 12846 } else version(win32_widgets) { 12847 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 12848 SetWindowTextW(hwnd, bfr.ptr); 12849 } 12850 } 12851 12852 void addText(string txt) { 12853 if(useCustomWidget) { 12854 version(use_new_text_system) { 12855 textLayout.appendText(txt); 12856 tdh.adjustScrollbarSizes(); 12857 redraw(); 12858 } else if(trash_text) { 12859 textLayout.addText(txt); 12860 12861 { 12862 // FIXME: it should be able to get this info easier 12863 auto painter = draw(); 12864 textLayout.redoLayout(painter); 12865 } 12866 auto cbb = textLayout.contentBoundingBox(); 12867 setContentSize(cbb.width, cbb.height); 12868 } 12869 } else version(win32_widgets) { 12870 // get the current selection 12871 DWORD StartPos, EndPos; 12872 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 12873 12874 // move the caret to the end of the text 12875 int outLength = GetWindowTextLengthW(hwnd); 12876 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 12877 12878 // insert the text at the new caret position 12879 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 12880 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 12881 12882 // restore the previous selection 12883 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 12884 } 12885 } 12886 12887 version(custom_widgets) 12888 version(trash_text) 12889 override void paintFrameAndBackground(WidgetPainter painter) { 12890 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 12891 } 12892 12893 version(use_new_text_system) 12894 TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 12895 return new TextDisplayHelper(textLayout, smw); 12896 } 12897 12898 version(use_new_text_system) 12899 TextStyle defaultTextStyle() { 12900 return new TextDisplayHelper.MyTextStyle(getUsedFont()); 12901 } 12902 12903 version(use_new_text_system) 12904 private OperatingSystemFont getUsedFont() { 12905 auto cs = getComputedStyle(); 12906 auto font = cs.font; 12907 if(font is null) { 12908 font = new OperatingSystemFont; 12909 font.loadDefault(); 12910 } 12911 return font; 12912 } 12913 12914 version(use_new_text_system) { 12915 TextLayouter textLayout; 12916 12917 void setupCustomTextEditing() { 12918 textLayout = new TextLayouter(defaultTextStyle()); 12919 12920 auto smw = new ScrollMessageWidget(this); 12921 if(!showingHorizontalScroll) 12922 smw.horizontalScrollBar.hide(); 12923 if(!showingVerticalScroll) 12924 smw.verticalScrollBar.hide(); 12925 this.tabStop = false; 12926 smw.tabStop = false; 12927 tdh = textDisplayHelperFactory(textLayout, smw); 12928 } 12929 12930 override void newParentWindow(Window old, Window n) { 12931 if(n is null) return; 12932 this.parentWindow.addEventListener((scope DpiChangedEvent dce) { 12933 if(textLayout) { 12934 if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) { 12935 // the dpi change can change the font, so this informs the layouter that it has changed too 12936 style.font_ = getUsedFont(); 12937 12938 // arsd.core.writeln(this.parentWindow.win.actualDpi); 12939 } 12940 } 12941 }); 12942 } 12943 12944 } else version(trash_text) { 12945 static if(SimpledisplayTimerAvailable) 12946 Timer caretTimer; 12947 etc.TextLayout textLayout; 12948 12949 void setupCustomTextEditing() { 12950 textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4)); 12951 textLayout.selectionXorColor = getComputedStyle().activeListXorColor; 12952 } 12953 12954 override void paint(WidgetPainter painter) { 12955 if(parentWindow.win.closed) return; 12956 12957 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 12958 12959 /* 12960 painter.outlineColor = Color.white; 12961 painter.fillColor = Color.white; 12962 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 12963 */ 12964 12965 painter.outlineColor = Color.black; 12966 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 12967 12968 textLayout.caretShowingOnScreen = false; 12969 12970 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 12971 } 12972 } 12973 12974 static class Style : Widget.Style { 12975 override WidgetBackground background() { 12976 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 12977 } 12978 12979 override Color foregroundColor() { 12980 return WidgetPainter.visualTheme.foregroundColor; 12981 } 12982 12983 override FrameStyle borderStyle() { 12984 return FrameStyle.sunk; 12985 } 12986 12987 override MouseCursor cursor() { 12988 return GenericCursor.Text; 12989 } 12990 } 12991 mixin OverrideStyle!Style; 12992 12993 version(trash_text) 12994 version(custom_widgets) 12995 override void defaultEventHandler_mousedown(MouseDownEvent ev) { 12996 super.defaultEventHandler_mousedown(ev); 12997 if(parentWindow.win.closed) return; 12998 if(ev.button == MouseButton.left) { 12999 if(textLayout.selectNone()) 13000 redraw(); 13001 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 13002 this.focus(); 13003 //this.parentWindow.win.grabInput(); 13004 } else if(ev.button == MouseButton.middle) { 13005 static if(UsingSimpledisplayX11) { 13006 getPrimarySelection(parentWindow.win, (in char[] txt) { 13007 textLayout.insert(txt); 13008 redraw(); 13009 13010 auto cbb = textLayout.contentBoundingBox(); 13011 setContentSize(cbb.width, cbb.height); 13012 }); 13013 } 13014 } 13015 } 13016 13017 version(trash_text) 13018 version(custom_widgets) 13019 override void defaultEventHandler_mouseup(MouseUpEvent ev) { 13020 //this.parentWindow.win.releaseInputGrab(); 13021 super.defaultEventHandler_mouseup(ev); 13022 } 13023 13024 version(trash_text) 13025 version(custom_widgets) 13026 override void defaultEventHandler_mousemove(MouseMoveEvent ev) { 13027 super.defaultEventHandler_mousemove(ev); 13028 if(ev.state & ModifierState.leftButtonDown) { 13029 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 13030 redraw(); 13031 } 13032 } 13033 13034 version(trash_text) 13035 version(custom_widgets) 13036 override void defaultEventHandler_focus(Event ev) { 13037 super.defaultEventHandler_focus(ev); 13038 if(parentWindow.win.closed) return; 13039 auto painter = this.draw(); 13040 textLayout.drawCaret(painter); 13041 13042 static if(SimpledisplayTimerAvailable) 13043 if(caretTimer) { 13044 caretTimer.destroy(); 13045 caretTimer = null; 13046 } 13047 13048 bool blinkingCaret = true; 13049 static if(UsingSimpledisplayX11) 13050 if(!Image.impl.xshmAvailable) 13051 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 13052 13053 if(blinkingCaret) 13054 static if(SimpledisplayTimerAvailable) 13055 caretTimer = new Timer(500, { 13056 if(parentWindow.win.closed) { 13057 caretTimer.destroy(); 13058 return; 13059 } 13060 if(isFocused()) { 13061 auto painter = this.draw(); 13062 textLayout.drawCaret(painter); 13063 } else if(textLayout.caretShowingOnScreen) { 13064 auto painter = this.draw(); 13065 textLayout.eraseCaret(painter); 13066 } 13067 }); 13068 } 13069 13070 version(trash_text) { 13071 private string lastContentBlur; 13072 13073 override void defaultEventHandler_blur(Event ev) { 13074 super.defaultEventHandler_blur(ev); 13075 if(parentWindow.win.closed) return; 13076 version(custom_widgets) { 13077 auto painter = this.draw(); 13078 textLayout.eraseCaret(painter); 13079 static if(SimpledisplayTimerAvailable) 13080 if(caretTimer) { 13081 caretTimer.destroy(); 13082 caretTimer = null; 13083 } 13084 } 13085 13086 if(this.content != lastContentBlur) { 13087 auto evt = new ChangeEvent!string(this, &this.content); 13088 evt.dispatch(); 13089 lastContentBlur = this.content; 13090 } 13091 } 13092 } 13093 13094 version(win32_widgets) { 13095 private string lastContentBlur; 13096 13097 override void defaultEventHandler_blur(Event ev) { 13098 super.defaultEventHandler_blur(ev); 13099 13100 if(!useCustomWidget) 13101 if(this.content != lastContentBlur) { 13102 auto evt = new ChangeEvent!string(this, &this.content); 13103 evt.dispatch(); 13104 lastContentBlur = this.content; 13105 } 13106 } 13107 } 13108 13109 13110 version(trash_text) 13111 version(custom_widgets) 13112 override void defaultEventHandler_char(CharEvent ev) { 13113 super.defaultEventHandler_char(ev); 13114 textLayout.insert(ev.character); 13115 redraw(); 13116 13117 // FIXME: too inefficient 13118 auto cbb = textLayout.contentBoundingBox(); 13119 setContentSize(cbb.width, cbb.height); 13120 } 13121 version(trash_text) 13122 version(custom_widgets) 13123 override void defaultEventHandler_keydown(KeyDownEvent ev) { 13124 //super.defaultEventHandler_keydown(ev); 13125 switch(ev.key) { 13126 case Key.Delete: 13127 textLayout.delete_(); 13128 redraw(); 13129 break; 13130 case Key.Left: 13131 textLayout.moveLeft(); 13132 redraw(); 13133 break; 13134 case Key.Right: 13135 textLayout.moveRight(); 13136 redraw(); 13137 break; 13138 case Key.Up: 13139 textLayout.moveUp(); 13140 redraw(); 13141 break; 13142 case Key.Down: 13143 textLayout.moveDown(); 13144 redraw(); 13145 break; 13146 case Key.Home: 13147 textLayout.moveHome(); 13148 redraw(); 13149 break; 13150 case Key.End: 13151 textLayout.moveEnd(); 13152 redraw(); 13153 break; 13154 case Key.PageUp: 13155 foreach(i; 0 .. 32) 13156 textLayout.moveUp(); 13157 redraw(); 13158 break; 13159 case Key.PageDown: 13160 foreach(i; 0 .. 32) 13161 textLayout.moveDown(); 13162 redraw(); 13163 break; 13164 13165 default: 13166 {} // intentionally blank, let "char" handle it 13167 } 13168 /* 13169 if(ev.key == Key.Backspace) { 13170 textLayout.backspace(); 13171 redraw(); 13172 } 13173 */ 13174 ensureVisibleInScroll(textLayout.caretBoundingBox()); 13175 } 13176 13177 version(use_new_text_system) { 13178 bool showingVerticalScroll() { return true; } 13179 bool showingHorizontalScroll() { return true; } 13180 } 13181 } 13182 13183 /// 13184 class LineEdit : EditableTextWidget { 13185 override bool showingVerticalScroll() { return false; } 13186 override bool showingHorizontalScroll() { return false; } 13187 13188 override int flexBasisWidth() { return 250; } 13189 13190 /// 13191 this(Widget parent) { 13192 super(parent); 13193 version(win32_widgets) { 13194 createWin32Window(this, "edit"w, "", 13195 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 13196 } else version(custom_widgets) { 13197 version(trash_text) { 13198 setupCustomTextEditing(); 13199 addEventListener(delegate(CharEvent ev) { 13200 if(ev.character == '\n') 13201 ev.preventDefault(); 13202 }); 13203 } 13204 } else static assert(false); 13205 } 13206 13207 private this(bool useCustomWidget, Widget parent) { 13208 if(!useCustomWidget) 13209 this(parent); 13210 else 13211 super(true, parent); 13212 } 13213 13214 version(use_new_text_system) 13215 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13216 auto tdh = new TextDisplayHelper(textLayout, smw); 13217 tdh.singleLine = true; 13218 return tdh; 13219 } 13220 13221 version(win32_widgets) { 13222 mixin Padding!q{0}; 13223 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 13224 override int maxHeight() { return minHeight; } 13225 } 13226 13227 /+ 13228 @property void passwordMode(bool p) { 13229 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 13230 } 13231 +/ 13232 } 13233 13234 /// ditto 13235 class CustomLineEdit : LineEdit { 13236 this(Widget parent) { 13237 super(true, parent); 13238 } 13239 } 13240 13241 /++ 13242 A [LineEdit] that displays `*` in place of the actual characters. 13243 13244 Alas, Windows requires the window to be created differently to use this style, 13245 so it had to be a new class instead of a toggle on and off on an existing object. 13246 13247 FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now. 13248 13249 History: 13250 Added January 24, 2021 13251 +/ 13252 class PasswordEdit : EditableTextWidget { 13253 override bool showingVerticalScroll() { return false; } 13254 override bool showingHorizontalScroll() { return false; } 13255 13256 override int flexBasisWidth() { return 250; } 13257 13258 version(use_new_text_system) 13259 override TextStyle defaultTextStyle() { 13260 auto cs = getComputedStyle(); 13261 13262 auto osf = new class OperatingSystemFont { 13263 this() { 13264 super(cs.font); 13265 } 13266 override int stringWidth(scope const(char)[] text, SimpleWindow window = null) { 13267 int count = 0; 13268 foreach(dchar ch; text) 13269 count++; 13270 return count * super.stringWidth("*", window); 13271 } 13272 }; 13273 13274 return new TextDisplayHelper.MyTextStyle(osf); 13275 } 13276 13277 version(use_new_text_system) 13278 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13279 static class TDH : TextDisplayHelper { 13280 this(TextLayouter textLayout, ScrollMessageWidget smw) { 13281 singleLine = true; 13282 super(textLayout, smw); 13283 } 13284 13285 override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 13286 char[256] buffer = void; 13287 int bufferLength = 0; 13288 foreach(dchar ch; text) 13289 buffer[bufferLength++] = '*'; 13290 painter.drawText(upperLeft, buffer[0..bufferLength]); 13291 } 13292 } 13293 13294 return new TDH(textLayout, smw); 13295 } 13296 13297 /// 13298 this(Widget parent) { 13299 super(parent); 13300 version(win32_widgets) { 13301 createWin32Window(this, "edit"w, "", 13302 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 13303 } else version(custom_widgets) { 13304 version(trash_text) { 13305 setupCustomTextEditing(); 13306 13307 // should this be under trash text? i think so. 13308 addEventListener(delegate(CharEvent ev) { 13309 if(ev.character == '\n') 13310 ev.preventDefault(); 13311 }); 13312 } 13313 } else static assert(false); 13314 } 13315 13316 private this(bool useCustomWidget, Widget parent) { 13317 if(!useCustomWidget) 13318 this(parent); 13319 else 13320 super(true, parent); 13321 } 13322 13323 version(win32_widgets) { 13324 mixin Padding!q{2}; 13325 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 13326 override int maxHeight() { return minHeight; } 13327 } 13328 } 13329 13330 /// ditto 13331 class CustomPasswordEdit : PasswordEdit { 13332 this(Widget parent) { 13333 super(true, parent); 13334 } 13335 } 13336 13337 13338 /// 13339 class TextEdit : EditableTextWidget { 13340 /// 13341 this(Widget parent) { 13342 super(parent); 13343 version(win32_widgets) { 13344 createWin32Window(this, "edit"w, "", 13345 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 13346 } else version(custom_widgets) { 13347 version(trash_text) 13348 setupCustomTextEditing(); 13349 } else static assert(false); 13350 } 13351 13352 private this(bool useCustomWidget, Widget parent) { 13353 if(!useCustomWidget) 13354 this(parent); 13355 else 13356 super(true, parent); 13357 } 13358 13359 override int maxHeight() { return int.max; } 13360 override int heightStretchiness() { return 7; } 13361 13362 override int flexBasisWidth() { return 250; } 13363 override int flexBasisHeight() { return 25; } 13364 } 13365 13366 /// ditto 13367 class CustomTextEdit : TextEdit { 13368 this(Widget parent) { 13369 super(true, parent); 13370 } 13371 } 13372 13373 /+ 13374 /++ 13375 13376 +/ 13377 version(none) 13378 class RichTextDisplay : Widget { 13379 @property void content(string c) {} 13380 void appendContent(string c) {} 13381 } 13382 +/ 13383 13384 /++ 13385 A read-only text display 13386 13387 History: 13388 Added October 31, 2023 (dub v11.3) 13389 +/ 13390 class TextDisplay : EditableTextWidget { 13391 this(string text, Widget parent) { 13392 super(parent); 13393 this.content = text; 13394 } 13395 13396 override int maxHeight() { return int.max; } 13397 override int minHeight() { return 50; } 13398 override int heightStretchiness() { return 7; } 13399 13400 override int flexBasisWidth() { return 250; } 13401 override int flexBasisHeight() { return 50; } 13402 13403 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13404 return new MyTextDisplayHelper(textLayout, smw); 13405 } 13406 13407 override void registerMovement() { 13408 super.registerMovement(); 13409 this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally 13410 } 13411 13412 static class MyTextDisplayHelper : TextDisplayHelper { 13413 this(TextLayouter textLayout, ScrollMessageWidget smw) { 13414 smw.verticalScrollBar.hide(); 13415 smw.horizontalScrollBar.hide(); 13416 super(textLayout, smw); 13417 this.readonly = true; 13418 } 13419 13420 override void registerMovement() { 13421 super.registerMovement(); 13422 13423 // FIXME: do the horizontal one too as needed and make sure that it does 13424 // wordwrapping again 13425 if(l.height + smw.horizontalScrollBar.height > this.height) 13426 smw.verticalScrollBar.show(); 13427 else 13428 smw.verticalScrollBar.hide(); 13429 13430 l.wordWrapWidth = this.width; 13431 13432 smw.verticalScrollBar.setPosition = 0; 13433 } 13434 } 13435 13436 class Style : Widget.Style { 13437 // just want the generic look for these 13438 } 13439 13440 mixin OverrideStyle!Style; 13441 } 13442 13443 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active 13444 /++ 13445 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. 13446 13447 13448 When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`. 13449 13450 Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand. 13451 13452 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. 13453 13454 History: 13455 Added August 12, 2024 (dub v11.6) 13456 +/ 13457 abstract class GenericListViewWidget : Widget { 13458 /++ 13459 13460 +/ 13461 this(Widget parent) { 13462 super(parent); 13463 13464 smw = new ScrollMessageWidget(this); 13465 smw.addDefaultKeyboardListeners(); 13466 smw.addDefaultWheelListeners(itemSize.height, itemSize.width); 13467 13468 inner = new GenericListViewWidgetInner(this, smw); 13469 } 13470 13471 private ScrollMessageWidget smw; 13472 private GenericListViewWidgetInner inner; 13473 13474 /++ 13475 13476 +/ 13477 abstract GenericListViewItem itemFactory(Widget parent); 13478 // in device-dependent pixels 13479 /++ 13480 13481 +/ 13482 abstract Size itemSize(); // use 0 to indicate it can stretch? 13483 13484 enum LayoutMode { 13485 rows, 13486 columns, 13487 gridRowsFirst, 13488 gridColumnsFirst 13489 } 13490 LayoutMode layoutMode() { 13491 return LayoutMode.rows; 13492 } 13493 13494 private int itemCount_; 13495 13496 /++ 13497 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. 13498 +/ 13499 void setItemCount(int count) { 13500 smw.setTotalArea(inner.width, count * itemSize().height); 13501 smw.setViewableArea(inner.width, inner.height); 13502 this.itemCount_ = count; 13503 } 13504 13505 /++ 13506 Returns the current count of items expected to available in the list. 13507 +/ 13508 int itemCount() { 13509 return this.itemCount_; 13510 } 13511 13512 /++ 13513 Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data. 13514 13515 Note you must $(I also) call [setItemCount] if the total item count has changed. 13516 +/ 13517 void notifyItemsChanged(int index, int count = 1) { 13518 } 13519 /// ditto 13520 void notifyItemsInserted(int index, int count = 1) { 13521 } 13522 /// ditto 13523 void notifyItemsRemoved(int index, int count = 1) { 13524 } 13525 /// ditto 13526 void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) { 13527 } 13528 13529 private GenericListViewItem[] items; 13530 } 13531 13532 /// ditto 13533 abstract class GenericListViewItem : Widget { 13534 /++ 13535 +/ 13536 this(Widget parent) { 13537 super(parent); 13538 } 13539 13540 private int _currentIndex = -1; 13541 13542 private void showItemPrivate(int idx) { 13543 showItem(idx); 13544 _currentIndex = idx; 13545 } 13546 13547 /++ 13548 Implement this to show an item from your data backing to the list. 13549 13550 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. 13551 +/ 13552 abstract void showItem(int idx); 13553 13554 /++ 13555 Maintained by the library after calling [showItem] so the object knows which data index it currently has. 13556 13557 It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent). 13558 13559 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. 13560 +/ 13561 final int currentIndexLoaded() { 13562 return _currentIndex; 13563 } 13564 } 13565 13566 /// 13567 unittest { 13568 import arsd.minigui; 13569 13570 import std.conv; 13571 13572 void main() { 13573 auto mw = new MainWindow(); 13574 13575 static class MyListViewItem : GenericListViewItem { 13576 this(Widget parent) { 13577 super(parent); 13578 13579 label = new TextLabel("unloaded", TextAlignment.Left, this); 13580 button = new Button("Click", this); 13581 13582 button.addEventListener("triggered", (){ 13583 messageBox(text("clicked ", currentIndexLoaded())); 13584 }); 13585 } 13586 override void showItem(int idx) { 13587 label.label = "Item " ~ to!string(idx); 13588 } 13589 13590 TextLabel label; 13591 Button button; 13592 } 13593 13594 auto widget = new class GenericListViewWidget { 13595 this() { 13596 super(mw); 13597 } 13598 override GenericListViewItem itemFactory(Widget parent) { 13599 return new MyListViewItem(parent); 13600 } 13601 override Size itemSize() { 13602 return Size(0, scaleWithDpi(80)); 13603 } 13604 }; 13605 13606 widget.setItemCount(5000); 13607 13608 mw.loop(); 13609 } 13610 } 13611 13612 private class GenericListViewWidgetInner : Widget { 13613 this(GenericListViewWidget glvw, ScrollMessageWidget smw) { 13614 super(smw); 13615 this.glvw = glvw; 13616 this.tabStop = false; 13617 13618 reloadVisible(); 13619 13620 smw.addEventListener("scroll", () { 13621 reloadVisible(); 13622 }); 13623 } 13624 13625 override void registerMovement() { 13626 super.registerMovement(); 13627 if(glvw && glvw.smw) 13628 glvw.smw.setViewableArea(this.width, this.height); 13629 } 13630 13631 void reloadVisible() { 13632 auto y = glvw.smw.position.y / glvw.itemSize.height; 13633 int offset = glvw.smw.position.y % glvw.itemSize.height; 13634 13635 if(offset || y >= glvw.itemCount()) 13636 y--; 13637 if(y < 0) 13638 y = 0; 13639 13640 recomputeChildLayout(); 13641 13642 foreach(item; glvw.items) { 13643 if(y < glvw.itemCount()) { 13644 item.showItemPrivate(y); 13645 item.show(); 13646 } else { 13647 item.hide(); 13648 } 13649 y++; 13650 } 13651 13652 this.redraw(); 13653 } 13654 13655 private GenericListViewWidget glvw; 13656 13657 private bool inRcl; 13658 override void recomputeChildLayout() { 13659 if(inRcl) 13660 return; 13661 inRcl = true; 13662 scope(exit) 13663 inRcl = false; 13664 13665 auto ih = glvw.itemSize().height; 13666 13667 auto itemCount = this.height / ih + 2; // extra for partial display before and after 13668 bool hadNew; 13669 while(glvw.items.length < itemCount) { 13670 // FIXME: free the old items? maybe just set length 13671 glvw.items ~= glvw.itemFactory(this); 13672 hadNew = true; 13673 } 13674 13675 if(hadNew) 13676 reloadVisible(); 13677 13678 int y = -(glvw.smw.position.y % ih); 13679 foreach(child; children) { 13680 child.x = 0; 13681 child.y = y; 13682 y += glvw.itemSize().height; 13683 child.width = this.width; 13684 child.height = ih; 13685 13686 child.recomputeChildLayout(); 13687 } 13688 } 13689 } 13690 13691 13692 13693 /// 13694 class MessageBox : Window { 13695 private string message; 13696 MessageBoxButton buttonPressed = MessageBoxButton.None; 13697 /// 13698 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 13699 super(300, 100); 13700 13701 assert(buttons.length); 13702 assert(buttons.length == buttonIds.length); 13703 13704 this.message = message; 13705 13706 auto label = new TextDisplay(message, this); 13707 13708 auto hl = new HorizontalLayout(this); 13709 auto spacer = new HorizontalSpacer(hl); // to right align 13710 13711 foreach(idx, buttonText; buttons) { 13712 auto button = new CommandButton(buttonText, hl); 13713 13714 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 13715 this.buttonPressed = buttonIds[idx]; 13716 win.close(); 13717 }; })(idx)); 13718 13719 if(idx == 0) 13720 button.focus(); 13721 } 13722 13723 if(buttons.length == 1) 13724 auto spacer2 = new HorizontalSpacer(hl); // to center it 13725 13726 win.resize(scaleWithDpi(300), this.minHeight()); 13727 13728 win.show(); 13729 redraw(); 13730 } 13731 13732 mixin Padding!q{16}; 13733 } 13734 13735 /// 13736 enum MessageBoxStyle { 13737 OK, /// 13738 OKCancel, /// 13739 RetryCancel, /// 13740 YesNo, /// 13741 YesNoCancel, /// 13742 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. 13743 } 13744 13745 /// 13746 enum MessageBoxIcon { 13747 None, /// 13748 Info, /// 13749 Warning, /// 13750 Error /// 13751 } 13752 13753 /// Identifies the button the user pressed on a message box. 13754 enum MessageBoxButton { 13755 None, /// The user closed the message box without clicking any of the buttons. 13756 OK, /// 13757 Cancel, /// 13758 Retry, /// 13759 Yes, /// 13760 No, /// 13761 Continue /// 13762 } 13763 13764 13765 /++ 13766 Displays a modal message box, blocking until the user dismisses it. 13767 13768 Returns: the button pressed. 13769 +/ 13770 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13771 version(win32_widgets) { 13772 WCharzBuffer t = WCharzBuffer(title); 13773 WCharzBuffer m = WCharzBuffer(message); 13774 UINT type; 13775 with(MessageBoxStyle) 13776 final switch(style) { 13777 case OK: type |= MB_OK; break; 13778 case OKCancel: type |= MB_OKCANCEL; break; 13779 case RetryCancel: type |= MB_RETRYCANCEL; break; 13780 case YesNo: type |= MB_YESNO; break; 13781 case YesNoCancel: type |= MB_YESNOCANCEL; break; 13782 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 13783 } 13784 with(MessageBoxIcon) 13785 final switch(icon) { 13786 case None: break; 13787 case Info: type |= MB_ICONINFORMATION; break; 13788 case Warning: type |= MB_ICONWARNING; break; 13789 case Error: type |= MB_ICONERROR; break; 13790 } 13791 switch(MessageBoxW(null, m.ptr, t.ptr, type)) { 13792 case IDOK: return MessageBoxButton.OK; 13793 case IDCANCEL: return MessageBoxButton.Cancel; 13794 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 13795 case IDYES: return MessageBoxButton.Yes; 13796 case IDNO: return MessageBoxButton.No; 13797 case IDCONTINUE: return MessageBoxButton.Continue; 13798 default: return MessageBoxButton.None; 13799 } 13800 } else { 13801 string[] buttons; 13802 MessageBoxButton[] buttonIds; 13803 with(MessageBoxStyle) 13804 final switch(style) { 13805 case OK: 13806 buttons = ["OK"]; 13807 buttonIds = [MessageBoxButton.OK]; 13808 break; 13809 case OKCancel: 13810 buttons = ["OK", "Cancel"]; 13811 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 13812 break; 13813 case RetryCancel: 13814 buttons = ["Retry", "Cancel"]; 13815 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 13816 break; 13817 case YesNo: 13818 buttons = ["Yes", "No"]; 13819 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 13820 break; 13821 case YesNoCancel: 13822 buttons = ["Yes", "No", "Cancel"]; 13823 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 13824 break; 13825 case RetryCancelContinue: 13826 buttons = ["Try Again", "Cancel", "Continue"]; 13827 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 13828 break; 13829 } 13830 auto mb = new MessageBox(message, buttons, buttonIds); 13831 EventLoop el = EventLoop.get; 13832 el.run(() { return !mb.win.closed; }); 13833 return mb.buttonPressed; 13834 } 13835 } 13836 13837 /// ditto 13838 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13839 return messageBox(null, message, style, icon); 13840 } 13841 13842 13843 13844 /// 13845 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 13846 13847 /++ 13848 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 13849 13850 History: 13851 The data members were `public` (albiet undocumented and not intended for use) prior to May 13, 2021. They are now `private`, reflecting the single intended use of this object. 13852 +/ 13853 struct EventListener { 13854 private Widget widget; 13855 private string event; 13856 private EventHandler handler; 13857 private bool useCapture; 13858 13859 /// 13860 void disconnect() { 13861 widget.removeEventListener(this); 13862 } 13863 } 13864 13865 /++ 13866 The purpose of this enum was to give a compile-time checked version of various standard event strings. 13867 13868 Now, I recommend you use a statically typed event object instead. 13869 13870 See_Also: [Event] 13871 +/ 13872 enum EventType : string { 13873 click = "click", /// 13874 13875 mouseenter = "mouseenter", /// 13876 mouseleave = "mouseleave", /// 13877 mousein = "mousein", /// 13878 mouseout = "mouseout", /// 13879 mouseup = "mouseup", /// 13880 mousedown = "mousedown", /// 13881 mousemove = "mousemove", /// 13882 13883 keydown = "keydown", /// 13884 keyup = "keyup", /// 13885 char_ = "char", /// 13886 13887 focus = "focus", /// 13888 blur = "blur", /// 13889 13890 triggered = "triggered", /// 13891 13892 change = "change", /// 13893 } 13894 13895 /++ 13896 Represents an event that is currently being processed. 13897 13898 13899 Minigui's event model is based on the web browser. An event has a name, a target, 13900 and an associated data object. It starts from the window and works its way down through 13901 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 13902 then goes back up again all the way back to the window, triggering bubble phase handlers. At 13903 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 13904 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 13905 was called; that just stops it from calling other handlers in the widget tree, but the default happens 13906 whenever propagation is done, not only if it gets to the end of the chain). 13907 13908 This model has several nice points: 13909 13910 $(LIST 13911 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 13912 with event handlers set, then add/remove children as much as you want without needing 13913 to manage the event handlers on them - the parent alone can manage everything. 13914 13915 * It is easy to create new custom events in your application. 13916 13917 * It is familiar to many web developers. 13918 ) 13919 13920 There's a few downsides though: 13921 13922 $(LIST 13923 * There's not a lot of type safety. 13924 13925 * You don't get a static list of what events a widget can emit. 13926 13927 * Tracing where an event got cancelled along the chain can get difficult; the downside of 13928 the central delegation benefit is it can be lead to debugging of action at a distance. 13929 ) 13930 13931 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 13932 while keeping the benefits - and most compatibility with - the existing model. The main idea is 13933 to simply use a D object type which provides a static interface as well as a built-in event name. 13934 Then, a new static interface allows you to see what an event can emit and attach handlers to it 13935 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 13936 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 13937 to having a little more help from the D compiler and documentation generator. 13938 13939 Your code would change like this: 13940 13941 --- 13942 // old 13943 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 13944 13945 // new 13946 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 13947 --- 13948 13949 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. 13950 13951 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 13952 13953 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 13954 13955 Thus the family of functions are: 13956 13957 [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. 13958 13959 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 13960 13961 [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. 13962 13963 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 13964 13965 --- 13966 class MyCheckbox : Widget { 13967 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 13968 /// It is NOT actually required but should be used whenever possible. 13969 mixin Emits!(ChangeEvent!bool); 13970 13971 this(Widget parent) { 13972 super(parent); 13973 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 13974 } 13975 13976 private bool _checked; 13977 @property bool checked() { return _checked; } 13978 @property void checked(bool set) { 13979 _checked = set; 13980 emit!(ChangeEvent!bool)(&checked); 13981 } 13982 } 13983 --- 13984 13985 ## Creating Your Own Events 13986 13987 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. 13988 13989 --- 13990 class MyEvent : Event { 13991 this(Widget target) { super(EventString, target); } 13992 mixin Register; // adds EventString and other reflection information 13993 } 13994 --- 13995 13996 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 13997 13998 History: 13999 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. 14000 14001 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. 14002 +/ 14003 /+ 14004 14005 ## General Conventions 14006 14007 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. 14008 14009 14010 ## Qt-style signals and slots 14011 14012 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. 14013 14014 The intention is for events to be used when 14015 14016 --- 14017 class Demo : Widget { 14018 this() { 14019 myPropertyChanged = Signal!int(this); 14020 } 14021 @property myProperty(int v) { 14022 myPropertyChanged.emit(v); 14023 } 14024 14025 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 14026 // but it can just genuinely not care about `this` since that's not really passed. 14027 } 14028 14029 class Foo : Widget { 14030 // the slot uda is not necessary, but it helps the script and ui builder find it. 14031 @slot void setValue(int v) { ... } 14032 } 14033 14034 demo.myPropertyChanged.connect(&foo.setValue); 14035 --- 14036 14037 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 14038 14039 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 14040 14041 class StringChangeEvent : ChangeEvent, Signal!string { 14042 mixin SignalImpl 14043 } 14044 14045 +/ 14046 class Event : ReflectableProperties { 14047 /// Creates an event without populating any members and without sending it. See [dispatch] 14048 this(string eventName, Widget emittedBy) { 14049 this.eventName = eventName; 14050 this.srcElement = emittedBy; 14051 } 14052 14053 14054 /// Implementations for the [ReflectableProperties] interface/ 14055 void getPropertiesList(scope void delegate(string name) sink) const {} 14056 /// ditto 14057 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 14058 /// ditto 14059 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 14060 return SetPropertyResult.notPermitted; 14061 } 14062 14063 14064 /+ 14065 /++ 14066 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 14067 14068 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. 14069 +/ 14070 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 14071 if(value.length == 0) { 14072 finalSink(memberName, `""`); 14073 return; 14074 } 14075 14076 char[1024] bufferBacking; 14077 char[] buffer = bufferBacking; 14078 int bufferPosition; 14079 14080 void sink(char ch) { 14081 if(bufferPosition >= buffer.length) 14082 buffer.length = buffer.length + 1024; 14083 buffer[bufferPosition++] = ch; 14084 } 14085 14086 sink('"'); 14087 14088 foreach(ch; value) { 14089 switch(ch) { 14090 case '\\': 14091 sink('\\'); sink('\\'); 14092 break; 14093 case '"': 14094 sink('\\'); sink('"'); 14095 break; 14096 case '\n': 14097 sink('\\'); sink('n'); 14098 break; 14099 case '\r': 14100 sink('\\'); sink('r'); 14101 break; 14102 case '\t': 14103 sink('\\'); sink('t'); 14104 break; 14105 default: 14106 sink(ch); 14107 } 14108 } 14109 14110 sink('"'); 14111 14112 finalSink(memberName, buffer[0 .. bufferPosition]); 14113 } 14114 +/ 14115 14116 /+ 14117 enum EventInitiator { 14118 system, 14119 minigui, 14120 user 14121 } 14122 14123 immutable EventInitiator; initiatedBy; 14124 +/ 14125 14126 /++ 14127 Events should generally follow the propagation model, but there's some exceptions 14128 to that rule. If so, they should override this to return false. In that case, only 14129 bubbling event handlers on the target itself and capturing event handlers on the containing 14130 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 14131 capture -> target -> bubble process.) 14132 14133 History: 14134 Added May 12, 2021 14135 +/ 14136 bool propagates() const pure nothrow @nogc @safe { 14137 return true; 14138 } 14139 14140 /++ 14141 hints as to whether preventDefault will actually do anything. not entirely reliable. 14142 14143 History: 14144 Added May 14, 2021 14145 +/ 14146 bool cancelable() const pure nothrow @nogc @safe { 14147 return true; 14148 } 14149 14150 /++ 14151 You can mix this into child class to register some boilerplate. It includes the `EventString` 14152 member, a constructor, and implementations of the dynamic get data interfaces. 14153 14154 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 14155 14156 14157 You can override the default EventString by simply providing your own in the form of 14158 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 14159 which provides some namespace protection against conflicts in other libraries while still being fairly 14160 easy to use. 14161 14162 If you provide your own constructor, it will override the default constructor provided here. A constructor 14163 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 14164 first argument to your constructor. 14165 14166 History: 14167 Added May 13, 2021. 14168 +/ 14169 protected static mixin template Register() { 14170 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 14171 this(Widget target) { super(EventString, target); } 14172 14173 mixin ReflectableProperties.RegisterGetters; 14174 } 14175 14176 /++ 14177 This is the widget that emitted the event. 14178 14179 14180 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 14181 14182 History: 14183 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 14184 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 14185 so I don't intend to remove these aliases. 14186 +/ 14187 Widget source; 14188 /// ditto 14189 alias source target; 14190 /// ditto 14191 alias source srcElement; 14192 14193 Widget relatedTarget; /// Note: likely to be deprecated at some point. 14194 14195 /// Prevents the default event handler (if there is one) from being called 14196 void preventDefault() { 14197 lastDefaultPrevented = true; 14198 defaultPrevented = true; 14199 } 14200 14201 /// Stops the event propagation immediately. 14202 void stopPropagation() { 14203 propagationStopped = true; 14204 } 14205 14206 private bool defaultPrevented; 14207 private bool propagationStopped; 14208 private string eventName; 14209 14210 private bool isBubbling; 14211 14212 /// 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. 14213 protected void adjustScrolling() { } 14214 /// ditto 14215 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 14216 14217 /++ 14218 this sends it only to the target. If you want propagation, use dispatch() instead. 14219 14220 This should be made private!!! 14221 14222 +/ 14223 void sendDirectly() { 14224 if(srcElement is null) 14225 return; 14226 14227 // 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. 14228 14229 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 14230 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 14231 14232 adjustScrolling(); 14233 14234 if(auto e = target.parentWindow) { 14235 if(auto handlers = "*" in e.capturingEventHandlers) 14236 foreach(handler; *handlers) 14237 if(handler) handler(e, this); 14238 if(auto handlers = eventName in e.capturingEventHandlers) 14239 foreach(handler; *handlers) 14240 if(handler) handler(e, this); 14241 } 14242 14243 auto e = srcElement; 14244 14245 if(auto handlers = eventName in e.bubblingEventHandlers) 14246 foreach(handler; *handlers) 14247 if(handler) handler(e, this); 14248 14249 if(auto handlers = "*" in e.bubblingEventHandlers) 14250 foreach(handler; *handlers) 14251 if(handler) handler(e, this); 14252 14253 // there's never a default for a catch-all event 14254 if(!defaultPrevented) 14255 if(eventName in e.defaultEventHandlers) 14256 e.defaultEventHandlers[eventName](e, this); 14257 } 14258 14259 /// this dispatches the element using the capture -> target -> bubble process 14260 void dispatch() { 14261 if(srcElement is null) 14262 return; 14263 14264 if(!propagates) { 14265 sendDirectly; 14266 return; 14267 } 14268 14269 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 14270 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 14271 14272 adjustScrolling(); 14273 // first capture, then bubble 14274 14275 Widget[] chain; 14276 Widget curr = srcElement; 14277 while(curr) { 14278 auto l = curr; 14279 chain ~= l; 14280 curr = curr.parent; 14281 } 14282 14283 isBubbling = false; 14284 14285 foreach_reverse(e; chain) { 14286 if(auto handlers = "*" in e.capturingEventHandlers) 14287 foreach(handler; *handlers) if(handler !is null) handler(e, this); 14288 14289 if(propagationStopped) 14290 break; 14291 14292 if(auto handlers = eventName in e.capturingEventHandlers) 14293 foreach(handler; *handlers) if(handler !is null) handler(e, this); 14294 14295 // the default on capture should really be to always do nothing 14296 14297 //if(!defaultPrevented) 14298 // if(eventName in e.defaultEventHandlers) 14299 // e.defaultEventHandlers[eventName](e.element, this); 14300 14301 if(propagationStopped) 14302 break; 14303 } 14304 14305 int adjustX; 14306 int adjustY; 14307 14308 isBubbling = true; 14309 if(!propagationStopped) 14310 foreach(e; chain) { 14311 if(auto handlers = eventName in e.bubblingEventHandlers) 14312 foreach(handler; *handlers) if(handler !is null) handler(e, this); 14313 14314 if(propagationStopped) 14315 break; 14316 14317 if(auto handlers = "*" in e.bubblingEventHandlers) 14318 foreach(handler; *handlers) if(handler !is null) handler(e, this); 14319 14320 if(propagationStopped) 14321 break; 14322 14323 if(e.encapsulatedChildren()) { 14324 adjustClientCoordinates(adjustX, adjustY); 14325 target = e; 14326 } else { 14327 adjustX += e.x; 14328 adjustY += e.y; 14329 } 14330 } 14331 14332 if(!defaultPrevented) 14333 foreach(e; chain) { 14334 if(eventName in e.defaultEventHandlers) 14335 e.defaultEventHandlers[eventName](e, this); 14336 } 14337 } 14338 14339 14340 /* old compatibility things */ 14341 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 14342 final @property { 14343 Key key() { return (cast(KeyEventBase) this).key; } 14344 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 14345 14346 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 14347 bool altKey() { return (cast(KeyEventBase) this).altKey; } 14348 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 14349 } 14350 14351 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 14352 final @property { 14353 int clientX() { return (cast(MouseEventBase) this).clientX; } 14354 int clientY() { return (cast(MouseEventBase) this).clientY; } 14355 14356 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 14357 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 14358 14359 int button() { return (cast(MouseEventBase) this).button; } 14360 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 14361 } 14362 14363 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 14364 final @property { 14365 int state() { 14366 if(auto meb = cast(MouseEventBase) this) 14367 return meb.state; 14368 if(auto keb = cast(KeyEventBase) this) 14369 return keb.state; 14370 assert(0); 14371 } 14372 } 14373 14374 deprecated("Use a CharEvent instead of Event in your handler going forward") 14375 final @property { 14376 dchar character() { 14377 if(auto ce = cast(CharEvent) this) 14378 return ce.character; 14379 return dchar.init; 14380 } 14381 } 14382 14383 // for change events 14384 @property { 14385 /// 14386 int intValue() { return 0; } 14387 /// 14388 string stringValue() { return null; } 14389 } 14390 } 14391 14392 /++ 14393 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 14394 14395 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 14396 dynamic and custom events, but the static list helps ensure you get them right. 14397 14398 If this is declared, you can use [Widget.emit] to send the event. 14399 14400 All events work the same way though, following the capture->widget->bubble model described under [Event]. 14401 14402 History: 14403 Added May 4, 2021 14404 +/ 14405 mixin template Emits(EventType) { 14406 import arsd.minigui : EventString; 14407 static if(is(EventType : Event) && !is(EventType == Event)) 14408 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 14409 else 14410 static assert(0, "You can only emit subclasses of Event"); 14411 } 14412 14413 /// ditto 14414 mixin template Emits(string eventString) { 14415 mixin("private Event[0] emits_" ~ eventString ~";"); 14416 } 14417 14418 /* 14419 class SignalEvent(string name) : Event { 14420 14421 } 14422 */ 14423 14424 /++ 14425 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". 14426 14427 14428 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. 14429 14430 History: 14431 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 14432 +/ 14433 class CommandEvent : Event { 14434 enum EventString = "command"; 14435 this(Widget source, string CommandString = EventString) { 14436 super(CommandString, source); 14437 } 14438 } 14439 14440 /++ 14441 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 14442 +/ 14443 class CommandEventWithArgs(Args...) : CommandEvent { 14444 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 14445 Args args; 14446 } 14447 14448 /++ 14449 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. 14450 14451 See [CommandEvent] for more information. 14452 14453 Returns: 14454 The [EventListener] you can use to remove the handler. 14455 +/ 14456 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 14457 return w.addEventListener(CommandString, (Event ev) { 14458 if(ev.target is w) 14459 return; // it does not consume its own commands! 14460 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 14461 handler(cev.args); 14462 ev.stopPropagation(); 14463 } 14464 }); 14465 } 14466 14467 /++ 14468 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. 14469 +/ 14470 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 14471 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 14472 event.dispatch(); 14473 } 14474 14475 class ResizeEvent : Event { 14476 enum EventString = "resize"; 14477 14478 this(Widget target) { super(EventString, target); } 14479 14480 override bool propagates() const { return false; } 14481 } 14482 14483 /++ 14484 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 14485 14486 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. 14487 14488 History: 14489 Added June 21, 2021 (dub v10.1) 14490 +/ 14491 class ClosingEvent : Event { 14492 enum EventString = "closing"; 14493 14494 this(Widget target) { super(EventString, target); } 14495 14496 override bool propagates() const { return false; } 14497 override bool cancelable() const { return true; } 14498 } 14499 14500 /// ditto 14501 class ClosedEvent : Event { 14502 enum EventString = "closed"; 14503 14504 this(Widget target) { super(EventString, target); } 14505 14506 override bool propagates() const { return false; } 14507 override bool cancelable() const { return false; } 14508 } 14509 14510 /// 14511 class BlurEvent : Event { 14512 enum EventString = "blur"; 14513 14514 // FIXME: related target? 14515 this(Widget target) { super(EventString, target); } 14516 14517 override bool propagates() const { return false; } 14518 } 14519 14520 /// 14521 class FocusEvent : Event { 14522 enum EventString = "focus"; 14523 14524 // FIXME: related target? 14525 this(Widget target) { super(EventString, target); } 14526 14527 override bool propagates() const { return false; } 14528 } 14529 14530 /++ 14531 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 14532 14533 History: 14534 Added July 3, 2021 14535 +/ 14536 class FocusInEvent : Event { 14537 enum EventString = "focusin"; 14538 14539 // FIXME: related target? 14540 this(Widget target) { super(EventString, target); } 14541 14542 override bool cancelable() const { return false; } 14543 } 14544 14545 /// ditto 14546 class FocusOutEvent : Event { 14547 enum EventString = "focusout"; 14548 14549 // FIXME: related target? 14550 this(Widget target) { super(EventString, target); } 14551 14552 override bool cancelable() const { return false; } 14553 } 14554 14555 /// 14556 class ScrollEvent : Event { 14557 enum EventString = "scroll"; 14558 this(Widget target) { super(EventString, target); } 14559 14560 override bool cancelable() const { return false; } 14561 } 14562 14563 /++ 14564 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 14565 14566 History: 14567 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 14568 +/ 14569 class CharEvent : Event { 14570 enum EventString = "char"; 14571 this(Widget target, dchar ch) { 14572 character = ch; 14573 super(EventString, target); 14574 } 14575 14576 immutable dchar character; 14577 } 14578 14579 /++ 14580 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 14581 +/ 14582 abstract class ChangeEventBase : Event { 14583 enum EventString = "change"; 14584 this(Widget target) { 14585 super(EventString, target); 14586 } 14587 14588 /+ 14589 // idk where or how exactly i want to do this. 14590 // i might come back to it later. 14591 14592 // If a widget itself broadcasts one of theses itself, it stops propagation going down 14593 // this way the source doesn't get too confused (think of a nested scroll widget) 14594 // 14595 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 14596 // then you consume that command and change you scroll x position to whatever. then you do 14597 // some kind of change event that is broadcast back to the children and any horizontal scroll 14598 // listeners are now able to update, without having an explicit connection between them. 14599 void broadcastToChildren(string fieldName) { 14600 14601 } 14602 +/ 14603 } 14604 14605 /++ 14606 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. 14607 14608 14609 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). 14610 14611 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);` 14612 14613 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 14614 14615 History: 14616 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. 14617 +/ 14618 class ChangeEvent(T) : ChangeEventBase { 14619 this(Widget target, T delegate() getNewValue) { 14620 assert(getNewValue !is null); 14621 this.getNewValue = getNewValue; 14622 super(target); 14623 } 14624 14625 private T delegate() getNewValue; 14626 14627 /++ 14628 Gets the new value that just changed. 14629 +/ 14630 @property T value() { 14631 return getNewValue(); 14632 } 14633 14634 /// compatibility method for old generic Events 14635 static if(is(immutable T == immutable int)) 14636 override int intValue() { return value; } 14637 /// ditto 14638 static if(is(immutable T == immutable string)) 14639 override string stringValue() { return value; } 14640 } 14641 14642 /++ 14643 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 14644 14645 14646 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14647 14648 History: 14649 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14650 +/ 14651 abstract class KeyEventBase : Event { 14652 this(string name, Widget target) { 14653 super(name, target); 14654 } 14655 14656 // for key events 14657 Key key; /// 14658 14659 KeyEvent originalKeyEvent; 14660 14661 /++ 14662 Indicates the current state of the given keyboard modifier keys. 14663 14664 History: 14665 Added to events on April 15, 2020. 14666 +/ 14667 bool ctrlKey; 14668 14669 /// ditto 14670 bool altKey; 14671 14672 /// ditto 14673 bool shiftKey; 14674 14675 /++ 14676 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 14677 14678 See [arsd.simpledisplay.ModifierState] for other possible flags. 14679 +/ 14680 int state; 14681 14682 mixin Register; 14683 } 14684 14685 /++ 14686 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]. 14687 14688 14689 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14690 14691 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. 14692 14693 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. 14694 14695 See_Also: [KeyUpEvent], [CharEvent] 14696 14697 History: 14698 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 14699 +/ 14700 class KeyDownEvent : KeyEventBase { 14701 enum EventString = "keydown"; 14702 this(Widget target) { super(EventString, target); } 14703 } 14704 14705 /++ 14706 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 14707 14708 14709 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14710 14711 See_Also: [KeyDownEvent], [CharEvent] 14712 14713 History: 14714 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 14715 +/ 14716 class KeyUpEvent : KeyEventBase { 14717 enum EventString = "keyup"; 14718 this(Widget target) { super(EventString, target); } 14719 } 14720 14721 /++ 14722 Contains shared properties for various mouse events; 14723 14724 14725 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14726 14727 History: 14728 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14729 +/ 14730 abstract class MouseEventBase : Event { 14731 this(string name, Widget target) { 14732 super(name, target); 14733 } 14734 14735 // for mouse events 14736 int clientX; /// The mouse event location relative to the target widget 14737 int clientY; /// ditto 14738 14739 int viewportX; /// The mouse event location relative to the window origin 14740 int viewportY; /// ditto 14741 14742 int button; /// See: [MouseEvent.button] 14743 int buttonLinear; /// See: [MouseEvent.buttonLinear] 14744 14745 /++ 14746 Indicates the current state of the given keyboard modifier keys. 14747 14748 History: 14749 Added to mouse events on September 28, 2010. 14750 +/ 14751 bool ctrlKey; 14752 14753 /// ditto 14754 bool altKey; 14755 14756 /// ditto 14757 bool shiftKey; 14758 14759 14760 14761 int state; /// 14762 14763 /++ 14764 for consistent names with key event. 14765 14766 History: 14767 Added September 28, 2021 (dub v10.3) 14768 +/ 14769 alias modifierState = state; 14770 14771 /++ 14772 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 14773 14774 History: 14775 Added May 15, 2021 14776 +/ 14777 bool isMouseWheel() { 14778 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 14779 } 14780 14781 // private 14782 override void adjustClientCoordinates(int deltaX, int deltaY) { 14783 clientX += deltaX; 14784 clientY += deltaY; 14785 } 14786 14787 override void adjustScrolling() { 14788 version(custom_widgets) { // TEMP 14789 viewportX = clientX; 14790 viewportY = clientY; 14791 if(auto se = cast(ScrollableWidget) srcElement) { 14792 clientX += se.scrollOrigin.x; 14793 clientY += se.scrollOrigin.y; 14794 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 14795 //clientX += se.scrollX_; 14796 //clientY += se.scrollY_; 14797 } 14798 } 14799 } 14800 14801 mixin Register; 14802 } 14803 14804 /++ 14805 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 14806 14807 14808 $(WARNING 14809 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 14810 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 14811 behavior. 14812 ) 14813 14814 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 14815 14816 [MouseUpEvent] is sent when the user releases a mouse button. 14817 14818 [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.) 14819 14820 [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. 14821 14822 [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 differnet 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. 14823 14824 [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. 14825 14826 [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. 14827 14828 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 14829 14830 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 14831 14832 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14833 14834 Rationale: 14835 14836 If you only want to do drag, mousedown/up works just fine being consistently sent. 14837 14838 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). 14839 14840 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. 14841 14842 History: 14843 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. 14844 +/ 14845 class MouseUpEvent : MouseEventBase { 14846 enum EventString = "mouseup"; /// 14847 this(Widget target) { super(EventString, target); } 14848 } 14849 /// ditto 14850 class MouseDownEvent : MouseEventBase { 14851 enum EventString = "mousedown"; /// 14852 this(Widget target) { super(EventString, target); } 14853 } 14854 /// ditto 14855 class MouseMoveEvent : MouseEventBase { 14856 enum EventString = "mousemove"; /// 14857 this(Widget target) { super(EventString, target); } 14858 } 14859 /// ditto 14860 class ClickEvent : MouseEventBase { 14861 enum EventString = "click"; /// 14862 this(Widget target) { super(EventString, target); } 14863 } 14864 /// ditto 14865 class DoubleClickEvent : MouseEventBase { 14866 enum EventString = "dblclick"; /// 14867 this(Widget target) { super(EventString, target); } 14868 } 14869 /// ditto 14870 class MouseOverEvent : Event { 14871 enum EventString = "mouseover"; /// 14872 this(Widget target) { super(EventString, target); } 14873 } 14874 /// ditto 14875 class MouseOutEvent : Event { 14876 enum EventString = "mouseout"; /// 14877 this(Widget target) { super(EventString, target); } 14878 } 14879 /// ditto 14880 class MouseEnterEvent : Event { 14881 enum EventString = "mouseenter"; /// 14882 this(Widget target) { super(EventString, target); } 14883 14884 override bool propagates() const { return false; } 14885 } 14886 /// ditto 14887 class MouseLeaveEvent : Event { 14888 enum EventString = "mouseleave"; /// 14889 this(Widget target) { super(EventString, target); } 14890 14891 override bool propagates() const { return false; } 14892 } 14893 14894 private bool isAParentOf(Widget a, Widget b) { 14895 if(a is null || b is null) 14896 return false; 14897 14898 while(b !is null) { 14899 if(a is b) 14900 return true; 14901 b = b.parent; 14902 } 14903 14904 return false; 14905 } 14906 14907 private struct WidgetAtPointResponse { 14908 Widget widget; 14909 14910 // x, y relative to the widget in the response. 14911 int x; 14912 int y; 14913 } 14914 14915 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 14916 assert(starting !is null); 14917 14918 starting.addScrollPosition(x, y); 14919 14920 auto child = starting.getChildAtPosition(x, y); 14921 while(child) { 14922 if(child.hidden) 14923 continue; 14924 starting = child; 14925 x -= child.x; 14926 y -= child.y; 14927 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 14928 child = r.widget; 14929 if(child is starting) 14930 break; 14931 } 14932 return WidgetAtPointResponse(starting, x, y); 14933 } 14934 14935 version(win32_widgets) { 14936 private: 14937 import core.sys.windows.commctrl; 14938 14939 pragma(lib, "comctl32"); 14940 shared static this() { 14941 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 14942 INITCOMMONCONTROLSEX ic; 14943 ic.dwSize = cast(DWORD) ic.sizeof; 14944 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 14945 if(!InitCommonControlsEx(&ic)) { 14946 //writeln("ICC failed"); 14947 } 14948 } 14949 14950 14951 // everything from here is just win32 headers copy pasta 14952 private: 14953 extern(Windows): 14954 14955 alias HANDLE HMENU; 14956 HMENU CreateMenu(); 14957 bool SetMenu(HWND, HMENU); 14958 HMENU CreatePopupMenu(); 14959 enum MF_POPUP = 0x10; 14960 enum MF_STRING = 0; 14961 14962 14963 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 14964 struct INITCOMMONCONTROLSEX { 14965 DWORD dwSize; 14966 DWORD dwICC; 14967 } 14968 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 14969 enum { 14970 IDB_STD_SMALL_COLOR, 14971 IDB_STD_LARGE_COLOR, 14972 IDB_VIEW_SMALL_COLOR = 4, 14973 IDB_VIEW_LARGE_COLOR = 5 14974 } 14975 enum { 14976 STD_CUT, 14977 STD_COPY, 14978 STD_PASTE, 14979 STD_UNDO, 14980 STD_REDOW, 14981 STD_DELETE, 14982 STD_FILENEW, 14983 STD_FILEOPEN, 14984 STD_FILESAVE, 14985 STD_PRINTPRE, 14986 STD_PROPERTIES, 14987 STD_HELP, 14988 STD_FIND, 14989 STD_REPLACE, 14990 STD_PRINT // = 14 14991 } 14992 14993 alias HANDLE HIMAGELIST; 14994 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 14995 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 14996 BOOL ImageList_Destroy(HIMAGELIST); 14997 14998 uint MAKELONG(ushort a, ushort b) { 14999 return cast(uint) ((b << 16) | a); 15000 } 15001 15002 15003 struct TBBUTTON { 15004 int iBitmap; 15005 int idCommand; 15006 BYTE fsState; 15007 BYTE fsStyle; 15008 version(Win64) 15009 BYTE[6] bReserved; 15010 else 15011 BYTE[2] bReserved; 15012 DWORD dwData; 15013 INT_PTR iString; 15014 } 15015 15016 enum { 15017 TB_ADDBUTTONSA = WM_USER + 20, 15018 TB_INSERTBUTTONA = WM_USER + 21, 15019 TB_GETIDEALSIZE = WM_USER + 99, 15020 } 15021 15022 struct SIZE { 15023 LONG cx; 15024 LONG cy; 15025 } 15026 15027 15028 enum { 15029 TBSTATE_CHECKED = 1, 15030 TBSTATE_PRESSED = 2, 15031 TBSTATE_ENABLED = 4, 15032 TBSTATE_HIDDEN = 8, 15033 TBSTATE_INDETERMINATE = 16, 15034 TBSTATE_WRAP = 32 15035 } 15036 15037 15038 15039 enum { 15040 ILC_COLOR = 0, 15041 ILC_COLOR4 = 4, 15042 ILC_COLOR8 = 8, 15043 ILC_COLOR16 = 16, 15044 ILC_COLOR24 = 24, 15045 ILC_COLOR32 = 32, 15046 ILC_COLORDDB = 254, 15047 ILC_MASK = 1, 15048 ILC_PALETTE = 2048 15049 } 15050 15051 15052 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 15053 15054 15055 enum { 15056 TB_ENABLEBUTTON = WM_USER + 1, 15057 TB_CHECKBUTTON, 15058 TB_PRESSBUTTON, 15059 TB_HIDEBUTTON, 15060 TB_INDETERMINATE, // = WM_USER + 5, 15061 TB_ISBUTTONENABLED = WM_USER + 9, 15062 TB_ISBUTTONCHECKED, 15063 TB_ISBUTTONPRESSED, 15064 TB_ISBUTTONHIDDEN, 15065 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 15066 TB_SETSTATE = WM_USER + 17, 15067 TB_GETSTATE = WM_USER + 18, 15068 TB_ADDBITMAP = WM_USER + 19, 15069 TB_DELETEBUTTON = WM_USER + 22, 15070 TB_GETBUTTON, 15071 TB_BUTTONCOUNT, 15072 TB_COMMANDTOINDEX, 15073 TB_SAVERESTOREA, 15074 TB_CUSTOMIZE, 15075 TB_ADDSTRINGA, 15076 TB_GETITEMRECT, 15077 TB_BUTTONSTRUCTSIZE, 15078 TB_SETBUTTONSIZE, 15079 TB_SETBITMAPSIZE, 15080 TB_AUTOSIZE, // = WM_USER + 33, 15081 TB_GETTOOLTIPS = WM_USER + 35, 15082 TB_SETTOOLTIPS = WM_USER + 36, 15083 TB_SETPARENT = WM_USER + 37, 15084 TB_SETROWS = WM_USER + 39, 15085 TB_GETROWS, 15086 TB_GETBITMAPFLAGS, 15087 TB_SETCMDID, 15088 TB_CHANGEBITMAP, 15089 TB_GETBITMAP, 15090 TB_GETBUTTONTEXTA, 15091 TB_REPLACEBITMAP, // = WM_USER + 46, 15092 TB_GETBUTTONSIZE = WM_USER + 58, 15093 TB_SETBUTTONWIDTH = WM_USER + 59, 15094 TB_GETBUTTONTEXTW = WM_USER + 75, 15095 TB_SAVERESTOREW = WM_USER + 76, 15096 TB_ADDSTRINGW = WM_USER + 77, 15097 } 15098 15099 extern(Windows) 15100 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 15101 15102 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 15103 15104 15105 enum { 15106 TB_SETINDENT = WM_USER + 47, 15107 TB_SETIMAGELIST, 15108 TB_GETIMAGELIST, 15109 TB_LOADIMAGES, 15110 TB_GETRECT, 15111 TB_SETHOTIMAGELIST, 15112 TB_GETHOTIMAGELIST, 15113 TB_SETDISABLEDIMAGELIST, 15114 TB_GETDISABLEDIMAGELIST, 15115 TB_SETSTYLE, 15116 TB_GETSTYLE, 15117 //TB_GETBUTTONSIZE, 15118 //TB_SETBUTTONWIDTH, 15119 TB_SETMAXTEXTROWS, 15120 TB_GETTEXTROWS // = WM_USER + 61 15121 } 15122 15123 enum { 15124 CCM_FIRST = 0x2000, 15125 CCM_LAST = CCM_FIRST + 0x200, 15126 CCM_SETBKCOLOR = 8193, 15127 CCM_SETCOLORSCHEME = 8194, 15128 CCM_GETCOLORSCHEME = 8195, 15129 CCM_GETDROPTARGET = 8196, 15130 CCM_SETUNICODEFORMAT = 8197, 15131 CCM_GETUNICODEFORMAT = 8198, 15132 CCM_SETVERSION = 0x2007, 15133 CCM_GETVERSION = 0x2008, 15134 CCM_SETNOTIFYWINDOW = 0x2009 15135 } 15136 15137 15138 enum { 15139 PBM_SETRANGE = WM_USER + 1, 15140 PBM_SETPOS, 15141 PBM_DELTAPOS, 15142 PBM_SETSTEP, 15143 PBM_STEPIT, // = WM_USER + 5 15144 PBM_SETRANGE32 = 1030, 15145 PBM_GETRANGE, 15146 PBM_GETPOS, 15147 PBM_SETBARCOLOR, // = 1033 15148 PBM_SETBKCOLOR = CCM_SETBKCOLOR 15149 } 15150 15151 enum { 15152 PBS_SMOOTH = 1, 15153 PBS_VERTICAL = 4 15154 } 15155 15156 enum { 15157 ICC_LISTVIEW_CLASSES = 1, 15158 ICC_TREEVIEW_CLASSES = 2, 15159 ICC_BAR_CLASSES = 4, 15160 ICC_TAB_CLASSES = 8, 15161 ICC_UPDOWN_CLASS = 16, 15162 ICC_PROGRESS_CLASS = 32, 15163 ICC_HOTKEY_CLASS = 64, 15164 ICC_ANIMATE_CLASS = 128, 15165 ICC_WIN95_CLASSES = 255, 15166 ICC_DATE_CLASSES = 256, 15167 ICC_USEREX_CLASSES = 512, 15168 ICC_COOL_CLASSES = 1024, 15169 ICC_STANDARD_CLASSES = 0x00004000, 15170 } 15171 15172 enum WM_USER = 1024; 15173 } 15174 15175 version(win32_widgets) 15176 pragma(lib, "comdlg32"); 15177 15178 15179 /// 15180 enum GenericIcons : ushort { 15181 None, /// 15182 // these happen to match the win32 std icons numerically if you just subtract one from the value 15183 Cut, /// 15184 Copy, /// 15185 Paste, /// 15186 Undo, /// 15187 Redo, /// 15188 Delete, /// 15189 New, /// 15190 Open, /// 15191 Save, /// 15192 PrintPreview, /// 15193 Properties, /// 15194 Help, /// 15195 Find, /// 15196 Replace, /// 15197 Print, /// 15198 } 15199 15200 enum FileDialogType { 15201 Automatic, 15202 Open, 15203 Save 15204 } 15205 string previousFileReferenced; 15206 15207 /++ 15208 Used in automatic menu functions to indicate that the user should be able to browse for a file. 15209 15210 Params: 15211 storage = an alias to a `static string` variable that stores the last file referenced. It will 15212 use this to pre-fill the dialog with a suggestion. 15213 15214 Please note that it MUST be `static` or you will get compile errors. 15215 15216 filters = the filters param to [getFileName] 15217 15218 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 15219 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 15220 a save dialog box. Otherwise, it will show an open dialog box. 15221 +/ 15222 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 15223 string name; 15224 alias name this; 15225 } 15226 15227 /++ 15228 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. 15229 15230 History: 15231 onCancel was added November 6, 2021. 15232 15233 The dialog itself on Linux was modified on December 2, 2021 to include 15234 a directory picker in addition to the command line completion view. 15235 15236 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 15237 Future_directions: 15238 I want to add some kind of custom preview and maybe thumbnail thing in the future, 15239 at least on Linux, maybe on Windows too. 15240 +/ 15241 void getOpenFileName( 15242 void delegate(string) onOK, 15243 string prefilledName = null, 15244 string[] filters = null, 15245 void delegate() onCancel = null, 15246 string initialDirectory = null, 15247 ) 15248 { 15249 return getFileName(true, onOK, prefilledName, filters, onCancel, initialDirectory); 15250 } 15251 15252 /// ditto 15253 void getSaveFileName( 15254 void delegate(string) onOK, 15255 string prefilledName = null, 15256 string[] filters = null, 15257 void delegate() onCancel = null, 15258 string initialDirectory = null, 15259 ) 15260 { 15261 return getFileName(false, onOK, prefilledName, filters, onCancel, initialDirectory); 15262 } 15263 15264 void getFileName( 15265 bool openOrSave, 15266 void delegate(string) onOK, 15267 string prefilledName = null, 15268 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 15269 void delegate() onCancel = null, 15270 string initialDirectory = null, 15271 ) 15272 { 15273 15274 version(win32_widgets) { 15275 import core.sys.windows.commdlg; 15276 /* 15277 Ofn.lStructSize = sizeof(OPENFILENAME); 15278 Ofn.hwndOwner = hWnd; 15279 Ofn.lpstrFilter = szFilter; 15280 Ofn.lpstrFile= szFile; 15281 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 15282 Ofn.lpstrFileTitle = szFileTitle; 15283 Ofn.nMaxFileTitle = sizeof(szFileTitle); 15284 Ofn.lpstrInitialDir = (LPSTR)NULL; 15285 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 15286 Ofn.lpstrTitle = szTitle; 15287 */ 15288 15289 15290 wchar[1024] file = 0; 15291 wchar[1024] filterBuffer = 0; 15292 makeWindowsString(prefilledName, file[]); 15293 OPENFILENAME ofn; 15294 ofn.lStructSize = ofn.sizeof; 15295 if(filters.length) { 15296 string filter; 15297 foreach(i, f; filters) { 15298 filter ~= f; 15299 filter ~= "\0"; 15300 } 15301 filter ~= "\0"; 15302 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 15303 } 15304 ofn.lpstrFile = file.ptr; 15305 ofn.nMaxFile = file.length; 15306 15307 wchar[1024] initialDir = 0; 15308 if(initialDirectory !is null) { 15309 makeWindowsString(initialDirectory, initialDir[]); 15310 ofn.lpstrInitialDir = file.ptr; 15311 } 15312 15313 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 15314 { 15315 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 15316 if(okString.length && okString[$-1] == '\0') 15317 okString = okString[0..$-1]; 15318 onOK(okString); 15319 } else { 15320 if(onCancel) 15321 onCancel(); 15322 } 15323 } else version(custom_widgets) { 15324 if(filters.length == 0) 15325 filters = ["All Files\0*.*"]; 15326 auto picker = new FilePicker(prefilledName, filters, initialDirectory); 15327 picker.onOK = onOK; 15328 picker.onCancel = onCancel; 15329 picker.show(); 15330 } 15331 } 15332 15333 version(custom_widgets) 15334 private 15335 class FilePicker : Dialog { 15336 void delegate(string) onOK; 15337 void delegate() onCancel; 15338 LineEdit lineEdit; 15339 15340 // returns common prefix 15341 string loadFiles(string cwd, string[] filters...) { 15342 string[] files; 15343 string[] dirs; 15344 15345 string commonPrefix; 15346 15347 getFiles(cwd, (string name, bool isDirectory) { 15348 if(name == ".") 15349 return; // skip this as unnecessary 15350 if(isDirectory) 15351 dirs ~= name; 15352 else { 15353 foreach(filter; filters) 15354 if( 15355 filter.length <= 1 || 15356 filter == "*.*" || 15357 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 15358 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 15359 ) 15360 { 15361 files ~= name; 15362 15363 if(filter.length > 0 && filter[$-1] == '*') { 15364 if(commonPrefix is null) { 15365 commonPrefix = name; 15366 } else { 15367 foreach(idx, char i; name) { 15368 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 15369 commonPrefix = commonPrefix[0 .. idx]; 15370 break; 15371 } 15372 } 15373 } 15374 } 15375 15376 break; 15377 } 15378 } 15379 }); 15380 15381 extern(C) static int comparator(scope const void* a, scope const void* b) { 15382 auto sa = *cast(string*) a; 15383 auto sb = *cast(string*) b; 15384 15385 for(int i = 0; i < sa.length; i++) { 15386 if(i == sb.length) 15387 return 1; 15388 auto diff = sa[i] - sb[i]; 15389 if(diff) 15390 return diff; 15391 } 15392 15393 return 0; 15394 } 15395 15396 nonPhobosSort(files, &comparator); 15397 nonPhobosSort(dirs, &comparator); 15398 15399 listWidget.clear(); 15400 dirWidget.clear(); 15401 foreach(name; dirs) 15402 dirWidget.addOption(name); 15403 foreach(name; files) 15404 listWidget.addOption(name); 15405 15406 return commonPrefix; 15407 } 15408 15409 ListWidget listWidget; 15410 ListWidget dirWidget; 15411 15412 string currentDirectory; 15413 string[] processedFilters; 15414 15415 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"] 15416 this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) { 15417 super(300, 200, "Choose File..."); // owner); 15418 15419 foreach(filter; filters) { 15420 while(filter.length && filter[0] != 0) { 15421 filter = filter[1 .. $]; 15422 } 15423 if(filter.length) 15424 filter = filter[1 .. $]; // trim off the 0 15425 15426 while(filter.length) { 15427 int idx = 0; 15428 while(idx < filter.length && filter[idx] != ';') { 15429 idx++; 15430 } 15431 15432 processedFilters ~= filter[0 .. idx]; 15433 if(idx < filter.length) 15434 idx++; // skip the ; 15435 filter = filter[idx .. $]; 15436 } 15437 } 15438 15439 currentDirectory = initialDirectory is null ? "." : initialDirectory; 15440 15441 { 15442 auto hl = new HorizontalLayout(this); 15443 dirWidget = new ListWidget(hl); 15444 listWidget = new ListWidget(hl); 15445 15446 // double click events normally trigger something else but 15447 // here user might be clicking kinda fast and we'd rather just 15448 // keep it 15449 dirWidget.addEventListener((scope DoubleClickEvent dev) { 15450 auto ce = new ChangeEvent!void(dirWidget, () {}); 15451 ce.dispatch(); 15452 }); 15453 15454 dirWidget.addEventListener((scope ChangeEvent!void sce) { 15455 string v; 15456 foreach(o; dirWidget.options) 15457 if(o.selected) { 15458 v = o.label; 15459 break; 15460 } 15461 if(v.length) { 15462 currentDirectory ~= "/" ~ v; 15463 loadFiles(currentDirectory, processedFilters); 15464 } 15465 }); 15466 15467 // double click here, on the other hand, selects the file 15468 // and moves on 15469 listWidget.addEventListener((scope DoubleClickEvent dev) { 15470 OK(); 15471 }); 15472 } 15473 15474 lineEdit = new LineEdit(this); 15475 lineEdit.focus(); 15476 lineEdit.addEventListener(delegate(CharEvent event) { 15477 if(event.character == '\t' || event.character == '\n') 15478 event.preventDefault(); 15479 }); 15480 15481 listWidget.addEventListener(EventType.change, () { 15482 foreach(o; listWidget.options) 15483 if(o.selected) 15484 lineEdit.content = o.label; 15485 }); 15486 15487 loadFiles(currentDirectory, processedFilters); 15488 15489 lineEdit.addEventListener((KeyDownEvent event) { 15490 if(event.key == Key.Tab) { 15491 15492 auto current = lineEdit.content; 15493 if(current.length >= 2 && current[0 ..2] == "./") 15494 current = current[2 .. $]; 15495 15496 auto commonPrefix = loadFiles(currentDirectory, current ~ "*"); 15497 15498 if(commonPrefix.length) 15499 lineEdit.content = commonPrefix; 15500 15501 // FIXME: if that is a directory, add the slash? or even go inside? 15502 15503 event.preventDefault(); 15504 } 15505 }); 15506 15507 lineEdit.content = prefilledName; 15508 15509 auto hl = new HorizontalLayout(60, this); 15510 auto cancelButton = new Button("Cancel", hl); 15511 auto okButton = new Button("OK", hl); 15512 15513 cancelButton.addEventListener(EventType.triggered, &Cancel); 15514 okButton.addEventListener(EventType.triggered, &OK); 15515 15516 this.addEventListener((KeyDownEvent event) { 15517 if(event.key == Key.Enter || event.key == Key.PadEnter) { 15518 event.preventDefault(); 15519 OK(); 15520 } 15521 if(event.key == Key.Escape) 15522 Cancel(); 15523 }); 15524 15525 } 15526 15527 override void OK() { 15528 if(lineEdit.content.length) { 15529 string accepted; 15530 auto c = lineEdit.content; 15531 if(c.length && c[0] == '/') 15532 accepted = c; 15533 else 15534 accepted = currentDirectory ~ "/" ~ lineEdit.content; 15535 15536 if(isDir(accepted)) { 15537 // FIXME: would be kinda nice to support ~ and collapse these paths too 15538 // FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later. 15539 currentDirectory = accepted; 15540 loadFiles(currentDirectory, processedFilters); 15541 lineEdit.content = ""; 15542 return; 15543 } 15544 15545 if(onOK) 15546 onOK(accepted); 15547 } 15548 close(); 15549 } 15550 15551 override void Cancel() { 15552 if(onCancel) 15553 onCancel(); 15554 close(); 15555 } 15556 } 15557 15558 private bool isDir(string name) { 15559 version(Windows) { 15560 auto ws = WCharzBuffer(name); 15561 auto ret = GetFileAttributesW(ws.ptr); 15562 if(ret == INVALID_FILE_ATTRIBUTES) 15563 return false; 15564 return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0; 15565 } else version(Posix) { 15566 import core.sys.posix.sys.stat; 15567 stat_t buf; 15568 auto ret = stat((name ~ '\0').ptr, &buf); 15569 if(ret == -1) 15570 return false; // I could probably check more specific errors tbh 15571 return (buf.st_mode & S_IFMT) == S_IFDIR; 15572 } else return false; 15573 } 15574 15575 /* 15576 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 15577 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 15578 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 15579 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 15580 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 15581 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 15582 http://www.sbin.org/doc/Xlib/chapt_03.html 15583 15584 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 15585 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 15586 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 15587 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 15588 */ 15589 15590 15591 // These are all for setMenuAndToolbarFromAnnotatedCode 15592 /// This item in the menu will be preceded by a separator line 15593 /// Group: generating_from_code 15594 struct separator {} 15595 deprecated("It was misspelled, use separator instead") alias seperator = separator; 15596 /// Program-wide keyboard shortcut to trigger the action 15597 /// Group: generating_from_code 15598 struct accelerator { string keyString; } 15599 /// tells which menu the action will be on 15600 /// Group: generating_from_code 15601 struct menu { string name; } 15602 /// Describes which toolbar section the action appears on 15603 /// Group: generating_from_code 15604 struct toolbar { string groupName; } 15605 /// 15606 /// Group: generating_from_code 15607 struct icon { ushort id; } 15608 /// 15609 /// Group: generating_from_code 15610 struct label { string label; } 15611 /// 15612 /// Group: generating_from_code 15613 struct hotkey { dchar ch; } 15614 /// 15615 /// Group: generating_from_code 15616 struct tip { string tip; } 15617 15618 15619 /++ 15620 Observes and allows inspection of an object via automatic gui 15621 +/ 15622 /// Group: generating_from_code 15623 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 15624 return new ObjectInspectionWindowImpl!(T)(t); 15625 } 15626 15627 class ObjectInspectionWindow : Window { 15628 this(int a, int b, string c) { 15629 super(a, b, c); 15630 } 15631 15632 abstract void readUpdatesFromObject(); 15633 } 15634 15635 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 15636 T t; 15637 this(T t) { 15638 this.t = t; 15639 15640 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 15641 15642 foreach(memberName; __traits(derivedMembers, T)) {{ 15643 alias member = I!(__traits(getMember, t, memberName))[0]; 15644 alias type = typeof(member); 15645 static if(is(type == int)) { 15646 auto le = new LabeledLineEdit(memberName ~ ": ", this); 15647 //le.addEventListener("char", (Event ev) { 15648 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 15649 //ev.preventDefault(); 15650 //}); 15651 le.addEventListener(EventType.change, (Event ev) { 15652 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 15653 }); 15654 15655 updateMemberDelegates[memberName] = () { 15656 le.content = toInternal!string(__traits(getMember, t, memberName)); 15657 }; 15658 } 15659 }} 15660 } 15661 15662 void delegate()[string] updateMemberDelegates; 15663 15664 override void readUpdatesFromObject() { 15665 foreach(k, v; updateMemberDelegates) 15666 v(); 15667 } 15668 } 15669 15670 /++ 15671 Creates a dialog based on a data structure. 15672 15673 --- 15674 dialog((YourStructure value) { 15675 // the user filled in the struct and clicked OK, 15676 // you can check the members now 15677 }); 15678 --- 15679 15680 Params: 15681 initialData = the initial value to show in the dialog. It will not modify this unless 15682 it is a class then it might, no promises. 15683 15684 History: 15685 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 15686 +/ 15687 /// Group: generating_from_code 15688 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15689 dialog(T.init, onOK, onCancel, title); 15690 } 15691 /// ditto 15692 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15693 auto dg = new AutomaticDialog!T(initialData, onOK, onCancel, title); 15694 dg.show(); 15695 } 15696 15697 private static template I(T...) { alias I = T; } 15698 15699 15700 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 15701 if(name == "id") 15702 return allLowerCase ? name : "ID"; 15703 15704 char[160] buffer; 15705 int bufferIndex = 0; 15706 bool shouldCap = true; 15707 bool shouldSpace; 15708 bool lastWasCap; 15709 foreach(idx, char ch; name) { 15710 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15711 15712 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 15713 if(lastWasCap) { 15714 // two caps in a row, don't change. Prolly acronym. 15715 } else { 15716 if(idx) 15717 shouldSpace = true; // new word, add space 15718 } 15719 15720 lastWasCap = true; 15721 } else { 15722 lastWasCap = false; 15723 } 15724 15725 if(shouldSpace) { 15726 buffer[bufferIndex++] = space; 15727 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15728 shouldSpace = false; 15729 } 15730 if(shouldCap) { 15731 if(ch >= 'a' && ch <= 'z') 15732 ch -= 32; 15733 shouldCap = false; 15734 } 15735 if(allLowerCase && ch >= 'A' && ch <= 'Z') 15736 ch += 32; 15737 buffer[bufferIndex++] = ch; 15738 } 15739 return buffer[0 .. bufferIndex].idup; 15740 } 15741 15742 /++ 15743 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. 15744 +/ 15745 class AutomaticDialog(T) : Dialog { 15746 T t; 15747 15748 void delegate(T) onOK; 15749 void delegate() onCancel; 15750 15751 override int paddingTop() { return defaultLineHeight; } 15752 override int paddingBottom() { return defaultLineHeight; } 15753 override int paddingRight() { return defaultLineHeight; } 15754 override int paddingLeft() { return defaultLineHeight; } 15755 15756 this(T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 15757 assert(onOK !is null); 15758 15759 t = initialData; 15760 15761 static if(is(T == class)) { 15762 if(t is null) 15763 t = new T(); 15764 } 15765 this.onOK = onOK; 15766 this.onCancel = onCancel; 15767 super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title); 15768 15769 static if(is(T == class)) 15770 this.addDataControllerWidget(t); 15771 else 15772 this.addDataControllerWidget(&t); 15773 15774 auto hl = new HorizontalLayout(this); 15775 auto stretch = new HorizontalSpacer(hl); // to right align 15776 auto ok = new CommandButton("OK", hl); 15777 auto cancel = new CommandButton("Cancel", hl); 15778 ok.addEventListener(EventType.triggered, &OK); 15779 cancel.addEventListener(EventType.triggered, &Cancel); 15780 15781 this.addEventListener((KeyDownEvent ev) { 15782 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 15783 ok.focus(); 15784 OK(); 15785 ev.preventDefault(); 15786 } 15787 if(ev.key == Key.Escape) { 15788 Cancel(); 15789 ev.preventDefault(); 15790 } 15791 }); 15792 15793 this.addEventListener((scope ClosedEvent ce) { 15794 if(onCancel) 15795 onCancel(); 15796 }); 15797 15798 //this.children[0].focus(); 15799 } 15800 15801 override void OK() { 15802 onOK(t); 15803 close(); 15804 } 15805 15806 override void Cancel() { 15807 if(onCancel) 15808 onCancel(); 15809 close(); 15810 } 15811 } 15812 15813 private template baseClassCount(Class) { 15814 private int helper() { 15815 int count = 0; 15816 static if(is(Class bases == super)) { 15817 foreach(base; bases) 15818 static if(is(base == class)) 15819 count += 1 + baseClassCount!base; 15820 } 15821 return count; 15822 } 15823 15824 enum int baseClassCount = helper(); 15825 } 15826 15827 private long stringToLong(string s) { 15828 long ret; 15829 if(s.length == 0) 15830 return ret; 15831 bool negative = s[0] == '-'; 15832 if(negative) 15833 s = s[1 .. $]; 15834 foreach(ch; s) { 15835 if(ch >= '0' && ch <= '9') { 15836 ret *= 10; 15837 ret += ch - '0'; 15838 } 15839 } 15840 if(negative) 15841 ret = -ret; 15842 return ret; 15843 } 15844 15845 15846 interface ReflectableProperties { 15847 /++ 15848 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 15849 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 15850 json in the current implementation. 15851 15852 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 15853 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 15854 as of the June 2, 2021 release. 15855 15856 History: 15857 Added June 2, 2021. 15858 15859 See_Also: [getPropertyAsString], [setPropertyFromString] 15860 +/ 15861 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 15862 /++ 15863 Requests a property to be delivered to you as a string, through your `sink` delegate. 15864 15865 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 15866 be interpreted as json, otherwise, it is just a plain string. 15867 15868 The sink should always be called exactly once for each call (it is basically a return value, but it might 15869 use a local buffer it maintains instead of allocating a return value). 15870 15871 History: 15872 Added June 2, 2021. 15873 15874 See_Also: [getPropertiesList], [setPropertyFromString] 15875 +/ 15876 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 15877 /++ 15878 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. 15879 15880 History: 15881 Added June 2, 2021. 15882 15883 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 15884 +/ 15885 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 15886 15887 /// [setPropertyFromString] possible return values 15888 enum SetPropertyResult { 15889 success = 0, /// the property has been successfully set to the request value 15890 notPermitted = -1, /// the property exists but it cannot be changed at this time 15891 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 15892 noSuchProperty = -3, /// there is no property by that name 15893 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 15894 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) 15895 } 15896 15897 /++ 15898 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 15899 15900 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15901 15902 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15903 rarely need to use these building blocks directly. 15904 +/ 15905 mixin template RegisterSetters() { 15906 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 15907 switch(name) { 15908 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15909 case memberName: 15910 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15911 if(value != "true" && value != "false") 15912 return SetPropertyResult.wrongFormat; 15913 __traits(getMember, this, memberName) = value == "true" ? true : false; 15914 return SetPropertyResult.success; 15915 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15916 import core.stdc.stdlib; 15917 char[128] zero = 0; 15918 if(buffer.length + 1 >= zero.length) 15919 return SetPropertyResult.wrongFormat; 15920 zero[0 .. buffer.length] = buffer[]; 15921 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 15922 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15923 import core.stdc.stdlib; 15924 char[128] zero = 0; 15925 if(buffer.length + 1 >= zero.length) 15926 return SetPropertyResult.wrongFormat; 15927 zero[0 .. buffer.length] = buffer[]; 15928 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 15929 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15930 __traits(getMember, this, memberName) = value.idup; 15931 } else { 15932 return SetPropertyResult.notImplemented; 15933 } 15934 15935 } 15936 default: 15937 return super.setPropertyFromString(name, value, valueIsJson); 15938 } 15939 } 15940 } 15941 15942 /++ 15943 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 15944 15945 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15946 15947 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15948 rarely need to use these building blocks directly. 15949 +/ 15950 mixin template RegisterGetters() { 15951 override void getPropertiesList(scope void delegate(string name) sink) const { 15952 super.getPropertiesList(sink); 15953 15954 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15955 sink(memberName); 15956 } 15957 } 15958 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 15959 switch(name) { 15960 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15961 case memberName: 15962 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15963 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 15964 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15965 import core.stdc.stdio; 15966 char[32] buffer; 15967 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 15968 sink(name, buffer[0 .. len], true); 15969 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15970 import core.stdc.stdio; 15971 char[32] buffer; 15972 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 15973 sink(name, buffer[0 .. len], true); 15974 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15975 sink(name, __traits(getMember, this, memberName), false); 15976 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 15977 } else { 15978 sink(name, null, true); 15979 } 15980 15981 return; 15982 } 15983 default: 15984 return super.getPropertyAsString(name, sink); 15985 } 15986 } 15987 } 15988 } 15989 15990 private struct Stack(T) { 15991 this(int maxSize) { 15992 internalLength = 0; 15993 arr = initialBuffer[]; 15994 } 15995 15996 ///. 15997 void push(T t) { 15998 if(internalLength >= arr.length) { 15999 auto oldarr = arr; 16000 if(arr.length < 4096) 16001 arr = new T[arr.length * 2]; 16002 else 16003 arr = new T[arr.length + 4096]; 16004 arr[0 .. oldarr.length] = oldarr[]; 16005 } 16006 16007 arr[internalLength] = t; 16008 internalLength++; 16009 } 16010 16011 ///. 16012 T pop() { 16013 assert(internalLength); 16014 internalLength--; 16015 return arr[internalLength]; 16016 } 16017 16018 ///. 16019 T peek() { 16020 assert(internalLength); 16021 return arr[internalLength - 1]; 16022 } 16023 16024 ///. 16025 @property bool empty() { 16026 return internalLength ? false : true; 16027 } 16028 16029 ///. 16030 private T[] arr; 16031 private size_t internalLength; 16032 private T[64] initialBuffer; 16033 // 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), 16034 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 16035 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 16036 } 16037 16038 /// 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. 16039 private struct WidgetStream { 16040 16041 ///. 16042 @property Widget front() { 16043 return current.widget; 16044 } 16045 16046 /// Use Widget.tree instead. 16047 this(Widget start) { 16048 current.widget = start; 16049 current.childPosition = -1; 16050 isEmpty = false; 16051 stack = typeof(stack)(0); 16052 } 16053 16054 /* 16055 Handle it 16056 handle its children 16057 16058 */ 16059 16060 ///. 16061 void popFront() { 16062 more: 16063 if(isEmpty) return; 16064 16065 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 16066 16067 current.childPosition++; 16068 if(current.childPosition >= current.widget.children.length) { 16069 if(stack.empty()) 16070 isEmpty = true; 16071 else { 16072 current = stack.pop(); 16073 goto more; 16074 } 16075 } else { 16076 stack.push(current); 16077 current.widget = current.widget.children[current.childPosition]; 16078 current.childPosition = -1; 16079 } 16080 } 16081 16082 ///. 16083 @property bool empty() { 16084 return isEmpty; 16085 } 16086 16087 private: 16088 16089 struct Current { 16090 Widget widget; 16091 int childPosition; 16092 } 16093 16094 Current current; 16095 16096 Stack!(Current) stack; 16097 16098 bool isEmpty; 16099 } 16100 16101 16102 /+ 16103 16104 I could fix up the hierarchy kinda like this 16105 16106 class Widget { 16107 Widget[] children() { return null; } 16108 } 16109 interface WidgetContainer { 16110 Widget asWidget(); 16111 void addChild(Widget w); 16112 16113 // alias asWidget this; // but meh 16114 } 16115 16116 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 16117 16118 class Layout : Widget, WidgetContainer {} 16119 16120 class Window : WidgetContainer {} 16121 16122 16123 All constructors that previously took Widgets should now take WidgetContainers instead 16124 16125 16126 16127 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". 16128 +/ 16129 16130 /+ 16131 LAYOUTS 2.0 16132 16133 can just be assigned as a function. assigning a new one will cause it to be immediately called. 16134 16135 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 16136 16137 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 16138 16139 and even Paint can just use computedStyle... 16140 16141 background color 16142 font 16143 border color and style 16144 16145 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 16146 please note that many widgets and in some modes will completely ignore properties as they will. 16147 they are just hints you set, not promises. 16148 16149 16150 16151 16152 16153 So generally the existing virtual functions are just the default for the class. But individual objects 16154 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 16155 +/ 16156 16157 /++ 16158 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. 16159 16160 History: 16161 Added May 24, 2021. 16162 +/ 16163 struct WidgetBackground { 16164 /++ 16165 A background with the given solid color. 16166 +/ 16167 this(Color color) { 16168 this.color = color; 16169 } 16170 16171 this(WidgetBackground bg) { 16172 this = bg; 16173 } 16174 16175 /++ 16176 Creates a widget from the string. 16177 16178 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 16179 +/ 16180 static WidgetBackground fromString(string s) { 16181 return WidgetBackground(Color.fromString(s)); 16182 } 16183 16184 /++ 16185 The background is not necessarily a solid color, but you can always specify a color as a fallback. 16186 16187 History: 16188 Made `public` on December 18, 2022 (dub v10.10). 16189 +/ 16190 Color color; 16191 } 16192 16193 /++ 16194 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!) 16195 16196 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. 16197 16198 You should not inherit from this directly, but instead use [VisualTheme]. 16199 16200 History: 16201 Added May 8, 2021 16202 +/ 16203 abstract class BaseVisualTheme { 16204 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 16205 abstract void doPaint(Widget widget, WidgetPainter painter); 16206 16207 /+ 16208 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 16209 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 16210 +/ 16211 16212 /++ 16213 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 16214 where the interpretation of the string varies for each property and may include things like measurement units. 16215 +/ 16216 abstract string getPropertyString(Widget widget, string propertyName); 16217 16218 /++ 16219 Default background color of the window. Widgets also use this to simulate transparency. 16220 16221 Probably some shade of grey. 16222 +/ 16223 abstract Color windowBackgroundColor(); 16224 abstract Color widgetBackgroundColor(); 16225 abstract Color foregroundColor(); 16226 abstract Color lightAccentColor(); 16227 abstract Color darkAccentColor(); 16228 16229 /++ 16230 Colors used to indicate active selections in lists and text boxes, etc. 16231 +/ 16232 abstract Color selectionForegroundColor(); 16233 /// ditto 16234 abstract Color selectionBackgroundColor(); 16235 16236 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 16237 16238 /++ 16239 If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally. 16240 +/ 16241 abstract OperatingSystemFont defaultFont(int dpi); 16242 16243 private OperatingSystemFont[int] defaultFontCache_; 16244 private OperatingSystemFont defaultFontCached(int dpi) { 16245 if(dpi !in defaultFontCache_) { 16246 // FIXME: set this to false if X disconnect or if visual theme changes 16247 defaultFontCache_[dpi] = defaultFont(dpi); 16248 } 16249 return defaultFontCache_[dpi]; 16250 } 16251 } 16252 16253 /+ 16254 A widget should have: 16255 classList 16256 dataset 16257 attributes 16258 computedStyles 16259 state (persistent) 16260 dynamic state (focused, hover, etc) 16261 +/ 16262 16263 // visualTheme.computedStyle(this).paddingLeft 16264 16265 16266 /++ 16267 This is your entry point to create your own visual theme for custom widgets. 16268 16269 You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods. 16270 16271 Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating. 16272 +/ 16273 abstract class VisualTheme(CRTP) : BaseVisualTheme { 16274 override string getPropertyString(Widget widget, string propertyName) { 16275 return null; 16276 } 16277 16278 /+ 16279 mixin StyleOverride!Widget 16280 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 16281 w.useStyleProperties(dg); 16282 } 16283 +/ 16284 16285 final override void doPaint(Widget widget, WidgetPainter painter) { 16286 auto derived = cast(CRTP) cast(void*) this; 16287 16288 scope void delegate(Widget, WidgetPainter) bestMatch; 16289 int bestMatchScore; 16290 16291 static if(__traits(hasMember, CRTP, "paint")) 16292 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 16293 static if(is(typeof(overload) Params == __parameters)) { 16294 static assert(Params.length == 2); 16295 static assert(is(Params[0] : Widget)); 16296 static assert(is(Params[1] == WidgetPainter)); 16297 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); 16298 16299 alias type = Params[0]; 16300 if(cast(type) widget) { 16301 auto score = baseClassCount!type; 16302 16303 if(score > bestMatchScore) { 16304 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 16305 bestMatchScore = score; 16306 } 16307 } 16308 } else static assert(0, "paint should be a method."); 16309 } 16310 16311 if(bestMatch) 16312 bestMatch(widget, painter); 16313 else 16314 widget.paint(painter); 16315 } 16316 16317 deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; } 16318 16319 // 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 16320 // mixin Beautiful95Theme; 16321 mixin DefaultLightTheme; 16322 16323 private static struct Cached { 16324 // i prolly want to do this 16325 } 16326 } 16327 16328 /// ditto 16329 mixin template Beautiful95Theme() { 16330 override Color windowBackgroundColor() { return Color(212, 212, 212); } 16331 override Color widgetBackgroundColor() { return Color.white; } 16332 override Color foregroundColor() { return Color.black; } 16333 override Color darkAccentColor() { return Color(172, 172, 172); } 16334 override Color lightAccentColor() { return Color(223, 223, 223); } 16335 override Color selectionForegroundColor() { return Color.white; } 16336 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 16337 override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr 16338 } 16339 16340 /// ditto 16341 mixin template DefaultLightTheme() { 16342 override Color windowBackgroundColor() { return Color(232, 232, 232); } 16343 override Color widgetBackgroundColor() { return Color.white; } 16344 override Color foregroundColor() { return Color.black; } 16345 override Color darkAccentColor() { return Color(172, 172, 172); } 16346 override Color lightAccentColor() { return Color(223, 223, 223); } 16347 override Color selectionForegroundColor() { return Color.white; } 16348 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 16349 override OperatingSystemFont defaultFont(int dpi) { 16350 version(Windows) 16351 return new OperatingSystemFont("Segoe UI"); 16352 else static if(UsingSimpledisplayCocoa) { 16353 return (new OperatingSystemFont()).loadDefault; 16354 } else { 16355 // FIXME: undo xft's scaling so we don't end up double scaled 16356 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 16357 } 16358 } 16359 } 16360 16361 /// ditto 16362 mixin template DefaultDarkTheme() { 16363 override Color windowBackgroundColor() { return Color(64, 64, 64); } 16364 override Color widgetBackgroundColor() { return Color.black; } 16365 override Color foregroundColor() { return Color.white; } 16366 override Color darkAccentColor() { return Color(20, 20, 20); } 16367 override Color lightAccentColor() { return Color(80, 80, 80); } 16368 override Color selectionForegroundColor() { return Color.white; } 16369 override Color selectionBackgroundColor() { return Color(128, 0, 128); } 16370 override OperatingSystemFont defaultFont(int dpi) { 16371 version(Windows) 16372 return new OperatingSystemFont("Segoe UI", 12); 16373 else static if(UsingSimpledisplayCocoa) { 16374 return (new OperatingSystemFont()).loadDefault; 16375 } else { 16376 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 16377 } 16378 } 16379 } 16380 16381 /// ditto 16382 alias DefaultTheme = DefaultLightTheme; 16383 16384 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 16385 /+ 16386 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 16387 Color windowBackgroundColor() { return Color(242, 242, 242); } 16388 Color darkAccentColor() { return windowBackgroundColor; } 16389 Color lightAccentColor() { return windowBackgroundColor; } 16390 +/ 16391 } 16392 16393 /++ 16394 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 16395 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 16396 16397 History: 16398 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 16399 +/ 16400 class StateChanged(alias field) : Event { 16401 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 16402 override bool cancelable() const { return false; } 16403 this(Widget target, typeof(field) newValue) { 16404 this.newValue = newValue; 16405 super(EventString, target); 16406 } 16407 16408 typeof(field) newValue; 16409 } 16410 16411 /++ 16412 Convenience function to add a `triggered` event listener. 16413 16414 Its implementation is simply `w.addEventListener("triggered", dg);` 16415 16416 History: 16417 Added November 27, 2021 (dub v10.4) 16418 +/ 16419 void addWhenTriggered(Widget w, void delegate() dg) { 16420 w.addEventListener("triggered", dg); 16421 } 16422 16423 /++ 16424 Observable varables can be added to widgets and when they are changed, it fires 16425 off a [StateChanged] event so you can react to it. 16426 16427 It is implemented as a getter and setter property, along with another helper you 16428 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 16429 event through the usual means. Just give the name of the variable. See [StateChanged] for an 16430 example. 16431 16432 History: 16433 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 16434 +/ 16435 mixin template Observable(T, string name) { 16436 private T backing; 16437 16438 mixin(q{ 16439 void } ~ name ~ q{_changed (void delegate(T) dg) { 16440 this.addEventListener((StateChanged!this_thing ev) { 16441 dg(ev.newValue); 16442 }); 16443 } 16444 16445 @property T } ~ name ~ q{ () { 16446 return backing; 16447 } 16448 16449 @property void } ~ name ~ q{ (T t) { 16450 backing = t; 16451 auto event = new StateChanged!this_thing(this, t); 16452 event.dispatch(); 16453 } 16454 }); 16455 16456 mixin("private alias this_thing = " ~ name ~ ";"); 16457 } 16458 16459 16460 private bool startsWith(string test, string thing) { 16461 if(test.length < thing.length) 16462 return false; 16463 return test[0 .. thing.length] == thing; 16464 } 16465 16466 private bool endsWith(string test, string thing) { 16467 if(test.length < thing.length) 16468 return false; 16469 return test[$ - thing.length .. $] == thing; 16470 } 16471 16472 // still do layout delegation 16473 // and... split off Window from Widget. 16474 16475 version(minigui_screenshots) 16476 struct Screenshot { 16477 string name; 16478 } 16479 16480 version(minigui_screenshots) 16481 static if(__VERSION__ > 2092) 16482 mixin(q{ 16483 shared static this() { 16484 import core.runtime; 16485 16486 static UnitTestResult screenshotMagic() { 16487 string name; 16488 16489 import arsd.png; 16490 16491 auto results = new Window(); 16492 auto button = new Button("do it", results); 16493 16494 Window.newWindowCreated = delegate(Window w) { 16495 Timer timer; 16496 timer = new Timer(250, { 16497 auto img = w.win.takeScreenshot(); 16498 timer.destroy(); 16499 16500 version(Windows) 16501 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 16502 else 16503 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 16504 16505 w.close(); 16506 }); 16507 }; 16508 16509 button.addWhenTriggered( { 16510 16511 foreach(test; __traits(getUnitTests, mixin(__MODULE__))) { 16512 name = null; 16513 static foreach(attr; __traits(getAttributes, test)) { 16514 static if(is(typeof(attr) == Screenshot)) 16515 name = attr.name; 16516 } 16517 if(name.length) { 16518 test(); 16519 } 16520 } 16521 16522 }); 16523 16524 results.loop(); 16525 16526 return UnitTestResult(0, 0, false, false); 16527 } 16528 16529 16530 Runtime.extendedModuleUnitTester = &screenshotMagic; 16531 } 16532 }); 16533 version(minigui_screenshots) { 16534 version(unittest) 16535 void main() {} 16536 else static assert(0, "dont forget the -unittest flag to dmd"); 16537 } 16538 16539 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 16540 // FIXME: make multiple accelerators disambiguate based ona rgs 16541 // FIXME: MainWindow ctor should have same arg order as Window 16542 // FIXME: mainwindow ctor w/ client area size instead of total size. 16543 // 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. 16544 // FIXME: tri-state checkbox 16545 // FIXME: subordinate controls grouping...