1 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx 2 3 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever. 4 5 // responsive minigui, menu search, and file open with a preview hook on the side. 6 7 // FIXME: add menu checkbox and menu icon eventually 8 9 /* 10 11 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 12 13 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 14 */ 15 16 // FIXME: text label must be copyable to the clipboard, at least as a full chunk. 17 18 // FIXME: opt-in file picker widget with image support 19 20 // FIXME: number widget 21 22 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm 23 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles 24 25 // osx style menu search. 26 27 // would be cool for a scroll bar to have marking capabilities 28 // kinda like vim's marks just on clicks etc and visual representation 29 // generically. may be cool to add an up arrow to the bottom too 30 // 31 // leave a shadow of where you last were for going back easily 32 33 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 34 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 35 // the window. 36 37 // so what about context menus? 38 39 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 40 41 // FIXME: make the scroll thing go to bottom when the content changes. 42 43 // 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 44 45 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 46 47 48 // FIXME: add a command search thingy built in and implement tip. 49 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 50 51 // On Windows: 52 // FIXME: various labels look broken in high contrast mode 53 // FIXME: changing themes while the program is upen doesn't trigger a redraw 54 55 // add note about manifest to documentation. also icons. 56 57 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 58 // FIXME: clear the corner of scrollbars if they pop up 59 60 // minigui needs to have a stdout redirection for gui mode on windows writeln 61 62 // I kinda wanna do state reacting. sort of. idk tho 63 64 // need a viewer widget that works like a web page - arrows scroll down consistently 65 66 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 67 68 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 69 // and help info about menu items. 70 // and search in menus? 71 72 // FIXME: a scroll area event signaling when a thing comes into view might be good 73 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 74 75 // FIXME: unify Windows style line endings 76 77 /* 78 TODO: 79 80 pie menu 81 82 class Form with submit behavior -- see AutomaticDialog 83 84 disabled widgets and menu items 85 86 event cleanup 87 tooltips. 88 api improvements 89 90 margins are kinda broken, they don't collapse like they should. at least. 91 92 a table form btw would be a horizontal layout of vertical layouts holding each column 93 that would give the same width things 94 */ 95 96 /* 97 98 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 99 */ 100 101 /++ 102 minigui is a smallish GUI widget library, aiming to be on par with at least 103 HTML4 forms and a few other expected gui components. It uses native controls 104 on Windows and does its own thing on Linux (Mac is not currently supported but 105 may be later, and should use native controls) to keep size down. The Linux 106 appearance is similar to Windows 95 and avoids using images to maintain network 107 efficiency on remote X connections, though you can customize that. 108 109 110 minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color], 111 on which it is built. simpledisplay provides the low-level interfaces and minigui 112 builds the concept of widgets inside the windows on top of it. 113 114 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 115 It isn't hugely concerned with appearance - on Windows, it just uses the native 116 controls and native theme, and on Linux, it keeps it simple and I may change that 117 at any time, though after May 2021, you can customize some things with css-inspired 118 [Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`, 119 you can use the custom implementation there too, but... you shouldn't.) 120 121 The event model is similar to what you use in the browser with Javascript and the 122 layout engine tries to automatically fit things in, similar to a css flexbox. 123 124 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 125 `-L/SUBSYSTEM:WINDOWS:5.0`, for example, because otherwise you'll get a 126 console and other visual bugs. 127 128 HTML_To_Classes: 129 $(SMALL_TABLE 130 HTML Code | Minigui Class 131 132 `<input type="text">` | [LineEdit] 133 `<textarea>` | [TextEdit] 134 `<select>` | [DropDownSelection] 135 `<input type="checkbox">` | [Checkbox] 136 `<input type="radio">` | [Radiobox] 137 `<button>` | [Button] 138 ) 139 140 141 Stretchiness: 142 The default is 4. You can use larger numbers for things that should 143 consume a lot of space, and lower numbers for ones that are better at 144 smaller sizes. 145 146 Overlapped_input: 147 COMING EVENTUALLY: 148 minigui will include a little bit of I/O functionality that just works 149 with the event loop. If you want to get fancy, I suggest spinning up 150 another thread and posting events back and forth. 151 152 $(H2 Add ons) 153 See the `minigui_addons` directory in the arsd repo for some add on widgets 154 you can import separately too. 155 156 $(H3 XML definitions) 157 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 158 159 $(H3 Scriptability) 160 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 161 in this documentation, it means you can call it from the script language. 162 163 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 164 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 165 166 --- 167 import arsd.minigui_xml; 168 import arsd.script; 169 170 var globals = var.emptyObject; 171 globals.makeWidgetFromString = &makeWidgetFromString; 172 173 // this now works 174 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 175 --- 176 177 More to come. 178 179 History: 180 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 181 182 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 183 tag this as version 2.0. 184 185 Among the changes: 186 $(LIST 187 * 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. 188 189 See [Event] for details. 190 191 * 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. 192 193 See [DoubleClickEvent] for details. 194 195 * 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. 196 197 See [Widget.Style] for details. 198 199 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 200 201 * 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. 202 203 * 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. 204 205 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 206 207 * 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. 208 209 * Various non-breaking additions. 210 ) 211 +/ 212 module arsd.minigui; 213 214 /++ 215 This hello world sample will have an oversized button, but that's ok, you see your first window! 216 +/ 217 version(Demo) 218 unittest { 219 import arsd.minigui; 220 221 void main() { 222 auto window = new MainWindow(); 223 224 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 225 auto button = new Button("Close", window); 226 button.addWhenTriggered({ 227 window.close(); 228 }); 229 230 window.loop(); 231 } 232 233 main(); // exclude from docs 234 } 235 236 /++ 237 This example shows one way you can partition your window into a header 238 and sidebar. Here, the header and sidebar have a fixed width, while the 239 rest of the content sizes with the window. 240 241 It might be a new way of thinking about window layout to do things this 242 way - perhaps [GridLayout] more matches your style of thought - but the 243 concept here is to partition the window into sub-boxes with a particular 244 size, then partition those boxes into further boxes. 245 246 $(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.) 247 248 So to make the header, start with a child layout that has a max height. 249 It will use that space from the top, then the remaining children will 250 split the remaining area, meaning you can think of is as just being another 251 box you can split again. Keep splitting until you have the look you desire. 252 +/ 253 // https://github.com/adamdruppe/arsd/issues/310 254 version(minigui_screenshots) 255 @Screenshot("layout") 256 unittest { 257 import arsd.minigui; 258 259 // This helper class is just to help make the layout boxes visible. 260 // think of it like a <div style="background-color: whatever;"></div> in HTML. 261 class ColorWidget : Widget { 262 this(Color color, Widget parent) { 263 this.color = color; 264 super(parent); 265 } 266 Color color; 267 class Style : Widget.Style { 268 override WidgetBackground background() { return WidgetBackground(color); } 269 } 270 mixin OverrideStyle!Style; 271 } 272 273 void main() { 274 auto window = new Window; 275 276 // the key is to give it a max height. This is one way to do it: 277 auto header = new class HorizontalLayout { 278 this() { super(window); } 279 override int maxHeight() { return 50; } 280 }; 281 // this next line is a shortcut way of doing it too, but it only works 282 // for HorizontalLayout and VerticalLayout, and is less explicit, so it 283 // is good to know how to make a new class like above anyway. 284 // auto header = new HorizontalLayout(50, window); 285 286 auto bar = new HorizontalLayout(window); 287 288 // or since this is so common, VerticalLayout and HorizontalLayout both 289 // can just take an argument in their constructor for max width/height respectively 290 291 // (could have tone this above too, but I wanted to demo both techniques) 292 auto left = new VerticalLayout(100, bar); 293 294 // and this is the main section's container. A plain Widget instance is good enough here. 295 auto container = new Widget(bar); 296 297 // and these just add color to the containers we made above for the screenshot. 298 // in a real application, you can just add your actual controls instead of these. 299 auto headerColorBox = new ColorWidget(Color.teal, header); 300 auto leftColorBox = new ColorWidget(Color.green, left); 301 auto rightColorBox = new ColorWidget(Color.purple, container); 302 303 window.loop(); 304 } 305 306 main(); // exclude from docs 307 } 308 309 310 public import arsd.simpledisplay; 311 /++ 312 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 313 314 History: 315 Was private until May 15, 2021. 316 +/ 317 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 318 319 version(Windows) { 320 import core.sys.windows.winnls; 321 import core.sys.windows.windef; 322 import core.sys.windows.basetyps; 323 import core.sys.windows.winbase; 324 import core.sys.windows.winuser; 325 import core.sys.windows.wingdi; 326 static import gdi = core.sys.windows.wingdi; 327 } 328 329 version(Windows) { 330 version(minigui_manifest) {} else version=minigui_no_manifest; 331 332 version(minigui_no_manifest) {} else 333 static if(__VERSION__ >= 2_083) 334 version(CRuntime_Microsoft) { // FIXME: mingw? 335 // assume we want commctrl6 whenever possible since there's really no reason not to 336 // and this avoids some of the manifest hassle 337 pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""); 338 } 339 } 340 341 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 342 private bool lastDefaultPrevented; 343 344 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 345 alias scriptable = arsd_jsvar_compatible; 346 347 version(Windows) { 348 // use native widgets when available unless specifically asked otherwise 349 version(custom_widgets) { 350 enum bool UsingCustomWidgets = true; 351 enum bool UsingWin32Widgets = false; 352 } else { 353 version = win32_widgets; 354 enum bool UsingCustomWidgets = false; 355 enum bool UsingWin32Widgets = true; 356 } 357 // and native theming when needed 358 //version = win32_theming; 359 } else { 360 enum bool UsingCustomWidgets = true; 361 enum bool UsingWin32Widgets = false; 362 version=custom_widgets; 363 } 364 365 366 367 /* 368 369 The main goals of minigui.d are to: 370 1) Provide basic widgets that just work in a lightweight lib. 371 I basically want things comparable to a plain HTML form, 372 plus the easy and obvious things you expect from Windows 373 apps like a menu. 374 2) Use native things when possible for best functionality with 375 least library weight. 376 3) Give building blocks to provide easy extension for your 377 custom widgets, or hooking into additional native widgets 378 I didn't wrap. 379 4) Provide interfaces for easy interaction between third 380 party minigui extensions. (event model, perhaps 381 signals/slots, drop-in ease of use bits.) 382 5) Zero non-system dependencies, including Phobos as much as 383 I reasonably can. It must only import arsd.color and 384 my simpledisplay.d. If you need more, it will have to be 385 an extension module. 386 6) An easy layout system that generally works. 387 388 A stretch goal is to make it easy to make gui forms with code, 389 some kind of resource file (xml?) and even a wysiwyg designer. 390 391 Another stretch goal is to make it easy to hook data into the gui, 392 including from reflection. So like auto-generate a form from a 393 function signature or struct definition, or show a list from an 394 array that automatically updates as the array is changed. Then, 395 your program focuses on the data more than the gui interaction. 396 397 398 399 STILL NEEDED: 400 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 401 * slider 402 * listbox 403 * spinner 404 * label? 405 * rich text 406 */ 407 408 409 /+ 410 enum LayoutMethods { 411 verticalFlex, 412 horizontalFlex, 413 inlineBlock, // left to right, no stretch, goes to next line as needed 414 static, // just set to x, y 415 verticalNoStretch, // browser style default 416 417 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 418 419 grid, // magic 420 } 421 +/ 422 423 /++ 424 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. 425 426 427 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. 428 429 --- 430 class MinimalWidget : Widget { 431 this(Widget parent) { 432 super(parent); 433 } 434 } 435 --- 436 437 $(SIDEBAR 438 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. 439 ) 440 441 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. 442 443 Among the things you'll most likely want to change in your custom widget: 444 445 $(LIST 446 * 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.) 447 448 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. 449 450 Do this $(I after) calling the `super` constructor. 451 452 * 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. 453 454 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 455 456 * 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. 457 458 * 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. 459 ) 460 461 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. 462 463 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 464 465 Your own custom-drawn and native system controls can exist side-by-side. 466 467 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. 468 +/ 469 class Widget : ReflectableProperties { 470 471 private bool willDraw() { 472 return true; 473 } 474 475 /+ 476 /++ 477 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 478 479 History: 480 Added September 15, 2021 481 implemented.... ??? 482 +/ 483 void prepareReflection(this This)() { 484 485 } 486 +/ 487 488 private bool _enabled = true; 489 490 /++ 491 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. 492 493 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. 494 495 History: 496 Added November 23, 2021 (dub v10.4) 497 498 Warning: the specific behavior of disabling with parents may change in the future. 499 Bugs: 500 Currently only implemented for widgets backed by native Windows controls. 501 502 See_Also: [disabledReason], [disabledBy] 503 +/ 504 @property bool enabled() { 505 return disabledBy() is null; 506 } 507 508 /// ditto 509 @property void enabled(bool yes) { 510 _enabled = yes; 511 version(win32_widgets) { 512 if(hwnd) 513 EnableWindow(hwnd, yes); 514 } 515 setDynamicState(DynamicState.disabled, yes); 516 } 517 518 private string disabledReason_; 519 520 /++ 521 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. 522 523 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 524 525 History: 526 Added November 23, 2021 (dub v10.4) 527 See_Also: [enabled], [disabledBy] 528 +/ 529 @property string disabledReason() { 530 auto w = disabledBy(); 531 return (w is null) ? null : w.disabledReason_; 532 } 533 534 /// ditto 535 @property void disabledReason(string reason) { 536 disabledReason_ = reason; 537 } 538 539 /++ 540 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. 541 542 History: 543 Added November 25, 2021 (dub v10.4) 544 See_Also: [enabled], [disabledReason] 545 +/ 546 Widget disabledBy() { 547 Widget p = this; 548 while(p) { 549 if(!p._enabled) 550 return p; 551 p = p.parent; 552 } 553 return null; 554 } 555 556 /// Implementations of [ReflectableProperties] interface. See the interface for details. 557 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 558 if(valueIsJson) 559 return SetPropertyResult.wrongFormat; 560 switch(name) { 561 case "name": 562 this.name = value.idup; 563 return SetPropertyResult.success; 564 case "statusTip": 565 this.statusTip = value.idup; 566 return SetPropertyResult.success; 567 default: 568 return SetPropertyResult.noSuchProperty; 569 } 570 } 571 /// ditto 572 void getPropertiesList(scope void delegate(string name) sink) const { 573 sink("name"); 574 sink("statusTip"); 575 } 576 /// ditto 577 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 578 switch(name) { 579 case "name": 580 sink(name, this.name, false); 581 return; 582 case "statusTip": 583 sink(name, this.statusTip, false); 584 return; 585 default: 586 sink(name, null, true); 587 } 588 } 589 590 /++ 591 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 592 593 History: 594 Added November 25, 2021 (dub v10.5) 595 `Point` overload added January 12, 2022 (dub v10.6) 596 +/ 597 int scaleWithDpi(int value, int assumedDpi = 96) { 598 // avoid potential overflow with common special values 599 if(value == int.max) 600 return int.max; 601 if(value == int.min) 602 return int.min; 603 if(value == 0) 604 return 0; 605 606 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 607 //divide = 138; 608 // 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. 609 // this also covers the case when actualDpi returns 0. 610 if(divide < 96) 611 divide = 96; 612 return value * divide / assumedDpi; 613 } 614 615 /// ditto 616 Point scaleWithDpi(Point value, int assumedDpi = 96) { 617 return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi)); 618 } 619 620 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 621 // I'll think up something better eventually 622 protected final int defaultLineHeight() { 623 auto cs = getComputedStyle(); 624 if(cs.font && !cs.font.isNull) 625 return cs.font.height() * 5 / 4; 626 else 627 return scaleWithDpi(Window.lineHeight); 628 } 629 630 protected final int defaultTextWidth(const(char)[] text) { 631 auto cs = getComputedStyle(); 632 if(cs.font && !cs.font.isNull) 633 return cs.font.stringWidth(text); 634 else 635 return scaleWithDpi(Window.lineHeight * cast(int) text.length / 2); 636 } 637 638 /++ 639 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. 640 641 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. 642 643 History: 644 Added May 22, 2021 645 +/ 646 protected bool encapsulatedChildren() { 647 return false; 648 } 649 650 private void privateDpiChanged() { 651 dpiChanged(); 652 foreach(child; children) 653 child.privateDpiChanged(); 654 } 655 656 /++ 657 Virtual hook to update any caches or fonts you need on the event of a dpi scaling change. 658 659 History: 660 Added January 12, 2022 (dub v10.6) 661 +/ 662 protected void dpiChanged() { 663 664 } 665 666 // Default layout properties { 667 668 int minWidth() { return 0; } 669 int minHeight() { 670 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 671 int sum = 0; 672 foreach(child; children) { 673 sum += child.minHeight(); 674 sum += child.marginTop(); 675 sum += child.marginBottom(); 676 } 677 678 return sum; 679 } 680 int maxWidth() { return int.max; } 681 int maxHeight() { return int.max; } 682 int widthStretchiness() { return 4; } 683 int heightStretchiness() { return 4; } 684 685 /++ 686 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 687 688 History: 689 Added June 15, 2021 (dub v10.1) 690 +/ 691 int widthShrinkiness() { return 0; } 692 /// ditto 693 int heightShrinkiness() { return 0; } 694 695 /++ 696 The initial size of the widget for layout calculations. Default is 0. 697 698 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 699 700 History: 701 Added June 15, 2021 (dub v10.1) 702 +/ 703 int flexBasisWidth() { return 0; } 704 /// ditto 705 int flexBasisHeight() { return 0; } 706 707 int marginLeft() { return 0; } 708 int marginRight() { return 0; } 709 int marginTop() { return 0; } 710 int marginBottom() { return 0; } 711 int paddingLeft() { return 0; } 712 int paddingRight() { return 0; } 713 int paddingTop() { return 0; } 714 int paddingBottom() { return 0; } 715 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 716 717 private bool recomputeChildLayoutRequired = true; 718 private static class RecomputeEvent {} 719 private __gshared rce = new RecomputeEvent(); 720 protected final void queueRecomputeChildLayout() { 721 recomputeChildLayoutRequired = true; 722 723 if(this.parentWindow) { 724 auto sw = this.parentWindow.win; 725 assert(sw !is null); 726 if(!sw.eventQueued!RecomputeEvent) { 727 sw.postEvent(rce); 728 // import std.stdio; writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 729 } 730 } 731 732 } 733 734 protected final void recomputeChildLayoutEntry() { 735 if(recomputeChildLayoutRequired) { 736 recomputeChildLayout(); 737 recomputeChildLayoutRequired = false; 738 redraw(); 739 } else { 740 // I still need to check the tree just in case one of them was queued up 741 // and the event came up here instead of there. 742 foreach(child; children) 743 child.recomputeChildLayoutEntry(); 744 } 745 } 746 747 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 748 void recomputeChildLayout() { 749 .recomputeChildLayout!"height"(this); 750 } 751 752 // } 753 754 755 /++ 756 Returns the style's tag name string this object uses. 757 758 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. 759 760 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 761 762 History: 763 Added May 10, 2021 764 +/ 765 string styleTagName() const { 766 string n = typeid(this).name; 767 foreach_reverse(idx, ch; n) 768 if(ch == '.') { 769 n = n[idx + 1 .. $]; 770 break; 771 } 772 return n; 773 } 774 775 /// API for the [styleClassList] 776 static struct ClassList { 777 private Widget widget; 778 779 /// 780 void add(string s) { 781 widget.styleClassList_ ~= s; 782 } 783 784 /// 785 void remove(string s) { 786 foreach(idx, s1; widget.styleClassList_) 787 if(s1 == s) { 788 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 789 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 790 widget.styleClassList_.assumeSafeAppend(); 791 return; 792 } 793 } 794 795 /// Returns true if it was added, false if it was removed. 796 bool toggle(string s) { 797 if(contains(s)) { 798 remove(s); 799 return false; 800 } else { 801 add(s); 802 return true; 803 } 804 } 805 806 /// 807 bool contains(string s) const { 808 foreach(s1; widget.styleClassList_) 809 if(s1 == s) 810 return true; 811 return false; 812 813 } 814 } 815 816 private string[] styleClassList_; 817 818 /++ 819 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. 820 821 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 822 823 History: 824 Added May 10, 2021 825 +/ 826 inout(ClassList) styleClassList() inout { 827 return cast(inout(ClassList)) ClassList(cast() this); 828 } 829 830 /++ 831 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. 832 833 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. 834 835 The upper 32 bits are available for your own extensions. 836 837 History: 838 Added May 10, 2021 839 +/ 840 enum DynamicState : ulong { 841 focus = (1 << 0), /// the widget currently has the keyboard focus 842 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 843 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!) 844 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!) 845 checked = (1 << 4), /// the widget is toggleable and currently toggled on 846 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 847 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 848 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 849 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. 850 851 USER_BEGIN = (1UL << 32), 852 } 853 854 // I want to add the primary and cancel styles to buttons at least at some point somehow. 855 856 /// ditto 857 @property ulong dynamicState() { return dynamicState_; } 858 /// ditto 859 @property ulong dynamicState(ulong newValue) { 860 if(dynamicState != newValue) { 861 auto old = dynamicState_; 862 dynamicState_ = newValue; 863 864 useStyleProperties((scope Widget.Style s) { 865 if(s.variesWithState(old ^ newValue)) 866 redraw(); 867 }); 868 } 869 return dynamicState_; 870 } 871 872 /// ditto 873 void setDynamicState(ulong flags, bool state) { 874 auto ds = dynamicState_; 875 if(state) 876 ds |= flags; 877 else 878 ds &= ~flags; 879 880 dynamicState = ds; 881 } 882 883 private ulong dynamicState_; 884 885 deprecated("Use dynamic styles instead now") { 886 Color backgroundColor() { return backgroundColor_; } 887 void backgroundColor(Color c){ this.backgroundColor_ = c; } 888 889 MouseCursor cursor() { return GenericCursor.Default; } 890 } private Color backgroundColor_ = Color.transparent; 891 892 893 /++ 894 Style properties are defined as an accessory class so they can be referenced and overridden independently. 895 896 It is here so there can be a specificity switch. 897 898 See [OverrideStyle] for a helper function to use your own. 899 900 History: 901 Added May 11, 2021 902 +/ 903 static class Style/* : StyleProperties*/ { 904 public Widget widget; // public because the mixin template needs access to it 905 906 /++ 907 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 908 909 History: 910 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. 911 +/ 912 bool variesWithState(ulong dynamicStateFlags) { 913 version(win32_widgets) { 914 if(widget.hwnd) 915 return false; 916 } 917 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 918 } 919 920 /// 921 Color foregroundColor() { 922 return WidgetPainter.visualTheme.foregroundColor; 923 } 924 925 /// 926 WidgetBackground background() { 927 // the default is a "transparent" background, which means 928 // it goes as far up as it can to get the color 929 if (widget.backgroundColor_ != Color.transparent) 930 return WidgetBackground(widget.backgroundColor_); 931 if (widget.parent) 932 return widget.parent.getComputedStyle.background; 933 return WidgetBackground(widget.backgroundColor_); 934 } 935 936 private static OperatingSystemFont fontCached_; 937 private OperatingSystemFont fontCached() { 938 if(fontCached_ is null) 939 fontCached_ = font(); 940 return fontCached_; 941 } 942 943 /++ 944 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. 945 +/ 946 OperatingSystemFont font() { 947 return null; 948 } 949 950 /++ 951 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. 952 953 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 954 955 History: 956 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 957 +/ 958 MouseCursor cursor() { 959 return GenericCursor.Default; 960 } 961 962 FrameStyle borderStyle() { 963 return FrameStyle.none; 964 } 965 966 /++ 967 +/ 968 Color borderColor() { 969 return Color.transparent; 970 } 971 972 FrameStyle outlineStyle() { 973 if(widget.dynamicState & DynamicState.focus) 974 return FrameStyle.dotted; 975 else 976 return FrameStyle.none; 977 } 978 979 Color outlineColor() { 980 return foregroundColor; 981 } 982 } 983 984 /++ 985 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 986 The basic usage is simple: 987 988 --- 989 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 990 // override style hints as-needed here 991 } 992 OverrideStyle!Style; // add the method 993 --- 994 995 $(TIP 996 While the class is not forced to be `static`, for best results, it should be. A non-static class 997 can not be inherited by other objects whereas the static one can. A property on the base class, 998 called [Widget.Style.widget|widget], is available for you to access its properties. 999 ) 1000 1001 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 1002 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 1003 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 1004 1005 1006 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 1007 You may also just override `variesWithState` when you use this flag. 1008 1009 --- 1010 mixin OverrideStyle!( 1011 DynamicState.focus, YourFocusedStyle, 1012 DynamicState.hover, YourHoverStyle, 1013 YourDefaultStyle 1014 ) 1015 --- 1016 1017 It checks if `dynamicState` matches the state and if so, returns the object given. 1018 1019 If there is no state mask given, the next one matches everything. The first match given is used. 1020 1021 However, since in most cases you'll want check state inside your individual methods, you probably won't 1022 find much use for this whole-class swap out. 1023 1024 History: 1025 Added May 16, 2021 1026 +/ 1027 static protected mixin template OverrideStyle(S...) { 1028 static import amg = arsd.minigui; 1029 override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) { 1030 ulong mask = 0; 1031 foreach(idx, thing; S) { 1032 static if(is(typeof(thing) : ulong)) { 1033 mask = thing; 1034 } else { 1035 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 1036 //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."); 1037 scope amg.Widget.Style s = new thing(); 1038 s.widget = this; 1039 dg(s); 1040 return; 1041 } 1042 } 1043 } 1044 } 1045 } 1046 /++ 1047 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 1048 +/ 1049 void useStyleProperties(scope void delegate(scope Style props) dg) { 1050 scope Style s = new Style(); 1051 s.widget = this; 1052 dg(s); 1053 } 1054 1055 1056 protected void sendResizeEvent() { 1057 this.emit!ResizeEvent(); 1058 } 1059 1060 Menu contextMenu(int x, int y) { return null; } 1061 1062 final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) { 1063 if(parentWindow is null || parentWindow.win is null) return false; 1064 1065 auto menu = this.contextMenu(x, y); 1066 if(menu is null) 1067 return false; 1068 1069 version(win32_widgets) { 1070 // FIXME: if it is -1, -1, do it at the current selection location instead 1071 // tho the corner of the window, whcih it does now, isn't the literal worst. 1072 1073 if(screenX < 0 && screenY < 0) { 1074 auto p = this.globalCoordinates(); 1075 if(screenX == -2) 1076 p.x += x; 1077 if(screenY == -2) 1078 p.y += y; 1079 1080 screenX = p.x; 1081 screenY = p.y; 1082 } 1083 1084 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1085 throw new Exception("TrackContextMenuEx"); 1086 } else version(custom_widgets) { 1087 menu.popup(this, x, y); 1088 } 1089 1090 return true; 1091 } 1092 1093 /++ 1094 Removes this widget from its parent. 1095 1096 History: 1097 `removeWidget` was made `final` on May 11, 2021. 1098 +/ 1099 @scriptable 1100 final void removeWidget() { 1101 auto p = this.parent; 1102 if(p) { 1103 int item; 1104 for(item = 0; item < p._children.length; item++) 1105 if(p._children[item] is this) 1106 break; 1107 auto idx = item; 1108 for(; item < p._children.length - 1; item++) 1109 p._children[item] = p._children[item + 1]; 1110 p._children = p._children[0 .. $-1]; 1111 1112 this.parent.widgetRemoved(idx, this); 1113 //this.parent = null; 1114 } 1115 version(win32_widgets) { 1116 removeAllChildren(); 1117 if(hwnd) { 1118 DestroyWindow(hwnd); 1119 hwnd = null; 1120 } 1121 } 1122 } 1123 1124 /++ 1125 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. 1126 1127 History: 1128 Added September 19, 2021 1129 +/ 1130 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 1131 1132 /++ 1133 Removes all child widgets from `this`. You should not use the removed widgets again. 1134 1135 Note that on Windows, it also destroys the native handles for the removed children recursively. 1136 1137 History: 1138 Added July 1, 2021 (dub v10.2) 1139 +/ 1140 void removeAllChildren() { 1141 version(win32_widgets) 1142 foreach(child; _children) { 1143 child.removeAllChildren(); 1144 if(child.hwnd) { 1145 DestroyWindow(child.hwnd); 1146 child.hwnd = null; 1147 } 1148 } 1149 auto orig = this._children; 1150 this._children = null; 1151 foreach(idx, w; orig) 1152 this.widgetRemoved(idx, w); 1153 } 1154 1155 /++ 1156 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1157 +/ 1158 @scriptable 1159 Widget getChildByName(string name) { 1160 return getByName(name); 1161 } 1162 /++ 1163 Finds the nearest descendant with the requested type and [name]. May return `this`. 1164 +/ 1165 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1166 if(this.name == name) 1167 if(auto c = cast(WidgetClass) this) 1168 return c; 1169 foreach(child; children) { 1170 auto w = child.getByName(name); 1171 if(auto c = cast(WidgetClass) w) 1172 return c; 1173 } 1174 return null; 1175 } 1176 1177 /++ 1178 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. 1179 Names should be unique in a window. 1180 1181 See_Also: [getByName], [getChildByName] 1182 +/ 1183 @scriptable string name; 1184 1185 private EventHandler[][string] bubblingEventHandlers; 1186 private EventHandler[][string] capturingEventHandlers; 1187 1188 /++ 1189 Default event handlers. These are called on the appropriate 1190 event unless [Event.preventDefault] is called on the event at 1191 some point through the bubbling process. 1192 1193 1194 If you are implementing your own widget and want to add custom 1195 events, you should follow the same pattern here: create a virtual 1196 function named `defaultEventHandler_eventname` with the implementation, 1197 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1198 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1199 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1200 This ensures virtual dispatch based on the correct subclass. 1201 1202 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1203 overridden version. 1204 1205 You only need to do that on parent classes adding NEW event types. If you 1206 just want to change the default behavior of an existing event type in a subclass, 1207 you override the function (and optionally call `super.method_name`) like normal. 1208 1209 +/ 1210 protected EventHandler[string] defaultEventHandlers; 1211 1212 /// ditto 1213 void setupDefaultEventHandlers() { 1214 defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); }; 1215 defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); }; 1216 defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); }; 1217 defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); }; 1218 defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); }; 1219 defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); }; 1220 defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); }; 1221 defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); }; 1222 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); }; 1223 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); }; 1224 defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); }; 1225 defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); }; 1226 defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); }; 1227 defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); }; 1228 defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); }; 1229 defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); }; 1230 defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); }; 1231 defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); }; 1232 } 1233 1234 /// ditto 1235 void defaultEventHandler_click(ClickEvent event) {} 1236 /// ditto 1237 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1238 /// ditto 1239 void defaultEventHandler_keydown(KeyDownEvent event) {} 1240 /// ditto 1241 void defaultEventHandler_keyup(KeyUpEvent event) {} 1242 /// ditto 1243 void defaultEventHandler_mousedown(MouseDownEvent event) { 1244 if(event.button == MouseButton.left) { 1245 if(this.tabStop) 1246 this.focus(); 1247 } 1248 } 1249 /// ditto 1250 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1251 /// ditto 1252 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1253 /// ditto 1254 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1255 /// ditto 1256 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1257 /// ditto 1258 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1259 /// ditto 1260 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1261 /// ditto 1262 void defaultEventHandler_char(CharEvent event) {} 1263 /// ditto 1264 void defaultEventHandler_triggered(Event event) {} 1265 /// ditto 1266 void defaultEventHandler_change(Event event) {} 1267 /// ditto 1268 void defaultEventHandler_focus(Event event) {} 1269 /// ditto 1270 void defaultEventHandler_blur(Event event) {} 1271 /// ditto 1272 void defaultEventHandler_focusin(Event event) {} 1273 /// ditto 1274 void defaultEventHandler_focusout(Event event) {} 1275 1276 /++ 1277 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1278 1279 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1280 1281 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1282 of participating in handler delegation. 1283 1284 $(TIP 1285 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. 1286 ) 1287 +/ 1288 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1289 return addEventListener(event, (Widget, scope Event e) { 1290 if(e.srcElement is this) 1291 handler(); 1292 }, useCapture); 1293 } 1294 1295 /// ditto 1296 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1297 return addEventListener(event, (Widget, Event e) { 1298 if(e.srcElement is this) 1299 handler(e); 1300 }, useCapture); 1301 } 1302 1303 /// ditto 1304 EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) { 1305 static if(is(Handler Fn == delegate)) { 1306 static if(is(Fn Params == __parameters)) { 1307 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1308 if(e.srcElement !is this) 1309 return; 1310 auto ty = cast(Params[0]) e; 1311 if(ty !is null) 1312 handler(ty); 1313 }, useCapture); 1314 } else static assert(0); 1315 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1316 } 1317 1318 /// ditto 1319 @scriptable 1320 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1321 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1322 } 1323 1324 /// ditto 1325 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1326 static if(is(Handler Fn == delegate)) { 1327 static if(is(Fn Params == __parameters)) { 1328 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1329 auto ty = cast(Params[0]) e; 1330 if(ty !is null) 1331 handler(ty); 1332 }, useCapture); 1333 } else static assert(0); 1334 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1335 } 1336 1337 /// ditto 1338 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1339 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1340 } 1341 1342 /// ditto 1343 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1344 if(event.length > 2 && event[0..2] == "on") 1345 event = event[2 .. $]; 1346 1347 if(useCapture) 1348 capturingEventHandlers[event] ~= handler; 1349 else 1350 bubblingEventHandlers[event] ~= handler; 1351 1352 return EventListener(this, event, handler, useCapture); 1353 } 1354 1355 /// ditto 1356 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1357 if(event.length > 2 && event[0..2] == "on") 1358 event = event[2 .. $]; 1359 1360 if(useCapture) { 1361 if(event in capturingEventHandlers) 1362 foreach(ref evt; capturingEventHandlers[event]) 1363 if(evt is handler) evt = null; 1364 } else { 1365 if(event in bubblingEventHandlers) 1366 foreach(ref evt; bubblingEventHandlers[event]) 1367 if(evt is handler) evt = null; 1368 } 1369 } 1370 1371 /// ditto 1372 void removeEventListener(EventListener listener) { 1373 removeEventListener(listener.event, listener.handler, listener.useCapture); 1374 } 1375 1376 static if(UsingSimpledisplayX11) { 1377 void discardXConnectionState() { 1378 foreach(child; children) 1379 child.discardXConnectionState(); 1380 } 1381 1382 void recreateXConnectionState() { 1383 foreach(child; children) 1384 child.recreateXConnectionState(); 1385 redraw(); 1386 } 1387 } 1388 1389 /++ 1390 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1391 1392 History: 1393 `globalCoordinates` was made `final` on May 11, 2021. 1394 +/ 1395 Point globalCoordinates() { 1396 int x = this.x; 1397 int y = this.y; 1398 auto p = this.parent; 1399 while(p) { 1400 x += p.x; 1401 y += p.y; 1402 p = p.parent; 1403 } 1404 1405 static if(UsingSimpledisplayX11) { 1406 auto dpy = XDisplayConnection.get; 1407 arsd.simpledisplay.Window dummyw; 1408 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1409 } else { 1410 POINT pt; 1411 pt.x = x; 1412 pt.y = y; 1413 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1414 x = pt.x; 1415 y = pt.y; 1416 } 1417 1418 return Point(x, y); 1419 } 1420 1421 version(win32_widgets) 1422 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1423 1424 version(win32_widgets) 1425 /// Called when a WM_COMMAND is sent to the associated hwnd. 1426 void handleWmCommand(ushort cmd, ushort id) {} 1427 1428 version(win32_widgets) 1429 /++ 1430 Called when a WM_NOTIFY is sent to the associated hwnd. 1431 1432 History: 1433 +/ 1434 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1435 1436 version(win32_widgets) 1437 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); } 1438 1439 /++ 1440 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1441 1442 Updates to this variable will only be made visible on the next mouse enter event. 1443 +/ 1444 @scriptable string statusTip; 1445 // string toolTip; 1446 // string helpText; 1447 1448 /++ 1449 If true, this widget can be focused via keyboard control with the tab key. 1450 1451 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1452 +/ 1453 bool tabStop = true; 1454 /++ 1455 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.) 1456 +/ 1457 int tabOrder; 1458 1459 version(win32_widgets) { 1460 static Widget[HWND] nativeMapping; 1461 /// The native handle, if there is one. 1462 HWND hwnd; 1463 WNDPROC originalWindowProcedure; 1464 1465 SimpleWindow simpleWindowWrappingHwnd; 1466 1467 // please note it IGNORES your return value and does NOT forward it to Windows! 1468 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1469 return 0; 1470 } 1471 } 1472 private bool implicitlyCreated; 1473 1474 /// 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. 1475 int x; 1476 /// ditto 1477 int y; 1478 private int _width; 1479 private int _height; 1480 private Widget[] _children; 1481 private Widget _parent; 1482 private Window _parentWindow; 1483 1484 /++ 1485 Returns the window to which this widget is attached. 1486 1487 History: 1488 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. 1489 +/ 1490 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1491 private @property void parentWindow(Window parent) { 1492 _parentWindow = parent; 1493 foreach(child; children) 1494 child.parentWindow = parent; // please note that this is recursive 1495 } 1496 1497 /++ 1498 Returns the list of the widget's children. 1499 1500 History: 1501 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1502 1503 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1504 +/ 1505 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1506 1507 /++ 1508 Returns the widget's parent. 1509 1510 History: 1511 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1512 1513 The parent should only be managed by the [addChild] and [removeWidget] method. 1514 +/ 1515 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1516 1517 /// The widget's current size. 1518 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1519 /// ditto 1520 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1521 1522 /// Only the layout manager should be calling these. 1523 final protected @property int width(int a) @safe { return _width = a; } 1524 /// ditto 1525 final protected @property int height(int a) @safe { return _height = a; } 1526 1527 /++ 1528 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. 1529 1530 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 1531 +/ 1532 protected void registerMovement() { 1533 version(win32_widgets) { 1534 if(hwnd) { 1535 auto pos = getChildPositionRelativeToParentHwnd(this); 1536 MoveWindow(hwnd, pos[0], pos[1], width, height, false); // 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 1537 } 1538 } 1539 sendResizeEvent(); 1540 } 1541 1542 /// Creates the widget and adds it to the parent. 1543 this(Widget parent) { 1544 if(parent !is null) 1545 parent.addChild(this); 1546 setupDefaultEventHandlers(); 1547 } 1548 1549 /// 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. 1550 @scriptable 1551 bool isFocused() { 1552 return parentWindow && parentWindow.focusedWidget is this; 1553 } 1554 1555 private bool showing_ = true; 1556 /// 1557 bool showing() { return showing_; } 1558 /// 1559 bool hidden() { return !showing_; } 1560 /++ 1561 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. 1562 +/ 1563 void showing(bool s, bool recalculate = true) { 1564 auto so = showing_; 1565 showing_ = s; 1566 if(s != so) { 1567 version(win32_widgets) 1568 if(hwnd) 1569 ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE); 1570 1571 if(parent && recalculate) { 1572 parent.queueRecomputeChildLayout(); 1573 parent.redraw(); 1574 } 1575 1576 foreach(child; children) 1577 child.showing(s, false); 1578 1579 } 1580 queueRecomputeChildLayout(); 1581 redraw(); 1582 } 1583 /// Convenience method for `showing = true` 1584 @scriptable 1585 void show() { 1586 showing = true; 1587 } 1588 /// Convenience method for `showing = false` 1589 @scriptable 1590 void hide() { 1591 showing = false; 1592 } 1593 1594 /// 1595 @scriptable 1596 void focus() { 1597 assert(parentWindow !is null); 1598 if(isFocused()) 1599 return; 1600 1601 if(parentWindow.focusedWidget) { 1602 // FIXME: more details here? like from and to 1603 auto from = parentWindow.focusedWidget; 1604 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 1605 parentWindow.focusedWidget = null; 1606 from.emit!BlurEvent(); 1607 this.emit!FocusOutEvent(); 1608 } 1609 1610 1611 version(win32_widgets) { 1612 if(this.hwnd !is null) 1613 SetFocus(this.hwnd); 1614 } 1615 //else static if(UsingSimpledisplayX11) 1616 //this.parentWindow.win.focus(); 1617 1618 parentWindow.focusedWidget = this; 1619 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 1620 this.emit!FocusEvent(); 1621 this.emit!FocusInEvent(); 1622 } 1623 1624 /+ 1625 /++ 1626 Unfocuses the widget. This may reset 1627 +/ 1628 @scriptable 1629 void blur() { 1630 1631 } 1632 +/ 1633 1634 1635 /++ 1636 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 1637 1638 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 1639 +/ 1640 void attachedToWindow(Window w) {} 1641 /++ 1642 Callback when the widget is added to another widget. 1643 1644 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 1645 +/ 1646 void addedTo(Widget w) {} 1647 1648 /++ 1649 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. 1650 1651 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 1652 +/ 1653 protected void addChild(Widget w, int position = int.max) { 1654 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 1655 assert(w !is this, "Child cannot be its own parent!"); 1656 w._parent = this; 1657 if(position == int.max || position == children.length) { 1658 _children ~= w; 1659 } else { 1660 assert(position < _children.length); 1661 _children.length = _children.length + 1; 1662 for(int i = cast(int) _children.length - 1; i > position; i--) 1663 _children[i] = _children[i - 1]; 1664 _children[position] = w; 1665 } 1666 1667 this.parentWindow = this._parentWindow; 1668 1669 w.addedTo(this); 1670 1671 if(this.hidden) 1672 w.showing = false; 1673 1674 if(parentWindow !is null) { 1675 w.attachedToWindow(parentWindow); 1676 parentWindow.queueRecomputeChildLayout(); 1677 parentWindow.redraw(); 1678 } 1679 } 1680 1681 /++ 1682 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. 1683 +/ 1684 Widget getChildAtPosition(int x, int y) { 1685 // it goes backward so the last one to show gets picked first 1686 // might use z-index later 1687 foreach_reverse(child; children) { 1688 if(child.hidden) 1689 continue; 1690 if(child.x <= x && child.y <= y 1691 && ((x - child.x) < child.width) 1692 && ((y - child.y) < child.height)) 1693 { 1694 return child; 1695 } 1696 } 1697 1698 return null; 1699 } 1700 1701 /++ 1702 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. 1703 1704 History: 1705 Added July 2, 2021 (v10.2) 1706 +/ 1707 protected void addScrollPosition(ref int x, ref int y) {}; 1708 1709 /++ 1710 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. 1711 1712 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. 1713 1714 [paint] is not called for system widgets as the OS library draws them instead. 1715 1716 1717 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. 1718 1719 You should also look at [WidgetPainter.visualTheme] to be theme aware. 1720 1721 History: 1722 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. 1723 +/ 1724 void paint(WidgetPainter painter) { 1725 version(win32_widgets) 1726 if(hwnd) { 1727 return; 1728 } 1729 painter.drawThemed(&paintContent); // note this refers to the following overload 1730 } 1731 1732 /++ 1733 Responsible for drawing the content as the theme engine is responsible for other elements. 1734 1735 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 1736 1737 Params: 1738 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. 1739 1740 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. 1741 1742 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. 1743 1744 Returns: 1745 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. 1746 1747 History: 1748 Added May 15, 2021 1749 +/ 1750 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 1751 return bounds; 1752 } 1753 1754 deprecated("Change ScreenPainter to WidgetPainter") 1755 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 1756 1757 /// I don't actually like the name of this 1758 /// this draws a background on it 1759 void erase(WidgetPainter painter) { 1760 version(win32_widgets) 1761 if(hwnd) return; // Windows will do it. I think. 1762 1763 auto c = getComputedStyle().background.color; 1764 painter.fillColor = c; 1765 painter.outlineColor = c; 1766 1767 version(win32_widgets) { 1768 HANDLE b, p; 1769 if(c.a == 0 && parent is parentWindow) { 1770 // I don't remember why I had this really... 1771 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 1772 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 1773 } 1774 } 1775 painter.drawRectangle(Point(0, 0), width, height); 1776 version(win32_widgets) { 1777 if(c.a == 0 && parent is parentWindow) { 1778 SelectObject(painter.impl.hdc, p); 1779 SelectObject(painter.impl.hdc, b); 1780 } 1781 } 1782 } 1783 1784 /// 1785 WidgetPainter draw() { 1786 int x = this.x, y = this.y; 1787 auto parent = this.parent; 1788 while(parent) { 1789 x += parent.x; 1790 y += parent.y; 1791 parent = parent.parent; 1792 } 1793 1794 auto painter = parentWindow.win.draw(true); 1795 painter.originX = x; 1796 painter.originY = y; 1797 painter.setClipRectangle(Point(0, 0), width, height); 1798 return WidgetPainter(painter, this); 1799 } 1800 1801 /// 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. 1802 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 1803 if(hidden) 1804 return; 1805 1806 int paintX = x; 1807 int paintY = y; 1808 if(this.useNativeDrawing()) { 1809 paintX = 0; 1810 paintY = 0; 1811 lox = 0; 1812 loy = 0; 1813 containment = Rectangle(0, 0, int.max, int.max); 1814 } 1815 1816 painter.originX = lox + paintX; 1817 painter.originY = loy + paintY; 1818 1819 bool actuallyPainted = false; 1820 1821 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 1822 if(clip == Rectangle.init) { 1823 //import std.stdio; writeln(this, " clipped out"); 1824 return; 1825 } 1826 1827 bool invalidateChildren = invalidate; 1828 1829 if(redrawRequested || force) { 1830 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 1831 1832 painter.drawingUpon = this; 1833 1834 erase(painter); 1835 if(painter.visualTheme) 1836 painter.visualTheme.doPaint(this, painter); 1837 else 1838 paint(painter); 1839 1840 if(invalidate) { 1841 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 1842 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 1843 painter.invalidateRect(region); 1844 // children are contained inside this, so no need to do extra work 1845 invalidateChildren = false; 1846 } 1847 1848 redrawRequested = false; 1849 actuallyPainted = true; 1850 } 1851 1852 foreach(child; children) { 1853 version(win32_widgets) 1854 if(child.useNativeDrawing()) continue; 1855 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 1856 } 1857 1858 version(win32_widgets) 1859 foreach(child; children) { 1860 if(child.useNativeDrawing) { 1861 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 1862 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 1863 } 1864 } 1865 } 1866 1867 protected bool useNativeDrawing() nothrow { 1868 version(win32_widgets) 1869 return hwnd !is null; 1870 else 1871 return false; 1872 } 1873 1874 private static class RedrawEvent {} 1875 private __gshared re = new RedrawEvent(); 1876 1877 private bool redrawRequested; 1878 /// 1879 final void redraw(string file = __FILE__, size_t line = __LINE__) { 1880 redrawRequested = true; 1881 1882 if(this.parentWindow) { 1883 auto sw = this.parentWindow.win; 1884 assert(sw !is null); 1885 if(!sw.eventQueued!RedrawEvent) { 1886 sw.postEvent(re); 1887 // import std.stdio; writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1888 } 1889 } 1890 } 1891 1892 private SimpleWindow drawableWindow; 1893 1894 /++ 1895 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. 1896 1897 Returns: 1898 `true` if you should do your default behavior. 1899 1900 History: 1901 Added May 5, 2021 1902 1903 Bugs: 1904 It does not do the static checks on gdc right now. 1905 +/ 1906 final protected bool emit(EventType, this This, Args...)(Args args) { 1907 version(GNU) {} else 1908 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1909 auto e = new EventType(this, args); 1910 e.dispatch(); 1911 return !e.defaultPrevented; 1912 } 1913 /// ditto 1914 final protected bool emit(string eventString, this This)() { 1915 auto e = new Event(eventString, this); 1916 e.dispatch(); 1917 return !e.defaultPrevented; 1918 } 1919 1920 /++ 1921 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. 1922 1923 History: 1924 Added May 5, 2021 1925 +/ 1926 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 1927 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1928 return addEventListener(handler); 1929 } 1930 1931 /++ 1932 Gets the computed style properties from the visual theme. 1933 1934 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].) 1935 1936 History: 1937 Added May 8, 2021 1938 +/ 1939 final StyleInformation getComputedStyle() { 1940 return StyleInformation(this); 1941 } 1942 1943 int focusableWidgets(scope int delegate(Widget) dg) { 1944 foreach(widget; WidgetStream(this)) { 1945 if(widget.tabStop && !widget.hidden) { 1946 int result = dg(widget); 1947 if (result) 1948 return result; 1949 } 1950 } 1951 return 0; 1952 } 1953 1954 1955 // FIXME: I kinda want to hide events from implementation widgets 1956 // so it just catches them all and stops propagation... 1957 // i guess i can do it with a event listener on star. 1958 1959 mixin Emits!KeyDownEvent; /// 1960 mixin Emits!KeyUpEvent; /// 1961 mixin Emits!CharEvent; /// 1962 1963 mixin Emits!MouseDownEvent; /// 1964 mixin Emits!MouseUpEvent; /// 1965 mixin Emits!ClickEvent; /// 1966 mixin Emits!DoubleClickEvent; /// 1967 mixin Emits!MouseMoveEvent; /// 1968 mixin Emits!MouseOverEvent; /// 1969 mixin Emits!MouseOutEvent; /// 1970 mixin Emits!MouseEnterEvent; /// 1971 mixin Emits!MouseLeaveEvent; /// 1972 1973 mixin Emits!ResizeEvent; /// 1974 1975 mixin Emits!BlurEvent; /// 1976 mixin Emits!FocusEvent; /// 1977 1978 mixin Emits!FocusInEvent; /// 1979 mixin Emits!FocusOutEvent; /// 1980 } 1981 1982 /+ 1983 /++ 1984 Interface to indicate that the widget has a simple value property. 1985 1986 History: 1987 Added August 26, 2021 1988 +/ 1989 interface HasValue!T { 1990 /// Getter 1991 @property T value(); 1992 /// Setter 1993 @property void value(T); 1994 } 1995 1996 /++ 1997 Interface to indicate that the widget has a range of possible values for its simple value property. 1998 This would be present on something like a slider or possibly a number picker. 1999 2000 History: 2001 Added September 11, 2021 2002 +/ 2003 interface HasRangeOfValues!T : HasValue!T { 2004 /// The minimum and maximum values in the range, inclusive. 2005 @property T minValue(); 2006 @property void minValue(T); /// ditto 2007 @property T maxValue(); /// ditto 2008 @property void maxValue(T); /// ditto 2009 2010 /// The smallest step the user interface allows. User may still type in values without this limitation. 2011 @property void step(T); 2012 @property T step(); /// ditto 2013 } 2014 2015 /++ 2016 Interface to indicate that the widget has a list of possible values the user can choose from. 2017 This would be present on something like a drop-down selector. 2018 2019 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2020 combobox. 2021 2022 History: 2023 Added September 11, 2021 2024 +/ 2025 interface HasListOfValues!T : HasValue!T { 2026 @property T[] values; 2027 @property void values(T[]); 2028 2029 @property int selectedIndex(); // note it may return -1! 2030 @property void selectedIndex(int); 2031 } 2032 +/ 2033 2034 /++ 2035 History: 2036 Added September 2021 (dub v10.4) 2037 +/ 2038 class GridLayout : Layout { 2039 2040 // 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. 2041 2042 /++ 2043 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2044 +/ 2045 enum Gravity { 2046 Center = 0, 2047 NorthWest = North | West, 2048 North = 0b10_00, 2049 NorthEast = North | East, 2050 West = 0b00_10, 2051 East = 0b00_01, 2052 SouthWest = South | West, 2053 South = 0b01_00, 2054 SouthEast = South | East, 2055 } 2056 2057 /++ 2058 The width and height are in some proportional units and can often just be 12. 2059 +/ 2060 this(int width, int height, Widget parent) { 2061 this.gridWidth = width; 2062 this.gridHeight = height; 2063 super(parent); 2064 } 2065 2066 /++ 2067 Sets the position of the given child. 2068 2069 The units of these arguments are in the proportional grid units you set in the constructor. 2070 +/ 2071 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2072 // ensure it is in bounds 2073 // then ensure no overlaps 2074 2075 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2076 2077 foreach(ref position; positions) { 2078 if(position.widget is child) { 2079 position = p; 2080 goto set; 2081 } 2082 } 2083 2084 positions ~= p; 2085 2086 set: 2087 2088 // FIXME: should this batch? 2089 queueRecomputeChildLayout(); 2090 2091 return child; 2092 } 2093 2094 override void addChild(Widget w, int position = int.max) { 2095 super.addChild(w, position); 2096 //positions ~= ChildPosition(w); 2097 if(position != int.max) { 2098 // FIXME: align it so they actually match. 2099 } 2100 } 2101 2102 override void widgetRemoved(size_t idx, Widget w) { 2103 // FIXME: keep the positions array aligned 2104 // positions[idx].widget = null; 2105 } 2106 2107 override void recomputeChildLayout() { 2108 registerMovement(); 2109 int onGrid = cast(int) positions.length; 2110 c: foreach(child; children) { 2111 // just snap it to the grid 2112 if(onGrid) 2113 foreach(position; positions) 2114 if(position.widget is child) { 2115 child.x = this.width * position.x / this.gridWidth; 2116 child.y = this.height * position.y / this.gridHeight; 2117 child.width = this.width * position.width / this.gridWidth; 2118 child.height = this.height * position.height / this.gridHeight; 2119 2120 auto diff = child.width - child.maxWidth(); 2121 // FIXME: gravity? 2122 if(diff > 0) { 2123 child.width = child.width - diff; 2124 2125 if(position.gravity & Gravity.West) { 2126 // nothing needed, already aligned 2127 } else if(position.gravity & Gravity.East) { 2128 child.x += diff; 2129 } else { 2130 child.x += diff / 2; 2131 } 2132 } 2133 2134 diff = child.height - child.maxHeight(); 2135 // FIXME: gravity? 2136 if(diff > 0) { 2137 child.height = child.height - diff; 2138 2139 if(position.gravity & Gravity.North) { 2140 // nothing needed, already aligned 2141 } else if(position.gravity & Gravity.South) { 2142 child.y += diff; 2143 } else { 2144 child.y += diff / 2; 2145 } 2146 } 2147 2148 2149 child.recomputeChildLayout(); 2150 onGrid--; 2151 continue c; 2152 } 2153 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2154 } 2155 } 2156 2157 private struct ChildPosition { 2158 Widget widget; 2159 int x; 2160 int y; 2161 int width; 2162 int height; 2163 Gravity gravity; 2164 } 2165 private ChildPosition[] positions; 2166 2167 int gridWidth = 12; 2168 int gridHeight = 12; 2169 } 2170 2171 /// 2172 abstract class ComboboxBase : Widget { 2173 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2174 // or to always show the list, we want CBS_SIMPLE == 1 2175 version(win32_widgets) 2176 this(uint style, Widget parent) { 2177 super(parent); 2178 createWin32Window(this, "ComboBox"w, null, style); 2179 } 2180 else version(custom_widgets) 2181 this(Widget parent) { 2182 super(parent); 2183 2184 addEventListener((KeyDownEvent event) { 2185 if(event.key == Key.Up) { 2186 if(selection_ > -1) { // -1 means select blank 2187 selection_--; 2188 fireChangeEvent(); 2189 } 2190 event.preventDefault(); 2191 } 2192 if(event.key == Key.Down) { 2193 if(selection_ + 1 < options.length) { 2194 selection_++; 2195 fireChangeEvent(); 2196 } 2197 event.preventDefault(); 2198 } 2199 2200 }); 2201 2202 } 2203 else static assert(false); 2204 2205 /++ 2206 Returns the current list of options in the selection. 2207 2208 History: 2209 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2210 +/ 2211 final @property string[] options() const { 2212 return cast(string[]) options_; 2213 } 2214 2215 private string[] options_; 2216 private int selection_ = -1; 2217 2218 /++ 2219 Adds an option to the end of options array. 2220 +/ 2221 void addOption(string s) { 2222 options_ ~= s; 2223 version(win32_widgets) 2224 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2225 } 2226 2227 /++ 2228 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2229 +/ 2230 int getSelection() { 2231 return selection_; 2232 } 2233 2234 /++ 2235 Returns the current selection as a string. 2236 2237 History: 2238 Added November 17, 2021 2239 +/ 2240 string getSelectionString() { 2241 return selection_ == -1 ? null : options[selection_]; 2242 } 2243 2244 /++ 2245 Sets the current selection to an index in the options array, or to the given option if present. 2246 Please note that the string version may do a linear lookup. 2247 2248 Returns: 2249 the index you passed in 2250 2251 History: 2252 The `string` based overload was added on March 1, 2022 (dub v10.7). 2253 2254 The return value was `void` prior to March 1, 2022. 2255 +/ 2256 int setSelection(int idx) { 2257 selection_ = idx; 2258 version(win32_widgets) 2259 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2260 2261 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2262 t.dispatch(); 2263 2264 return idx; 2265 } 2266 2267 /// ditto 2268 int setSelection(string s) { 2269 if(s !is null) 2270 foreach(idx, item; options) 2271 if(item == s) { 2272 return setSelection(cast(int) idx); 2273 } 2274 return setSelection(-1); 2275 } 2276 2277 /++ 2278 This event is fired when the selection changes. Note it inherits 2279 from ChangeEvent!string, meaning you can use that as well, and it also 2280 fills in [Event.intValue]. 2281 +/ 2282 static class SelectionChangedEvent : ChangeEvent!string { 2283 this(Widget target, int iv, string sv) { 2284 super(target, &stringValue); 2285 this.iv = iv; 2286 this.sv = sv; 2287 } 2288 immutable int iv; 2289 immutable string sv; 2290 2291 override @property string stringValue() { return sv; } 2292 override @property int intValue() { return iv; } 2293 } 2294 2295 version(win32_widgets) 2296 override void handleWmCommand(ushort cmd, ushort id) { 2297 if(cmd == CBN_SELCHANGE) { 2298 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2299 fireChangeEvent(); 2300 } 2301 } 2302 2303 private void fireChangeEvent() { 2304 if(selection_ >= options.length) 2305 selection_ = -1; 2306 2307 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2308 t.dispatch(); 2309 } 2310 2311 version(win32_widgets) { 2312 override int minHeight() { return defaultLineHeight + 6; } 2313 override int maxHeight() { return defaultLineHeight + 6; } 2314 } else { 2315 override int minHeight() { return defaultLineHeight + 4; } 2316 override int maxHeight() { return defaultLineHeight + 4; } 2317 } 2318 2319 version(custom_widgets) { 2320 2321 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2322 2323 SimpleWindow dropDown; 2324 void popup() { 2325 auto w = width; 2326 // FIXME: suggestedDropdownHeight see below 2327 auto h = cast(int) this.options.length * defaultLineHeight + 8; 2328 2329 auto coord = this.globalCoordinates(); 2330 auto dropDown = new SimpleWindow( 2331 w, h, 2332 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null); 2333 2334 dropDown.move(coord.x, coord.y + this.height); 2335 2336 { 2337 auto cs = getComputedStyle(); 2338 auto painter = dropDown.draw(); 2339 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2340 auto p = Point(4, 4); 2341 painter.outlineColor = cs.foregroundColor; 2342 foreach(option; options) { 2343 painter.drawText(p, option); 2344 p.y += defaultLineHeight; 2345 } 2346 } 2347 2348 dropDown.setEventHandlers( 2349 (MouseEvent event) { 2350 if(event.type == MouseEventType.buttonReleased) { 2351 dropDown.close(); 2352 auto element = (event.y - 4) / defaultLineHeight; 2353 if(element >= 0 && element <= options.length) { 2354 selection_ = element; 2355 2356 fireChangeEvent(); 2357 } 2358 } 2359 } 2360 ); 2361 2362 dropDown.visibilityChanged = (bool visible) { 2363 if(visible) { 2364 this.redraw(); 2365 dropDown.grabInput(); 2366 } else { 2367 dropDown.releaseInputGrab(); 2368 } 2369 }; 2370 2371 dropDown.show(); 2372 } 2373 2374 } 2375 } 2376 2377 /++ 2378 A drop-down list where the user must select one of the 2379 given options. Like `<select>` in HTML. 2380 +/ 2381 class DropDownSelection : ComboboxBase { 2382 this(Widget parent) { 2383 version(win32_widgets) 2384 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 2385 else version(custom_widgets) { 2386 super(parent); 2387 2388 addEventListener("focus", () { this.redraw; }); 2389 addEventListener("blur", () { this.redraw; }); 2390 addEventListener(EventType.change, () { this.redraw; }); 2391 addEventListener("mousedown", () { this.focus(); this.popup(); }); 2392 addEventListener((KeyDownEvent event) { 2393 if(event.key == Key.Space) 2394 popup(); 2395 }); 2396 } else static assert(false); 2397 } 2398 2399 mixin Padding!q{2}; 2400 static class Style : Widget.Style { 2401 override FrameStyle borderStyle() { return FrameStyle.risen; } 2402 } 2403 mixin OverrideStyle!Style; 2404 2405 version(custom_widgets) 2406 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2407 auto cs = getComputedStyle(); 2408 2409 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 2410 2411 painter.outlineColor = cs.foregroundColor; 2412 painter.fillColor = cs.foregroundColor; 2413 Point[4] triangle; 2414 enum padding = 6; 2415 enum paddingV = 7; 2416 enum triangleWidth = 10; 2417 triangle[0] = Point(width - padding - triangleWidth, paddingV); 2418 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 2419 triangle[2] = Point(width - padding - 0, paddingV); 2420 triangle[3] = triangle[0]; 2421 painter.drawPolygon(triangle[]); 2422 2423 return bounds; 2424 } 2425 2426 version(win32_widgets) 2427 override void registerMovement() { 2428 version(win32_widgets) { 2429 if(hwnd) { 2430 auto pos = getChildPositionRelativeToParentHwnd(this); 2431 // the height given to this from Windows' perspective is supposed 2432 // to include the drop down's height. so I add to it to give some 2433 // room for that. 2434 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 2435 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 2436 } 2437 } 2438 sendResizeEvent(); 2439 } 2440 } 2441 2442 /++ 2443 A text box with a drop down arrow listing selections. 2444 The user can choose from the list, or type their own. 2445 +/ 2446 class FreeEntrySelection : ComboboxBase { 2447 this(Widget parent) { 2448 version(win32_widgets) 2449 super(2 /* CBS_DROPDOWN */, parent); 2450 else version(custom_widgets) { 2451 super(parent); 2452 auto hl = new HorizontalLayout(this); 2453 lineEdit = new LineEdit(hl); 2454 2455 tabStop = false; 2456 2457 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2458 2459 auto btn = new class ArrowButton { 2460 this() { 2461 super(ArrowDirection.down, hl); 2462 } 2463 override int maxHeight() { 2464 return int.max; 2465 } 2466 }; 2467 //btn.addDirectEventListener("focus", &lineEdit.focus); 2468 btn.addEventListener("triggered", &this.popup); 2469 addEventListener(EventType.change, (Event event) { 2470 lineEdit.content = event.stringValue; 2471 lineEdit.focus(); 2472 redraw(); 2473 }); 2474 } 2475 else static assert(false); 2476 } 2477 2478 version(custom_widgets) { 2479 LineEdit lineEdit; 2480 } 2481 } 2482 2483 /++ 2484 A combination of free entry with a list below it. 2485 +/ 2486 class ComboBox : ComboboxBase { 2487 this(Widget parent) { 2488 version(win32_widgets) 2489 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 2490 else version(custom_widgets) { 2491 super(parent); 2492 lineEdit = new LineEdit(this); 2493 listWidget = new ListWidget(this); 2494 listWidget.multiSelect = false; 2495 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 2496 string c = null; 2497 foreach(option; listWidget.options) 2498 if(option.selected) { 2499 c = option.label; 2500 break; 2501 } 2502 lineEdit.content = c; 2503 }); 2504 2505 listWidget.tabStop = false; 2506 this.tabStop = false; 2507 listWidget.addEventListener("focus", &lineEdit.focus); 2508 this.addEventListener("focus", &lineEdit.focus); 2509 2510 addDirectEventListener(EventType.change, { 2511 listWidget.setSelection(selection_); 2512 if(selection_ != -1) 2513 lineEdit.content = options[selection_]; 2514 lineEdit.focus(); 2515 redraw(); 2516 }); 2517 2518 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2519 2520 listWidget.addDirectEventListener(EventType.change, { 2521 int set = -1; 2522 foreach(idx, opt; listWidget.options) 2523 if(opt.selected) { 2524 set = cast(int) idx; 2525 break; 2526 } 2527 if(set != selection_) 2528 this.setSelection(set); 2529 }); 2530 } else static assert(false); 2531 } 2532 2533 override int minHeight() { return defaultLineHeight * 3; } 2534 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 2535 override int heightStretchiness() { return 5; } 2536 2537 version(custom_widgets) { 2538 LineEdit lineEdit; 2539 ListWidget listWidget; 2540 2541 override void addOption(string s) { 2542 listWidget.options ~= ListWidget.Option(s); 2543 ComboboxBase.addOption(s); 2544 } 2545 } 2546 } 2547 2548 /+ 2549 class Spinner : Widget { 2550 version(win32_widgets) 2551 this(Widget parent) { 2552 super(parent); 2553 parentWindow = parent.parentWindow; 2554 auto hlayout = new HorizontalLayout(this); 2555 lineEdit = new LineEdit(hlayout); 2556 upDownControl = new UpDownControl(hlayout); 2557 } 2558 2559 LineEdit lineEdit; 2560 UpDownControl upDownControl; 2561 } 2562 2563 class UpDownControl : Widget { 2564 version(win32_widgets) 2565 this(Widget parent) { 2566 super(parent); 2567 parentWindow = parent.parentWindow; 2568 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 2569 } 2570 2571 override int minHeight() { return defaultLineHeight; } 2572 override int maxHeight() { return defaultLineHeight * 3/2; } 2573 2574 override int minWidth() { return defaultLineHeight * 3/2; } 2575 override int maxWidth() { return defaultLineHeight * 3/2; } 2576 } 2577 +/ 2578 2579 /+ 2580 class DataView : Widget { 2581 // this is the omnibus data viewer 2582 // the internal data layout is something like: 2583 // string[string][] but also each node can have parents 2584 } 2585 +/ 2586 2587 2588 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 2589 2590 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 2591 2592 // FIXME: menus should prolly capture the mouse. ugh i kno. 2593 /* 2594 TextEdit needs: 2595 2596 * caret manipulation 2597 * selection control 2598 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 2599 2600 For example: 2601 2602 connect(paste, &textEdit.insertTextAtCaret); 2603 2604 would be nice. 2605 2606 2607 2608 I kinda want an omnibus dataview that combines list, tree, 2609 and table - it can be switched dynamically between them. 2610 2611 Flattening policy: only show top level, show recursive, show grouped 2612 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 2613 2614 Single select, multi select, organization, drag+drop 2615 */ 2616 2617 //static if(UsingSimpledisplayX11) 2618 version(win32_widgets) {} 2619 else version(custom_widgets) { 2620 enum scrollClickRepeatInterval = 50; 2621 2622 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 2623 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 2624 enum activeTabColor = lightAccentColor; 2625 enum hoveringColor = Color(228, 228, 228); 2626 enum buttonColor = windowBackgroundColor; 2627 enum depressedButtonColor = darkAccentColor; 2628 enum activeListXorColor = Color(255, 255, 127); 2629 enum progressBarColor = Color(0, 0, 128); 2630 enum activeMenuItemColor = Color(0, 0, 128); 2631 2632 }} 2633 else static assert(false); 2634 deprecated("Get these properties off the `visualTheme` instead.") { 2635 // these are used by horizontal rule so not just custom_widgets. for now at least. 2636 enum darkAccentColor = Color(172, 172, 172); 2637 enum lightAccentColor = Color(223, 223, 223); // used to be 223 2638 } 2639 2640 private const(wchar)* toWstringzInternal(in char[] s) { 2641 wchar[] str; 2642 str.reserve(s.length + 1); 2643 foreach(dchar ch; s) 2644 str ~= ch; 2645 str ~= '\0'; 2646 return str.ptr; 2647 } 2648 2649 static if(SimpledisplayTimerAvailable) 2650 void setClickRepeat(Widget w, int interval, int delay = 250) { 2651 Timer timer; 2652 int delayRemaining = delay / interval; 2653 if(delayRemaining <= 1) 2654 delayRemaining = 2; 2655 2656 immutable originalDelayRemaining = delayRemaining; 2657 2658 w.addDirectEventListener((scope MouseDownEvent ev) { 2659 if(ev.srcElement !is w) 2660 return; 2661 if(timer !is null) { 2662 timer.destroy(); 2663 timer = null; 2664 } 2665 delayRemaining = originalDelayRemaining; 2666 timer = new Timer(interval, () { 2667 if(delayRemaining > 0) 2668 delayRemaining--; 2669 else { 2670 auto ev = new Event("triggered", w); 2671 ev.sendDirectly(); 2672 } 2673 }); 2674 }); 2675 2676 w.addDirectEventListener((scope MouseUpEvent ev) { 2677 if(ev.srcElement !is w) 2678 return; 2679 if(timer !is null) { 2680 timer.destroy(); 2681 timer = null; 2682 } 2683 }); 2684 2685 w.addDirectEventListener((scope MouseLeaveEvent ev) { 2686 if(ev.srcElement !is w) 2687 return; 2688 if(timer !is null) { 2689 timer.destroy(); 2690 timer = null; 2691 } 2692 }); 2693 2694 } 2695 else 2696 void setClickRepeat(Widget w, int interval, int delay = 250) {} 2697 2698 enum FrameStyle { 2699 none, /// 2700 risen, /// a 3d pop-out effect (think Windows 95 button) 2701 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 2702 solid, /// 2703 dotted, /// 2704 fantasy, /// a style based on a popular fantasy video game 2705 } 2706 2707 version(custom_widgets) 2708 deprecated 2709 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 2710 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2711 } 2712 2713 version(custom_widgets) 2714 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 2715 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 2716 } 2717 2718 version(custom_widgets) 2719 deprecated 2720 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 2721 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2722 } 2723 2724 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 2725 int borderWidth; 2726 final switch(style) { 2727 case FrameStyle.sunk, FrameStyle.risen: 2728 // outer layer 2729 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 2730 borderWidth = 2; 2731 break; 2732 case FrameStyle.none: 2733 painter.outlineColor = background; 2734 borderWidth = 0; 2735 break; 2736 case FrameStyle.solid: 2737 painter.pen = Pen(border, 1); 2738 borderWidth = 1; 2739 break; 2740 case FrameStyle.dotted: 2741 painter.pen = Pen(border, 1, Pen.Style.Dotted); 2742 borderWidth = 1; 2743 break; 2744 case FrameStyle.fantasy: 2745 painter.pen = Pen(border, 3); 2746 borderWidth = 3; 2747 break; 2748 } 2749 2750 painter.fillColor = background; 2751 painter.drawRectangle(Point(x + 0, y + 0), width, height); 2752 2753 2754 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 2755 // 3d effect 2756 auto vt = WidgetPainter.visualTheme; 2757 2758 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 2759 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 2760 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 2761 2762 // inner layer 2763 //right, bottom 2764 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 2765 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 2766 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 2767 // left, top 2768 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 2769 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 2770 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 2771 } else if(style == FrameStyle.fantasy) { 2772 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 2773 painter.fillColor = Color.transparent; 2774 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 2775 } 2776 2777 return borderWidth; 2778 } 2779 2780 /++ 2781 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. 2782 2783 See_Also: 2784 [MenuItem] 2785 [ToolButton] 2786 [Menu.addItem] 2787 +/ 2788 class Action { 2789 version(win32_widgets) { 2790 private int id; 2791 private static int lastId = 9000; 2792 private static Action[int] mapping; 2793 } 2794 2795 KeyEvent accelerator; 2796 2797 // FIXME: disable message 2798 // and toggle thing? 2799 // ??? and trigger arguments too ??? 2800 2801 /++ 2802 Params: 2803 label = the textual label 2804 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 2805 triggered = initial handler, more can be added via the [triggered] member. 2806 +/ 2807 this(string label, ushort icon = 0, void delegate() triggered = null) { 2808 this.label = label; 2809 this.iconId = icon; 2810 if(triggered !is null) 2811 this.triggered ~= triggered; 2812 version(win32_widgets) { 2813 id = ++lastId; 2814 mapping[id] = this; 2815 } 2816 } 2817 2818 private string label; 2819 private ushort iconId; 2820 // icon 2821 2822 // when it is triggered, the triggered event is fired on the window 2823 /// The list of handlers when it is triggered. 2824 void delegate()[] triggered; 2825 } 2826 2827 /* 2828 plan: 2829 keyboard accelerators 2830 2831 * menus (and popups and tooltips) 2832 * status bar 2833 * toolbars and buttons 2834 2835 sortable table view 2836 2837 maybe notification area icons 2838 basic clipboard 2839 2840 * radio box 2841 splitter 2842 toggle buttons (optionally mutually exclusive, like in Paint) 2843 label, rich text display, multi line plain text (selectable) 2844 * fieldset 2845 * nestable grid layout 2846 single line text input 2847 * multi line text input 2848 slider 2849 spinner 2850 list box 2851 drop down 2852 combo box 2853 auto complete box 2854 * progress bar 2855 2856 terminal window/widget (on unix it might even be a pty but really idk) 2857 2858 ok button 2859 cancel button 2860 2861 keyboard hotkeys 2862 2863 scroll widget 2864 2865 event redirections and network transparency 2866 script integration 2867 */ 2868 2869 2870 /* 2871 MENUS 2872 2873 auto bar = new MenuBar(window); 2874 window.menuBar = bar; 2875 2876 auto fileMenu = bar.addItem(new Menu("&File")); 2877 fileMenu.addItem(new MenuItem("&Exit")); 2878 2879 2880 EVENTS 2881 2882 For controls, you should usually use "triggered" rather than "click", etc., because 2883 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 2884 This is the case on menus and pushbuttons. 2885 2886 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 2887 */ 2888 2889 2890 /* 2891 enum LinePreference { 2892 AlwaysOnOwnLine, // always on its own line 2893 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 2894 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 2895 } 2896 */ 2897 2898 /++ 2899 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. 2900 2901 --- 2902 class MyWidget : Widget { 2903 this(Widget parent) { super(parent); } 2904 2905 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 2906 mixin Padding!q{4}; 2907 2908 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 2909 mixin Margin!q{8}; 2910 2911 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 2912 // while Top/Bottom/Right remain 8 from the mixin above. 2913 override int marginLeft() { return 2; } 2914 } 2915 --- 2916 2917 2918 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]). 2919 2920 Padding is the area inside a widget where its background is drawn, but the content avoids. 2921 2922 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!). 2923 2924 * 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. 2925 +/ 2926 mixin template Padding(string code) { 2927 override int paddingLeft() { return mixin(code);} 2928 override int paddingRight() { return mixin(code);} 2929 override int paddingTop() { return mixin(code);} 2930 override int paddingBottom() { return mixin(code);} 2931 } 2932 2933 /// ditto 2934 mixin template Margin(string code) { 2935 override int marginLeft() { return mixin(code);} 2936 override int marginRight() { return mixin(code);} 2937 override int marginTop() { return mixin(code);} 2938 override int marginBottom() { return mixin(code);} 2939 } 2940 2941 private 2942 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 2943 enum calcingV = relevantMeasure == "height"; 2944 2945 parent.registerMovement(); 2946 2947 if(parent.children.length == 0) 2948 return; 2949 2950 auto parentStyle = parent.getComputedStyle(); 2951 2952 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 2953 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 2954 2955 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 2956 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 2957 2958 // my own width and height should already be set by the caller of this function... 2959 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 2960 mixin("parentStyle.padding"~firstThingy~"()") - 2961 mixin("parentStyle.padding"~secondThingy~"()"); 2962 2963 int stretchinessSum; 2964 int stretchyChildSum; 2965 int lastMargin = 0; 2966 2967 int shrinkinessSum; 2968 int shrinkyChildSum; 2969 2970 // set initial size 2971 foreach(child; parent.children) { 2972 2973 auto childStyle = child.getComputedStyle(); 2974 2975 if(cast(StaticPosition) child) 2976 continue; 2977 if(child.hidden) 2978 continue; 2979 2980 const iw = child.flexBasisWidth(); 2981 const ih = child.flexBasisHeight(); 2982 2983 static if(calcingV) { 2984 child.width = parent.width - 2985 mixin("childStyle.margin"~otherFirstThingy~"()") - 2986 mixin("childStyle.margin"~otherSecondThingy~"()") - 2987 mixin("parentStyle.padding"~otherFirstThingy~"()") - 2988 mixin("parentStyle.padding"~otherSecondThingy~"()"); 2989 2990 if(child.width < 0) 2991 child.width = 0; 2992 if(child.width > childStyle.maxWidth()) 2993 child.width = childStyle.maxWidth(); 2994 2995 if(iw > 0) { 2996 auto totalPossible = child.width; 2997 if(child.width > iw && child.widthStretchiness() == 0) 2998 child.width = iw; 2999 } 3000 3001 child.height = mymax(childStyle.minHeight(), ih); 3002 } else { 3003 // set to take all the space 3004 child.height = parent.height - 3005 mixin("childStyle.margin"~firstThingy~"()") - 3006 mixin("childStyle.margin"~secondThingy~"()") - 3007 mixin("parentStyle.padding"~firstThingy~"()") - 3008 mixin("parentStyle.padding"~secondThingy~"()"); 3009 3010 // then clamp it 3011 if(child.height < 0) 3012 child.height = 0; 3013 if(child.height > childStyle.maxHeight()) 3014 child.height = childStyle.maxHeight(); 3015 3016 // and if possible, respect the ideal target 3017 if(ih > 0) { 3018 auto totalPossible = child.height; 3019 if(child.height > ih && child.heightStretchiness() == 0) 3020 child.height = ih; 3021 } 3022 3023 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3024 child.width = mymax(childStyle.minWidth(), iw); 3025 } 3026 3027 spaceRemaining -= mixin("child." ~ relevantMeasure); 3028 3029 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3030 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3031 lastMargin = margin; 3032 spaceRemaining -= thisMargin + margin; 3033 3034 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3035 stretchinessSum += s; 3036 if(s > 0) 3037 stretchyChildSum++; 3038 3039 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3040 shrinkinessSum += s2; 3041 if(s2 > 0) 3042 shrinkyChildSum++; 3043 } 3044 3045 if(spaceRemaining < 0 && shrinkyChildSum) { 3046 // shrink to get into the space if it is possible 3047 auto toRemove = -spaceRemaining; 3048 auto removalPerItem = toRemove * shrinkinessSum / shrinkyChildSum; 3049 auto remainder = toRemove * shrinkinessSum % shrinkyChildSum; 3050 3051 // FIXME: wtf why am i shrinking things with no shrinkiness? 3052 3053 foreach(child; parent.children) { 3054 auto childStyle = child.getComputedStyle(); 3055 if(cast(StaticPosition) child) 3056 continue; 3057 if(child.hidden) 3058 continue; 3059 static if(calcingV) { 3060 auto maximum = childStyle.maxHeight(); 3061 } else { 3062 auto maximum = childStyle.maxWidth(); 3063 } 3064 3065 mixin("child._" ~ relevantMeasure) -= removalPerItem + remainder; // this is removing more than needed to trigger the next thing. ugh. 3066 3067 spaceRemaining += removalPerItem + remainder; 3068 } 3069 } 3070 3071 // stretch to fill space 3072 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3073 auto spacePerChild = spaceRemaining / stretchinessSum; 3074 bool spreadEvenly; 3075 bool giveToBiggest; 3076 if(spacePerChild <= 0) { 3077 spacePerChild = spaceRemaining / stretchyChildSum; 3078 spreadEvenly = true; 3079 } 3080 if(spacePerChild <= 0) { 3081 giveToBiggest = true; 3082 } 3083 int previousSpaceRemaining = spaceRemaining; 3084 stretchinessSum = 0; 3085 Widget mostStretchy; 3086 int mostStretchyS; 3087 foreach(child; parent.children) { 3088 auto childStyle = child.getComputedStyle(); 3089 if(cast(StaticPosition) child) 3090 continue; 3091 if(child.hidden) 3092 continue; 3093 static if(calcingV) { 3094 auto maximum = childStyle.maxHeight(); 3095 } else { 3096 auto maximum = childStyle.maxWidth(); 3097 } 3098 3099 if(mixin("child." ~ relevantMeasure) >= maximum) { 3100 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3101 mixin("child._" ~ relevantMeasure) -= adj; 3102 spaceRemaining += adj; 3103 continue; 3104 } 3105 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3106 if(s <= 0) 3107 continue; 3108 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3109 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3110 spaceRemaining -= spaceAdjustment; 3111 if(mixin("child." ~ relevantMeasure) > maximum) { 3112 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3113 mixin("child._" ~ relevantMeasure) -= diff; 3114 spaceRemaining += diff; 3115 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3116 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3117 if(mostStretchy is null || s >= mostStretchyS) { 3118 mostStretchy = child; 3119 mostStretchyS = s; 3120 } 3121 } 3122 } 3123 3124 if(giveToBiggest && mostStretchy !is null) { 3125 auto child = mostStretchy; 3126 auto childStyle = child.getComputedStyle(); 3127 int spaceAdjustment = spaceRemaining; 3128 3129 static if(calcingV) 3130 auto maximum = childStyle.maxHeight(); 3131 else 3132 auto maximum = childStyle.maxWidth(); 3133 3134 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3135 spaceRemaining -= spaceAdjustment; 3136 if(mixin("child._" ~ relevantMeasure) > maximum) { 3137 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3138 mixin("child._" ~ relevantMeasure) -= diff; 3139 spaceRemaining += diff; 3140 } 3141 } 3142 3143 if(spaceRemaining == previousSpaceRemaining) 3144 break; // apparently nothing more we can do 3145 3146 } 3147 3148 // position 3149 lastMargin = 0; 3150 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3151 foreach(child; parent.children) { 3152 auto childStyle = child.getComputedStyle(); 3153 if(cast(StaticPosition) child) { 3154 child.recomputeChildLayout(); 3155 continue; 3156 } 3157 if(child.hidden) 3158 continue; 3159 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3160 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3161 currentPos += thisMargin; 3162 static if(calcingV) { 3163 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3164 child.y = currentPos; 3165 } else { 3166 child.x = currentPos; 3167 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3168 3169 } 3170 currentPos += mixin("child." ~ relevantMeasure); 3171 currentPos += margin; 3172 lastMargin = margin; 3173 3174 child.recomputeChildLayout(); 3175 } 3176 } 3177 3178 int mymax(int a, int b) { return a > b ? a : b; } 3179 int mymax(int a, int b, int c) { 3180 auto d = mymax(a, b); 3181 return c > d ? c : d; 3182 } 3183 3184 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3185 // and here, it must be integrable with the layout, the event system, and not be painted over. 3186 version(win32_widgets) { 3187 3188 // this function just does stuff that a parent window needs for redirection 3189 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3190 this_.hookedWndProc(msg, wParam, lParam); 3191 3192 switch(msg) { 3193 3194 case WM_VSCROLL, WM_HSCROLL: 3195 auto pos = HIWORD(wParam); 3196 auto m = LOWORD(wParam); 3197 3198 auto scrollbarHwnd = cast(HWND) lParam; 3199 3200 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3201 3202 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3203 3204 switch(m) { 3205 /+ 3206 // I don't think those messages are ever actually sent normally by the widget itself, 3207 // they are more used for the keyboard interface. methinks. 3208 case SB_BOTTOM: 3209 //import std.stdio; writeln("end"); 3210 auto event = new Event("scrolltoend", *widgetp); 3211 event.dispatch(); 3212 //if(!event.defaultPrevented) 3213 break; 3214 case SB_TOP: 3215 //import std.stdio; writeln("top"); 3216 auto event = new Event("scrolltobeginning", *widgetp); 3217 event.dispatch(); 3218 break; 3219 case SB_ENDSCROLL: 3220 // idk 3221 break; 3222 +/ 3223 case SB_LINEDOWN: 3224 (*widgetp).emitCommand!"scrolltonextline"(); 3225 return 0; 3226 case SB_LINEUP: 3227 (*widgetp).emitCommand!"scrolltopreviousline"(); 3228 return 0; 3229 case SB_PAGEDOWN: 3230 (*widgetp).emitCommand!"scrolltonextpage"(); 3231 return 0; 3232 case SB_PAGEUP: 3233 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3234 return 0; 3235 case SB_THUMBPOSITION: 3236 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3237 ev.dispatch(); 3238 return 0; 3239 case SB_THUMBTRACK: 3240 // eh kinda lying but i like the real time update display 3241 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3242 ev.dispatch(); 3243 3244 // the event loop doesn't seem to carry on with a requested redraw.. 3245 // so we request it to get our dirty bit set... 3246 // then we need to immediately actually redraw it too for instant feedback to user 3247 SimpleWindow.processAllCustomEvents(); 3248 SimpleWindow.processAllCustomEvents(); 3249 //if(this_.parentWindow) 3250 //this_.parentWindow.actualRedraw(); 3251 3252 // and this ensures the WM_PAINT message is sent fairly quickly 3253 // still seems to lag a little in large windows but meh it basically works. 3254 if(this_.parentWindow) { 3255 // FIXME: if painting is slow, this does still lag 3256 // we probably will want to expose some user hook to ScrollWindowEx 3257 // or something. 3258 UpdateWindow(this_.parentWindow.hwnd); 3259 } 3260 return 0; 3261 default: 3262 } 3263 } 3264 break; 3265 3266 case WM_CONTEXTMENU: 3267 auto hwndFrom = cast(HWND) wParam; 3268 3269 auto xPos = cast(short) LOWORD(lParam); 3270 auto yPos = cast(short) HIWORD(lParam); 3271 3272 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3273 POINT p; 3274 p.x = xPos; 3275 p.y = yPos; 3276 ScreenToClient(hwnd, &p); 3277 auto clientX = cast(ushort) p.x; 3278 auto clientY = cast(ushort) p.y; 3279 3280 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 3281 3282 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 3283 return 0; 3284 } 3285 } 3286 break; 3287 3288 case WM_DRAWITEM: 3289 auto dis = cast(DRAWITEMSTRUCT*) lParam; 3290 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 3291 return (*widgetp).handleWmDrawItem(dis); 3292 } 3293 break; 3294 3295 case WM_NOTIFY: 3296 auto hdr = cast(NMHDR*) lParam; 3297 auto hwndFrom = hdr.hwndFrom; 3298 auto code = hdr.code; 3299 3300 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3301 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 3302 } 3303 break; 3304 case WM_COMMAND: 3305 auto handle = cast(HWND) lParam; 3306 auto cmd = HIWORD(wParam); 3307 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 3308 3309 default: 3310 // pass it on 3311 } 3312 return 0; 3313 } 3314 3315 3316 3317 extern(Windows) 3318 private 3319 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 3320 // but can i merge them?! 3321 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3322 //import std.stdio; try { writeln(iMessage); } catch(Exception e) {}; 3323 3324 if(auto te = hWnd in Widget.nativeMapping) { 3325 try { 3326 3327 te.hookedWndProc(iMessage, wParam, lParam); 3328 3329 int mustReturn; 3330 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 3331 if(mustReturn) 3332 return ret; 3333 3334 if(iMessage == WM_SETFOCUS) { 3335 auto lol = *te; 3336 while(lol !is null && lol.implicitlyCreated) 3337 lol = lol.parent; 3338 lol.focus(); 3339 //(*te).parentWindow.focusedWidget = lol; 3340 } 3341 3342 3343 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 3344 SetBkMode(cast(HDC) wParam, TRANSPARENT); 3345 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 3346 //GetStockObject(NULL_BRUSH); 3347 } 3348 3349 auto pos = getChildPositionRelativeToParentOrigin(*te); 3350 lastDefaultPrevented = false; 3351 // try {import std.stdio; writeln(typeid(*te)); } catch(Exception e) {} 3352 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 3353 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 3354 else { 3355 // it was something we recognized, should only call the window procedure if the default was not prevented 3356 } 3357 } catch(Exception e) { 3358 assert(0, e.toString()); 3359 } 3360 return 0; 3361 } 3362 assert(0, "shouldn't be receiving messages for this window...."); 3363 //import std.conv; 3364 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 3365 } 3366 3367 extern(Windows) 3368 private 3369 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 3370 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3371 if(iMessage == WM_ERASEBKGND) { 3372 auto dc = GetDC(hWnd); 3373 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 3374 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 3375 RECT r; 3376 GetWindowRect(hWnd, &r); 3377 // since the pen is null, to fill the whole space, we need the +1 on both. 3378 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 3379 SelectObject(dc, p); 3380 SelectObject(dc, b); 3381 ReleaseDC(hWnd, dc); 3382 InvalidateRect(hWnd, null, false); // redraw the border 3383 return 1; 3384 } 3385 return HookedWndProc(hWnd, iMessage, wParam, lParam); 3386 } 3387 3388 /++ 3389 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 3390 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 3391 of minigui's expectations. 3392 3393 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 3394 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 3395 3396 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 3397 3398 To check if you can use this, use `static if(UsingWin32Widgets)`. 3399 +/ 3400 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 3401 assert(p.parentWindow !is null); 3402 assert(p.parentWindow.win.impl.hwnd !is null); 3403 3404 auto bsgroupbox = style == BS_GROUPBOX; 3405 3406 HWND phwnd; 3407 3408 auto wtf = p.parent; 3409 while(wtf) { 3410 if(wtf.hwnd !is null) { 3411 phwnd = wtf.hwnd; 3412 break; 3413 } 3414 wtf = wtf.parent; 3415 } 3416 3417 if(phwnd is null) 3418 phwnd = p.parentWindow.win.impl.hwnd; 3419 3420 assert(phwnd !is null); 3421 3422 WCharzBuffer wt = WCharzBuffer(windowText); 3423 3424 style |= WS_VISIBLE | WS_CHILD; 3425 //if(className != WC_TABCONTROL) 3426 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 3427 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 3428 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 3429 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 3430 3431 assert(p.hwnd !is null); 3432 3433 3434 static HFONT font; 3435 if(font is null) { 3436 NONCLIENTMETRICS params; 3437 params.cbSize = params.sizeof; 3438 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 3439 font = CreateFontIndirect(¶ms.lfMessageFont); 3440 } 3441 } 3442 3443 if(font) 3444 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 3445 3446 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 3447 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 3448 Widget.nativeMapping[p.hwnd] = p; 3449 3450 if(bsgroupbox) 3451 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 3452 else 3453 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3454 3455 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 3456 3457 p.registerMovement(); 3458 } 3459 } 3460 3461 version(win32_widgets) 3462 private 3463 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 3464 if(hwnd is null || hwnd in Widget.nativeMapping) 3465 return true; 3466 auto parent = cast(Widget) cast(void*) lparam; 3467 Widget p = new Widget(null); 3468 p._parent = parent; 3469 p.parentWindow = parent.parentWindow; 3470 p.hwnd = hwnd; 3471 p.implicitlyCreated = true; 3472 Widget.nativeMapping[p.hwnd] = p; 3473 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3474 return true; 3475 } 3476 3477 /++ 3478 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 3479 +/ 3480 struct WidgetPainter { 3481 this(ScreenPainter screenPainter, Widget drawingUpon) { 3482 this.drawingUpon = drawingUpon; 3483 this.screenPainter = screenPainter; 3484 if(auto font = visualTheme.defaultFontCached) 3485 this.screenPainter.setFont(font); 3486 } 3487 3488 /++ 3489 EXPERIMENTAL. subject to change. 3490 3491 When you draw a cursor, you can draw this to notify your window of where it is, 3492 for IME systems to use. 3493 +/ 3494 void notifyCursorPosition(int x, int y, int width, int height) { 3495 if(auto a = drawingUpon.parentWindow) 3496 if(auto w = a.inputProxy) { 3497 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 3498 } 3499 } 3500 3501 3502 /// 3503 ScreenPainter screenPainter; 3504 /// Forward to the screen painter for other methods 3505 alias screenPainter this; 3506 3507 private Widget drawingUpon; 3508 3509 /++ 3510 This is the list of rectangles that actually need to be redrawn. 3511 3512 Not actually implemented yet. 3513 +/ 3514 Rectangle[] invalidatedRectangles; 3515 3516 private static BaseVisualTheme _visualTheme; 3517 3518 /++ 3519 Functions to access the visual theme and helpers to easily use it. 3520 3521 These are aware of the current widget's computed style out of the theme. 3522 +/ 3523 static @property BaseVisualTheme visualTheme() { 3524 if(_visualTheme is null) 3525 _visualTheme = new DefaultVisualTheme(); 3526 return _visualTheme; 3527 } 3528 3529 /// ditto 3530 static @property void visualTheme(BaseVisualTheme theme) { 3531 _visualTheme = theme; 3532 } 3533 3534 /// ditto 3535 Color themeForeground() { 3536 return drawingUpon.getComputedStyle().foregroundColor(); 3537 } 3538 3539 /// ditto 3540 Color themeBackground() { 3541 return drawingUpon.getComputedStyle().background.color; 3542 } 3543 3544 int isDarkTheme() { 3545 return 0; // unspecified, yes, no as enum. FIXME 3546 } 3547 3548 /++ 3549 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. 3550 3551 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 3552 3553 If you change teh clip rectangle, you should change it back before you return. 3554 3555 3556 The sequence it uses is: 3557 background 3558 content (delegated to you) 3559 border 3560 focused outline 3561 selected overlay 3562 3563 Example code: 3564 3565 --- 3566 void paint(WidgetPainter painter) { 3567 painter.drawThemed((bounds) { 3568 return bounds; // if the selection overlay should be contained, you can return it here. 3569 }); 3570 } 3571 --- 3572 +/ 3573 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 3574 drawThemed((WidgetPainter painter, const Rectangle bounds) { 3575 return drawBody(bounds); 3576 }); 3577 } 3578 // this overload is actually mroe for setting the delegate to a virtual function 3579 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 3580 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 3581 3582 auto cs = drawingUpon.getComputedStyle(); 3583 3584 auto bg = cs.background.color; 3585 3586 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 3587 3588 rect.left += borderWidth; 3589 rect.right -= borderWidth; 3590 rect.top += borderWidth; 3591 rect.bottom -= borderWidth; 3592 3593 auto insideBorderRect = rect; 3594 3595 rect.left += cs.paddingLeft; 3596 rect.right -= cs.paddingRight; 3597 rect.top += cs.paddingTop; 3598 rect.bottom += cs.paddingBottom; 3599 3600 this.outlineColor = this.themeForeground; 3601 this.fillColor = bg; 3602 3603 auto widgetFont = cs.fontCached; 3604 if(widgetFont !is null) 3605 this.setFont(widgetFont); 3606 3607 rect = drawBody(this, rect); 3608 3609 if(widgetFont !is null) { 3610 if(auto vtFont = visualTheme.defaultFontCached) 3611 this.setFont(vtFont); 3612 else 3613 this.setFont(null); 3614 } 3615 3616 if(auto os = cs.outlineStyle()) { 3617 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 3618 this.fillColor = Color.transparent; 3619 this.drawRectangle(insideBorderRect); 3620 } 3621 } 3622 3623 /++ 3624 First, draw the background. 3625 Then draw your content. 3626 Next, draw the border. 3627 And the focused indicator. 3628 And the is-selected box. 3629 3630 If it is focused i can draw the outline too... 3631 3632 If selected i can even do the xor action but that's at the end. 3633 +/ 3634 void drawThemeBackground() { 3635 3636 } 3637 3638 void drawThemeBorder() { 3639 3640 } 3641 3642 // all this stuff is a dangerous experiment.... 3643 static class ScriptableVersion { 3644 ScreenPainterImplementation* p; 3645 int originX, originY; 3646 3647 @scriptable: 3648 void drawRectangle(int x, int y, int width, int height) { 3649 p.drawRectangle(x + originX, y + originY, width, height); 3650 } 3651 void drawLine(int x1, int y1, int x2, int y2) { 3652 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 3653 } 3654 void drawText(int x, int y, string text) { 3655 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 3656 } 3657 void setOutlineColor(int r, int g, int b) { 3658 p.pen = Pen(Color(r,g,b), 1); 3659 } 3660 void setFillColor(int r, int g, int b) { 3661 p.fillColor = Color(r,g,b); 3662 } 3663 } 3664 3665 ScriptableVersion toArsdJsvar() { 3666 auto sv = new ScriptableVersion; 3667 sv.p = this.screenPainter.impl; 3668 sv.originX = this.screenPainter.originX; 3669 sv.originY = this.screenPainter.originY; 3670 return sv; 3671 } 3672 3673 static WidgetPainter fromJsVar(T)(T t) { 3674 return WidgetPainter.init; 3675 } 3676 // done.......... 3677 } 3678 3679 3680 struct Style { 3681 static struct helper(string m, T) { 3682 enum method = m; 3683 T v; 3684 3685 mixin template MethodOverride(typeof(this) v) { 3686 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 3687 } 3688 } 3689 3690 static auto opDispatch(string method, T)(T value) { 3691 return helper!(method, T)(value); 3692 } 3693 } 3694 3695 /++ 3696 Implementation detail of the [ControlledBy] UDA. 3697 3698 History: 3699 Added Oct 28, 2020 3700 +/ 3701 struct ControlledBy_(T, Args...) { 3702 Args args; 3703 3704 static if(Args.length) 3705 this(Args args) { 3706 this.args = args; 3707 } 3708 3709 private T construct(Widget parent) { 3710 return new T(args, parent); 3711 } 3712 } 3713 3714 /++ 3715 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 3716 3717 History: 3718 Added Oct 28, 2020 3719 +/ 3720 auto ControlledBy(T, Args...)(Args args) { 3721 return ControlledBy_!(T, Args)(args); 3722 } 3723 3724 struct ContainerMeta { 3725 string name; 3726 ContainerMeta[] children; 3727 Widget function(Widget parent) factory; 3728 3729 Widget instantiate(Widget parent) { 3730 auto n = factory(parent); 3731 n.name = name; 3732 foreach(child; children) 3733 child.instantiate(n); 3734 return n; 3735 } 3736 } 3737 3738 /++ 3739 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 3740 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 3741 3742 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 3743 structures. It works fine on structs declared inside functions though. 3744 3745 See: https://issues.dlang.org/show_bug.cgi?id=21984 3746 +/ 3747 template Container(CArgs...) { 3748 static if(CArgs.length && is(CArgs[0] : Widget)) { 3749 private alias Super = CArgs[0]; 3750 private alias CArgs2 = CArgs[1 .. $]; 3751 } else { 3752 private alias Super = Layout; 3753 private alias CArgs2 = CArgs; 3754 } 3755 3756 class Container : Super { 3757 this(Widget parent) { super(parent); } 3758 3759 // just to partially support old gdc versions 3760 version(GNU) { 3761 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 3762 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 3763 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 3764 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 3765 } else mixin(q{ 3766 static foreach(Arg; CArgs2) { 3767 mixin Arg.MethodOverride!(Arg); 3768 } 3769 }); 3770 3771 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 3772 return ContainerMeta( 3773 name, 3774 children.dup, 3775 function (Widget parent) { return new typeof(this)(parent); } 3776 ); 3777 } 3778 3779 static ContainerMeta opCall(ContainerMeta[] children...) { 3780 return opCall(null, children); 3781 } 3782 } 3783 } 3784 3785 /++ 3786 The data controller widget is created by reflecting over the given 3787 data type. You can use [ControlledBy] as a UDA on a struct or 3788 just let it create things automatically. 3789 3790 Unlike [dialog], this uses real-time updating of the data and 3791 you add it to another window yourself. 3792 3793 --- 3794 struct Test { 3795 int x; 3796 int y; 3797 } 3798 3799 auto window = new Window(); 3800 auto dcw = new DataControllerWidget!Test(new Test, window); 3801 --- 3802 3803 The way it works is any public members are given a widget based 3804 on their data type, and public methods trigger an action button 3805 if no relevant parameters or a dialog action if it does have 3806 parameters, similar to the [menu] facility. 3807 3808 If you change data programmatically, without going through the 3809 DataControllerWidget methods, you will have to tell it something 3810 has changed and it needs to redraw. This is done with the `invalidate` 3811 method. 3812 3813 History: 3814 Added Oct 28, 2020 3815 +/ 3816 /// Group: generating_from_code 3817 class DataControllerWidget(T) : WidgetContainer { 3818 static if(is(T == class) || is(T : const E[], E)) 3819 private alias Tref = T; 3820 else 3821 private alias Tref = T*; 3822 3823 Tref datum; 3824 3825 /++ 3826 See_also: [addDataControllerWidget] 3827 +/ 3828 this(Tref datum, Widget parent) { 3829 this.datum = datum; 3830 3831 Widget cp = this; 3832 3833 super(parent); 3834 3835 foreach(attr; __traits(getAttributes, T)) 3836 static if(is(typeof(attr) == ContainerMeta)) { 3837 cp = attr.instantiate(this); 3838 } 3839 3840 auto def = this.getByName("default"); 3841 if(def !is null) 3842 cp = def; 3843 3844 Widget helper(string name) { 3845 auto maybe = this.getByName(name); 3846 if(maybe is null) 3847 return cp; 3848 return maybe; 3849 3850 } 3851 3852 foreach(member; __traits(allMembers, T)) 3853 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 3854 static if(is(typeof(__traits(getMember, this.datum, member)))) 3855 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 3856 void delegate() update; 3857 3858 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 3859 3860 if(update) 3861 updaters ~= update; 3862 3863 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 3864 w.addEventListener("triggered", delegate() { 3865 makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))(); 3866 notifyDataUpdated(); 3867 }); 3868 } else static if(is(typeof(w.isChecked) == bool)) { 3869 w.addEventListener(EventType.change, (Event ev) { 3870 __traits(getMember, this.datum, member) = w.isChecked; 3871 }); 3872 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 3873 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 3874 } else static if(is(typeof(w.value) == int)) { 3875 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 3876 } else static if(is(typeof(w) == DropDownSelection)) { 3877 // special case for this to kinda support enums and such. coudl be better though 3878 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 3879 } else { 3880 static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 3881 } 3882 } 3883 } 3884 3885 /++ 3886 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 3887 3888 History: 3889 Added May 28, 2021 3890 +/ 3891 void notifyDataUpdated() { 3892 foreach(updater; updaters) 3893 updater(); 3894 3895 this.emit!(ChangeEvent!void)(delegate{}); 3896 } 3897 3898 private Widget[string] memberWidgets; 3899 private void delegate()[] updaters; 3900 3901 mixin Emits!(ChangeEvent!void); 3902 } 3903 3904 private int saturatedSum(int[] values...) { 3905 int sum; 3906 foreach(value; values) { 3907 if(value == int.max) 3908 return int.max; 3909 sum += value; 3910 } 3911 return sum; 3912 } 3913 3914 void genericSetValue(T, W)(T* where, W what) { 3915 import std.conv; 3916 *where = to!T(what); 3917 //*where = cast(T) stringToLong(what); 3918 } 3919 3920 /++ 3921 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 3922 3923 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 3924 3925 Note that this creates the widget but does not attach any event handlers to it. 3926 +/ 3927 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 3928 3929 string displayName = __traits(identifier, tt).beautify; 3930 3931 static if(controlledByCount!tt == 1) { 3932 foreach(i, attr; __traits(getAttributes, tt)) { 3933 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 3934 auto w = attr.construct(parent); 3935 static if(__traits(compiles, w.setPosition(*valptr))) 3936 update = () { w.setPosition(*valptr); }; 3937 else static if(__traits(compiles, w.setValue(*valptr))) 3938 update = () { w.setValue(*valptr); }; 3939 3940 if(update) 3941 update(); 3942 return w; 3943 } 3944 } 3945 } else static if(controlledByCount!tt == 0) { 3946 static if(is(typeof(tt) == enum)) { 3947 // FIXME: update 3948 auto dds = new DropDownSelection(parent); 3949 foreach(idx, option; __traits(allMembers, typeof(tt))) { 3950 dds.addOption(option); 3951 if(__traits(getMember, typeof(tt), option) == *valptr) 3952 dds.setSelection(cast(int) idx); 3953 } 3954 return dds; 3955 } else static if(is(typeof(tt) == bool)) { 3956 auto box = new Checkbox(displayName, parent); 3957 update = () { box.isChecked = *valptr; }; 3958 update(); 3959 return box; 3960 } else static if(is(typeof(tt) : const long)) { 3961 auto le = new LabeledLineEdit(displayName, parent); 3962 update = () { le.content = toInternal!string(*valptr); }; 3963 update(); 3964 return le; 3965 } else static if(is(typeof(tt) : const string)) { 3966 auto le = new LabeledLineEdit(displayName, parent); 3967 update = () { le.content = *valptr; }; 3968 update(); 3969 return le; 3970 } else static if(is(typeof(tt) == function)) { 3971 auto w = new Button(displayName, parent); 3972 return w; 3973 } 3974 } else static assert(0, "multiple controllers not yet supported"); 3975 } 3976 3977 private template controlledByCount(alias tt) { 3978 static int helper() { 3979 int count; 3980 foreach(i, attr; __traits(getAttributes, tt)) 3981 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 3982 count++; 3983 return count; 3984 } 3985 3986 enum controlledByCount = helper; 3987 } 3988 3989 /++ 3990 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 3991 3992 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 3993 3994 History: 3995 The `redrawOnChange` parameter was added on May 28, 2021. 3996 +/ 3997 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class)) { 3998 auto dcw = new DataControllerWidget!T(t, parent); 3999 initializeDataControllerWidget(dcw, redrawOnChange); 4000 return dcw; 4001 } 4002 4003 /// ditto 4004 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 4005 auto dcw = new DataControllerWidget!T(t, parent); 4006 initializeDataControllerWidget(dcw, redrawOnChange); 4007 return dcw; 4008 } 4009 4010 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 4011 if(redrawOnChange !is null) 4012 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 4013 } 4014 4015 /++ 4016 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. 4017 4018 History: 4019 Finalized on June 3, 2021 for the dub v10.0 release 4020 +/ 4021 struct StyleInformation { 4022 private Widget w; 4023 private BaseVisualTheme visualTheme; 4024 4025 private this(Widget w) { 4026 this.w = w; 4027 this.visualTheme = WidgetPainter.visualTheme; 4028 } 4029 4030 /++ 4031 Forwards to [Widget.Style] 4032 4033 Bugs: 4034 It is supposed to fall back to the [VisualTheme] if 4035 the style doesn't override the default, but that is 4036 not generally implemented. Many of them may end up 4037 being explicit overloads instead of the generic 4038 opDispatch fallback, like [font] is now. 4039 +/ 4040 public @property opDispatch(string name)() { 4041 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4042 w.useStyleProperties((scope Widget.Style props) { 4043 //visualTheme.useStyleProperties(w, (props) { 4044 prop = __traits(getMember, props, name); 4045 }); 4046 return prop; 4047 } 4048 4049 /++ 4050 Returns the cached font object associated with the widget, 4051 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 4052 4053 History: 4054 Prior to March 21, 2022 (dub v10.7), `font` went through 4055 [opDispatch], which did not use the cache. You can now call it 4056 repeatedly without guilt. 4057 +/ 4058 public @property OperatingSystemFont font() { 4059 OperatingSystemFont prop; 4060 w.useStyleProperties((scope Widget.Style props) { 4061 prop = props.fontCached; 4062 }); 4063 if(prop is null) 4064 prop = visualTheme.defaultFontCached; 4065 return prop; 4066 } 4067 4068 @property { 4069 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 4070 /** */ int paddingLeft() { return w.paddingLeft(); } 4071 /** */ int paddingRight() { return w.paddingRight(); } 4072 /** */ int paddingTop() { return w.paddingTop(); } 4073 /** */ int paddingBottom() { return w.paddingBottom(); } 4074 4075 /** */ int marginLeft() { return w.marginLeft(); } 4076 /** */ int marginRight() { return w.marginRight(); } 4077 /** */ int marginTop() { return w.marginTop(); } 4078 /** */ int marginBottom() { return w.marginBottom(); } 4079 4080 /** */ int maxHeight() { return w.maxHeight(); } 4081 /** */ int minHeight() { return w.minHeight(); } 4082 4083 /** */ int maxWidth() { return w.maxWidth(); } 4084 /** */ int minWidth() { return w.minWidth(); } 4085 4086 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 4087 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 4088 4089 /** */ int heightStretchiness() { return w.heightStretchiness(); } 4090 /** */ int widthStretchiness() { return w.widthStretchiness(); } 4091 4092 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 4093 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 4094 4095 // Global helpers some of these are unstable. 4096 static: 4097 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4098 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4099 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4100 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4101 4102 /** */ Color activeTabColor() { return lightAccentColor; } 4103 /** */ Color buttonColor() { return windowBackgroundColor; } 4104 /** */ Color depressedButtonColor() { return darkAccentColor; } 4105 /** */ Color hoveringColor() { return Color(228, 228, 228); } 4106 /** */ Color activeListXorColor() { 4107 auto c = WidgetPainter.visualTheme.selectionColor(); 4108 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4109 } 4110 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4111 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4112 } 4113 4114 4115 4116 /+ 4117 4118 private static auto extractStyleProperty(string name)(Widget w) { 4119 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4120 w.useStyleProperties((props) { 4121 prop = __traits(getMember, props, name); 4122 }); 4123 return prop; 4124 } 4125 4126 // FIXME: clear this upon a X server disconnect 4127 private static OperatingSystemFont[string] fontCache; 4128 4129 T getProperty(T)(string name, lazy T default_) { 4130 if(visualTheme !is null) { 4131 auto str = visualTheme.getPropertyString(w, name); 4132 if(str is null) 4133 return default_; 4134 static if(is(T == Color)) 4135 return Color.fromString(str); 4136 else static if(is(T == Measurement)) 4137 return Measurement(cast(int) toInternal!int(str)); 4138 else static if(is(T == WidgetBackground)) 4139 return WidgetBackground.fromString(str); 4140 else static if(is(T == OperatingSystemFont)) { 4141 if(auto f = str in fontCache) 4142 return *f; 4143 else 4144 return fontCache[str] = new OperatingSystemFont(str); 4145 } else static if(is(T == FrameStyle)) { 4146 switch(str) { 4147 default: 4148 return FrameStyle.none; 4149 foreach(style; __traits(allMembers, FrameStyle)) 4150 case style: 4151 return __traits(getMember, FrameStyle, style); 4152 } 4153 } else static assert(0); 4154 } else 4155 return default_; 4156 } 4157 4158 static struct Measurement { 4159 int value; 4160 alias value this; 4161 } 4162 4163 @property: 4164 4165 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 4166 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 4167 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 4168 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 4169 4170 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 4171 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 4172 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 4173 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 4174 4175 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 4176 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 4177 4178 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 4179 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 4180 4181 4182 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 4183 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 4184 4185 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 4186 4187 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 4188 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 4189 4190 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 4191 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 4192 4193 4194 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4195 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4196 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4197 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4198 4199 Color activeTabColor() { return lightAccentColor; } 4200 Color buttonColor() { return windowBackgroundColor; } 4201 Color depressedButtonColor() { return darkAccentColor; } 4202 Color hoveringColor() { return Color(228, 228, 228); } 4203 Color activeListXorColor() { 4204 auto c = WidgetPainter.visualTheme.selectionColor(); 4205 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4206 } 4207 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4208 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4209 +/ 4210 } 4211 4212 4213 4214 // pragma(msg, __traits(classInstanceSize, Widget)); 4215 4216 /*private*/ template EventString(E) { 4217 static if(is(typeof(E.EventString))) 4218 enum EventString = E.EventString; 4219 else 4220 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 4221 } 4222 4223 /*private*/ template EventStringIdentifier(E) { 4224 string helper() { 4225 auto es = EventString!E; 4226 char[] id = new char[](es.length * 2); 4227 size_t idx; 4228 foreach(char ch; es) { 4229 id[idx++] = cast(char)('a' + (ch >> 4)); 4230 id[idx++] = cast(char)('a' + (ch & 0x0f)); 4231 } 4232 return cast(string) id; 4233 } 4234 4235 enum EventStringIdentifier = helper(); 4236 } 4237 4238 4239 template classStaticallyEmits(This, EventType) { 4240 static if(is(This Base == super)) 4241 static if(is(Base : Widget)) 4242 enum baseEmits = classStaticallyEmits!(Base, EventType); 4243 else 4244 enum baseEmits = false; 4245 else 4246 enum baseEmits = false; 4247 4248 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 4249 4250 enum classStaticallyEmits = thisEmits || baseEmits; 4251 } 4252 4253 /++ 4254 A helper to make widgets out of other native windows. 4255 4256 History: 4257 Factored out of OpenGlWidget on November 5, 2021 4258 +/ 4259 class NestedChildWindowWidget : Widget { 4260 SimpleWindow win; 4261 4262 /++ 4263 Used on X to send focus to the appropriate child window when requested by the window manager. 4264 4265 Normally returns its own nested window. Can also return another child or null to revert to the parent 4266 if you override it in a child class. 4267 4268 History: 4269 Added April 2, 2022 (dub v10.8) 4270 +/ 4271 SimpleWindow focusableWindow() { 4272 return win; 4273 } 4274 4275 /// 4276 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4277 this(SimpleWindow win, Widget parent) { 4278 this.parentWindow = parent.parentWindow; 4279 this.win = win; 4280 4281 super(parent); 4282 windowsetup(win); 4283 } 4284 4285 static protected SimpleWindow getParentWindow(Widget parent) { 4286 assert(parent !is null); 4287 SimpleWindow pwin = parent.parentWindow.win; 4288 4289 version(win32_widgets) { 4290 HWND phwnd; 4291 auto wtf = parent; 4292 while(wtf) { 4293 if(wtf.hwnd) { 4294 phwnd = wtf.hwnd; 4295 break; 4296 } 4297 wtf = wtf.parent; 4298 } 4299 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 4300 if(phwnd) 4301 pwin = new SimpleWindow(phwnd); 4302 } 4303 4304 return pwin; 4305 } 4306 4307 /++ 4308 Called upon the nested window being destroyed. 4309 Remember the window has already been destroyed at 4310 this point, so don't use the native handle for anything. 4311 4312 History: 4313 Added April 3, 2022 (dub v10.8) 4314 +/ 4315 protected void dispose() { 4316 4317 } 4318 4319 protected void windowsetup(SimpleWindow w) { 4320 /* 4321 win.onFocusChange = (bool getting) { 4322 if(getting) 4323 this.focus(); 4324 }; 4325 */ 4326 4327 /+ 4328 win.onFocusChange = (bool getting) { 4329 if(getting) { 4330 this.parentWindow.focusedWidget = this; 4331 this.emit!FocusEvent(); 4332 this.emit!FocusInEvent(); 4333 } else { 4334 this.emit!BlurEvent(); 4335 this.emit!FocusOutEvent(); 4336 } 4337 }; 4338 +/ 4339 4340 win.onDestroyed = () { 4341 this.dispose(); 4342 }; 4343 4344 version(win32_widgets) { 4345 Widget.nativeMapping[win.hwnd] = this; 4346 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4347 } else { 4348 win.setEventHandlers( 4349 (MouseEvent e) { 4350 Widget p = this; 4351 while(p ! is parentWindow) { 4352 e.x += p.x; 4353 e.y += p.y; 4354 p = p.parent; 4355 } 4356 parentWindow.dispatchMouseEvent(e); 4357 }, 4358 (KeyEvent e) { 4359 //import std.stdio; writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 4360 parentWindow.dispatchKeyEvent(e); 4361 }, 4362 (dchar e) { 4363 parentWindow.dispatchCharEvent(e); 4364 }, 4365 ); 4366 } 4367 4368 } 4369 4370 override void showing(bool s, bool recalc) { 4371 auto cur = hidden; 4372 win.hidden = !s; 4373 if(cur != s && s) 4374 redraw(); 4375 } 4376 4377 /// OpenGL widgets cannot have child widgets. Do not call this. 4378 /* @disable */ final override void addChild(Widget, int) { 4379 throw new Error("cannot add children to OpenGL widgets"); 4380 } 4381 4382 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 4383 /// Keep in mind that events like mouse coordinates are still relative to your size. 4384 override void registerMovement() { 4385 //import std.stdio; writefln("%d %d %d %d", x,y,width,height); 4386 version(win32_widgets) 4387 auto pos = getChildPositionRelativeToParentHwnd(this); 4388 else 4389 auto pos = getChildPositionRelativeToParentOrigin(this); 4390 win.moveResize(pos[0], pos[1], width, height); 4391 4392 registerMovementAdditionalWork(); 4393 sendResizeEvent(); 4394 } 4395 4396 abstract void registerMovementAdditionalWork(); 4397 } 4398 4399 /++ 4400 Nests an opengl capable window inside this window as a widget. 4401 4402 You may also just want to create an additional [SimpleWindow] with 4403 [OpenGlOptions.yes] yourself. 4404 4405 An OpenGL widget cannot have child widgets. It will throw if you try. 4406 +/ 4407 static if(OpenGlEnabled) 4408 class OpenGlWidget : NestedChildWindowWidget { 4409 4410 override void registerMovementAdditionalWork() { 4411 win.setAsCurrentOpenGlContext(); 4412 } 4413 4414 /// 4415 this(Widget parent) { 4416 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4417 super(win, parent); 4418 } 4419 4420 override void paint(WidgetPainter painter) { 4421 win.setAsCurrentOpenGlContext(); 4422 glViewport(0, 0, this.width, this.height); 4423 win.redrawOpenGlSceneNow(); 4424 } 4425 4426 void redrawOpenGlScene(void delegate() dg) { 4427 win.redrawOpenGlScene = dg; 4428 } 4429 } 4430 4431 /++ 4432 This demo shows how to draw text in an opengl scene. 4433 +/ 4434 unittest { 4435 import arsd.minigui; 4436 import arsd.ttf; 4437 4438 void main() { 4439 auto window = new Window(); 4440 4441 auto widget = new OpenGlWidget(window); 4442 4443 // old means non-shader code so compatible with glBegin etc. 4444 // tbh I haven't implemented new one in font yet... 4445 // anyway, declaring here, will construct soon. 4446 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 4447 4448 // this is a little bit awkward, calling some methods through 4449 // the underlying SimpleWindow `win` method, and you can't do this 4450 // on a nanovega widget due to conflicts so I should probably fix 4451 // the api to be a bit easier. But here it will work. 4452 // 4453 // Alternatively, you could load the font on the first draw, inside 4454 // the redrawOpenGlScene, and keep a flag so you don't do it every 4455 // time. That'd be a bit easier since the lib sets up the context 4456 // by then guaranteed. 4457 // 4458 // But still, I wanna show this. 4459 widget.win.visibleForTheFirstTime = delegate { 4460 // must set the opengl context 4461 widget.win.setAsCurrentOpenGlContext(); 4462 4463 // if you were doing a OpenGL 3+ shader, this 4464 // gets especially important to do in order. With 4465 // old-style opengl, I think you can even do it 4466 // in main(), but meh, let's show it more correctly. 4467 4468 // Anyway, now it is time to load the font from the 4469 // OS (you can alternatively load one from a .ttf file 4470 // you bundle with the application), then load the 4471 // font into texture for drawing. 4472 4473 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 4474 4475 assert(!osfont.isNull()); // make sure it actually loaded 4476 4477 // using typeof to avoid repeating the long name lol 4478 glfont = new typeof(glfont)( 4479 // get the raw data from the font for loading in here 4480 // since it doesn't use the OS function to draw the 4481 // text, we gotta treat it more as a file than as 4482 // a drawing api. 4483 osfont.getTtfBytes(), 4484 18, // need to respecify size since opengl world is different coordinate system 4485 4486 // these last two numbers are why it is called 4487 // "Limited" font. It only loads the characters 4488 // in the given range, since the texture atlas 4489 // it references is all a big image generated ahead 4490 // of time. You could maybe do the whole thing but 4491 // idk how much memory that is. 4492 // 4493 // But here, 0-128 represents the ASCII range, so 4494 // good enough for most English things, numeric labels, 4495 // etc. 4496 0, 4497 128 4498 ); 4499 }; 4500 4501 widget.redrawOpenGlScene = () { 4502 // now we can use the glfont's drawString function 4503 4504 // first some opengl setup. You can do this in one place 4505 // on window first visible too in many cases, just showing 4506 // here cuz it is easier for me. 4507 4508 // gonna need some alpha blending or it just looks awful 4509 glEnable(GL_BLEND); 4510 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 4511 glClearColor(0,0,0,0); 4512 glDepthFunc(GL_LEQUAL); 4513 4514 // Also need to enable 2d textures, since it draws the 4515 // font characters as images baked in 4516 glMatrixMode(GL_MODELVIEW); 4517 glLoadIdentity(); 4518 glDisable(GL_DEPTH_TEST); 4519 glEnable(GL_TEXTURE_2D); 4520 4521 // the orthographic matrix is best for 2d things like text 4522 // so let's set that up. This matrix makes the coordinates 4523 // in the opengl scene be one-to-one with the actual pixels 4524 // on screen. (Not necessarily best, you may wish to scale 4525 // things, but it does help keep fonts looking normal.) 4526 glMatrixMode(GL_PROJECTION); 4527 glLoadIdentity(); 4528 glOrtho(0, widget.width, widget.height, 0, 0, 1); 4529 4530 // you can do other glScale, glRotate, glTranslate, etc 4531 // to the matrix here of course if you want. 4532 4533 // note the x,y coordinates here are for the text baseline 4534 // NOT the upper-left corner. The baseline is like the line 4535 // in the notebook you write on. Most the letters are actually 4536 // above it, but some, like p and q, dip a bit below it. 4537 // 4538 // So if you're used to the upper left coordinate like the 4539 // rest of simpledisplay/minigui usually do, do the 4540 // y + glfont.ascent to bring it down a little. So this 4541 // example puts the string in the upper left of the window. 4542 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 4543 4544 // re color btw: the function sets a solid color internally, 4545 // but you actually COULD do your own thing for rainbow effects 4546 // and the sort if you wanted too, by pulling its guts out. 4547 // Just view its source for an idea of how it actually draws: 4548 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 4549 4550 // it gets a bit complicated with the character positioning, 4551 // but the opengl parts are fairly simple: bind a texture, 4552 // set the color, draw a quad for each letter. 4553 4554 4555 // the last optional argument there btw is a bounding box 4556 // it will/ use to word wrap and return an object you can 4557 // use to implement scrolling or pagination; it tells how 4558 // much of the string didn't fit in the box. But for simple 4559 // labels we can just ignore that. 4560 4561 4562 // I'd suggest drawing text as the last step, after you 4563 // do your other drawing. You might use the push/pop matrix 4564 // stuff to keep your place. You, in theory, should be able 4565 // to do text in a 3d space but I've never actually tried 4566 // that.... 4567 }; 4568 4569 window.loop(); 4570 } 4571 } 4572 4573 version(custom_widgets) 4574 private alias ListWidgetBase = ScrollableWidget; 4575 else 4576 private alias ListWidgetBase = Widget; 4577 4578 /++ 4579 A list widget contains a list of strings that the user can examine and select. 4580 4581 4582 In the future, items in the list may be possible to be more than just strings. 4583 4584 See_Also: 4585 [TableView] 4586 +/ 4587 class ListWidget : ListWidgetBase { 4588 /// 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. 4589 mixin Emits!(ChangeEvent!void); 4590 4591 static struct Option { 4592 string label; 4593 bool selected; 4594 void* tag; 4595 } 4596 4597 /++ 4598 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 4599 +/ 4600 void setSelection(int y) { 4601 if(!multiSelect) 4602 foreach(ref opt; options) 4603 opt.selected = false; 4604 if(y >= 0 && y < options.length) 4605 options[y].selected = !options[y].selected; 4606 4607 this.emit!(ChangeEvent!void)(delegate {}); 4608 4609 version(custom_widgets) 4610 redraw(); 4611 } 4612 4613 /++ 4614 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 4615 Returns -1 if nothing is selected. 4616 +/ 4617 int getSelection() 4618 { 4619 foreach(i, opt; options) { 4620 if (opt.selected) 4621 return cast(int) i; 4622 } 4623 return -1; 4624 } 4625 4626 version(custom_widgets) 4627 override void defaultEventHandler_click(ClickEvent event) { 4628 this.focus(); 4629 if(event.button == MouseButton.left) { 4630 auto y = (event.clientY - 4) / defaultLineHeight; 4631 if(y >= 0 && y < options.length) { 4632 setSelection(y); 4633 } 4634 } 4635 super.defaultEventHandler_click(event); 4636 } 4637 4638 this(Widget parent) { 4639 tabStop = false; 4640 super(parent); 4641 version(win32_widgets) 4642 createWin32Window(this, WC_LISTBOX, "", 4643 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 4644 } 4645 4646 version(win32_widgets) 4647 override void handleWmCommand(ushort code, ushort id) { 4648 switch(code) { 4649 case LBN_SELCHANGE: 4650 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 4651 setSelection(cast(int) sel); 4652 break; 4653 default: 4654 } 4655 } 4656 4657 4658 version(custom_widgets) 4659 override void paintFrameAndBackground(WidgetPainter painter) { 4660 draw3dFrame(this, painter, FrameStyle.sunk, Color.white); 4661 } 4662 4663 version(custom_widgets) 4664 override void paint(WidgetPainter painter) { 4665 auto cs = getComputedStyle(); 4666 auto pos = Point(4, 4); 4667 foreach(idx, option; options) { 4668 painter.fillColor = Color.white; 4669 painter.outlineColor = Color.white; 4670 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4671 painter.outlineColor = cs.foregroundColor; 4672 painter.drawText(pos, option.label); 4673 if(option.selected) { 4674 painter.rasterOp = RasterOp.xor; 4675 painter.outlineColor = Color.white; 4676 painter.fillColor = cs.activeListXorColor; 4677 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4678 painter.rasterOp = RasterOp.normal; 4679 } 4680 pos.y += defaultLineHeight; 4681 } 4682 } 4683 4684 static class Style : Widget.Style { 4685 override WidgetBackground background() { 4686 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 4687 } 4688 } 4689 mixin OverrideStyle!Style; 4690 //mixin Padding!q{2}; 4691 4692 void addOption(string text, void* tag = null) { 4693 options ~= Option(text, false, tag); 4694 version(win32_widgets) { 4695 WCharzBuffer buffer = WCharzBuffer(text); 4696 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 4697 } 4698 version(custom_widgets) { 4699 setContentSize(width, cast(int) (options.length * defaultLineHeight)); 4700 redraw(); 4701 } 4702 } 4703 4704 void clear() { 4705 options = null; 4706 version(win32_widgets) { 4707 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 4708 {} 4709 4710 } else version(custom_widgets) { 4711 scrollTo(Point(0, 0)); 4712 redraw(); 4713 } 4714 } 4715 4716 Option[] options; 4717 version(win32_widgets) 4718 enum multiSelect = false; /// not implemented yet 4719 else 4720 bool multiSelect; 4721 4722 override int heightStretchiness() { return 6; } 4723 } 4724 4725 4726 4727 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 4728 enum ScrollBarShowPolicy { 4729 automatic, /// automatically show the scroll bar if it is necessary 4730 never, /// never show the scroll bar (scrolling must be done programmatically) 4731 always /// always show the scroll bar, even if it is disabled 4732 } 4733 4734 /++ 4735 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 4736 4737 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 4738 +/ 4739 // FIXME ScrollBarShowPolicy 4740 // FIXME: use the ScrollMessageWidget in here now that it exists 4741 class ScrollableWidget : Widget { 4742 // FIXME: make line size configurable 4743 // FIXME: add keyboard controls 4744 version(win32_widgets) { 4745 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 4746 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 4747 auto pos = HIWORD(wParam); 4748 auto m = LOWORD(wParam); 4749 4750 // FIXME: I can reintroduce the 4751 // scroll bars now by using this 4752 // in the top-level window handler 4753 // to forward comamnds 4754 auto scrollbarHwnd = lParam; 4755 switch(m) { 4756 case SB_BOTTOM: 4757 if(msg == WM_HSCROLL) 4758 horizontalScrollTo(contentWidth_); 4759 else 4760 verticalScrollTo(contentHeight_); 4761 break; 4762 case SB_TOP: 4763 if(msg == WM_HSCROLL) 4764 horizontalScrollTo(0); 4765 else 4766 verticalScrollTo(0); 4767 break; 4768 case SB_ENDSCROLL: 4769 // idk 4770 break; 4771 case SB_LINEDOWN: 4772 if(msg == WM_HSCROLL) 4773 horizontalScroll(scaleWithDpi(16)); 4774 else 4775 verticalScroll(scaleWithDpi(16)); 4776 break; 4777 case SB_LINEUP: 4778 if(msg == WM_HSCROLL) 4779 horizontalScroll(scaleWithDpi(-16)); 4780 else 4781 verticalScroll(scaleWithDpi(-16)); 4782 break; 4783 case SB_PAGEDOWN: 4784 if(msg == WM_HSCROLL) 4785 horizontalScroll(scaleWithDpi(100)); 4786 else 4787 verticalScroll(scaleWithDpi(100)); 4788 break; 4789 case SB_PAGEUP: 4790 if(msg == WM_HSCROLL) 4791 horizontalScroll(scaleWithDpi(-100)); 4792 else 4793 verticalScroll(scaleWithDpi(-100)); 4794 break; 4795 case SB_THUMBPOSITION: 4796 case SB_THUMBTRACK: 4797 if(msg == WM_HSCROLL) 4798 horizontalScrollTo(pos); 4799 else 4800 verticalScrollTo(pos); 4801 4802 if(m == SB_THUMBTRACK) { 4803 // the event loop doesn't seem to carry on with a requested redraw.. 4804 // so we request it to get our dirty bit set... 4805 redraw(); 4806 4807 // then we need to immediately actually redraw it too for instant feedback to user 4808 4809 SimpleWindow.processAllCustomEvents(); 4810 //if(parentWindow) 4811 //parentWindow.actualRedraw(); 4812 } 4813 break; 4814 default: 4815 } 4816 } 4817 return super.hookedWndProc(msg, wParam, lParam); 4818 } 4819 } 4820 /// 4821 this(Widget parent) { 4822 this.parentWindow = parent.parentWindow; 4823 4824 version(win32_widgets) { 4825 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 4826 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 4827 super(parent); 4828 } else version(custom_widgets) { 4829 outerContainer = new InternalScrollableContainerWidget(this, parent); 4830 super(outerContainer); 4831 } else static assert(0); 4832 } 4833 4834 version(custom_widgets) 4835 InternalScrollableContainerWidget outerContainer; 4836 4837 override void defaultEventHandler_click(ClickEvent event) { 4838 if(event.button == MouseButton.wheelUp) 4839 verticalScroll(scaleWithDpi(-16)); 4840 if(event.button == MouseButton.wheelDown) 4841 verticalScroll(scaleWithDpi(16)); 4842 super.defaultEventHandler_click(event); 4843 } 4844 4845 override void defaultEventHandler_keydown(KeyDownEvent event) { 4846 switch(event.key) { 4847 case Key.Left: 4848 horizontalScroll(scaleWithDpi(-16)); 4849 break; 4850 case Key.Right: 4851 horizontalScroll(scaleWithDpi(16)); 4852 break; 4853 case Key.Up: 4854 verticalScroll(scaleWithDpi(-16)); 4855 break; 4856 case Key.Down: 4857 verticalScroll(scaleWithDpi(16)); 4858 break; 4859 case Key.Home: 4860 verticalScrollTo(0); 4861 break; 4862 case Key.End: 4863 verticalScrollTo(contentHeight); 4864 break; 4865 case Key.PageUp: 4866 verticalScroll(scaleWithDpi(-160)); 4867 break; 4868 case Key.PageDown: 4869 verticalScroll(scaleWithDpi(160)); 4870 break; 4871 default: 4872 } 4873 super.defaultEventHandler_keydown(event); 4874 } 4875 4876 4877 version(win32_widgets) 4878 override void recomputeChildLayout() { 4879 super.recomputeChildLayout(); 4880 SCROLLINFO info; 4881 info.cbSize = info.sizeof; 4882 info.nPage = viewportHeight; 4883 info.fMask = SIF_PAGE | SIF_RANGE; 4884 info.nMin = 0; 4885 info.nMax = contentHeight_; 4886 SetScrollInfo(hwnd, SB_VERT, &info, true); 4887 4888 info.cbSize = info.sizeof; 4889 info.nPage = viewportWidth; 4890 info.fMask = SIF_PAGE | SIF_RANGE; 4891 info.nMin = 0; 4892 info.nMax = contentWidth_; 4893 SetScrollInfo(hwnd, SB_HORZ, &info, true); 4894 } 4895 4896 /* 4897 Scrolling 4898 ------------ 4899 4900 You are assigned a width and a height by the layout engine, which 4901 is your viewport box. However, you may draw more than that by setting 4902 a contentWidth and contentHeight. 4903 4904 If these can be contained by the viewport, no scrollbar is displayed. 4905 If they cannot fit though, it will automatically show scroll as necessary. 4906 4907 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 4908 is zero, no vertical scrolling is performed. 4909 4910 If scrolling is necessary, the lib will automatically work with the bars. 4911 When you redraw, the origin and clipping info in the painter is set so if 4912 you just draw everything, it will work, but you can be more efficient by checking 4913 the viewportWidth, viewportHeight, and scrollOrigin members. 4914 */ 4915 4916 /// 4917 final @property int viewportWidth() { 4918 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 4919 } 4920 /// 4921 final @property int viewportHeight() { 4922 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 4923 } 4924 4925 // FIXME property 4926 Point scrollOrigin_; 4927 4928 /// 4929 final const(Point) scrollOrigin() { 4930 return scrollOrigin_; 4931 } 4932 4933 // the user sets these two 4934 private int contentWidth_ = 0; 4935 private int contentHeight_ = 0; 4936 4937 /// 4938 int contentWidth() { return contentWidth_; } 4939 /// 4940 int contentHeight() { return contentHeight_; } 4941 4942 /// 4943 void setContentSize(int width, int height) { 4944 contentWidth_ = width; 4945 contentHeight_ = height; 4946 4947 version(custom_widgets) { 4948 if(showingVerticalScroll || showingHorizontalScroll) { 4949 outerContainer.recomputeChildLayout(); 4950 } 4951 4952 if(showingVerticalScroll()) 4953 outerContainer.verticalScrollBar.redraw(); 4954 if(showingHorizontalScroll()) 4955 outerContainer.horizontalScrollBar.redraw(); 4956 } else version(win32_widgets) { 4957 recomputeChildLayout(); 4958 } else static assert(0); 4959 } 4960 4961 /// 4962 void verticalScroll(int delta) { 4963 verticalScrollTo(scrollOrigin.y + delta); 4964 } 4965 /// 4966 void verticalScrollTo(int pos) { 4967 scrollOrigin_.y = pos; 4968 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 4969 scrollOrigin_.y = contentHeight - viewportHeight; 4970 4971 if(scrollOrigin_.y < 0) 4972 scrollOrigin_.y = 0; 4973 4974 version(win32_widgets) { 4975 SCROLLINFO info; 4976 info.cbSize = info.sizeof; 4977 info.fMask = SIF_POS; 4978 info.nPos = scrollOrigin_.y; 4979 SetScrollInfo(hwnd, SB_VERT, &info, true); 4980 } else version(custom_widgets) { 4981 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 4982 } else static assert(0); 4983 4984 redraw(); 4985 } 4986 4987 /// 4988 void horizontalScroll(int delta) { 4989 horizontalScrollTo(scrollOrigin.x + delta); 4990 } 4991 /// 4992 void horizontalScrollTo(int pos) { 4993 scrollOrigin_.x = pos; 4994 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 4995 scrollOrigin_.x = contentWidth - viewportWidth; 4996 4997 if(scrollOrigin_.x < 0) 4998 scrollOrigin_.x = 0; 4999 5000 version(win32_widgets) { 5001 SCROLLINFO info; 5002 info.cbSize = info.sizeof; 5003 info.fMask = SIF_POS; 5004 info.nPos = scrollOrigin_.x; 5005 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5006 } else version(custom_widgets) { 5007 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 5008 } else static assert(0); 5009 5010 redraw(); 5011 } 5012 /// 5013 void scrollTo(Point p) { 5014 verticalScrollTo(p.y); 5015 horizontalScrollTo(p.x); 5016 } 5017 5018 /// 5019 void ensureVisibleInScroll(Point p) { 5020 auto rect = viewportRectangle(); 5021 if(rect.contains(p)) 5022 return; 5023 if(p.x < rect.left) 5024 horizontalScroll(p.x - rect.left); 5025 else if(p.x > rect.right) 5026 horizontalScroll(p.x - rect.right); 5027 5028 if(p.y < rect.top) 5029 verticalScroll(p.y - rect.top); 5030 else if(p.y > rect.bottom) 5031 verticalScroll(p.y - rect.bottom); 5032 } 5033 5034 /// 5035 void ensureVisibleInScroll(Rectangle rect) { 5036 ensureVisibleInScroll(rect.upperLeft); 5037 ensureVisibleInScroll(rect.lowerRight); 5038 } 5039 5040 /// 5041 Rectangle viewportRectangle() { 5042 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 5043 } 5044 5045 /// 5046 bool showingHorizontalScroll() { 5047 return contentWidth > width; 5048 } 5049 /// 5050 bool showingVerticalScroll() { 5051 return contentHeight > height; 5052 } 5053 5054 /// This is called before the ordinary paint delegate, 5055 /// giving you a chance to draw the window frame, etc, 5056 /// before the scroll clip takes effect 5057 void paintFrameAndBackground(WidgetPainter painter) { 5058 version(win32_widgets) { 5059 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5060 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5061 // since the pen is null, to fill the whole space, we need the +1 on both. 5062 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5063 SelectObject(painter.impl.hdc, p); 5064 SelectObject(painter.impl.hdc, b); 5065 } 5066 5067 } 5068 5069 // make space for the scroll bar, and that's it. 5070 final override int paddingRight() { return scaleWithDpi(16); } 5071 final override int paddingBottom() { return scaleWithDpi(16); } 5072 5073 /* 5074 END SCROLLING 5075 */ 5076 5077 override WidgetPainter draw() { 5078 int x = this.x, y = this.y; 5079 auto parent = this.parent; 5080 while(parent) { 5081 x += parent.x; 5082 y += parent.y; 5083 parent = parent.parent; 5084 } 5085 5086 //version(win32_widgets) { 5087 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5088 //} else { 5089 auto painter = parentWindow.win.draw(true); 5090 //} 5091 painter.originX = x; 5092 painter.originY = y; 5093 5094 painter.originX = painter.originX - scrollOrigin.x; 5095 painter.originY = painter.originY - scrollOrigin.y; 5096 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 5097 5098 return WidgetPainter(painter, this); 5099 } 5100 5101 mixin ScrollableChildren; 5102 } 5103 5104 // you need to have a Point scrollOrigin in the class somewhere 5105 // and a paintFrameAndBackground 5106 private mixin template ScrollableChildren() { 5107 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5108 if(hidden) 5109 return; 5110 5111 //version(win32_widgets) 5112 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5113 5114 painter.originX = lox + x; 5115 painter.originY = loy + y; 5116 5117 bool actuallyPainted = false; 5118 5119 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 5120 if(clip == Rectangle.init) 5121 return; 5122 5123 if(force || redrawRequested) { 5124 //painter.setClipRectangle(scrollOrigin, width, height); 5125 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5126 paintFrameAndBackground(painter); 5127 } 5128 5129 painter.originX = painter.originX - scrollOrigin.x; 5130 painter.originY = painter.originY - scrollOrigin.y; 5131 if(force || redrawRequested) { 5132 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 5133 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 5134 5135 //erase(painter); // we paintFrameAndBackground above so no need 5136 if(painter.visualTheme) 5137 painter.visualTheme.doPaint(this, painter); 5138 else 5139 paint(painter); 5140 5141 if(invalidate) { 5142 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5143 // children are contained inside this, so no need to do extra work 5144 invalidate = false; 5145 } 5146 5147 5148 actuallyPainted = true; 5149 redrawRequested = false; 5150 } 5151 foreach(child; children) { 5152 if(cast(FixedPosition) child) 5153 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5154 else 5155 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5156 } 5157 } 5158 } 5159 5160 private class InternalScrollableContainerInsideWidget : ContainerWidget { 5161 ScrollableContainerWidget scw; 5162 5163 this(ScrollableContainerWidget parent) { 5164 scw = parent; 5165 super(parent); 5166 } 5167 5168 version(custom_widgets) 5169 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5170 if(hidden) 5171 return; 5172 5173 bool actuallyPainted = false; 5174 5175 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 5176 5177 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 5178 if(clip == Rectangle.init) 5179 return; 5180 5181 painter.originX = lox + x - scrollOrigin.x; 5182 painter.originY = loy + y - scrollOrigin.y; 5183 if(force || redrawRequested) { 5184 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5185 5186 erase(painter); 5187 if(painter.visualTheme) 5188 painter.visualTheme.doPaint(this, painter); 5189 else 5190 paint(painter); 5191 5192 if(invalidate) { 5193 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5194 // children are contained inside this, so no need to do extra work 5195 invalidate = false; 5196 } 5197 5198 actuallyPainted = true; 5199 redrawRequested = false; 5200 } 5201 foreach(child; children) { 5202 if(cast(FixedPosition) child) 5203 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5204 else 5205 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5206 } 5207 } 5208 5209 version(custom_widgets) 5210 override protected void addScrollPosition(ref int x, ref int y) { 5211 x += scw.scrollX_; 5212 y += scw.scrollY_; 5213 } 5214 } 5215 5216 /++ 5217 A widget meant to contain other widgets that may need to scroll. 5218 5219 Currently buggy. 5220 5221 History: 5222 Added July 1, 2021 (dub v10.2) 5223 5224 On January 3, 2022, I tried to use it in a few other cases 5225 and found it only worked well in the original test case. Since 5226 it still sucks, I think I'm going to rewrite it again. 5227 +/ 5228 class ScrollableContainerWidget : ContainerWidget { 5229 /// 5230 this(Widget parent) { 5231 super(parent); 5232 5233 container = new InternalScrollableContainerInsideWidget(this); 5234 hsb = new HorizontalScrollbar(this); 5235 vsb = new VerticalScrollbar(this); 5236 5237 tabStop = false; 5238 container.tabStop = false; 5239 magic = true; 5240 5241 5242 vsb.addEventListener("scrolltonextline", () { 5243 scrollBy(0, scaleWithDpi(16)); 5244 }); 5245 vsb.addEventListener("scrolltopreviousline", () { 5246 scrollBy(0,scaleWithDpi( -16)); 5247 }); 5248 vsb.addEventListener("scrolltonextpage", () { 5249 scrollBy(0, container.height); 5250 }); 5251 vsb.addEventListener("scrolltopreviouspage", () { 5252 scrollBy(0, -container.height); 5253 }); 5254 vsb.addEventListener((scope ScrollToPositionEvent spe) { 5255 scrollTo(scrollX_, spe.value); 5256 }); 5257 5258 this.addEventListener(delegate (scope ClickEvent e) { 5259 if(e.button == MouseButton.wheelUp) { 5260 if(!e.defaultPrevented) 5261 scrollBy(0, scaleWithDpi(-16)); 5262 e.stopPropagation(); 5263 } else if(e.button == MouseButton.wheelDown) { 5264 if(!e.defaultPrevented) 5265 scrollBy(0, scaleWithDpi(16)); 5266 e.stopPropagation(); 5267 } 5268 }); 5269 } 5270 5271 /+ 5272 override void defaultEventHandler_click(ClickEvent e) { 5273 } 5274 +/ 5275 5276 override void removeAllChildren() { 5277 container.removeAllChildren(); 5278 } 5279 5280 void scrollTo(int x, int y) { 5281 scrollBy(x - scrollX_, y - scrollY_); 5282 } 5283 5284 void scrollBy(int x, int y) { 5285 auto ox = scrollX_; 5286 auto oy = scrollY_; 5287 5288 auto nx = ox + x; 5289 auto ny = oy + y; 5290 5291 if(nx < 0) 5292 nx = 0; 5293 if(ny < 0) 5294 ny = 0; 5295 5296 auto maxX = hsb.max - container.width; 5297 if(maxX < 0) maxX = 0; 5298 auto maxY = vsb.max - container.height; 5299 if(maxY < 0) maxY = 0; 5300 5301 if(nx > maxX) 5302 nx = maxX; 5303 if(ny > maxY) 5304 ny = maxY; 5305 5306 auto dx = nx - ox; 5307 auto dy = ny - oy; 5308 5309 if(dx || dy) { 5310 version(win32_widgets) 5311 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 5312 else { 5313 redraw(); 5314 } 5315 5316 hsb.setPosition = nx; 5317 vsb.setPosition = ny; 5318 5319 scrollX_ = nx; 5320 scrollY_ = ny; 5321 } 5322 } 5323 5324 private int scrollX_; 5325 private int scrollY_; 5326 5327 void setTotalArea(int width, int height) { 5328 hsb.setMax(width); 5329 vsb.setMax(height); 5330 } 5331 5332 /// 5333 void setViewableArea(int width, int height) { 5334 hsb.setViewableArea(width); 5335 vsb.setViewableArea(height); 5336 } 5337 5338 private bool magic; 5339 override void addChild(Widget w, int position = int.max) { 5340 if(magic) 5341 container.addChild(w, position); 5342 else 5343 super.addChild(w, position); 5344 } 5345 5346 override void recomputeChildLayout() { 5347 if(hsb is null || vsb is null || container is null) return; 5348 5349 /+ 5350 import std.stdio; writeln(x, " ", y , " ", width, " ", height); 5351 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 5352 +/ 5353 5354 registerMovement(); 5355 5356 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 5357 hsb.x = 0; 5358 hsb.y = this.height - hsb.height; 5359 hsb.width = this.width - scaleWithDpi(16); 5360 hsb.recomputeChildLayout(); 5361 5362 vsb.width = scaleWithDpi(16); // FIXME? 5363 vsb.x = this.width - vsb.width; 5364 vsb.y = 0; 5365 vsb.height = this.height - scaleWithDpi(16); 5366 vsb.recomputeChildLayout(); 5367 5368 container.x = 0; 5369 container.y = 0; 5370 container.width = this.width - vsb.width; 5371 container.height = this.height - hsb.height; 5372 container.recomputeChildLayout(); 5373 5374 scrollX_ = 0; 5375 scrollY_ = 0; 5376 5377 hsb.setPosition(0); 5378 vsb.setPosition(0); 5379 5380 int mw, mh; 5381 Widget c = container; 5382 // FIXME: hack here to handle a layout inside... 5383 if(c.children.length == 1 && cast(Layout) c.children[0]) 5384 c = c.children[0]; 5385 foreach(child; c.children) { 5386 auto w = child.x + child.width; 5387 auto h = child.y + child.height; 5388 5389 if(w > mw) mw = w; 5390 if(h > mh) mh = h; 5391 } 5392 5393 setTotalArea(mw, mh); 5394 setViewableArea(width, height); 5395 } 5396 5397 override int minHeight() { return scaleWithDpi(64); } 5398 5399 HorizontalScrollbar hsb; 5400 VerticalScrollbar vsb; 5401 ContainerWidget container; 5402 } 5403 5404 5405 version(custom_widgets) 5406 private class InternalScrollableContainerWidget : Widget { 5407 5408 ScrollableWidget sw; 5409 5410 VerticalScrollbar verticalScrollBar; 5411 HorizontalScrollbar horizontalScrollBar; 5412 5413 this(ScrollableWidget sw, Widget parent) { 5414 this.sw = sw; 5415 5416 this.tabStop = false; 5417 5418 horizontalScrollBar = new HorizontalScrollbar(this); 5419 verticalScrollBar = new VerticalScrollbar(this); 5420 5421 horizontalScrollBar.showing_ = false; 5422 verticalScrollBar.showing_ = false; 5423 5424 horizontalScrollBar.addEventListener("scrolltonextline", { 5425 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 5426 sw.horizontalScrollTo(horizontalScrollBar.position); 5427 }); 5428 horizontalScrollBar.addEventListener("scrolltopreviousline", { 5429 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 5430 sw.horizontalScrollTo(horizontalScrollBar.position); 5431 }); 5432 verticalScrollBar.addEventListener("scrolltonextline", { 5433 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 5434 sw.verticalScrollTo(verticalScrollBar.position); 5435 }); 5436 verticalScrollBar.addEventListener("scrolltopreviousline", { 5437 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 5438 sw.verticalScrollTo(verticalScrollBar.position); 5439 }); 5440 horizontalScrollBar.addEventListener("scrolltonextpage", { 5441 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 5442 sw.horizontalScrollTo(horizontalScrollBar.position); 5443 }); 5444 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 5445 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 5446 sw.horizontalScrollTo(horizontalScrollBar.position); 5447 }); 5448 verticalScrollBar.addEventListener("scrolltonextpage", { 5449 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 5450 sw.verticalScrollTo(verticalScrollBar.position); 5451 }); 5452 verticalScrollBar.addEventListener("scrolltopreviouspage", { 5453 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 5454 sw.verticalScrollTo(verticalScrollBar.position); 5455 }); 5456 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 5457 horizontalScrollBar.setPosition(event.intValue); 5458 sw.horizontalScrollTo(horizontalScrollBar.position); 5459 }); 5460 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 5461 verticalScrollBar.setPosition(event.intValue); 5462 sw.verticalScrollTo(verticalScrollBar.position); 5463 }); 5464 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 5465 horizontalScrollBar.setPosition(event.intValue); 5466 sw.horizontalScrollTo(horizontalScrollBar.position); 5467 }); 5468 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 5469 verticalScrollBar.setPosition(event.intValue); 5470 }); 5471 5472 super(parent); 5473 } 5474 5475 // this is supposed to be basically invisible... 5476 override int minWidth() { return sw.minWidth; } 5477 override int minHeight() { return sw.minHeight; } 5478 override int maxWidth() { return sw.maxWidth; } 5479 override int maxHeight() { return sw.maxHeight; } 5480 override int widthStretchiness() { return sw.widthStretchiness; } 5481 override int heightStretchiness() { return sw.heightStretchiness; } 5482 override int marginLeft() { return sw.marginLeft; } 5483 override int marginRight() { return sw.marginRight; } 5484 override int marginTop() { return sw.marginTop; } 5485 override int marginBottom() { return sw.marginBottom; } 5486 override int paddingLeft() { return sw.paddingLeft; } 5487 override int paddingRight() { return sw.paddingRight; } 5488 override int paddingTop() { return sw.paddingTop; } 5489 override int paddingBottom() { return sw.paddingBottom; } 5490 override void focus() { sw.focus(); } 5491 5492 5493 override void recomputeChildLayout() { 5494 // The stupid thing needs to calculate if a scroll bar is needed... 5495 recomputeChildLayoutHelper(); 5496 // then running it again will position things correctly if the bar is NOT needed 5497 recomputeChildLayoutHelper(); 5498 5499 // this sucks but meh it barely works 5500 } 5501 5502 private void recomputeChildLayoutHelper() { 5503 if(sw is null) return; 5504 5505 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 5506 if(horizontalScrollBar && verticalScrollBar) { 5507 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 5508 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 5509 horizontalScrollBar.x = 0; 5510 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 5511 5512 verticalScrollBar.width = verticalScrollBar.minWidth(); 5513 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 5514 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 5515 verticalScrollBar.y = 0 + 2; 5516 5517 sw.x = 0; 5518 sw.y = 0; 5519 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 5520 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 5521 5522 if(sw.contentWidth_ <= this.width) 5523 sw.scrollOrigin_.x = 0; 5524 if(sw.contentHeight_ <= this.height) 5525 sw.scrollOrigin_.y = 0; 5526 5527 horizontalScrollBar.recomputeChildLayout(); 5528 verticalScrollBar.recomputeChildLayout(); 5529 sw.recomputeChildLayout(); 5530 } 5531 5532 if(sw.contentWidth_ <= this.width) 5533 sw.scrollOrigin_.x = 0; 5534 if(sw.contentHeight_ <= this.height) 5535 sw.scrollOrigin_.y = 0; 5536 5537 if(sw.showingHorizontalScroll()) 5538 horizontalScrollBar.showing(true, false); 5539 else 5540 horizontalScrollBar.showing(false, false); 5541 if(sw.showingVerticalScroll()) 5542 verticalScrollBar.showing(true, false); 5543 else 5544 verticalScrollBar.showing(false, false); 5545 5546 verticalScrollBar.setViewableArea(sw.viewportHeight()); 5547 verticalScrollBar.setMax(sw.contentHeight); 5548 verticalScrollBar.setPosition(sw.scrollOrigin.y); 5549 5550 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 5551 horizontalScrollBar.setMax(sw.contentWidth); 5552 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 5553 } 5554 } 5555 5556 /* 5557 class ScrollableClientWidget : Widget { 5558 this(Widget parent) { 5559 super(parent); 5560 } 5561 override void paint(WidgetPainter p) { 5562 parent.paint(p); 5563 } 5564 } 5565 */ 5566 5567 /++ 5568 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. 5569 +/ 5570 abstract class Slider : Widget { 5571 this(int min, int max, int step, Widget parent) { 5572 min_ = min; 5573 max_ = max; 5574 step_ = step; 5575 page_ = step; 5576 super(parent); 5577 } 5578 5579 private int min_; 5580 private int max_; 5581 private int step_; 5582 private int position_; 5583 private int page_; 5584 5585 // selection start and selection end 5586 // tics 5587 // tooltip? 5588 // some way to see and just type the value 5589 // win32 buddy controls are labels 5590 5591 /// 5592 void setMin(int a) { 5593 min_ = a; 5594 version(custom_widgets) 5595 redraw(); 5596 version(win32_widgets) 5597 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 5598 } 5599 /// 5600 int min() { 5601 return min_; 5602 } 5603 /// 5604 void setMax(int a) { 5605 max_ = a; 5606 version(custom_widgets) 5607 redraw(); 5608 version(win32_widgets) 5609 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 5610 } 5611 /// 5612 int max() { 5613 return max_; 5614 } 5615 /// 5616 void setPosition(int a) { 5617 if(a > max) 5618 a = max; 5619 if(a < min) 5620 a = min; 5621 position_ = a; 5622 version(custom_widgets) 5623 setPositionCustom(a); 5624 5625 version(win32_widgets) 5626 setPositionWindows(a); 5627 } 5628 version(win32_widgets) { 5629 protected abstract void setPositionWindows(int a); 5630 } 5631 5632 protected abstract int win32direction(); 5633 5634 /++ 5635 Alias for [position] for better compatibility with generic code. 5636 5637 History: 5638 Added October 5, 2021 5639 +/ 5640 @property int value() { 5641 return position; 5642 } 5643 5644 /// 5645 int position() { 5646 return position_; 5647 } 5648 /// 5649 void setStep(int a) { 5650 step_ = a; 5651 version(win32_widgets) 5652 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 5653 } 5654 /// 5655 int step() { 5656 return step_; 5657 } 5658 /// 5659 void setPageSize(int a) { 5660 page_ = a; 5661 version(win32_widgets) 5662 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 5663 } 5664 /// 5665 int pageSize() { 5666 return page_; 5667 } 5668 5669 private void notify() { 5670 auto event = new ChangeEvent!int(this, &this.position); 5671 event.dispatch(); 5672 } 5673 5674 version(win32_widgets) 5675 void win32Setup(int style) { 5676 createWin32Window(this, TRACKBAR_CLASS, "", 5677 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 5678 5679 // the trackbar sends the same messages as scroll, which 5680 // our other layer sends as these... just gonna translate 5681 // here 5682 this.addDirectEventListener("scrolltoposition", (Event event) { 5683 event.stopPropagation(); 5684 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 5685 notify(); 5686 }); 5687 this.addDirectEventListener("scrolltonextline", (Event event) { 5688 event.stopPropagation(); 5689 this.setPosition(this.position + this.step_ * this.win32direction); 5690 notify(); 5691 }); 5692 this.addDirectEventListener("scrolltopreviousline", (Event event) { 5693 event.stopPropagation(); 5694 this.setPosition(this.position - this.step_ * this.win32direction); 5695 notify(); 5696 }); 5697 this.addDirectEventListener("scrolltonextpage", (Event event) { 5698 event.stopPropagation(); 5699 this.setPosition(this.position + this.page_ * this.win32direction); 5700 notify(); 5701 }); 5702 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 5703 event.stopPropagation(); 5704 this.setPosition(this.position - this.page_ * this.win32direction); 5705 notify(); 5706 }); 5707 5708 setMin(min_); 5709 setMax(max_); 5710 setStep(step_); 5711 setPageSize(page_); 5712 } 5713 5714 version(custom_widgets) { 5715 protected MouseTrackingWidget thumb; 5716 5717 protected abstract void setPositionCustom(int a); 5718 5719 override void defaultEventHandler_keydown(KeyDownEvent event) { 5720 switch(event.key) { 5721 case Key.Up: 5722 case Key.Right: 5723 setPosition(position() - step() * win32direction); 5724 changed(); 5725 break; 5726 case Key.Down: 5727 case Key.Left: 5728 setPosition(position() + step() * win32direction); 5729 changed(); 5730 break; 5731 case Key.Home: 5732 setPosition(win32direction > 0 ? min() : max()); 5733 changed(); 5734 break; 5735 case Key.End: 5736 setPosition(win32direction > 0 ? max() : min()); 5737 changed(); 5738 break; 5739 case Key.PageUp: 5740 setPosition(position() - pageSize() * win32direction); 5741 changed(); 5742 break; 5743 case Key.PageDown: 5744 setPosition(position() + pageSize() * win32direction); 5745 changed(); 5746 break; 5747 default: 5748 } 5749 super.defaultEventHandler_keydown(event); 5750 } 5751 5752 protected void changed() { 5753 auto ev = new ChangeEvent!int(this, &position); 5754 ev.dispatch(); 5755 } 5756 } 5757 } 5758 5759 /++ 5760 5761 +/ 5762 class VerticalSlider : Slider { 5763 this(int min, int max, int step, Widget parent) { 5764 version(custom_widgets) 5765 initialize(); 5766 5767 super(min, max, step, parent); 5768 5769 version(win32_widgets) 5770 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 5771 } 5772 5773 protected override int win32direction() { 5774 return -1; 5775 } 5776 5777 version(win32_widgets) 5778 protected override void setPositionWindows(int a) { 5779 // the windows thing makes the top 0 and i don't like that. 5780 SendMessage(hwnd, TBM_SETPOS, true, max - a); 5781 } 5782 5783 version(custom_widgets) 5784 private void initialize() { 5785 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 5786 5787 thumb.tabStop = false; 5788 5789 thumb.thumbWidth = width; 5790 thumb.thumbHeight = scaleWithDpi(16); 5791 5792 thumb.addEventListener(EventType.change, () { 5793 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 5794 sx = max - sx; 5795 //informProgramThatUserChangedPosition(sx); 5796 5797 position_ = sx; 5798 5799 changed(); 5800 }); 5801 } 5802 5803 version(custom_widgets) 5804 override void recomputeChildLayout() { 5805 thumb.thumbWidth = this.width; 5806 super.recomputeChildLayout(); 5807 setPositionCustom(position_); 5808 } 5809 5810 version(custom_widgets) 5811 protected override void setPositionCustom(int a) { 5812 if(max()) 5813 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 5814 redraw(); 5815 } 5816 } 5817 5818 /++ 5819 5820 +/ 5821 class HorizontalSlider : Slider { 5822 this(int min, int max, int step, Widget parent) { 5823 version(custom_widgets) 5824 initialize(); 5825 5826 super(min, max, step, parent); 5827 5828 version(win32_widgets) 5829 win32Setup(TBS_HORZ); 5830 } 5831 5832 version(win32_widgets) 5833 protected override void setPositionWindows(int a) { 5834 SendMessage(hwnd, TBM_SETPOS, true, a); 5835 } 5836 5837 protected override int win32direction() { 5838 return 1; 5839 } 5840 5841 version(custom_widgets) 5842 private void initialize() { 5843 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 5844 5845 thumb.tabStop = false; 5846 5847 thumb.thumbWidth = scaleWithDpi(16); 5848 thumb.thumbHeight = height; 5849 5850 thumb.addEventListener(EventType.change, () { 5851 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 5852 //informProgramThatUserChangedPosition(sx); 5853 5854 position_ = sx; 5855 5856 changed(); 5857 }); 5858 } 5859 5860 version(custom_widgets) 5861 override void recomputeChildLayout() { 5862 thumb.thumbHeight = this.height; 5863 super.recomputeChildLayout(); 5864 setPositionCustom(position_); 5865 } 5866 5867 version(custom_widgets) 5868 protected override void setPositionCustom(int a) { 5869 if(max()) 5870 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 5871 redraw(); 5872 } 5873 } 5874 5875 5876 /// 5877 abstract class ScrollbarBase : Widget { 5878 /// 5879 this(Widget parent) { 5880 super(parent); 5881 tabStop = false; 5882 step_ = scaleWithDpi(16); 5883 } 5884 5885 private int viewableArea_; 5886 private int max_; 5887 private int step_;// = 16; 5888 private int position_; 5889 5890 /// 5891 bool atEnd() { 5892 return position_ + viewableArea_ >= max_; 5893 } 5894 5895 /// 5896 bool atStart() { 5897 return position_ == 0; 5898 } 5899 5900 /// 5901 void setViewableArea(int a) { 5902 viewableArea_ = a; 5903 version(custom_widgets) 5904 redraw(); 5905 } 5906 /// 5907 void setMax(int a) { 5908 max_ = a; 5909 version(custom_widgets) 5910 redraw(); 5911 } 5912 /// 5913 int max() { 5914 return max_; 5915 } 5916 /// 5917 void setPosition(int a) { 5918 if(a == int.max) 5919 a = max; 5920 position_ = max ? a : 0; 5921 if(position_ + viewableArea_ > max) 5922 position_ = max - viewableArea_; 5923 if(position_ < 0) 5924 position_ = 0; 5925 version(custom_widgets) 5926 redraw(); 5927 } 5928 /// 5929 int position() { 5930 return position_; 5931 } 5932 /// 5933 void setStep(int a) { 5934 step_ = a; 5935 } 5936 /// 5937 int step() { 5938 return step_; 5939 } 5940 5941 // FIXME: remove this.... maybe 5942 /+ 5943 protected void informProgramThatUserChangedPosition(int n) { 5944 position_ = n; 5945 auto evt = new Event(EventType.change, this); 5946 evt.intValue = n; 5947 evt.dispatch(); 5948 } 5949 +/ 5950 5951 version(custom_widgets) { 5952 abstract protected int getBarDim(); 5953 int thumbSize() { 5954 if(viewableArea_ >= max_) 5955 return getBarDim(); 5956 5957 int res; 5958 if(max_) { 5959 res = getBarDim() * viewableArea_ / max_; 5960 } 5961 if(res < 6) 5962 res = 6; 5963 5964 return res; 5965 } 5966 5967 int thumbPosition() { 5968 /* 5969 viewableArea_ is the viewport height/width 5970 position_ is where we are 5971 */ 5972 if(max_) { 5973 if(position_ + viewableArea_ >= max_) 5974 return getBarDim - thumbSize; 5975 return getBarDim * position_ / max_; 5976 } 5977 return 0; 5978 } 5979 } 5980 } 5981 5982 //public import mgt; 5983 5984 /++ 5985 A mouse tracking widget is one that follows the mouse when dragged inside it. 5986 5987 Concrete subclasses may include a scrollbar thumb and a volume control. 5988 +/ 5989 //version(custom_widgets) 5990 class MouseTrackingWidget : Widget { 5991 5992 /// 5993 int positionX() { return positionX_; } 5994 /// 5995 int positionY() { return positionY_; } 5996 5997 /// 5998 void positionX(int p) { positionX_ = p; } 5999 /// 6000 void positionY(int p) { positionY_ = p; } 6001 6002 private int positionX_; 6003 private int positionY_; 6004 6005 /// 6006 enum Orientation { 6007 horizontal, /// 6008 vertical, /// 6009 twoDimensional, /// 6010 } 6011 6012 private int thumbWidth_; 6013 private int thumbHeight_; 6014 6015 /// 6016 int thumbWidth() { return thumbWidth_; } 6017 /// 6018 int thumbHeight() { return thumbHeight_; } 6019 /// 6020 int thumbWidth(int a) { return thumbWidth_ = a; } 6021 /// 6022 int thumbHeight(int a) { return thumbHeight_ = a; } 6023 6024 private bool dragging; 6025 private bool hovering; 6026 private int startMouseX, startMouseY; 6027 6028 /// 6029 this(Orientation orientation, Widget parent) { 6030 super(parent); 6031 6032 //assert(parentWindow !is null); 6033 6034 addEventListener((MouseDownEvent event) { 6035 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6036 dragging = true; 6037 startMouseX = event.clientX - positionX; 6038 startMouseY = event.clientY - positionY; 6039 parentWindow.captureMouse(this); 6040 } else { 6041 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6042 positionX = event.clientX - thumbWidth / 2; 6043 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6044 positionY = event.clientY - thumbHeight / 2; 6045 6046 if(positionX + thumbWidth > this.width) 6047 positionX = this.width - thumbWidth; 6048 if(positionY + thumbHeight > this.height) 6049 positionY = this.height - thumbHeight; 6050 6051 if(positionX < 0) 6052 positionX = 0; 6053 if(positionY < 0) 6054 positionY = 0; 6055 6056 6057 // this.emit!(ChangeEvent!void)(); 6058 auto evt = new Event(EventType.change, this); 6059 evt.sendDirectly(); 6060 6061 redraw(); 6062 6063 } 6064 }); 6065 6066 addEventListener(EventType.mouseup, (Event event) { 6067 dragging = false; 6068 parentWindow.releaseMouseCapture(); 6069 }); 6070 6071 addEventListener(EventType.mouseout, (Event event) { 6072 if(!hovering) 6073 return; 6074 hovering = false; 6075 redraw(); 6076 }); 6077 6078 int lpx, lpy; 6079 6080 addEventListener((MouseMoveEvent event) { 6081 auto oh = hovering; 6082 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6083 hovering = true; 6084 } else { 6085 hovering = false; 6086 } 6087 if(!dragging) { 6088 if(hovering != oh) 6089 redraw(); 6090 return; 6091 } 6092 6093 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6094 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 6095 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6096 positionY = event.clientY - startMouseY; 6097 6098 if(positionX + thumbWidth > this.width) 6099 positionX = this.width - thumbWidth; 6100 if(positionY + thumbHeight > this.height) 6101 positionY = this.height - thumbHeight; 6102 6103 if(positionX < 0) 6104 positionX = 0; 6105 if(positionY < 0) 6106 positionY = 0; 6107 6108 if(positionX != lpx || positionY != lpy) { 6109 auto evt = new Event(EventType.change, this); 6110 evt.sendDirectly(); 6111 6112 lpx = positionX; 6113 lpy = positionY; 6114 } 6115 6116 redraw(); 6117 }); 6118 } 6119 6120 version(custom_widgets) 6121 override void paint(WidgetPainter painter) { 6122 auto cs = getComputedStyle(); 6123 auto c = darken(cs.windowBackgroundColor, 0.2); 6124 painter.outlineColor = c; 6125 painter.fillColor = c; 6126 painter.drawRectangle(Point(0, 0), this.width, this.height); 6127 6128 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 6129 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 6130 } 6131 } 6132 6133 //version(custom_widgets) 6134 //private 6135 class HorizontalScrollbar : ScrollbarBase { 6136 6137 version(custom_widgets) { 6138 private MouseTrackingWidget thumb; 6139 6140 override int getBarDim() { 6141 return thumb.width; 6142 } 6143 } 6144 6145 override void setViewableArea(int a) { 6146 super.setViewableArea(a); 6147 6148 version(win32_widgets) { 6149 SCROLLINFO info; 6150 info.cbSize = info.sizeof; 6151 info.nPage = a + 1; 6152 info.fMask = SIF_PAGE; 6153 SetScrollInfo(hwnd, SB_CTL, &info, true); 6154 } else version(custom_widgets) { 6155 thumb.positionX = thumbPosition; 6156 thumb.thumbWidth = thumbSize; 6157 thumb.redraw(); 6158 } else static assert(0); 6159 6160 } 6161 6162 override void setMax(int a) { 6163 super.setMax(a); 6164 version(win32_widgets) { 6165 SCROLLINFO info; 6166 info.cbSize = info.sizeof; 6167 info.nMin = 0; 6168 info.nMax = max; 6169 info.fMask = SIF_RANGE; 6170 SetScrollInfo(hwnd, SB_CTL, &info, true); 6171 } else version(custom_widgets) { 6172 thumb.positionX = thumbPosition; 6173 thumb.thumbWidth = thumbSize; 6174 thumb.redraw(); 6175 } 6176 } 6177 6178 override void setPosition(int a) { 6179 super.setPosition(a); 6180 version(win32_widgets) { 6181 SCROLLINFO info; 6182 info.cbSize = info.sizeof; 6183 info.fMask = SIF_POS; 6184 info.nPos = position; 6185 SetScrollInfo(hwnd, SB_CTL, &info, true); 6186 } else version(custom_widgets) { 6187 thumb.positionX = thumbPosition(); 6188 thumb.thumbWidth = thumbSize; 6189 thumb.redraw(); 6190 } else static assert(0); 6191 } 6192 6193 this(Widget parent) { 6194 super(parent); 6195 6196 version(win32_widgets) { 6197 createWin32Window(this, "Scrollbar"w, "", 6198 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 6199 } else version(custom_widgets) { 6200 auto vl = new HorizontalLayout(this); 6201 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 6202 leftButton.setClickRepeat(scrollClickRepeatInterval); 6203 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 6204 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 6205 rightButton.setClickRepeat(scrollClickRepeatInterval); 6206 6207 leftButton.tabStop = false; 6208 rightButton.tabStop = false; 6209 thumb.tabStop = false; 6210 6211 leftButton.addEventListener(EventType.triggered, () { 6212 this.emitCommand!"scrolltopreviousline"(); 6213 //informProgramThatUserChangedPosition(position - step()); 6214 }); 6215 rightButton.addEventListener(EventType.triggered, () { 6216 this.emitCommand!"scrolltonextline"(); 6217 //informProgramThatUserChangedPosition(position + step()); 6218 }); 6219 6220 thumb.thumbWidth = this.minWidth; 6221 thumb.thumbHeight = scaleWithDpi(16); 6222 6223 thumb.addEventListener(EventType.change, () { 6224 auto sx = thumb.positionX * max() / thumb.width; 6225 //informProgramThatUserChangedPosition(sx); 6226 6227 auto ev = new ScrollToPositionEvent(this, sx); 6228 ev.dispatch(); 6229 }); 6230 } 6231 } 6232 6233 override int minHeight() { return scaleWithDpi(16); } 6234 override int maxHeight() { return scaleWithDpi(16); } 6235 override int minWidth() { return scaleWithDpi(48); } 6236 } 6237 6238 class ScrollToPositionEvent : Event { 6239 enum EventString = "scrolltoposition"; 6240 6241 this(Widget target, int value) { 6242 this.value = value; 6243 super(EventString, target); 6244 } 6245 6246 immutable int value; 6247 6248 override @property int intValue() { 6249 return value; 6250 } 6251 } 6252 6253 //version(custom_widgets) 6254 //private 6255 class VerticalScrollbar : ScrollbarBase { 6256 6257 version(custom_widgets) { 6258 override int getBarDim() { 6259 return thumb.height; 6260 } 6261 6262 private MouseTrackingWidget thumb; 6263 } 6264 6265 override void setViewableArea(int a) { 6266 super.setViewableArea(a); 6267 6268 version(win32_widgets) { 6269 SCROLLINFO info; 6270 info.cbSize = info.sizeof; 6271 info.nPage = a + 1; 6272 info.fMask = SIF_PAGE; 6273 SetScrollInfo(hwnd, SB_CTL, &info, true); 6274 } else version(custom_widgets) { 6275 thumb.positionY = thumbPosition; 6276 thumb.thumbHeight = thumbSize; 6277 thumb.redraw(); 6278 } else static assert(0); 6279 6280 } 6281 6282 override void setMax(int a) { 6283 super.setMax(a); 6284 version(win32_widgets) { 6285 SCROLLINFO info; 6286 info.cbSize = info.sizeof; 6287 info.nMin = 0; 6288 info.nMax = max; 6289 info.fMask = SIF_RANGE; 6290 SetScrollInfo(hwnd, SB_CTL, &info, true); 6291 } else version(custom_widgets) { 6292 thumb.positionY = thumbPosition; 6293 thumb.thumbHeight = thumbSize; 6294 thumb.redraw(); 6295 } 6296 } 6297 6298 override void setPosition(int a) { 6299 super.setPosition(a); 6300 version(win32_widgets) { 6301 SCROLLINFO info; 6302 info.cbSize = info.sizeof; 6303 info.fMask = SIF_POS; 6304 info.nPos = position; 6305 SetScrollInfo(hwnd, SB_CTL, &info, true); 6306 } else version(custom_widgets) { 6307 thumb.positionY = thumbPosition; 6308 thumb.thumbHeight = thumbSize; 6309 thumb.redraw(); 6310 } else static assert(0); 6311 } 6312 6313 this(Widget parent) { 6314 super(parent); 6315 6316 version(win32_widgets) { 6317 createWin32Window(this, "Scrollbar"w, "", 6318 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 6319 } else version(custom_widgets) { 6320 auto vl = new VerticalLayout(this); 6321 auto upButton = new ArrowButton(ArrowDirection.up, vl); 6322 upButton.setClickRepeat(scrollClickRepeatInterval); 6323 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 6324 auto downButton = new ArrowButton(ArrowDirection.down, vl); 6325 downButton.setClickRepeat(scrollClickRepeatInterval); 6326 6327 upButton.addEventListener(EventType.triggered, () { 6328 this.emitCommand!"scrolltopreviousline"(); 6329 //informProgramThatUserChangedPosition(position - step()); 6330 }); 6331 downButton.addEventListener(EventType.triggered, () { 6332 this.emitCommand!"scrolltonextline"(); 6333 //informProgramThatUserChangedPosition(position + step()); 6334 }); 6335 6336 thumb.thumbWidth = this.minWidth; 6337 thumb.thumbHeight = scaleWithDpi(16); 6338 6339 thumb.addEventListener(EventType.change, () { 6340 auto sy = thumb.positionY * max() / thumb.height; 6341 6342 auto ev = new ScrollToPositionEvent(this, sy); 6343 ev.dispatch(); 6344 6345 //informProgramThatUserChangedPosition(sy); 6346 }); 6347 6348 upButton.tabStop = false; 6349 downButton.tabStop = false; 6350 thumb.tabStop = false; 6351 } 6352 } 6353 6354 override int minWidth() { return scaleWithDpi(16); } 6355 override int maxWidth() { return scaleWithDpi(16); } 6356 override int minHeight() { return scaleWithDpi(48); } 6357 } 6358 6359 6360 /++ 6361 EXPERIMENTAL 6362 6363 A widget specialized for being a container for other widgets. 6364 6365 History: 6366 Added May 29, 2021. Not stabilized at this time. 6367 +/ 6368 class WidgetContainer : Widget { 6369 this(Widget parent) { 6370 tabStop = false; 6371 super(parent); 6372 } 6373 6374 override int maxHeight() { 6375 if(this.children.length == 1) { 6376 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 6377 } else { 6378 return int.max; 6379 } 6380 } 6381 6382 override int maxWidth() { 6383 if(this.children.length == 1) { 6384 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 6385 } else { 6386 return int.max; 6387 } 6388 } 6389 6390 /+ 6391 6392 override int minHeight() { 6393 int largest = 0; 6394 int margins = 0; 6395 int lastMargin = 0; 6396 foreach(child; children) { 6397 auto mh = child.minHeight(); 6398 if(mh > largest) 6399 largest = mh; 6400 margins += mymax(lastMargin, child.marginTop()); 6401 lastMargin = child.marginBottom(); 6402 } 6403 return largest + margins; 6404 } 6405 6406 override int maxHeight() { 6407 int largest = 0; 6408 int margins = 0; 6409 int lastMargin = 0; 6410 foreach(child; children) { 6411 auto mh = child.maxHeight(); 6412 if(mh == int.max) 6413 return int.max; 6414 if(mh > largest) 6415 largest = mh; 6416 margins += mymax(lastMargin, child.marginTop()); 6417 lastMargin = child.marginBottom(); 6418 } 6419 return largest + margins; 6420 } 6421 6422 override int minWidth() { 6423 int min; 6424 foreach(child; children) { 6425 auto cm = child.minWidth; 6426 if(cm > min) 6427 min = cm; 6428 } 6429 return min + paddingLeft + paddingRight; 6430 } 6431 6432 override int minHeight() { 6433 int min; 6434 foreach(child; children) { 6435 auto cm = child.minHeight; 6436 if(cm > min) 6437 min = cm; 6438 } 6439 return min + paddingTop + paddingBottom; 6440 } 6441 6442 override int maxHeight() { 6443 int largest = 0; 6444 int margins = 0; 6445 int lastMargin = 0; 6446 foreach(child; children) { 6447 auto mh = child.maxHeight(); 6448 if(mh == int.max) 6449 return int.max; 6450 if(mh > largest) 6451 largest = mh; 6452 margins += mymax(lastMargin, child.marginTop()); 6453 lastMargin = child.marginBottom(); 6454 } 6455 return largest + margins; 6456 } 6457 6458 override int heightStretchiness() { 6459 int max; 6460 foreach(child; children) { 6461 auto c = child.heightStretchiness; 6462 if(c > max) 6463 max = c; 6464 } 6465 return max; 6466 } 6467 6468 override int marginTop() { 6469 if(this.children.length) 6470 return this.children[0].marginTop; 6471 return 0; 6472 } 6473 +/ 6474 } 6475 6476 /// 6477 abstract class Layout : Widget { 6478 this(Widget parent) { 6479 tabStop = false; 6480 super(parent); 6481 } 6482 } 6483 6484 /++ 6485 Makes all children minimum width and height, placing them down 6486 left to right, top to bottom. 6487 6488 Useful if you want to make a list of buttons that automatically 6489 wrap to a new line when necessary. 6490 +/ 6491 class InlineBlockLayout : Layout { 6492 /// 6493 this(Widget parent) { super(parent); } 6494 6495 override void recomputeChildLayout() { 6496 registerMovement(); 6497 6498 int x = this.paddingLeft, y = this.paddingTop; 6499 6500 int lineHeight; 6501 int previousMargin = 0; 6502 int previousMarginBottom = 0; 6503 6504 foreach(child; children) { 6505 if(child.hidden) 6506 continue; 6507 if(cast(FixedPosition) child) { 6508 child.recomputeChildLayout(); 6509 continue; 6510 } 6511 child.width = child.flexBasisWidth(); 6512 if(child.width == 0) 6513 child.width = child.minWidth(); 6514 if(child.width == 0) 6515 child.width = 32; 6516 6517 child.height = child.flexBasisHeight(); 6518 if(child.height == 0) 6519 child.height = child.minHeight(); 6520 if(child.height == 0) 6521 child.height = 32; 6522 6523 if(x + child.width + paddingRight > this.width) { 6524 x = this.paddingLeft; 6525 y += lineHeight; 6526 lineHeight = 0; 6527 previousMargin = 0; 6528 previousMarginBottom = 0; 6529 } 6530 6531 auto margin = child.marginLeft; 6532 if(previousMargin > margin) 6533 margin = previousMargin; 6534 6535 x += margin; 6536 6537 child.x = x; 6538 child.y = y; 6539 6540 int marginTopApplied; 6541 if(child.marginTop > previousMarginBottom) { 6542 child.y += child.marginTop; 6543 marginTopApplied = child.marginTop; 6544 } 6545 6546 x += child.width; 6547 previousMargin = child.marginRight; 6548 6549 if(child.marginBottom > previousMarginBottom) 6550 previousMarginBottom = child.marginBottom; 6551 6552 auto h = child.height + previousMarginBottom + marginTopApplied; 6553 if(h > lineHeight) 6554 lineHeight = h; 6555 6556 child.recomputeChildLayout(); 6557 } 6558 6559 } 6560 6561 override int minWidth() { 6562 int min; 6563 foreach(child; children) { 6564 auto cm = child.minWidth; 6565 if(cm > min) 6566 min = cm; 6567 } 6568 return min + paddingLeft + paddingRight; 6569 } 6570 6571 override int minHeight() { 6572 int min; 6573 foreach(child; children) { 6574 auto cm = child.minHeight; 6575 if(cm > min) 6576 min = cm; 6577 } 6578 return min + paddingTop + paddingBottom; 6579 } 6580 } 6581 6582 /++ 6583 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 6584 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 6585 the [TabWidget] will automatically change pages of child widgets. 6586 6587 This allows you to react to it however you see fit rather than having to 6588 be tied to just the new sets of child widgets. 6589 6590 It sends the message in the form of `this.emitCommand!"changetab"();`. 6591 6592 History: 6593 Added December 24, 2021 (dub v10.5) 6594 +/ 6595 class TabMessageWidget : Widget { 6596 6597 protected void tabIndexClicked(int item) { 6598 this.emitCommand!"changetab"(); 6599 } 6600 6601 /++ 6602 Adds the a new tab to the control with the given title. 6603 6604 Returns: 6605 The index of the newly added tab. You will need to know 6606 this index to refer to it later and to know which tab to 6607 change to when you get a changetab message. 6608 +/ 6609 int addTab(string title, int pos = int.max) { 6610 version(win32_widgets) { 6611 TCITEM item; 6612 item.mask = TCIF_TEXT; 6613 WCharzBuffer buf = WCharzBuffer(title); 6614 item.pszText = buf.ptr; 6615 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 6616 } else version(custom_widgets) { 6617 if(pos >= tabs.length) { 6618 tabs ~= title; 6619 redraw(); 6620 return cast(int) tabs.length - 1; 6621 } else if(pos <= 0) { 6622 tabs = title ~ tabs; 6623 redraw(); 6624 return 0; 6625 } else { 6626 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 6627 redraw(); 6628 return pos; 6629 } 6630 } 6631 } 6632 6633 override void addChild(Widget child, int pos = int.max) { 6634 if(container) 6635 container.addChild(child, pos); 6636 else 6637 super.addChild(child, pos); 6638 } 6639 6640 protected Widget makeContainer() { 6641 return new Widget(this); 6642 } 6643 6644 private Widget container; 6645 6646 override void recomputeChildLayout() { 6647 version(win32_widgets) { 6648 this.registerMovement(); 6649 6650 RECT rect; 6651 GetWindowRect(hwnd, &rect); 6652 6653 auto left = rect.left; 6654 auto top = rect.top; 6655 6656 TabCtrl_AdjustRect(hwnd, false, &rect); 6657 foreach(child; children) { 6658 if(!child.showing) continue; 6659 child.x = rect.left - left; 6660 child.y = rect.top - top; 6661 child.width = rect.right - rect.left; 6662 child.height = rect.bottom - rect.top; 6663 child.recomputeChildLayout(); 6664 } 6665 } else version(custom_widgets) { 6666 this.registerMovement(); 6667 foreach(child; children) { 6668 if(!child.showing) continue; 6669 child.x = 2; 6670 child.y = tabBarHeight + 2; // for the border 6671 child.width = width - 4; // for the border 6672 child.height = height - tabBarHeight - 2 - 2; // for the border 6673 child.recomputeChildLayout(); 6674 } 6675 } else static assert(0); 6676 } 6677 6678 version(custom_widgets) 6679 string[] tabs; 6680 6681 this(Widget parent) { 6682 super(parent); 6683 6684 tabStop = false; 6685 6686 version(win32_widgets) { 6687 createWin32Window(this, WC_TABCONTROL, "", 0); 6688 } else version(custom_widgets) { 6689 addEventListener((ClickEvent event) { 6690 if(event.target !is this && this.container !is null && event.target !is this.container) return; 6691 if(event.clientY < tabBarHeight) { 6692 auto t = (event.clientX / tabWidth); 6693 if(t >= 0 && t < tabs.length) { 6694 currentTab_ = t; 6695 tabIndexClicked(t); 6696 redraw(); 6697 } 6698 } 6699 }); 6700 } else static assert(0); 6701 6702 this.container = makeContainer(); 6703 } 6704 6705 override int marginTop() { return 4; } 6706 override int paddingBottom() { return 4; } 6707 6708 override int minHeight() { 6709 int max = 0; 6710 foreach(child; children) 6711 max = mymax(child.minHeight, max); 6712 6713 6714 version(win32_widgets) { 6715 RECT rect; 6716 rect.right = this.width; 6717 rect.bottom = max; 6718 TabCtrl_AdjustRect(hwnd, true, &rect); 6719 6720 max = rect.bottom; 6721 } else { 6722 max += defaultLineHeight + 4; 6723 } 6724 6725 6726 return max; 6727 } 6728 6729 version(win32_widgets) 6730 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 6731 switch(code) { 6732 case TCN_SELCHANGE: 6733 auto sel = TabCtrl_GetCurSel(hwnd); 6734 tabIndexClicked(sel); 6735 break; 6736 default: 6737 } 6738 return 0; 6739 } 6740 6741 version(custom_widgets) { 6742 private int currentTab_; 6743 private int tabBarHeight() { return defaultLineHeight; } 6744 int tabWidth = 80; 6745 } 6746 6747 version(win32_widgets) 6748 override void paint(WidgetPainter painter) {} 6749 6750 version(custom_widgets) 6751 override void paint(WidgetPainter painter) { 6752 auto cs = getComputedStyle(); 6753 6754 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 6755 6756 int posX = 0; 6757 foreach(idx, title; tabs) { 6758 auto isCurrent = idx == getCurrentTab(); 6759 6760 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 6761 6762 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 6763 painter.outlineColor = cs.foregroundColor; 6764 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 6765 6766 if(isCurrent) { 6767 painter.outlineColor = cs.windowBackgroundColor; 6768 painter.fillColor = Color.transparent; 6769 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 6770 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 6771 6772 painter.outlineColor = Color.white; 6773 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 6774 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 6775 painter.outlineColor = cs.activeTabColor; 6776 painter.drawPixel(Point(posX, tabBarHeight - 1)); 6777 } 6778 6779 posX += tabWidth - 2; 6780 } 6781 } 6782 6783 /// 6784 @scriptable 6785 void setCurrentTab(int item) { 6786 version(win32_widgets) 6787 TabCtrl_SetCurSel(hwnd, item); 6788 else version(custom_widgets) 6789 currentTab_ = item; 6790 else static assert(0); 6791 6792 tabIndexClicked(item); 6793 } 6794 6795 /// 6796 @scriptable 6797 int getCurrentTab() { 6798 version(win32_widgets) 6799 return TabCtrl_GetCurSel(hwnd); 6800 else version(custom_widgets) 6801 return currentTab_; // FIXME 6802 else static assert(0); 6803 } 6804 6805 /// 6806 @scriptable 6807 void removeTab(int item) { 6808 if(item && item == getCurrentTab()) 6809 setCurrentTab(item - 1); 6810 6811 version(win32_widgets) { 6812 TabCtrl_DeleteItem(hwnd, item); 6813 } 6814 6815 for(int a = item; a < children.length - 1; a++) 6816 this._children[a] = this._children[a + 1]; 6817 this._children = this._children[0 .. $-1]; 6818 } 6819 6820 } 6821 6822 6823 /++ 6824 A tab widget is a set of clickable tab buttons followed by a content area. 6825 6826 6827 Tabs can change existing content or can be new pages. 6828 6829 When the user picks a different tab, a `change` message is generated. 6830 +/ 6831 class TabWidget : TabMessageWidget { 6832 this(Widget parent) { 6833 super(parent); 6834 } 6835 6836 override protected Widget makeContainer() { 6837 return null; 6838 } 6839 6840 override void addChild(Widget child, int pos = int.max) { 6841 if(auto twp = cast(TabWidgetPage) child) { 6842 Widget.addChild(child, pos); 6843 if(pos == int.max) 6844 pos = cast(int) this.children.length - 1; 6845 6846 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 6847 6848 if(pos != getCurrentTab) { 6849 child.showing = false; 6850 } 6851 } else { 6852 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 6853 } 6854 } 6855 6856 // FIXME: add tab icons at some point, Windows supports them 6857 /++ 6858 Adds a page and its associated tab with the given label to the widget. 6859 6860 Returns: 6861 The added page object, to which you can add other widgets. 6862 +/ 6863 @scriptable 6864 TabWidgetPage addPage(string title) { 6865 return new TabWidgetPage(title, this); 6866 } 6867 6868 /++ 6869 Gets the page at the given tab index, or `null` if the index is bad. 6870 6871 History: 6872 Added December 24, 2021. 6873 +/ 6874 TabWidgetPage getPage(int index) { 6875 if(index < this.children.length) 6876 return null; 6877 return cast(TabWidgetPage) this.children[index]; 6878 } 6879 6880 /++ 6881 While you can still use the addTab from the parent class, 6882 *strongly* recommend you use [addPage] insteaad. 6883 6884 History: 6885 Added December 24, 2021 to fulful the interface 6886 requirement that came from adding [TabMessageWidget]. 6887 6888 You should not use it though since the [addPage] function 6889 is much easier to use here. 6890 +/ 6891 override int addTab(string title, int pos = int.max) { 6892 auto p = addPage(title); 6893 foreach(idx, child; this.children) 6894 if(child is p) 6895 return cast(int) idx; 6896 return -1; 6897 } 6898 6899 protected override void tabIndexClicked(int item) { 6900 foreach(idx, child; children) { 6901 child.showing(false, false); // batch the recalculates for the end 6902 } 6903 6904 foreach(idx, child; children) { 6905 if(idx == item) { 6906 child.showing(true, false); 6907 if(parentWindow) { 6908 auto f = parentWindow.getFirstFocusable(child); 6909 if(f) 6910 f.focus(); 6911 } 6912 recomputeChildLayout(); 6913 } 6914 } 6915 6916 version(win32_widgets) { 6917 InvalidateRect(hwnd, null, true); 6918 } else version(custom_widgets) { 6919 this.redraw(); 6920 } 6921 } 6922 6923 } 6924 6925 /++ 6926 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 6927 6928 You add [TabWidgetPage]s to it. 6929 +/ 6930 class PageWidget : Widget { 6931 this(Widget parent) { 6932 super(parent); 6933 } 6934 6935 override int minHeight() { 6936 int max = 0; 6937 foreach(child; children) 6938 max = mymax(child.minHeight, max); 6939 6940 return max; 6941 } 6942 6943 6944 override void addChild(Widget child, int pos = int.max) { 6945 if(auto twp = cast(TabWidgetPage) child) { 6946 super.addChild(child, pos); 6947 if(pos == int.max) 6948 pos = cast(int) this.children.length - 1; 6949 6950 if(pos != getCurrentTab) { 6951 child.showing = false; 6952 } 6953 } else { 6954 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 6955 } 6956 } 6957 6958 override void recomputeChildLayout() { 6959 this.registerMovement(); 6960 foreach(child; children) { 6961 child.x = 0; 6962 child.y = 0; 6963 child.width = width; 6964 child.height = height; 6965 child.recomputeChildLayout(); 6966 } 6967 } 6968 6969 private int currentTab_; 6970 6971 /// 6972 @scriptable 6973 void setCurrentTab(int item) { 6974 currentTab_ = item; 6975 6976 showOnly(item); 6977 } 6978 6979 /// 6980 @scriptable 6981 int getCurrentTab() { 6982 return currentTab_; 6983 } 6984 6985 /// 6986 @scriptable 6987 void removeTab(int item) { 6988 if(item && item == getCurrentTab()) 6989 setCurrentTab(item - 1); 6990 6991 for(int a = item; a < children.length - 1; a++) 6992 this._children[a] = this._children[a + 1]; 6993 this._children = this._children[0 .. $-1]; 6994 } 6995 6996 /// 6997 @scriptable 6998 TabWidgetPage addPage(string title) { 6999 return new TabWidgetPage(title, this); 7000 } 7001 7002 private void showOnly(int item) { 7003 foreach(idx, child; children) 7004 if(idx == item) { 7005 child.show(); 7006 child.recomputeChildLayout(); 7007 } else { 7008 child.hide(); 7009 } 7010 } 7011 7012 } 7013 7014 /++ 7015 7016 +/ 7017 class TabWidgetPage : Widget { 7018 string title; 7019 this(string title, Widget parent) { 7020 this.title = title; 7021 this.tabStop = false; 7022 super(parent); 7023 7024 ///* 7025 version(win32_widgets) { 7026 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 7027 } 7028 //*/ 7029 } 7030 7031 override int minHeight() { 7032 int sum = 0; 7033 foreach(child; children) 7034 sum += child.minHeight(); 7035 return sum; 7036 } 7037 } 7038 7039 version(none) 7040 /++ 7041 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 7042 7043 I think I need to modify the layout algorithms to support this. 7044 +/ 7045 class CollapsableSidebar : Widget { 7046 7047 } 7048 7049 /// Stacks the widgets vertically, taking all the available width for each child. 7050 class VerticalLayout : Layout { 7051 // most of this is intentionally blank - widget's default is vertical layout right now 7052 /// 7053 this(Widget parent) { super(parent); } 7054 7055 /++ 7056 Sets a max width for the layout so you don't have to subclass. The max width 7057 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7058 7059 History: 7060 Added November 29, 2021 (dub v10.5) 7061 +/ 7062 this(int maxWidth, Widget parent) { 7063 this.mw = maxWidth; 7064 super(parent); 7065 } 7066 7067 private int mw = int.max; 7068 7069 override int maxWidth() { return scaleWithDpi(mw); } 7070 } 7071 7072 /// Stacks the widgets horizontally, taking all the available height for each child. 7073 class HorizontalLayout : Layout { 7074 /// 7075 this(Widget parent) { super(parent); } 7076 7077 /++ 7078 Sets a max height for the layout so you don't have to subclass. The max height 7079 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7080 7081 History: 7082 Added November 29, 2021 (dub v10.5) 7083 +/ 7084 this(int maxHeight, Widget parent) { 7085 this.mh = maxHeight; 7086 super(parent); 7087 } 7088 7089 private int mh = 0; 7090 7091 7092 7093 override void recomputeChildLayout() { 7094 .recomputeChildLayout!"width"(this); 7095 } 7096 7097 override int minHeight() { 7098 int largest = 0; 7099 int margins = 0; 7100 int lastMargin = 0; 7101 foreach(child; children) { 7102 auto mh = child.minHeight(); 7103 if(mh > largest) 7104 largest = mh; 7105 margins += mymax(lastMargin, child.marginTop()); 7106 lastMargin = child.marginBottom(); 7107 } 7108 return largest + margins; 7109 } 7110 7111 override int maxHeight() { 7112 if(mh != 0) 7113 return mymax(minHeight, scaleWithDpi(mh)); 7114 7115 int largest = 0; 7116 int margins = 0; 7117 int lastMargin = 0; 7118 foreach(child; children) { 7119 auto mh = child.maxHeight(); 7120 if(mh == int.max) 7121 return int.max; 7122 if(mh > largest) 7123 largest = mh; 7124 margins += mymax(lastMargin, child.marginTop()); 7125 lastMargin = child.marginBottom(); 7126 } 7127 return largest + margins; 7128 } 7129 7130 override int heightStretchiness() { 7131 int max; 7132 foreach(child; children) { 7133 auto c = child.heightStretchiness; 7134 if(c > max) 7135 max = c; 7136 } 7137 return max; 7138 } 7139 7140 } 7141 7142 version(win32_widgets) 7143 private 7144 extern(Windows) 7145 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 7146 Widget* pwin = hwnd in Widget.nativeMapping; 7147 if(pwin is null) 7148 return DefWindowProc(hwnd, message, wparam, lparam); 7149 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 7150 if(win is null) 7151 return DefWindowProc(hwnd, message, wparam, lparam); 7152 7153 switch(message) { 7154 case WM_SIZE: 7155 auto width = LOWORD(lparam); 7156 auto height = HIWORD(lparam); 7157 7158 // FIXME: could this be more efficient? it never relinquishes a large bitmap 7159 if(width > win.bmpWidth || height > win.bmpHeight) { 7160 auto hdc = GetDC(hwnd); 7161 auto oldBuffer = win.buffer; 7162 win.buffer = CreateCompatibleBitmap(hdc, width, height); 7163 7164 auto hdcBmp = CreateCompatibleDC(hdc); 7165 7166 auto oldBmp = SelectObject(hdcBmp, win.buffer); 7167 7168 if(oldBuffer) { 7169 auto hdcOldBmp = CreateCompatibleDC(hdc); 7170 auto oldOldBmp = SelectObject(hdcOldBmp, oldBuffer); 7171 7172 BitBlt(hdcBmp, 0, 0, win.bmpWidth, win.bmpHeight, hdcOldBmp, 0, 0, SRCCOPY); 7173 7174 SelectObject(hdcOldBmp, oldOldBmp); 7175 DeleteDC(hdcOldBmp); 7176 } 7177 7178 auto brush = GetSysColorBrush(COLOR_3DFACE); 7179 RECT r; 7180 r.left = win.bmpWidth; 7181 r.top = 0; 7182 r.right = width; 7183 r.bottom = height; 7184 FillRect(hdcBmp, &r, brush); 7185 7186 r.left = 0; 7187 r.top = win.bmpHeight; 7188 r.right = width; 7189 r.bottom = height; 7190 FillRect(hdcBmp, &r, brush); 7191 7192 SelectObject(hdcBmp, oldBmp); 7193 DeleteDC(hdcBmp); 7194 ReleaseDC(hwnd, hdc); 7195 7196 if(oldBuffer) 7197 DeleteObject(oldBuffer); 7198 7199 win.bmpWidth = width; 7200 win.bmpHeight = height; 7201 } 7202 break; 7203 case WM_PAINT: 7204 if(win.buffer is null) 7205 goto default; 7206 7207 BITMAP bm; 7208 PAINTSTRUCT ps; 7209 7210 HDC hdc = BeginPaint(hwnd, &ps); 7211 7212 HDC hdcMem = CreateCompatibleDC(hdc); 7213 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 7214 7215 GetObject(win.buffer, bm.sizeof, &bm); 7216 7217 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 7218 7219 SelectObject(hdcMem, hbmOld); 7220 DeleteDC(hdcMem); 7221 EndPaint(hwnd, &ps); 7222 break; 7223 default: 7224 return DefWindowProc(hwnd, message, wparam, lparam); 7225 } 7226 7227 return 0; 7228 } 7229 7230 private wstring Win32Class(wstring name)() { 7231 static bool classRegistered; 7232 if(!classRegistered) { 7233 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 7234 WNDCLASSEX wc; 7235 wc.cbSize = wc.sizeof; 7236 wc.hInstance = hInstance; 7237 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 7238 wc.lpfnWndProc = &DoubleBufferWndProc; 7239 wc.lpszClassName = name.ptr; 7240 if(!RegisterClassExW(&wc)) 7241 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 7242 classRegistered = true; 7243 } 7244 7245 return name; 7246 } 7247 7248 /+ 7249 version(win32_widgets) 7250 extern(Windows) 7251 private 7252 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 7253 switch(iMessage) { 7254 case WM_PAINT: 7255 if(auto te = hWnd in Widget.nativeMapping) { 7256 try { 7257 //te.redraw(); 7258 import std.stdio; writeln(te, " drawing"); 7259 } catch(Exception) {} 7260 } 7261 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7262 default: 7263 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7264 } 7265 } 7266 +/ 7267 7268 7269 /++ 7270 A widget specifically designed to hold other widgets. 7271 7272 History: 7273 Added July 1, 2021 7274 +/ 7275 class ContainerWidget : Widget { 7276 this(Widget parent) { 7277 super(parent); 7278 this.tabStop = false; 7279 7280 version(win32_widgets) { 7281 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 7282 } 7283 } 7284 } 7285 7286 /++ 7287 A widget that takes your widget, puts scroll bars around it, and sends 7288 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 7289 no effort to automatically scroll or clip its child widgets - it just sends 7290 the messages. 7291 7292 7293 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 7294 The scroll coordinates are all given in a unit you interpret as you wish. One 7295 of these units is moved on each press of the arrow buttons and represents the 7296 smallest amount the user can scroll. The intention is for this to be one line, 7297 one item in a list, one row in a table, etc. Whatever makes sense for your widget 7298 in each direction that the user might be interested in. 7299 7300 You can set a "page size" with the [step] property. (Yes, I regret the name...) 7301 This is the amount it jumps when the user pressed page up and page down, or clicks 7302 in the exposed part of the scroll bar. 7303 7304 You should add child content to the ScrollMessageWidget. However, it is important to 7305 note that the coordinates are always independent of the scroll position! It is YOUR 7306 responsibility to do any necessary transforms, clipping, etc., while drawing the 7307 content and interpreting mouse events if they are supposed to change with the scroll. 7308 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 7309 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 7310 you more control (which can be considerably more efficient and adapted to your actual data) 7311 at the expense of you also needing to be aware of its reality. 7312 7313 Please note that it does NOT react to mouse wheel events or various keyboard events as of 7314 version 10.3. Maybe this will change in the future.... 7315 +/ 7316 class ScrollMessageWidget : Widget { 7317 this(Widget parent) { 7318 super(parent); 7319 7320 container = new Widget(this); 7321 hsb = new HorizontalScrollbar(this); 7322 vsb = new VerticalScrollbar(this); 7323 7324 hsb.addEventListener("scrolltonextline", { 7325 hsb.setPosition(hsb.position + 1); 7326 notify(); 7327 }); 7328 hsb.addEventListener("scrolltopreviousline", { 7329 hsb.setPosition(hsb.position - 1); 7330 notify(); 7331 }); 7332 vsb.addEventListener("scrolltonextline", { 7333 vsb.setPosition(vsb.position + 1); 7334 notify(); 7335 }); 7336 vsb.addEventListener("scrolltopreviousline", { 7337 vsb.setPosition(vsb.position - 1); 7338 notify(); 7339 }); 7340 hsb.addEventListener("scrolltonextpage", { 7341 hsb.setPosition(hsb.position + hsb.step_); 7342 notify(); 7343 }); 7344 hsb.addEventListener("scrolltopreviouspage", { 7345 hsb.setPosition(hsb.position - hsb.step_); 7346 notify(); 7347 }); 7348 vsb.addEventListener("scrolltonextpage", { 7349 vsb.setPosition(vsb.position + vsb.step_); 7350 notify(); 7351 }); 7352 vsb.addEventListener("scrolltopreviouspage", { 7353 vsb.setPosition(vsb.position - vsb.step_); 7354 notify(); 7355 }); 7356 hsb.addEventListener("scrolltoposition", (Event event) { 7357 hsb.setPosition(event.intValue); 7358 notify(); 7359 }); 7360 vsb.addEventListener("scrolltoposition", (Event event) { 7361 vsb.setPosition(event.intValue); 7362 notify(); 7363 }); 7364 7365 7366 tabStop = false; 7367 container.tabStop = false; 7368 magic = true; 7369 } 7370 7371 /++ 7372 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 7373 7374 7375 The defaults for [addDefaultWheelListeners] are: 7376 7377 $(LIST 7378 * Mouse wheel scrolls vertically 7379 * Alt key + mouse wheel scrolls horiontally 7380 * Shift + mouse wheel scrolls faster. 7381 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 7382 ) 7383 7384 The defaults for [addDefaultKeyboardListeners] are: 7385 7386 $(LIST 7387 * Arrow keys scroll by the given amounts 7388 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 7389 * Page up and down scroll by the vertical viewable area 7390 * Home and end scroll to the start and end of the verticle viewable area. 7391 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 7392 ) 7393 7394 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 7395 7396 Params: 7397 horizontalArrowScrollAmount = 7398 verticalArrowScrollAmount = 7399 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 7400 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 7401 shiftMultiplier = multiplies the scroll amount by this when shift is held 7402 +/ 7403 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 7404 auto _this = this; 7405 7406 container.addEventListener((scope KeyDownEvent ke) { 7407 switch(ke.key) { 7408 case Key.Left: 7409 _this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7410 break; 7411 case Key.Right: 7412 _this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7413 break; 7414 case Key.Up: 7415 _this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7416 break; 7417 case Key.Down: 7418 _this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7419 break; 7420 case Key.PageUp: 7421 if(ke.altKey) 7422 _this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7423 else 7424 _this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7425 break; 7426 case Key.PageDown: 7427 if(ke.altKey) 7428 _this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7429 else 7430 _this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7431 break; 7432 case Key.Home: 7433 if(ke.altKey) 7434 _this.scrollLeft(short.max * 16); 7435 else 7436 _this.scrollUp(short.max * 16); 7437 break; 7438 case Key.End: 7439 if(ke.altKey) 7440 _this.scrollRight(short.max * 16); 7441 else 7442 _this.scrollDown(short.max * 16); 7443 break; 7444 7445 default: 7446 // ignore, not for us. 7447 } 7448 7449 }); 7450 } 7451 7452 /// ditto 7453 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 7454 auto _this = this; 7455 container.addEventListener((scope ClickEvent ce) { 7456 7457 if(ce.target && ce.target.tabStop) 7458 ce.target.focus(); 7459 7460 // ctrl is reserved for the application 7461 if(ce.ctrlKey) 7462 return; 7463 7464 if(horizontalWheelScrollAmount == 0 && ce.altKey) 7465 return; 7466 7467 if(shiftMultiplier == 0 && ce.shiftKey) 7468 return; 7469 7470 if(ce.button == MouseButton.wheelDown) { 7471 if(ce.altKey) 7472 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7473 else 7474 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7475 } else if(ce.button == MouseButton.wheelUp) { 7476 if(ce.altKey) 7477 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7478 else 7479 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7480 } 7481 }); 7482 } 7483 7484 /++ 7485 Scrolls the given amount. 7486 7487 History: 7488 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. 7489 +/ 7490 void scrollUp(int amount = 1) { 7491 vsb.setPosition(vsb.position - amount); 7492 notify(); 7493 } 7494 /// ditto 7495 void scrollDown(int amount = 1) { 7496 vsb.setPosition(vsb.position + amount); 7497 notify(); 7498 } 7499 /// ditto 7500 void scrollLeft(int amount = 1) { 7501 hsb.setPosition(hsb.position - amount); 7502 notify(); 7503 } 7504 /// ditto 7505 void scrollRight(int amount = 1) { 7506 hsb.setPosition(hsb.position + amount); 7507 notify(); 7508 } 7509 7510 /// 7511 VerticalScrollbar verticalScrollBar() { return vsb; } 7512 /// 7513 HorizontalScrollbar horizontalScrollBar() { return hsb; } 7514 7515 void notify() { 7516 static bool insideNotify; 7517 7518 if(insideNotify) 7519 return; // avoid the recursive call, even if it isn't strictly correct 7520 7521 insideNotify = true; 7522 scope(exit) insideNotify = false; 7523 7524 this.emit!ScrollEvent(); 7525 } 7526 7527 mixin Emits!ScrollEvent; 7528 7529 /// 7530 Point position() { 7531 return Point(hsb.position, vsb.position); 7532 } 7533 7534 /// 7535 void setPosition(int x, int y) { 7536 hsb.setPosition(x); 7537 vsb.setPosition(y); 7538 } 7539 7540 /// 7541 void setPageSize(int unitsX, int unitsY) { 7542 hsb.setStep(unitsX); 7543 vsb.setStep(unitsY); 7544 } 7545 7546 /// 7547 void setTotalArea(int width, int height) { 7548 hsb.setMax(width); 7549 vsb.setMax(height); 7550 } 7551 7552 /// Always set the viewable area AFTER setitng the total area if you are going to change both. 7553 /// NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 7554 /// If you need to do that, use [queueRecomputeChildLayout]. 7555 void setViewableArea(int width, int height) { 7556 7557 // actually there IS A need to dothis cuz the max might have changed since then 7558 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 7559 //return; // no need to do what is already done 7560 hsb.setViewableArea(width); 7561 vsb.setViewableArea(height); 7562 7563 bool needsNotify = false; 7564 7565 // FIXME: if at any point the rhs is outside the scrollbar, we need 7566 // to reset to 0. but it should remember the old position in case the 7567 // window resizes again, so it can kinda return ot where it was. 7568 // 7569 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 7570 if(width >= hsb.max) { 7571 // there's plenty of room to display it all so we need to reset to zero 7572 // FIXME: adjust so it matches the note above 7573 hsb.setPosition(0); 7574 needsNotify = true; 7575 } 7576 if(height >= vsb.max) { 7577 // there's plenty of room to display it all so we need to reset to zero 7578 // FIXME: adjust so it matches the note above 7579 vsb.setPosition(0); 7580 needsNotify = true; 7581 } 7582 if(needsNotify) 7583 notify(); 7584 } 7585 7586 private bool magic; 7587 override void addChild(Widget w, int position = int.max) { 7588 if(magic) 7589 container.addChild(w, position); 7590 else 7591 super.addChild(w, position); 7592 } 7593 7594 override void recomputeChildLayout() { 7595 if(hsb is null || vsb is null || container is null) return; 7596 7597 registerMovement(); 7598 7599 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 7600 hsb.x = 0; 7601 hsb.y = this.height - hsb.height; 7602 hsb.width = this.width - scaleWithDpi(16); 7603 hsb.recomputeChildLayout(); 7604 7605 vsb.width = scaleWithDpi(16); // FIXME? 7606 vsb.x = this.width - vsb.width; 7607 vsb.y = 0; 7608 vsb.height = this.height - scaleWithDpi(16); 7609 vsb.recomputeChildLayout(); 7610 7611 if(this.header is null) { 7612 container.x = 0; 7613 container.y = 0; 7614 container.width = this.width - vsb.width; 7615 container.height = this.height - hsb.height; 7616 container.recomputeChildLayout(); 7617 } else { 7618 header.x = 0; 7619 header.y = 0; 7620 header.width = this.width - vsb.width; 7621 header.height = scaleWithDpi(16); // size of the button 7622 header.recomputeChildLayout(); 7623 7624 container.x = 0; 7625 container.y = scaleWithDpi(16); 7626 container.width = this.width - vsb.width; 7627 container.height = this.height - hsb.height - scaleWithDpi(16); 7628 container.recomputeChildLayout(); 7629 } 7630 } 7631 7632 HorizontalScrollbar hsb; 7633 VerticalScrollbar vsb; 7634 Widget container; 7635 private Widget header; 7636 7637 /++ 7638 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 7639 7640 History: 7641 Added September 27, 2021 (dub v10.3) 7642 +/ 7643 Widget getHeader() { 7644 if(this.header is null) { 7645 magic = false; 7646 scope(exit) magic = true; 7647 this.header = new Widget(this); 7648 recomputeChildLayout(); 7649 } 7650 return this.header; 7651 } 7652 } 7653 7654 /++ 7655 $(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") 7656 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 7657 +/ 7658 version(minigui_screenshots) 7659 @Screenshot("ScrollMessageWidget") 7660 unittest { 7661 auto window = new Window("ScrollMessageWidget"); 7662 7663 auto smw = new ScrollMessageWidget(window); 7664 smw.addDefaultKeyboardListeners(); 7665 smw.addDefaultWheelListeners(); 7666 7667 window.loop(); 7668 } 7669 7670 /++ 7671 Bypasses automatic layout for its children, using manual positioning and sizing only. 7672 While you need to manually position them, you must ensure they are inside the StaticLayout's 7673 bounding box to avoid undefined behavior. 7674 7675 You should almost never use this. 7676 +/ 7677 class StaticLayout : Layout { 7678 /// 7679 this(Widget parent) { super(parent); } 7680 override void recomputeChildLayout() { 7681 registerMovement(); 7682 foreach(child; children) 7683 child.recomputeChildLayout(); 7684 } 7685 } 7686 7687 /++ 7688 Bypasses automatic positioning when being laid out. It is your responsibility to make 7689 room for this widget in the parent layout. 7690 7691 Its children are laid out normally, unless there is exactly one, in which case it takes 7692 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 7693 can do that with `padding`). 7694 +/ 7695 class StaticPosition : Layout { 7696 /// 7697 this(Widget parent) { super(parent); } 7698 7699 override void recomputeChildLayout() { 7700 registerMovement(); 7701 if(this.children.length == 1) { 7702 auto child = children[0]; 7703 child.x = 0; 7704 child.y = 0; 7705 child.width = this.width; 7706 child.height = this.height; 7707 child.recomputeChildLayout(); 7708 } else 7709 foreach(child; children) 7710 child.recomputeChildLayout(); 7711 } 7712 7713 alias width = typeof(super).width; 7714 alias height = typeof(super).height; 7715 7716 @property int width(int w) @nogc pure @safe nothrow { 7717 return this._width = w; 7718 } 7719 7720 @property int height(int w) @nogc pure @safe nothrow { 7721 return this._height = w; 7722 } 7723 7724 } 7725 7726 /++ 7727 FixedPosition is like [StaticPosition], but its coordinates 7728 are always relative to the viewport, meaning they do not scroll with 7729 the parent content. 7730 +/ 7731 class FixedPosition : StaticPosition { 7732 /// 7733 this(Widget parent) { super(parent); } 7734 } 7735 7736 version(win32_widgets) 7737 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 7738 if(true) { 7739 // cmd == 0 = menu, cmd == 1 = accelerator 7740 if(auto item = idm in Action.mapping) { 7741 foreach(handler; (*item).triggered) 7742 handler(); 7743 /* 7744 auto event = new Event("triggered", *item); 7745 event.button = idm; 7746 event.dispatch(); 7747 */ 7748 return 0; 7749 } 7750 } 7751 if(handle) 7752 if(auto widgetp = handle in Widget.nativeMapping) { 7753 (*widgetp).handleWmCommand(cmd, idm); 7754 return 0; 7755 } 7756 return 1; 7757 } 7758 7759 7760 /// 7761 class Window : Widget { 7762 int mouseCaptureCount = 0; 7763 Widget mouseCapturedBy; 7764 void captureMouse(Widget byWhom) { 7765 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 7766 mouseCaptureCount++; 7767 mouseCapturedBy = byWhom; 7768 win.grabInput(); 7769 } 7770 void releaseMouseCapture() { 7771 mouseCaptureCount--; 7772 mouseCapturedBy = null; 7773 win.releaseInputGrab(); 7774 } 7775 7776 /++ 7777 Sets the window icon which is often seen in title bars and taskbars. 7778 7779 History: 7780 Added April 5, 2022 (dub v10.8) 7781 +/ 7782 @property void icon(MemoryImage icon) { 7783 if(win && icon) 7784 win.icon = icon; 7785 } 7786 7787 /// 7788 @scriptable 7789 @property bool focused() { 7790 return win.focused; 7791 } 7792 7793 static class Style : Widget.Style { 7794 override WidgetBackground background() { 7795 version(custom_widgets) 7796 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 7797 else version(win32_widgets) 7798 return WidgetBackground(Color.transparent); 7799 else static assert(0); 7800 } 7801 } 7802 mixin OverrideStyle!Style; 7803 7804 /++ 7805 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. 7806 +/ 7807 static int lineHeight() { 7808 OperatingSystemFont font; 7809 if(auto vt = WidgetPainter.visualTheme) { 7810 font = vt.defaultFontCached(); 7811 } 7812 7813 if(font is null) { 7814 static int defaultHeightCache; 7815 if(defaultHeightCache == 0) { 7816 font = new OperatingSystemFont; 7817 font.loadDefault; 7818 defaultHeightCache = font.height() * 5 / 4; 7819 } 7820 return defaultHeightCache; 7821 } 7822 7823 return font.height() * 5 / 4; 7824 } 7825 7826 Widget focusedWidget; 7827 7828 private SimpleWindow win_; 7829 7830 @property { 7831 /++ 7832 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 7833 7834 History: 7835 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 7836 +/ 7837 public SimpleWindow win() { 7838 return win_; 7839 } 7840 /// 7841 protected void win(SimpleWindow w) { 7842 win_ = w; 7843 } 7844 } 7845 7846 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 7847 this(Widget p) { 7848 tabStop = false; 7849 super(p); 7850 } 7851 7852 private void actualRedraw() { 7853 if(recomputeChildLayoutRequired) 7854 recomputeChildLayoutEntry(); 7855 if(!showing) return; 7856 7857 assert(parentWindow !is null); 7858 7859 auto w = drawableWindow; 7860 if(w is null) 7861 w = parentWindow.win; 7862 7863 if(w.closed()) 7864 return; 7865 7866 auto ugh = this.parent; 7867 int lox, loy; 7868 while(ugh) { 7869 lox += ugh.x; 7870 loy += ugh.y; 7871 ugh = ugh.parent; 7872 } 7873 auto painter = w.draw(true); 7874 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 7875 // RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN); 7876 } 7877 7878 7879 private bool skipNextChar = false; 7880 7881 /++ 7882 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 7883 7884 This constructor is intended primarily for internal use and may be changed to `protected` later. 7885 +/ 7886 this(SimpleWindow win) { 7887 7888 static if(UsingSimpledisplayX11) { 7889 win.discardAdditionalConnectionState = &discardXConnectionState; 7890 win.recreateAdditionalConnectionState = &recreateXConnectionState; 7891 } 7892 7893 tabStop = false; 7894 super(null); 7895 this.win = win; 7896 7897 win.addEventListener((Widget.RedrawEvent) { 7898 if(win.eventQueued!RecomputeEvent) { 7899 // import std.stdio; writeln("skipping"); 7900 return; // let the recompute event do the actual redraw 7901 } 7902 this.actualRedraw(); 7903 }); 7904 7905 win.addEventListener((Widget.RecomputeEvent) { 7906 recomputeChildLayoutEntry(); 7907 if(win.eventQueued!RedrawEvent) 7908 return; // let the queued one do it 7909 else { 7910 // import std.stdio; writeln("drawing"); 7911 this.actualRedraw(); // if not queued, it needs to be done now anyway 7912 } 7913 }); 7914 7915 this.width = win.width; 7916 this.height = win.height; 7917 this.parentWindow = this; 7918 7919 win.closeQuery = () { 7920 if(this.emit!ClosingEvent()) 7921 win.close(); 7922 }; 7923 win.onClosing = () { 7924 this.emit!ClosedEvent(); 7925 }; 7926 7927 win.windowResized = (int w, int h) { 7928 this.width = w; 7929 this.height = h; 7930 recomputeChildLayout(); 7931 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 7932 //version(win32_widgets) 7933 //InvalidateRect(hwnd, null, true); 7934 redraw(); 7935 }; 7936 7937 win.onFocusChange = (bool getting) { 7938 if(this.focusedWidget) { 7939 if(getting) { 7940 this.focusedWidget.emit!FocusEvent(); 7941 this.focusedWidget.emit!FocusInEvent(); 7942 } else { 7943 this.focusedWidget.emit!BlurEvent(); 7944 this.focusedWidget.emit!FocusOutEvent(); 7945 } 7946 } 7947 7948 if(getting) { 7949 this.emit!FocusEvent(); 7950 this.emit!FocusInEvent(); 7951 } else { 7952 this.emit!BlurEvent(); 7953 this.emit!FocusOutEvent(); 7954 } 7955 }; 7956 7957 win.onDpiChanged = { 7958 this.queueRecomputeChildLayout(); 7959 auto event = new DpiChangedEvent(this); 7960 event.sendDirectly(); 7961 7962 privateDpiChanged(); 7963 }; 7964 7965 win.setEventHandlers( 7966 (MouseEvent e) { 7967 dispatchMouseEvent(e); 7968 }, 7969 (KeyEvent e) { 7970 //import std.stdio; 7971 //writefln("%x %s", cast(uint) e.key, e.key); 7972 dispatchKeyEvent(e); 7973 }, 7974 (dchar e) { 7975 if(e == 13) e = 10; // hack? 7976 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 7977 dispatchCharEvent(e); 7978 }, 7979 ); 7980 7981 addEventListener("char", (Widget, Event ev) { 7982 if(skipNextChar) { 7983 ev.preventDefault(); 7984 skipNextChar = false; 7985 } 7986 }); 7987 7988 version(win32_widgets) 7989 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 7990 if(hwnd !is this.win.impl.hwnd) 7991 return 1; // we don't care... pass it on 7992 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 7993 if(mustReturn) 7994 return ret; 7995 return 1; // pass it on 7996 }; 7997 7998 if(Window.newWindowCreated) 7999 Window.newWindowCreated(this); 8000 } 8001 8002 version(custom_widgets) 8003 override void defaultEventHandler_click(ClickEvent event) { 8004 if(event.target && event.target.tabStop) 8005 event.target.focus(); 8006 } 8007 8008 private static void delegate(Window) newWindowCreated; 8009 8010 version(win32_widgets) 8011 override void paint(WidgetPainter painter) { 8012 /* 8013 RECT rect; 8014 rect.right = this.width; 8015 rect.bottom = this.height; 8016 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 8017 */ 8018 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 8019 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 8020 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 8021 // since the pen is null, to fill the whole space, we need the +1 on both. 8022 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 8023 SelectObject(painter.impl.hdc, p); 8024 SelectObject(painter.impl.hdc, b); 8025 } 8026 version(custom_widgets) 8027 override void paint(WidgetPainter painter) { 8028 auto cs = getComputedStyle(); 8029 painter.fillColor = cs.windowBackgroundColor; 8030 painter.outlineColor = cs.windowBackgroundColor; 8031 painter.drawRectangle(Point(0, 0), this.width, this.height); 8032 } 8033 8034 8035 override void defaultEventHandler_keydown(KeyDownEvent event) { 8036 Widget _this = event.target; 8037 8038 if(event.key == Key.Tab) { 8039 /* Window tab ordering is a recursive thingy with each group */ 8040 8041 // FIXME inefficient 8042 Widget[] helper(Widget p) { 8043 if(p.hidden) 8044 return null; 8045 Widget[] childOrdering; 8046 8047 auto children = p.children.dup; 8048 8049 while(true) { 8050 // UIs should be generally small, so gonna brute force it a little 8051 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 8052 8053 Widget smallestTab; 8054 foreach(ref c; children) { 8055 if(c is null) continue; 8056 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 8057 smallestTab = c; 8058 c = null; 8059 } 8060 } 8061 if(smallestTab !is null) { 8062 if(smallestTab.tabStop && !smallestTab.hidden) 8063 childOrdering ~= smallestTab; 8064 if(!smallestTab.hidden) 8065 childOrdering ~= helper(smallestTab); 8066 } else 8067 break; 8068 8069 } 8070 8071 return childOrdering; 8072 } 8073 8074 Widget[] tabOrdering = helper(this); 8075 8076 Widget recipient; 8077 8078 if(tabOrdering.length) { 8079 bool seenThis = false; 8080 Widget previous; 8081 foreach(idx, child; tabOrdering) { 8082 if(child is focusedWidget) { 8083 8084 if(event.shiftKey) { 8085 if(idx == 0) 8086 recipient = tabOrdering[$-1]; 8087 else 8088 recipient = tabOrdering[idx - 1]; 8089 break; 8090 } 8091 8092 seenThis = true; 8093 if(idx + 1 == tabOrdering.length) { 8094 // we're at the end, either move to the next group 8095 // or start back over 8096 recipient = tabOrdering[0]; 8097 } 8098 continue; 8099 } 8100 if(seenThis) { 8101 recipient = child; 8102 break; 8103 } 8104 previous = child; 8105 } 8106 } 8107 8108 if(recipient !is null) { 8109 // import std.stdio; writeln(typeid(recipient)); 8110 recipient.focus(); 8111 8112 skipNextChar = true; 8113 } 8114 } 8115 8116 debug if(event.key == Key.F12) { 8117 if(devTools) { 8118 devTools.close(); 8119 devTools = null; 8120 } else { 8121 devTools = new DevToolWindow(this); 8122 devTools.show(); 8123 } 8124 } 8125 } 8126 8127 debug DevToolWindow devTools; 8128 8129 8130 /++ 8131 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 8132 8133 History: 8134 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 8135 8136 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 8137 +/ 8138 this(int width = 500, int height = 500, string title = null) { 8139 if(title is null) { 8140 import core.runtime; 8141 if(Runtime.args.length) 8142 title = Runtime.args[0]; 8143 } 8144 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus); 8145 8146 static if(UsingSimpledisplayX11) { 8147 ///+ 8148 // for input proxy 8149 auto display = XDisplayConnection.get; 8150 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 8151 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 8152 XMapWindow(display, inputProxy); 8153 //import std.stdio; writefln("input proxy: 0x%0x", inputProxy); 8154 this.inputProxy = new SimpleWindow(inputProxy); 8155 8156 XEvent lastEvent; 8157 this.inputProxy.handleNativeEvent = (XEvent ev) { 8158 lastEvent = ev; 8159 return 1; 8160 }; 8161 this.inputProxy.setEventHandlers( 8162 (MouseEvent e) { 8163 dispatchMouseEvent(e); 8164 }, 8165 (KeyEvent e) { 8166 //import std.stdio; 8167 //writefln("%x %s", cast(uint) e.key, e.key); 8168 if(dispatchKeyEvent(e)) { 8169 // FIXME: i should trap error 8170 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 8171 auto thing = nw.focusableWindow(); 8172 if(thing && thing.window) { 8173 lastEvent.xkey.window = thing.window; 8174 // import std.stdio; writeln("sending event ", lastEvent.xkey); 8175 trapXErrors( { 8176 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 8177 }); 8178 } 8179 } 8180 } 8181 }, 8182 (dchar e) { 8183 if(e == 13) e = 10; // hack? 8184 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8185 dispatchCharEvent(e); 8186 }, 8187 ); 8188 8189 this.inputProxy.populateXic(); 8190 // done 8191 //+/ 8192 } 8193 8194 8195 8196 win.setRequestedInputFocus = &this.setRequestedInputFocus; 8197 8198 this(win); 8199 } 8200 8201 SimpleWindow inputProxy; 8202 8203 private SimpleWindow setRequestedInputFocus() { 8204 return inputProxy; 8205 } 8206 8207 /// ditto 8208 this(string title, int width = 500, int height = 500) { 8209 this(width, height, title); 8210 } 8211 8212 /// 8213 @property string title() { return parentWindow.win.title; } 8214 /// 8215 @property void title(string title) { parentWindow.win.title = title; } 8216 8217 /// 8218 @scriptable 8219 void close() { 8220 win.close(); 8221 // I synchronize here upon window closing to ensure all child windows 8222 // get updated too before the event loop. This avoids some random X errors. 8223 static if(UsingSimpledisplayX11) { 8224 runInGuiThread( { 8225 XSync(XDisplayConnection.get, false); 8226 }); 8227 } 8228 } 8229 8230 bool dispatchKeyEvent(KeyEvent ev) { 8231 auto wid = focusedWidget; 8232 if(wid is null) 8233 wid = this; 8234 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 8235 event.originalKeyEvent = ev; 8236 event.key = ev.key; 8237 event.state = ev.modifierState; 8238 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8239 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8240 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8241 event.dispatch(); 8242 8243 return !event.propagationStopped; 8244 } 8245 8246 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 8247 bool dispatchCharEvent(dchar ch) { 8248 if(focusedWidget) { 8249 auto event = new CharEvent(focusedWidget, ch); 8250 event.dispatch(); 8251 return !event.propagationStopped; 8252 } 8253 return true; 8254 } 8255 8256 Widget mouseLastOver; 8257 Widget mouseLastDownOn; 8258 bool lastWasDoubleClick; 8259 bool dispatchMouseEvent(MouseEvent ev) { 8260 auto eleR = widgetAtPoint(this, ev.x, ev.y); 8261 auto ele = eleR.widget; 8262 8263 auto captureEle = ele; 8264 8265 if(mouseCapturedBy !is null) { 8266 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 8267 captureEle = mouseCapturedBy; 8268 } 8269 8270 // a hack to get it relative to the widget. 8271 eleR.x = ev.x; 8272 eleR.y = ev.y; 8273 auto pain = captureEle; 8274 while(pain) { 8275 eleR.x -= pain.x; 8276 eleR.y -= pain.y; 8277 pain.addScrollPosition(eleR.x, eleR.y); 8278 pain = pain.parent; 8279 } 8280 8281 void populateMouseEventBase(MouseEventBase event) { 8282 event.button = ev.button; 8283 event.buttonLinear = ev.buttonLinear; 8284 event.state = ev.modifierState; 8285 event.clientX = eleR.x; 8286 event.clientY = eleR.y; 8287 8288 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8289 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8290 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8291 } 8292 8293 if(ev.type == MouseEventType.buttonPressed) { 8294 { 8295 auto event = new MouseDownEvent(captureEle); 8296 populateMouseEventBase(event); 8297 event.dispatch(); 8298 } 8299 8300 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 8301 auto event = new DoubleClickEvent(captureEle); 8302 populateMouseEventBase(event); 8303 event.dispatch(); 8304 lastWasDoubleClick = ev.doubleClick; 8305 } else { 8306 lastWasDoubleClick = false; 8307 } 8308 8309 mouseLastDownOn = ele; 8310 } else if(ev.type == MouseEventType.buttonReleased) { 8311 { 8312 auto event = new MouseUpEvent(captureEle); 8313 populateMouseEventBase(event); 8314 event.dispatch(); 8315 } 8316 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 8317 auto event = new ClickEvent(captureEle); 8318 populateMouseEventBase(event); 8319 event.dispatch(); 8320 } 8321 } else if(ev.type == MouseEventType.motion) { 8322 // motion 8323 { 8324 auto event = new MouseMoveEvent(captureEle); 8325 populateMouseEventBase(event); // fills in button which is meaningless but meh 8326 event.dispatch(); 8327 } 8328 8329 if(mouseLastOver !is ele) { 8330 if(ele !is null) { 8331 if(!isAParentOf(ele, mouseLastOver)) { 8332 ele.setDynamicState(DynamicState.hover, true); 8333 auto event = new MouseEnterEvent(ele); 8334 event.relatedTarget = mouseLastOver; 8335 event.sendDirectly(); 8336 8337 ele.useStyleProperties((scope Widget.Style s) { 8338 ele.parentWindow.win.cursor = s.cursor; 8339 }); 8340 } 8341 } 8342 8343 if(mouseLastOver !is null) { 8344 if(!isAParentOf(mouseLastOver, ele)) { 8345 mouseLastOver.setDynamicState(DynamicState.hover, false); 8346 auto event = new MouseLeaveEvent(mouseLastOver); 8347 event.relatedTarget = ele; 8348 event.sendDirectly(); 8349 } 8350 } 8351 8352 if(ele !is null) { 8353 auto event = new MouseOverEvent(ele); 8354 event.relatedTarget = mouseLastOver; 8355 event.dispatch(); 8356 } 8357 8358 if(mouseLastOver !is null) { 8359 auto event = new MouseOutEvent(mouseLastOver); 8360 event.relatedTarget = ele; 8361 event.dispatch(); 8362 } 8363 8364 mouseLastOver = ele; 8365 } 8366 } 8367 8368 return true; // FIXME: the event default prevented? 8369 } 8370 8371 /++ 8372 Shows the window and runs the application event loop. 8373 8374 Blocks until this window is closed. 8375 8376 History: 8377 The [BlockingMode] parameter was added on December 8, 2021. 8378 The default behavior is to block until the application quits 8379 (so all windows have been closed), unless another minigui or 8380 simpledisplay event loop is already running, in which case it 8381 will block until this window closes specifically. 8382 +/ 8383 @scriptable 8384 void loop(BlockingMode bm = BlockingMode.automatic) { 8385 if(win.closed) 8386 return; // otherwise show will throw 8387 show(); 8388 win.eventLoopWithBlockingMode(bm, 0); 8389 } 8390 8391 private bool firstShow = true; 8392 8393 @scriptable 8394 override void show() { 8395 bool rd = false; 8396 if(firstShow) { 8397 firstShow = false; 8398 recomputeChildLayout(); 8399 auto f = getFirstFocusable(this); // FIXME: autofocus? 8400 if(f) 8401 f.focus(); 8402 redraw(); 8403 } 8404 win.show(); 8405 super.show(); 8406 } 8407 @scriptable 8408 override void hide() { 8409 win.hide(); 8410 super.hide(); 8411 } 8412 8413 static Widget getFirstFocusable(Widget start) { 8414 if(start is null) 8415 return null; 8416 8417 foreach(widget; &start.focusableWidgets) { 8418 return widget; 8419 } 8420 8421 return null; 8422 } 8423 8424 static Widget getLastFocusable(Widget start) { 8425 if(start is null) 8426 return null; 8427 8428 Widget last; 8429 foreach(widget; &start.focusableWidgets) { 8430 last = widget; 8431 } 8432 8433 return last; 8434 } 8435 8436 8437 mixin Emits!ClosingEvent; 8438 mixin Emits!ClosedEvent; 8439 } 8440 8441 /++ 8442 History: 8443 Added January 12, 2022 8444 +/ 8445 class DpiChangedEvent : Event { 8446 enum EventString = "dpichanged"; 8447 8448 this(Widget target) { 8449 super(EventString, target); 8450 } 8451 } 8452 8453 debug private class DevToolWindow : Window { 8454 Window p; 8455 8456 TextEdit parentList; 8457 TextEdit logWindow; 8458 TextLabel clickX, clickY; 8459 8460 this(Window p) { 8461 this.p = p; 8462 super(400, 300, "Developer Toolbox"); 8463 8464 logWindow = new TextEdit(this); 8465 parentList = new TextEdit(this); 8466 8467 auto hl = new HorizontalLayout(this); 8468 clickX = new TextLabel("", TextAlignment.Right, hl); 8469 clickY = new TextLabel("", TextAlignment.Right, hl); 8470 8471 parentListeners ~= p.addEventListener("*", (Event ev) { 8472 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 8473 }); 8474 8475 parentListeners ~= p.addEventListener((ClickEvent ev) { 8476 auto s = ev.srcElement; 8477 string list = s.toString(); 8478 s = s.parent; 8479 while(s) { 8480 list ~= "\n"; 8481 list ~= s.toString(); 8482 s = s.parent; 8483 } 8484 parentList.content = list; 8485 8486 clickX.label = toInternal!string(ev.clientX); 8487 clickY.label = toInternal!string(ev.clientY); 8488 }); 8489 } 8490 8491 EventListener[] parentListeners; 8492 8493 override void close() { 8494 assert(p !is null); 8495 foreach(p; parentListeners) 8496 p.disconnect(); 8497 parentListeners = null; 8498 p.devTools = null; 8499 p = null; 8500 super.close(); 8501 } 8502 8503 override void defaultEventHandler_keydown(KeyDownEvent ev) { 8504 if(ev.key == Key.F12) { 8505 this.close(); 8506 if(p) 8507 p.devTools = null; 8508 } else { 8509 super.defaultEventHandler_keydown(ev); 8510 } 8511 } 8512 8513 void log(T...)(T t) { 8514 string str; 8515 import std.conv; 8516 foreach(i; t) 8517 str ~= to!string(i); 8518 str ~= "\n"; 8519 logWindow.addText(str); 8520 8521 version(custom_widgets) 8522 logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 8523 } 8524 } 8525 8526 /++ 8527 A dialog is a transient window that intends to get information from 8528 the user before being dismissed. 8529 +/ 8530 abstract class Dialog : Window { 8531 /// 8532 this(int width, int height, string title = null) { 8533 super(width, height, title); 8534 } 8535 8536 /// 8537 abstract void OK(); 8538 8539 /// 8540 void Cancel() { 8541 this.close(); 8542 } 8543 } 8544 8545 /++ 8546 A custom widget similar to the HTML5 <details> tag. 8547 +/ 8548 version(none) 8549 class DetailsView : Widget { 8550 8551 } 8552 8553 // FIXME: maybe i should expose the other list views Windows offers too 8554 8555 /++ 8556 A TableView is a widget made to display a table of data strings. 8557 8558 8559 Future_Directions: 8560 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 8561 8562 I will add a selection changed event at some point, as well as item clicked events. 8563 History: 8564 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 8565 See_Also: 8566 [ListWidget] which displays a list of strings without additional columns. 8567 +/ 8568 class TableView : Widget { 8569 /++ 8570 8571 +/ 8572 this(Widget parent) { 8573 super(parent); 8574 8575 version(win32_widgets) { 8576 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 8577 } else version(custom_widgets) { 8578 auto smw = new ScrollMessageWidget(this); 8579 smw.addDefaultKeyboardListeners(); 8580 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 8581 tvwi = new TableViewWidgetInner(this, smw); 8582 } 8583 } 8584 8585 // FIXME: auto-size columns on double click of header thing like in Windows 8586 // it need only make the currently displayed things fit well. 8587 8588 8589 private ColumnInfo[] columns; 8590 private int itemCount; 8591 8592 version(custom_widgets) private { 8593 TableViewWidgetInner tvwi; 8594 } 8595 8596 /// Passed to [setColumnInfo] 8597 static struct ColumnInfo { 8598 const(char)[] name; /// the name displayed in the header 8599 /++ 8600 The default width, in pixels. As a special case, you can set this to -1 8601 if you want the system to try to automatically size the width to fit visible 8602 content. If it can't, it will try to pick a sensible default size. 8603 8604 Any other negative value is not allowed and may lead to unpredictable results. 8605 8606 History: 8607 The -1 behavior was specified on December 3, 2021. It actually worked before 8608 anyway on Win32 but now it is a formal feature with partial Linux support. 8609 8610 Bugs: 8611 It doesn't actually attempt to calculate a best-fit width on Linux as of 8612 December 3, 2021. I do plan to fix this in the future, but Windows is the 8613 priority right now. At least it doesn't break things when you use it now. 8614 +/ 8615 int width; 8616 8617 /++ 8618 Alignment of the text in the cell. Applies to the header as well as all data in this 8619 column. 8620 8621 Bugs: 8622 On Windows, the first column ignores this member and is always left aligned. 8623 You can work around this by inserting a dummy first column with width = 0 8624 then putting your actual data in the second column, which does respect the 8625 alignment. 8626 8627 This is a quirk of the operating system's implementation going back a very 8628 long time and is unlikely to ever be fixed. 8629 +/ 8630 TextAlignment alignment; 8631 8632 /++ 8633 After all the pixel widths have been assigned, any left over 8634 space is divided up among all columns and distributed to according 8635 to the widthPercent field. 8636 8637 8638 For example, if you have two fields, both with width 50 and one with 8639 widthPercent of 25 and the other with widthPercent of 75, and the 8640 container is 200 pixels wide, first both get their width of 50. 8641 then the 100 remaining pixels are split up, so the one gets a total 8642 of 75 pixels and the other gets a total of 125. 8643 8644 This is automatically applied as the window is resized. 8645 8646 If there is not enough space - that is, when a horizontal scrollbar 8647 needs to appear - there are 0 pixels divided up, and thus everyone 8648 gets 0. This can cause a column to shrink out of proportion when 8649 passing the scroll threshold. 8650 8651 It is important to still set a fixed width (that is, to populate the 8652 `width` field) even if you use the percents because that will be the 8653 default minimum in the event of a scroll bar appearing. 8654 8655 The percents total in the column can never exceed 100 or be less than 0. 8656 Doing this will trigger an assert error. 8657 8658 Implementation note: 8659 8660 Please note that percentages are only recalculated 1) upon original 8661 construction and 2) upon resizing the control. If the user adjusts the 8662 width of a column, the percentage items will not be updated. 8663 8664 On the other hand, if the user adjusts the width of a percentage column 8665 then resizes the window, it is recalculated, meaning their hand adjustment 8666 is discarded. This specific behavior may change in the future as it is 8667 arguably a bug, but I'm not certain yet. 8668 8669 History: 8670 Added November 10, 2021 (dub v10.4) 8671 +/ 8672 int widthPercent; 8673 8674 8675 private int calculatedWidth; 8676 } 8677 /++ 8678 Sets the number of columns along with information about the headers. 8679 8680 Please note: on Windows, the first column ignores your alignment preference 8681 and is always left aligned. 8682 +/ 8683 void setColumnInfo(ColumnInfo[] columns...) { 8684 8685 foreach(ref c; columns) { 8686 c.name = c.name.idup; 8687 } 8688 this.columns = columns.dup; 8689 8690 updateCalculatedWidth(false); 8691 8692 version(custom_widgets) { 8693 tvwi.header.updateHeaders(); 8694 tvwi.updateScrolls(); 8695 } else version(win32_widgets) 8696 foreach(i, column; this.columns) { 8697 LVCOLUMN lvColumn; 8698 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 8699 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 8700 8701 auto bfr = WCharzBuffer(column.name); 8702 lvColumn.pszText = bfr.ptr; 8703 8704 if(column.alignment & TextAlignment.Center) 8705 lvColumn.fmt = LVCFMT_CENTER; 8706 else if(column.alignment & TextAlignment.Right) 8707 lvColumn.fmt = LVCFMT_RIGHT; 8708 else 8709 lvColumn.fmt = LVCFMT_LEFT; 8710 8711 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 8712 throw new WindowsApiException("Insert Column Fail"); 8713 } 8714 } 8715 8716 private int getActualSetSize(size_t i, bool askWindows) { 8717 version(win32_widgets) 8718 if(askWindows) 8719 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 8720 auto w = columns[i].width; 8721 if(w == -1) 8722 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 8723 return w; 8724 } 8725 8726 private void updateCalculatedWidth(bool informWindows) { 8727 int padding; 8728 version(win32_widgets) 8729 padding = 4; 8730 int remaining = this.width; 8731 foreach(i, column; columns) 8732 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 8733 remaining -= padding; 8734 if(remaining < 0) 8735 remaining = 0; 8736 8737 int percentTotal; 8738 foreach(i, ref column; columns) { 8739 percentTotal += column.widthPercent; 8740 8741 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 8742 8743 column.calculatedWidth = c; 8744 8745 version(win32_widgets) 8746 if(informWindows) 8747 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 8748 } 8749 8750 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 8751 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)."); 8752 8753 8754 } 8755 8756 override void registerMovement() { 8757 super.registerMovement(); 8758 8759 updateCalculatedWidth(true); 8760 } 8761 8762 /++ 8763 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. 8764 +/ 8765 void setItemCount(int count) { 8766 this.itemCount = count; 8767 version(custom_widgets) { 8768 tvwi.updateScrolls(); 8769 redraw(); 8770 } else version(win32_widgets) { 8771 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 8772 } 8773 } 8774 8775 /++ 8776 Clears all items; 8777 +/ 8778 void clear() { 8779 this.itemCount = 0; 8780 this.columns = null; 8781 version(custom_widgets) { 8782 tvwi.header.updateHeaders(); 8783 tvwi.updateScrolls(); 8784 redraw(); 8785 } else version(win32_widgets) { 8786 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 8787 } 8788 } 8789 8790 /+ 8791 version(win32_widgets) 8792 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 8793 auto itemId = dis.itemID; 8794 auto hdc = dis.hDC; 8795 auto rect = dis.rcItem; 8796 switch(dis.itemAction) { 8797 case ODA_DRAWENTIRE: 8798 8799 // FIXME: do other items 8800 // FIXME: do the focus rectangle i guess 8801 // FIXME: alignment 8802 // FIXME: column width 8803 // FIXME: padding left 8804 // FIXME: check dpi scaling 8805 // FIXME: don't owner draw unless it is necessary. 8806 8807 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 8808 RECT itemRect; 8809 itemRect.top = 1; // subitem idx, 1-based 8810 itemRect.left = LVIR_BOUNDS; 8811 8812 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 8813 itemRect.left += padding; 8814 8815 getData(itemId, 0, (in char[] data) { 8816 auto wdata = WCharzBuffer(data); 8817 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 8818 8819 }); 8820 goto case; 8821 case ODA_FOCUS: 8822 if(dis.itemState & ODS_FOCUS) 8823 DrawFocusRect(hdc, &rect); 8824 break; 8825 case ODA_SELECT: 8826 // itemState & ODS_SELECTED 8827 break; 8828 default: 8829 } 8830 return 1; 8831 } 8832 +/ 8833 8834 version(win32_widgets) { 8835 CellStyle last; 8836 COLORREF defaultColor; 8837 COLORREF defaultBackground; 8838 } 8839 8840 version(win32_widgets) 8841 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 8842 switch(code) { 8843 case NM_CUSTOMDRAW: 8844 auto s = cast(NMLVCUSTOMDRAW*) hdr; 8845 switch(s.nmcd.dwDrawStage) { 8846 case CDDS_PREPAINT: 8847 if(getCellStyle is null) 8848 return 0; 8849 8850 mustReturn = true; 8851 return CDRF_NOTIFYITEMDRAW; 8852 case CDDS_ITEMPREPAINT: 8853 mustReturn = true; 8854 return CDRF_NOTIFYSUBITEMDRAW; 8855 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 8856 mustReturn = true; 8857 8858 if(getCellStyle is null) // this SHOULD never happen... 8859 return 0; 8860 8861 if(s.iSubItem == 0) { 8862 // Windows resets it per row so we'll use item 0 as a chance 8863 // to capture these for later 8864 defaultColor = s.clrText; 8865 defaultBackground = s.clrTextBk; 8866 } 8867 8868 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 8869 // if no special style and no reset needed... 8870 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 8871 return 0; // allow default processing to continue 8872 8873 last = style; 8874 8875 // might still need to reset or use the preference. 8876 8877 if(style.flags & CellStyle.Flags.textColorSet) 8878 s.clrText = style.textColor.asWindowsColorRef; 8879 else 8880 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 8881 if(style.flags & CellStyle.Flags.backgroundColorSet) 8882 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 8883 else 8884 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 8885 8886 return CDRF_NEWFONT; 8887 default: 8888 return 0; 8889 8890 } 8891 case NM_RETURN: // no need since i subclass keydown 8892 break; 8893 case LVN_COLUMNCLICK: 8894 auto info = cast(LPNMLISTVIEW) hdr; 8895 this.emit!HeaderClickedEvent(info.iSubItem); 8896 break; 8897 case NM_CLICK: 8898 case NM_DBLCLK: 8899 case NM_RCLICK: 8900 case NM_RDBLCLK: 8901 // the item/subitem is set here and that can be a useful notification 8902 // even beyond the normal click notification 8903 break; 8904 case LVN_GETDISPINFO: 8905 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 8906 if(info.item.mask & LVIF_TEXT) { 8907 if(getData) { 8908 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 8909 auto bfr = WCharzBuffer(dataReceived); 8910 auto len = info.item.cchTextMax; 8911 if(bfr.length < len) 8912 len = cast(typeof(len)) bfr.length; 8913 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 8914 info.item.pszText[len] = 0; 8915 }); 8916 } else { 8917 info.item.pszText[0] = 0; 8918 } 8919 //info.item.iItem 8920 //if(info.item.iSubItem) 8921 } 8922 break; 8923 default: 8924 } 8925 return 0; 8926 } 8927 8928 override bool encapsulatedChildren() { 8929 return true; 8930 } 8931 8932 /++ 8933 Informs the control that content has changed. 8934 8935 History: 8936 Added November 10, 2021 (dub v10.4) 8937 +/ 8938 void update() { 8939 version(custom_widgets) 8940 redraw(); 8941 else { 8942 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 8943 UpdateWindow(hwnd); 8944 } 8945 8946 8947 } 8948 8949 /++ 8950 Called by the system to request the text content of an individual cell. You 8951 should pass the text into the provided `sink` delegate. This function will be 8952 called for each visible cell as-needed when drawing. 8953 +/ 8954 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 8955 8956 /++ 8957 Available per-cell style customization options. Use one of the constructors 8958 provided to set the values conveniently, or default construct it and set individual 8959 values yourself. Just remember to set the `flags` so your values are actually used. 8960 If the flag isn't set, the field is ignored and the system default is used instead. 8961 8962 This is returned by the [getCellStyle] delegate. 8963 8964 Examples: 8965 --- 8966 // assumes you have a variables called `my_data` which is an array of arrays of numbers 8967 auto table = new TableView(window); 8968 // snip: you would set up columns here 8969 8970 // this is how you provide data to the table view class 8971 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 8972 import std.conv; 8973 sink(to!string(my_data[row][column])); 8974 }; 8975 8976 // and this is how you customize the colors 8977 table.getCellStyle = delegate(int row, int column) { 8978 return (my_data[row][column] < 0) ? 8979 TableView.CellStyle(Color.red); // make negative numbers red 8980 : TableView.CellStyle.init; // leave the rest alone 8981 }; 8982 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 8983 --- 8984 8985 History: 8986 Added November 27, 2021 (dub v10.4) 8987 +/ 8988 struct CellStyle { 8989 /// 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. 8990 this(Color textColor) { 8991 this.textColor = textColor; 8992 this.flags |= Flags.textColorSet; 8993 } 8994 /// Sets a custom text and background color. 8995 this(Color textColor, Color backgroundColor) { 8996 this.textColor = textColor; 8997 this.backgroundColor = backgroundColor; 8998 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 8999 } 9000 9001 Color textColor; 9002 Color backgroundColor; 9003 int flags; /// bitmask of [Flags] 9004 /// available options to combine into [flags] 9005 enum Flags { 9006 textColorSet = 1 << 0, 9007 backgroundColorSet = 1 << 1, 9008 } 9009 } 9010 /++ 9011 Companion delegate to [getData] that allows you to custom style each 9012 cell of the table. 9013 9014 Returns: 9015 A [CellStyle] structure that describes the desired style for the 9016 given cell. `return CellStyle.init` if you want the default style. 9017 9018 History: 9019 Added November 27, 2021 (dub v10.4) 9020 +/ 9021 CellStyle delegate(int row, int column) getCellStyle; 9022 9023 // i want to be able to do things like draw little colored things to show red for negative numbers 9024 // or background color indicators or even in-cell charts 9025 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 9026 9027 /++ 9028 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 9029 +/ 9030 mixin Emits!HeaderClickedEvent; 9031 } 9032 9033 /++ 9034 This is emitted by the [TableView] when a user clicks on a column header. 9035 9036 Its member `columnIndex` has the zero-based index of the column that was clicked. 9037 9038 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 9039 9040 History: 9041 Added November 27, 2021 (dub v10.4) 9042 +/ 9043 class HeaderClickedEvent : Event { 9044 enum EventString = "HeaderClicked"; 9045 this(Widget target, int columnIndex) { 9046 this.columnIndex = columnIndex; 9047 super(EventString, target); 9048 } 9049 9050 /// The index of the column 9051 int columnIndex; 9052 9053 /// 9054 override @property int intValue() { 9055 return columnIndex; 9056 } 9057 } 9058 9059 version(custom_widgets) 9060 private class TableViewWidgetInner : Widget { 9061 9062 // wrap this thing in a ScrollMessageWidget 9063 9064 TableView tvw; 9065 ScrollMessageWidget smw; 9066 HeaderWidget header; 9067 9068 this(TableView tvw, ScrollMessageWidget smw) { 9069 this.tvw = tvw; 9070 this.smw = smw; 9071 super(smw); 9072 9073 this.tabStop = true; 9074 9075 header = new HeaderWidget(this, smw.getHeader()); 9076 9077 smw.addEventListener("scroll", () { 9078 this.redraw(); 9079 header.redraw(); 9080 }); 9081 9082 9083 // I need headers outside the scroll area but rendered on the same line as the up arrow 9084 // FIXME: add a fixed header to the SMW 9085 } 9086 9087 enum padding = 3; 9088 9089 void updateScrolls() { 9090 int w; 9091 foreach(idx, column; tvw.columns) { 9092 if(column.width == 0) continue; 9093 w += tvw.getActualSetSize(idx, false);// + padding; 9094 } 9095 smw.setTotalArea(w, tvw.itemCount); 9096 columnsWidth = w; 9097 } 9098 9099 private int columnsWidth; 9100 9101 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 9102 9103 override void registerMovement() { 9104 super.registerMovement(); 9105 // FIXME: actual column width. it might need to be done per-pixel instead of per-colum 9106 smw.setViewableArea(this.width, this.height / lh); 9107 } 9108 9109 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 9110 int x; 9111 int y; 9112 9113 int row = smw.position.y; 9114 9115 foreach(lol; 0 .. this.height / lh) { 9116 if(row >= tvw.itemCount) 9117 break; 9118 x = 0; 9119 foreach(columnNumber, column; tvw.columns) { 9120 auto x2 = x + column.calculatedWidth; 9121 auto smwx = smw.position.x; 9122 9123 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 9124 auto startX = x; 9125 auto endX = x + column.calculatedWidth; 9126 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 9127 case TextAlignment.Left: startX += padding; break; 9128 case TextAlignment.Center: startX += padding; endX -= padding; break; 9129 case TextAlignment.Right: endX -= padding; break; 9130 default: /* broken */ break; 9131 } 9132 if(column.width != 0) // no point drawing an invisible column 9133 tvw.getData(row, cast(int) columnNumber, (info) { 9134 // auto clip = painter.setClipRectangle( 9135 9136 void dotext(WidgetPainter painter) { 9137 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 9138 } 9139 9140 if(tvw.getCellStyle !is null) { 9141 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 9142 9143 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 9144 auto tempPainter = painter; 9145 tempPainter.fillColor = style.backgroundColor; 9146 tempPainter.outlineColor = style.backgroundColor; 9147 9148 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 9149 Point(endX - smw.position.x, y + lh)); 9150 } 9151 auto tempPainter = painter; 9152 if(style.flags & TableView.CellStyle.Flags.textColorSet) 9153 tempPainter.outlineColor = style.textColor; 9154 9155 dotext(tempPainter); 9156 } else { 9157 dotext(painter); 9158 } 9159 }); 9160 } 9161 9162 x += column.calculatedWidth; 9163 } 9164 row++; 9165 y += lh; 9166 } 9167 return bounds; 9168 } 9169 9170 static class Style : Widget.Style { 9171 override WidgetBackground background() { 9172 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 9173 } 9174 } 9175 mixin OverrideStyle!Style; 9176 9177 private static class HeaderWidget : Widget { 9178 this(TableViewWidgetInner tvw, Widget parent) { 9179 super(parent); 9180 this.tvw = tvw; 9181 9182 this.remainder = new Button("", this); 9183 9184 this.addEventListener((scope ClickEvent ev) { 9185 int header = -1; 9186 foreach(idx, child; this.children[1 .. $]) { 9187 if(child is ev.target) { 9188 header = cast(int) idx; 9189 break; 9190 } 9191 } 9192 9193 if(header != -1) { 9194 auto hce = new HeaderClickedEvent(tvw.tvw, header); 9195 hce.dispatch(); 9196 } 9197 9198 }); 9199 } 9200 9201 void updateHeaders() { 9202 foreach(child; children[1 .. $]) 9203 child.removeWidget(); 9204 9205 foreach(column; tvw.tvw.columns) { 9206 // the cast is ok because I dup it above, just the type is never changed. 9207 // all this is private so it should never get messed up. 9208 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 9209 } 9210 } 9211 9212 Button remainder; 9213 TableViewWidgetInner tvw; 9214 9215 override void recomputeChildLayout() { 9216 registerMovement(); 9217 int pos; 9218 foreach(idx, child; children[1 .. $]) { 9219 if(idx >= tvw.tvw.columns.length) 9220 continue; 9221 child.x = pos; 9222 child.y = 0; 9223 child.width = tvw.tvw.columns[idx].calculatedWidth; 9224 child.height = scaleWithDpi(16);// this.height; 9225 pos += child.width; 9226 9227 child.recomputeChildLayout(); 9228 } 9229 9230 if(remainder is null) 9231 return; 9232 9233 remainder.x = pos; 9234 remainder.y = 0; 9235 if(pos < this.width) 9236 remainder.width = this.width - pos;// + 4; 9237 else 9238 remainder.width = 0; 9239 remainder.height = scaleWithDpi(16); 9240 9241 remainder.recomputeChildLayout(); 9242 } 9243 9244 // for the scrollable children mixin 9245 Point scrollOrigin() { 9246 return Point(tvw.smw.position.x, 0); 9247 } 9248 void paintFrameAndBackground(WidgetPainter painter) { } 9249 9250 mixin ScrollableChildren; 9251 } 9252 } 9253 9254 /+ 9255 9256 // given struct / array / number / string / etc, make it viewable and editable 9257 class DataViewerWidget : Widget { 9258 9259 } 9260 +/ 9261 9262 /++ 9263 A line edit box with an associated label. 9264 9265 History: 9266 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 9267 9268 ``` 9269 Old: ________ 9270 9271 New: 9272 ____________ 9273 ``` 9274 9275 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 9276 9277 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 9278 horizontal label but left aligned. You may also consider a [GridLayout]. 9279 +/ 9280 alias LabeledLineEdit = Labeled!LineEdit; 9281 9282 /++ 9283 History: 9284 Added May 19, 2021 9285 +/ 9286 class Labeled(T) : Widget { 9287 /// 9288 this(string label, Widget parent) { 9289 super(parent); 9290 initialize!VerticalLayout(label, TextAlignment.Left, parent); 9291 } 9292 9293 /++ 9294 History: 9295 The alignment parameter was added May 17, 2021 9296 +/ 9297 this(string label, TextAlignment alignment, Widget parent) { 9298 super(parent); 9299 initialize!HorizontalLayout(label, alignment, parent); 9300 } 9301 9302 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 9303 tabStop = false; 9304 horizontal = is(L == HorizontalLayout); 9305 auto hl = new L(this); 9306 this.label = new TextLabel(label, alignment, hl); 9307 this.lineEdit = new T(hl); 9308 9309 this.label.labelFor = this.lineEdit; 9310 } 9311 9312 private bool horizontal; 9313 9314 TextLabel label; /// 9315 T lineEdit; /// 9316 9317 override int flexBasisWidth() { return 250; } 9318 9319 override int minHeight() { return (horizontal ? 1 : 2) * defaultLineHeight + 4; } 9320 override int maxHeight() { return (horizontal ? 1 : 2) * defaultLineHeight + 4; } 9321 override int marginTop() { return 4; } 9322 override int marginBottom() { return 4; } 9323 9324 // FIXME: i should prolly call it value as well as content tbh 9325 9326 /// 9327 @property string content() { 9328 return lineEdit.content; 9329 } 9330 /// 9331 @property void content(string c) { 9332 return lineEdit.content(c); 9333 } 9334 9335 /// 9336 void selectAll() { 9337 lineEdit.selectAll(); 9338 } 9339 9340 override void focus() { 9341 lineEdit.focus(); 9342 } 9343 } 9344 9345 /++ 9346 A labeled password edit. 9347 9348 History: 9349 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 9350 9351 The default parameters for the constructors were also removed on May 19, 2021 9352 +/ 9353 alias LabeledPasswordEdit = Labeled!PasswordEdit; 9354 9355 private string toMenuLabel(string s) { 9356 string n; 9357 n.reserve(s.length); 9358 foreach(c; s) 9359 if(c == '_') 9360 n ~= ' '; 9361 else 9362 n ~= c; 9363 return n; 9364 } 9365 9366 private void autoExceptionHandler(Exception e) { 9367 messageBox(e.msg); 9368 } 9369 9370 private void delegate() makeAutomaticHandler(alias fn, T)(T t) { 9371 static if(is(T : void delegate())) { 9372 return () { 9373 try 9374 t(); 9375 catch(Exception e) 9376 autoExceptionHandler(e); 9377 }; 9378 } else static if(is(typeof(fn) Params == __parameters)) { 9379 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 9380 return () { 9381 void onOK(string s) { 9382 member = s; 9383 try 9384 t(Params[0](s)); 9385 catch(Exception e) 9386 autoExceptionHandler(e); 9387 } 9388 9389 if( 9390 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 9391 || type == FileDialogType.Save) 9392 { 9393 getSaveFileName(&onOK, member, filters, null); 9394 } else 9395 getOpenFileName(&onOK, member, filters, null); 9396 }; 9397 } else { 9398 struct S { 9399 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 9400 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 9401 } else mixin(q{ 9402 static foreach(idx, ignore; Params) { 9403 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 9404 } 9405 }); 9406 } 9407 return () { 9408 dialog((S s) { 9409 try 9410 cast(void) t(s.tupleof); 9411 catch(Exception e) 9412 autoExceptionHandler(e); 9413 }, null, __traits(identifier, fn)); 9414 }; 9415 } 9416 } 9417 } 9418 9419 private template hasAnyRelevantAnnotations(a...) { 9420 bool helper() { 9421 bool any; 9422 foreach(attr; a) { 9423 static if(is(typeof(attr) == .menu)) 9424 any = true; 9425 else static if(is(typeof(attr) == .toolbar)) 9426 any = true; 9427 else static if(is(attr == .separator)) 9428 any = true; 9429 else static if(is(typeof(attr) == .accelerator)) 9430 any = true; 9431 else static if(is(typeof(attr) == .hotkey)) 9432 any = true; 9433 else static if(is(typeof(attr) == .icon)) 9434 any = true; 9435 else static if(is(typeof(attr) == .label)) 9436 any = true; 9437 else static if(is(typeof(attr) == .tip)) 9438 any = true; 9439 } 9440 return any; 9441 } 9442 9443 enum bool hasAnyRelevantAnnotations = helper(); 9444 } 9445 9446 /++ 9447 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. 9448 +/ 9449 class MainWindow : Window { 9450 /// 9451 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 9452 super(initialWidth, initialHeight, title); 9453 9454 _clientArea = new ClientAreaWidget(); 9455 _clientArea.x = 0; 9456 _clientArea.y = 0; 9457 _clientArea.width = this.width; 9458 _clientArea.height = this.height; 9459 _clientArea.tabStop = false; 9460 9461 super.addChild(_clientArea); 9462 9463 statusBar = new StatusBar(this); 9464 } 9465 9466 /++ 9467 Adds a menu and toolbar from annotated functions. 9468 9469 --- 9470 struct Commands { 9471 @menu("File") { 9472 void New() {} 9473 void Open() {} 9474 void Save() {} 9475 @separator 9476 void Exit() @accelerator("Alt+F4") @hotkey('x') { 9477 window.close(); 9478 } 9479 } 9480 9481 @menu("Edit") { 9482 void Undo() { 9483 undo(); 9484 } 9485 @separator 9486 void Cut() {} 9487 void Copy() {} 9488 void Paste() {} 9489 } 9490 9491 @menu("Help") { 9492 void About() {} 9493 } 9494 } 9495 9496 Commands commands; 9497 9498 window.setMenuAndToolbarFromAnnotatedCode(commands); 9499 --- 9500 9501 Note that you can call this function multiple times and it will add the items in order to the given items. 9502 9503 +/ 9504 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 9505 setMenuAndToolbarFromAnnotatedCode_internal(t); 9506 } 9507 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 9508 setMenuAndToolbarFromAnnotatedCode_internal(t); 9509 } 9510 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 9511 Action[] toolbarActions; 9512 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 9513 Menu[string] mcs; 9514 9515 foreach(menu; menuBar.subMenus) { 9516 mcs[menu.label] = menu; 9517 } 9518 9519 foreach(memberName; __traits(derivedMembers, T)) { 9520 static if(memberName != "this") 9521 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 9522 .menu menu; 9523 .toolbar toolbar; 9524 bool separator; 9525 .accelerator accelerator; 9526 .hotkey hotkey; 9527 .icon icon; 9528 string label; 9529 string tip; 9530 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 9531 static if(is(typeof(attr) == .menu)) 9532 menu = attr; 9533 else static if(is(typeof(attr) == .toolbar)) 9534 toolbar = attr; 9535 else static if(is(attr == .separator)) 9536 separator = true; 9537 else static if(is(typeof(attr) == .accelerator)) 9538 accelerator = attr; 9539 else static if(is(typeof(attr) == .hotkey)) 9540 hotkey = attr; 9541 else static if(is(typeof(attr) == .icon)) 9542 icon = attr; 9543 else static if(is(typeof(attr) == .label)) 9544 label = attr.label; 9545 else static if(is(typeof(attr) == .tip)) 9546 tip = attr.tip; 9547 } 9548 9549 if(menu !is .menu.init || toolbar !is .toolbar.init) { 9550 ushort correctIcon = icon.id; // FIXME 9551 if(label.length == 0) 9552 label = memberName.toMenuLabel; 9553 9554 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName)); 9555 9556 auto action = new Action(label, correctIcon, handler); 9557 9558 if(accelerator.keyString.length) { 9559 auto ke = KeyEvent.parse(accelerator.keyString); 9560 action.accelerator = ke; 9561 accelerators[ke.toStr] = handler; 9562 } 9563 9564 if(toolbar !is .toolbar.init) 9565 toolbarActions ~= action; 9566 if(menu !is .menu.init) { 9567 Menu mc; 9568 if(menu.name in mcs) { 9569 mc = mcs[menu.name]; 9570 } else { 9571 mc = new Menu(menu.name, this); 9572 menuBar.addItem(mc); 9573 mcs[menu.name] = mc; 9574 } 9575 9576 if(separator) 9577 mc.addSeparator(); 9578 mc.addItem(new MenuItem(action)); 9579 } 9580 } 9581 } 9582 } 9583 9584 this.menuBar = menuBar; 9585 9586 if(toolbarActions.length) { 9587 auto tb = new ToolBar(toolbarActions, this); 9588 } 9589 } 9590 9591 void delegate()[string] accelerators; 9592 9593 override void defaultEventHandler_keydown(KeyDownEvent event) { 9594 auto str = event.originalKeyEvent.toStr; 9595 if(auto acl = str in accelerators) 9596 (*acl)(); 9597 super.defaultEventHandler_keydown(event); 9598 } 9599 9600 override void defaultEventHandler_mouseover(MouseOverEvent event) { 9601 super.defaultEventHandler_mouseover(event); 9602 if(this.statusBar !is null && event.target.statusTip.length) 9603 this.statusBar.parts[0].content = event.target.statusTip; 9604 else if(this.statusBar !is null && this.statusTip.length) 9605 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 9606 } 9607 9608 override void addChild(Widget c, int position = int.max) { 9609 if(auto tb = cast(ToolBar) c) 9610 version(win32_widgets) 9611 super.addChild(c, 0); 9612 else version(custom_widgets) 9613 super.addChild(c, menuBar ? 1 : 0); 9614 else static assert(0); 9615 else 9616 clientArea.addChild(c, position); 9617 } 9618 9619 ToolBar _toolBar; 9620 /// 9621 ToolBar toolBar() { return _toolBar; } 9622 /// 9623 ToolBar toolBar(ToolBar t) { 9624 _toolBar = t; 9625 foreach(child; this.children) 9626 if(child is t) 9627 return t; 9628 version(win32_widgets) 9629 super.addChild(t, 0); 9630 else version(custom_widgets) 9631 super.addChild(t, menuBar ? 1 : 0); 9632 else static assert(0); 9633 return t; 9634 } 9635 9636 MenuBar _menu; 9637 /// 9638 MenuBar menuBar() { return _menu; } 9639 /// 9640 MenuBar menuBar(MenuBar m) { 9641 if(m is _menu) { 9642 version(custom_widgets) 9643 recomputeChildLayout(); 9644 return m; 9645 } 9646 9647 if(_menu !is null) { 9648 // make sure it is sanely removed 9649 // FIXME 9650 } 9651 9652 _menu = m; 9653 9654 version(win32_widgets) { 9655 SetMenu(parentWindow.win.impl.hwnd, m.handle); 9656 } else version(custom_widgets) { 9657 super.addChild(m, 0); 9658 9659 // clientArea.y = menu.height; 9660 // clientArea.height = this.height - menu.height; 9661 9662 recomputeChildLayout(); 9663 } else static assert(false); 9664 9665 return _menu; 9666 } 9667 private Widget _clientArea; 9668 /// 9669 @property Widget clientArea() { return _clientArea; } 9670 protected @property void clientArea(Widget wid) { 9671 _clientArea = wid; 9672 } 9673 9674 private StatusBar _statusBar; 9675 /++ 9676 Returns the window's [StatusBar]. Be warned it may be `null`. 9677 +/ 9678 @property StatusBar statusBar() { return _statusBar; } 9679 /// ditto 9680 @property void statusBar(StatusBar bar) { 9681 if(_statusBar !is null) 9682 _statusBar.removeWidget(); 9683 _statusBar = bar; 9684 if(bar !is null) 9685 super.addChild(_statusBar); 9686 } 9687 } 9688 9689 /+ 9690 This is really an implementation detail of [MainWindow] 9691 +/ 9692 private class ClientAreaWidget : Widget { 9693 this() { 9694 this.tabStop = false; 9695 super(null); 9696 //sa = new ScrollableWidget(this); 9697 } 9698 /* 9699 ScrollableWidget sa; 9700 override void addChild(Widget w, int position) { 9701 if(sa is null) 9702 super.addChild(w, position); 9703 else { 9704 sa.addChild(w, position); 9705 sa.setContentSize(this.minWidth + 1, this.minHeight); 9706 import std.stdio; writeln(sa.contentWidth, "x", sa.contentHeight); 9707 } 9708 } 9709 */ 9710 } 9711 9712 /** 9713 Toolbars are lists of buttons (typically icons) that appear under the menu. 9714 Each button ought to correspond to a menu item, represented by [Action] objects. 9715 */ 9716 class ToolBar : Widget { 9717 version(win32_widgets) { 9718 private const int idealHeight; 9719 override int minHeight() { return idealHeight; } 9720 override int maxHeight() { return idealHeight; } 9721 } else version(custom_widgets) { 9722 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 9723 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 9724 } else static assert(false); 9725 override int heightStretchiness() { return 0; } 9726 9727 version(win32_widgets) 9728 HIMAGELIST imageList; 9729 9730 this(Widget parent) { 9731 this(null, parent); 9732 } 9733 9734 /// 9735 this(Action[] actions, Widget parent) { 9736 super(parent); 9737 9738 tabStop = false; 9739 9740 version(win32_widgets) { 9741 // so i like how the flat thing looks on windows, but not on wine 9742 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 9743 // leave it commented 9744 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 9745 9746 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 9747 9748 imageList = ImageList_Create( 9749 // width, height 9750 16, 16, 9751 ILC_COLOR16 | ILC_MASK, 9752 16 /*numberOfButtons*/, 0); 9753 9754 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageList); 9755 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 9756 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 9757 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 9758 9759 TBBUTTON[] buttons; 9760 9761 // FIXME: I_IMAGENONE is if here is no icon 9762 foreach(action; actions) 9763 buttons ~= TBBUTTON( 9764 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 9765 action.id, 9766 TBSTATE_ENABLED, // state 9767 0, // style 9768 0, // reserved array, just zero it out 9769 0, // dwData 9770 cast(size_t) toWstringzInternal(action.label) // INT_PTR 9771 ); 9772 9773 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 9774 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 9775 9776 SIZE size; 9777 import core.sys.windows.commctrl; 9778 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 9779 idealHeight = size.cy + 4; // the plus 4 is a hack 9780 9781 /* 9782 RECT rect; 9783 GetWindowRect(hwnd, &rect); 9784 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 9785 */ 9786 9787 assert(idealHeight); 9788 } else version(custom_widgets) { 9789 foreach(action; actions) 9790 new ToolButton(action, this); 9791 } else static assert(false); 9792 } 9793 9794 override void recomputeChildLayout() { 9795 .recomputeChildLayout!"width"(this); 9796 } 9797 } 9798 9799 enum toolbarIconSize = 24; 9800 9801 /// 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. 9802 class ToolButton : Button { 9803 /// 9804 this(string label, Widget parent) { 9805 super(label, parent); 9806 tabStop = false; 9807 } 9808 /// 9809 this(Action action, Widget parent) { 9810 super(action.label, parent); 9811 tabStop = false; 9812 this.action = action; 9813 } 9814 9815 version(custom_widgets) 9816 override void defaultEventHandler_click(ClickEvent event) { 9817 foreach(handler; action.triggered) 9818 handler(); 9819 } 9820 9821 Action action; 9822 9823 override int maxWidth() { return toolbarIconSize; } 9824 override int minWidth() { return toolbarIconSize; } 9825 override int maxHeight() { return toolbarIconSize; } 9826 override int minHeight() { return toolbarIconSize; } 9827 9828 version(custom_widgets) 9829 override void paint(WidgetPainter painter) { 9830 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 9831 painter.outlineColor = Color.black; 9832 9833 // I want to get from 16 to 24. that's * 3 / 2 9834 static assert(toolbarIconSize >= 16); 9835 enum multiplier = toolbarIconSize / 8; 9836 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 9837 switch(action.iconId) { 9838 case GenericIcons.New: 9839 painter.fillColor = Color.white; 9840 painter.drawPolygon( 9841 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 9842 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 9843 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 9844 ); 9845 break; 9846 case GenericIcons.Save: 9847 painter.fillColor = Color.white; 9848 painter.outlineColor = Color.black; 9849 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 9850 9851 // the label 9852 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 9853 9854 // the slider 9855 painter.fillColor = Color.black; 9856 painter.outlineColor = Color.black; 9857 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 9858 9859 painter.fillColor = Color.white; 9860 painter.outlineColor = Color.white; 9861 // the disc window 9862 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 9863 break; 9864 case GenericIcons.Open: 9865 painter.fillColor = Color.white; 9866 painter.drawPolygon( 9867 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 9868 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 9869 painter.drawPolygon( 9870 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 9871 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 9872 Point(2, 6) * multiplier / divisor); 9873 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 9874 break; 9875 case GenericIcons.Copy: 9876 painter.fillColor = Color.white; 9877 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 9878 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 9879 break; 9880 case GenericIcons.Cut: 9881 painter.fillColor = Color.transparent; 9882 painter.outlineColor = getComputedStyle.foregroundColor(); 9883 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 9884 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 9885 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 9886 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 9887 break; 9888 case GenericIcons.Paste: 9889 painter.fillColor = Color.white; 9890 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 9891 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 9892 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 9893 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 9894 painter.fillColor = Color.black; 9895 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 9896 break; 9897 case GenericIcons.Help: 9898 painter.outlineColor = getComputedStyle.foregroundColor(); 9899 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 9900 break; 9901 case GenericIcons.Undo: 9902 painter.fillColor = Color.transparent; 9903 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 9904 painter.outlineColor = Color.black; 9905 painter.fillColor = Color.black; 9906 painter.drawPolygon( 9907 Point(4, 4) * multiplier / divisor, 9908 Point(8, 2) * multiplier / divisor, 9909 Point(8, 6) * multiplier / divisor, 9910 Point(4, 4) * multiplier / divisor, 9911 ); 9912 break; 9913 case GenericIcons.Redo: 9914 painter.fillColor = Color.transparent; 9915 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 9916 painter.outlineColor = Color.black; 9917 painter.fillColor = Color.black; 9918 painter.drawPolygon( 9919 Point(10, 4) * multiplier / divisor, 9920 Point(6, 2) * multiplier / divisor, 9921 Point(6, 6) * multiplier / divisor, 9922 Point(10, 4) * multiplier / divisor, 9923 ); 9924 break; 9925 default: 9926 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 9927 } 9928 return bounds; 9929 }); 9930 } 9931 9932 } 9933 9934 9935 /// 9936 class MenuBar : Widget { 9937 MenuItem[] items; 9938 Menu[] subMenus; 9939 9940 version(win32_widgets) { 9941 HMENU handle; 9942 /// 9943 this(Widget parent = null) { 9944 super(parent); 9945 9946 handle = CreateMenu(); 9947 tabStop = false; 9948 } 9949 } else version(custom_widgets) { 9950 /// 9951 this(Widget parent = null) { 9952 tabStop = false; // these are selected some other way 9953 super(parent); 9954 } 9955 9956 mixin Padding!q{2}; 9957 } else static assert(false); 9958 9959 version(custom_widgets) 9960 override void paint(WidgetPainter painter) { 9961 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 9962 } 9963 9964 /// 9965 MenuItem addItem(MenuItem item) { 9966 this.addChild(item); 9967 items ~= item; 9968 version(win32_widgets) { 9969 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 9970 } 9971 return item; 9972 } 9973 9974 9975 /// 9976 Menu addItem(Menu item) { 9977 9978 subMenus ~= item; 9979 9980 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 9981 9982 addChild(mbItem); 9983 items ~= mbItem; 9984 9985 version(win32_widgets) { 9986 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 9987 } else version(custom_widgets) { 9988 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 9989 item.popup(mbItem); 9990 }; 9991 } else static assert(false); 9992 9993 return item; 9994 } 9995 9996 override void recomputeChildLayout() { 9997 .recomputeChildLayout!"width"(this); 9998 } 9999 10000 override int maxHeight() { return defaultLineHeight + 4; } 10001 override int minHeight() { return defaultLineHeight + 4; } 10002 } 10003 10004 10005 /** 10006 Status bars appear at the bottom of a MainWindow. 10007 They are made out of Parts, with a width and content. 10008 10009 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 10010 10011 10012 sb.parts[0].content = "Status bar text!"; 10013 */ 10014 class StatusBar : Widget { 10015 private Part[] partsArray; 10016 /// 10017 struct Parts { 10018 @disable this(); 10019 this(StatusBar owner) { this.owner = owner; } 10020 //@disable this(this); 10021 /// 10022 @property int length() { return cast(int) owner.partsArray.length; } 10023 private StatusBar owner; 10024 private this(StatusBar owner, Part[] parts) { 10025 this.owner.partsArray = parts; 10026 this.owner = owner; 10027 } 10028 /// 10029 Part opIndex(int p) { 10030 if(owner.partsArray.length == 0) 10031 this ~= new StatusBar.Part(300); 10032 return owner.partsArray[p]; 10033 } 10034 10035 /// 10036 Part opOpAssign(string op : "~" )(Part p) { 10037 assert(owner.partsArray.length < 255); 10038 p.owner = this.owner; 10039 p.idx = cast(int) owner.partsArray.length; 10040 owner.partsArray ~= p; 10041 version(win32_widgets) { 10042 int[256] pos; 10043 int cpos = 0; 10044 foreach(idx, part; owner.partsArray) { 10045 if(part.width) 10046 cpos += part.width; 10047 else 10048 cpos += 100; 10049 10050 if(idx + 1 == owner.partsArray.length) 10051 pos[idx] = -1; 10052 else 10053 pos[idx] = cpos; 10054 } 10055 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 10056 } else version(custom_widgets) { 10057 owner.redraw(); 10058 } else static assert(false); 10059 10060 return p; 10061 } 10062 } 10063 10064 private Parts _parts; 10065 /// 10066 final @property Parts parts() { 10067 return _parts; 10068 } 10069 10070 /// 10071 static class Part { 10072 int width; 10073 StatusBar owner; 10074 10075 /// 10076 this(int w = 100) { width = w; } 10077 10078 private int idx; 10079 private string _content; 10080 /// 10081 @property string content() { return _content; } 10082 /// 10083 @property void content(string s) { 10084 version(win32_widgets) { 10085 _content = s; 10086 WCharzBuffer bfr = WCharzBuffer(s); 10087 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 10088 } else version(custom_widgets) { 10089 if(_content != s) { 10090 _content = s; 10091 owner.redraw(); 10092 } 10093 } else static assert(false); 10094 } 10095 } 10096 string simpleModeContent; 10097 bool inSimpleMode; 10098 10099 10100 /// 10101 this(Widget parent) { 10102 super(null); // FIXME 10103 _parts = Parts(this); 10104 tabStop = false; 10105 version(win32_widgets) { 10106 parentWindow = parent.parentWindow; 10107 createWin32Window(this, "msctls_statusbar32"w, "", 0); 10108 10109 RECT rect; 10110 GetWindowRect(hwnd, &rect); 10111 idealHeight = rect.bottom - rect.top; 10112 assert(idealHeight); 10113 } else version(custom_widgets) { 10114 } else static assert(false); 10115 } 10116 10117 version(win32_widgets) 10118 override protected void dpiChanged() { 10119 RECT rect; 10120 GetWindowRect(hwnd, &rect); 10121 idealHeight = rect.bottom - rect.top; 10122 assert(idealHeight); 10123 } 10124 10125 version(custom_widgets) 10126 override void paint(WidgetPainter painter) { 10127 auto cs = getComputedStyle(); 10128 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10129 int cpos = 0; 10130 int remainingLength = this.width; 10131 foreach(idx, part; this.partsArray) { 10132 auto partWidth = part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 10133 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 10134 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 10135 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 10136 10137 painter.outlineColor = cs.foregroundColor(); 10138 painter.fillColor = cs.foregroundColor(); 10139 10140 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 10141 cpos += partWidth; 10142 remainingLength -= partWidth; 10143 } 10144 } 10145 10146 10147 version(win32_widgets) { 10148 private int idealHeight; 10149 override int maxHeight() { return idealHeight; } 10150 override int minHeight() { return idealHeight; } 10151 } else version(custom_widgets) { 10152 override int maxHeight() { return defaultLineHeight + 4; } 10153 override int minHeight() { return defaultLineHeight + 4; } 10154 } else static assert(false); 10155 } 10156 10157 /// Displays an in-progress indicator without known values 10158 version(none) 10159 class IndefiniteProgressBar : Widget { 10160 version(win32_widgets) 10161 this(Widget parent) { 10162 super(parent); 10163 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 10164 tabStop = false; 10165 } 10166 override int minHeight() { return 10; } 10167 } 10168 10169 /// A progress bar with a known endpoint and completion amount 10170 class ProgressBar : Widget { 10171 /++ 10172 History: 10173 Added March 16, 2022 (dub v10.7) 10174 +/ 10175 this(int min, int max, Widget parent) { 10176 this(parent); 10177 setRange(cast(ushort) min, cast(ushort) max); // FIXME 10178 } 10179 this(Widget parent) { 10180 version(win32_widgets) { 10181 super(parent); 10182 createWin32Window(this, "msctls_progress32"w, "", 0); 10183 tabStop = false; 10184 } else version(custom_widgets) { 10185 super(parent); 10186 max = 100; 10187 step = 10; 10188 tabStop = false; 10189 } else static assert(0); 10190 } 10191 10192 version(custom_widgets) 10193 override void paint(WidgetPainter painter) { 10194 auto cs = getComputedStyle(); 10195 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10196 painter.fillColor = cs.progressBarColor; 10197 painter.drawRectangle(Point(0, 0), width * current / max, height); 10198 } 10199 10200 10201 version(custom_widgets) { 10202 int current; 10203 int max; 10204 int step; 10205 } 10206 10207 /// 10208 void advanceOneStep() { 10209 version(win32_widgets) 10210 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 10211 else version(custom_widgets) 10212 addToPosition(step); 10213 else static assert(false); 10214 } 10215 10216 /// 10217 void setStepIncrement(int increment) { 10218 version(win32_widgets) 10219 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 10220 else version(custom_widgets) 10221 step = increment; 10222 else static assert(false); 10223 } 10224 10225 /// 10226 void addToPosition(int amount) { 10227 version(win32_widgets) 10228 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 10229 else version(custom_widgets) 10230 setPosition(current + amount); 10231 else static assert(false); 10232 } 10233 10234 /// 10235 void setPosition(int pos) { 10236 version(win32_widgets) 10237 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 10238 else version(custom_widgets) { 10239 current = pos; 10240 if(current > max) 10241 current = max; 10242 redraw(); 10243 } 10244 else static assert(false); 10245 } 10246 10247 /// 10248 void setRange(ushort min, ushort max) { 10249 version(win32_widgets) 10250 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 10251 else version(custom_widgets) { 10252 this.max = max; 10253 } 10254 else static assert(false); 10255 } 10256 10257 override int minHeight() { return 10; } 10258 } 10259 10260 version(custom_widgets) 10261 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 10262 thisLabel.reserve(label.length); 10263 bool justSawAmpersand; 10264 foreach(ch; label) { 10265 if(justSawAmpersand) { 10266 justSawAmpersand = false; 10267 if(ch == '&') { 10268 goto plain; 10269 } 10270 thisAccelerator = ch; 10271 } else { 10272 if(ch == '&') { 10273 justSawAmpersand = true; 10274 continue; 10275 } 10276 plain: 10277 thisLabel ~= ch; 10278 } 10279 } 10280 } 10281 10282 /++ 10283 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. 10284 10285 10286 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 10287 10288 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10289 10290 History: 10291 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. 10292 +/ 10293 class Fieldset : Widget { 10294 // FIXME: on Windows,it doesn't draw the background on the label 10295 // on X, it doesn't fix the clipping rectangle for it 10296 version(win32_widgets) 10297 override int paddingTop() { return defaultLineHeight; } 10298 else version(custom_widgets) 10299 override int paddingTop() { return defaultLineHeight + 2; } 10300 else static assert(false); 10301 override int paddingBottom() { return 6; } 10302 override int paddingLeft() { return 6; } 10303 override int paddingRight() { return 6; } 10304 10305 override int marginLeft() { return 6; } 10306 override int marginRight() { return 6; } 10307 override int marginTop() { return 2; } 10308 override int marginBottom() { return 2; } 10309 10310 string legend; 10311 10312 version(custom_widgets) private dchar accelerator; 10313 10314 this(string legend, Widget parent) { 10315 version(win32_widgets) { 10316 super(parent); 10317 this.legend = legend; 10318 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 10319 tabStop = false; 10320 } else version(custom_widgets) { 10321 super(parent); 10322 tabStop = false; 10323 10324 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 10325 } else static assert(0); 10326 } 10327 10328 version(custom_widgets) 10329 override void paint(WidgetPainter painter) { 10330 painter.fillColor = Color.transparent; 10331 auto cs = getComputedStyle(); 10332 painter.pen = Pen(cs.foregroundColor, 1); 10333 painter.drawRectangle(Point(0, defaultLineHeight / 2), width, height - Window.lineHeight / 2); 10334 10335 auto tx = painter.textSize(legend); 10336 painter.outlineColor = Color.transparent; 10337 10338 static if(UsingSimpledisplayX11) { 10339 painter.fillColor = getComputedStyle().windowBackgroundColor; 10340 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 10341 } else version(Windows) { 10342 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 10343 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 10344 SelectObject(painter.impl.hdc, b); 10345 } else static assert(0); 10346 painter.outlineColor = cs.foregroundColor; 10347 painter.drawText(Point(8, 0), legend); 10348 } 10349 10350 override int maxHeight() { 10351 auto m = paddingTop() + paddingBottom(); 10352 foreach(child; children) { 10353 auto mh = child.maxHeight(); 10354 if(mh == int.max) 10355 return int.max; 10356 m += mh; 10357 m += child.marginBottom(); 10358 m += child.marginTop(); 10359 } 10360 m += 6; 10361 if(m < minHeight) 10362 return minHeight; 10363 return m; 10364 } 10365 10366 override int minHeight() { 10367 auto m = paddingTop() + paddingBottom(); 10368 foreach(child; children) { 10369 m += child.minHeight(); 10370 m += child.marginBottom(); 10371 m += child.marginTop(); 10372 } 10373 return m + 6; 10374 } 10375 } 10376 10377 /++ 10378 $(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") 10379 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 10380 +/ 10381 version(minigui_screenshots) 10382 @Screenshot("Fieldset") 10383 unittest { 10384 auto window = new Window(200, 100); 10385 auto set = new Fieldset("Baby will", window); 10386 auto option1 = new Radiobox("Eat", set); 10387 auto option2 = new Radiobox("Cry", set); 10388 auto option3 = new Radiobox("Sleep", set); 10389 window.loop(); 10390 } 10391 10392 /// Draws a line 10393 class HorizontalRule : Widget { 10394 mixin Margin!q{ 2 }; 10395 override int minHeight() { return 2; } 10396 override int maxHeight() { return 2; } 10397 10398 /// 10399 this(Widget parent) { 10400 super(parent); 10401 } 10402 10403 override void paint(WidgetPainter painter) { 10404 auto cs = getComputedStyle(); 10405 painter.outlineColor = cs.darkAccentColor; 10406 painter.drawLine(Point(0, 0), Point(width, 0)); 10407 painter.outlineColor = cs.lightAccentColor; 10408 painter.drawLine(Point(0, 1), Point(width, 1)); 10409 } 10410 } 10411 10412 version(minigui_screenshots) 10413 @Screenshot("HorizontalRule") 10414 /++ 10415 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 10416 10417 +/ 10418 unittest { 10419 auto window = new Window(200, 100); 10420 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 10421 new HorizontalRule(window); 10422 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 10423 window.loop(); 10424 } 10425 10426 /// ditto 10427 class VerticalRule : Widget { 10428 mixin Margin!q{ 2 }; 10429 override int minWidth() { return 2; } 10430 override int maxWidth() { return 2; } 10431 10432 /// 10433 this(Widget parent) { 10434 super(parent); 10435 } 10436 10437 override void paint(WidgetPainter painter) { 10438 auto cs = getComputedStyle(); 10439 painter.outlineColor = cs.darkAccentColor; 10440 painter.drawLine(Point(0, 0), Point(0, height)); 10441 painter.outlineColor = cs.lightAccentColor; 10442 painter.drawLine(Point(1, 0), Point(1, height)); 10443 } 10444 } 10445 10446 10447 /// 10448 class Menu : Window { 10449 void remove() { 10450 foreach(i, child; parentWindow.children) 10451 if(child is this) { 10452 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 10453 break; 10454 } 10455 parentWindow.redraw(); 10456 10457 parentWindow.releaseMouseCapture(); 10458 } 10459 10460 /// 10461 void addSeparator() { 10462 version(win32_widgets) 10463 AppendMenu(handle, MF_SEPARATOR, 0, null); 10464 else version(custom_widgets) 10465 auto hr = new HorizontalRule(this); 10466 else static assert(0); 10467 } 10468 10469 override int paddingTop() { return 4; } 10470 override int paddingBottom() { return 4; } 10471 override int paddingLeft() { return 2; } 10472 override int paddingRight() { return 2; } 10473 10474 version(win32_widgets) {} 10475 else version(custom_widgets) { 10476 SimpleWindow dropDown; 10477 Widget menuParent; 10478 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 10479 this.menuParent = parent; 10480 10481 int w = 150; 10482 int h = paddingTop + paddingBottom; 10483 if(this.children.length) { 10484 // hacking it to get the ideal height out of recomputeChildLayout 10485 this.width = w; 10486 this.height = h; 10487 this.recomputeChildLayout(); 10488 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 10489 h += paddingBottom; 10490 10491 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 10492 } 10493 10494 if(offsetY == int.min) 10495 offsetY = parent.defaultLineHeight; 10496 10497 auto coord = parent.globalCoordinates(); 10498 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 10499 this.x = 0; 10500 this.y = 0; 10501 this.width = dropDown.width; 10502 this.height = dropDown.height; 10503 this.drawableWindow = dropDown; 10504 this.recomputeChildLayout(); 10505 10506 static if(UsingSimpledisplayX11) 10507 XSync(XDisplayConnection.get, 0); 10508 10509 dropDown.visibilityChanged = (bool visible) { 10510 if(visible) { 10511 this.redraw(); 10512 dropDown.grabInput(); 10513 } else { 10514 dropDown.releaseInputGrab(); 10515 } 10516 }; 10517 10518 dropDown.show(); 10519 10520 clickListener = this.addEventListener((scope ClickEvent ev) { 10521 unpopup(); 10522 // need to unlock asap just in case other user handlers block... 10523 static if(UsingSimpledisplayX11) 10524 flushGui(); 10525 }, true /* again for asap action */); 10526 } 10527 10528 EventListener clickListener; 10529 } 10530 else static assert(false); 10531 10532 version(custom_widgets) 10533 void unpopup() { 10534 mouseLastOver = mouseLastDownOn = null; 10535 dropDown.hide(); 10536 if(!menuParent.parentWindow.win.closed) { 10537 if(auto maw = cast(MouseActivatedWidget) menuParent) { 10538 maw.setDynamicState(DynamicState.depressed, false); 10539 maw.setDynamicState(DynamicState.hover, false); 10540 maw.redraw(); 10541 } 10542 // menuParent.parentWindow.win.focus(); 10543 } 10544 clickListener.disconnect(); 10545 } 10546 10547 MenuItem[] items; 10548 10549 /// 10550 MenuItem addItem(MenuItem item) { 10551 addChild(item); 10552 items ~= item; 10553 version(win32_widgets) { 10554 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10555 } 10556 return item; 10557 } 10558 10559 string label; 10560 10561 version(win32_widgets) { 10562 HMENU handle; 10563 /// 10564 this(string label, Widget parent) { 10565 // not actually passing the parent since it effs up the drawing 10566 super(cast(Widget) null);// parent); 10567 this.label = label; 10568 handle = CreatePopupMenu(); 10569 } 10570 } else version(custom_widgets) { 10571 /// 10572 this(string label, Widget parent) { 10573 10574 if(dropDown) { 10575 dropDown.close(); 10576 } 10577 dropDown = new SimpleWindow( 10578 150, 4, 10579 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 10580 10581 this.label = label; 10582 10583 super(dropDown); 10584 } 10585 } else static assert(false); 10586 10587 override int maxHeight() { return defaultLineHeight; } 10588 override int minHeight() { return defaultLineHeight; } 10589 10590 version(custom_widgets) 10591 override void paint(WidgetPainter painter) { 10592 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 10593 } 10594 } 10595 10596 /++ 10597 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 10598 +/ 10599 class MenuItem : MouseActivatedWidget { 10600 Menu submenu; 10601 10602 Action action; 10603 string label; 10604 10605 override int paddingLeft() { return 4; } 10606 10607 override int maxHeight() { return defaultLineHeight + 4; } 10608 override int minHeight() { return defaultLineHeight + 4; } 10609 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 10610 override int maxWidth() { 10611 if(cast(MenuBar) parent) { 10612 return minWidth(); 10613 } 10614 return int.max; 10615 } 10616 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 10617 this(string lbl, Widget parent = null) { 10618 super(parent); 10619 //label = lbl; // FIXME 10620 foreach(char ch; lbl) // FIXME 10621 if(ch != '&') // FIXME 10622 label ~= ch; // FIXME 10623 tabStop = false; // these are selected some other way 10624 } 10625 10626 /// 10627 this(Action action, Widget parent = null) { 10628 assert(action !is null); 10629 this(action.label, parent); 10630 this.action = action; 10631 tabStop = false; // these are selected some other way 10632 } 10633 10634 version(custom_widgets) 10635 override void paint(WidgetPainter painter) { 10636 auto cs = getComputedStyle(); 10637 if(dynamicState & DynamicState.depressed) 10638 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10639 if(dynamicState & DynamicState.hover) 10640 painter.outlineColor = cs.activeMenuItemColor; 10641 else 10642 painter.outlineColor = cs.foregroundColor; 10643 painter.fillColor = Color.transparent; 10644 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 10645 if(action && action.accelerator !is KeyEvent.init) { 10646 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 10647 10648 } 10649 } 10650 10651 static class Style : Widget.Style { 10652 override bool variesWithState(ulong dynamicStateFlags) { 10653 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 10654 } 10655 } 10656 mixin OverrideStyle!Style; 10657 10658 override void defaultEventHandler_triggered(Event event) { 10659 if(action) 10660 foreach(handler; action.triggered) 10661 handler(); 10662 10663 if(auto pmenu = cast(Menu) this.parent) 10664 pmenu.remove(); 10665 10666 super.defaultEventHandler_triggered(event); 10667 } 10668 } 10669 10670 version(win32_widgets) 10671 /// A "mouse activiated widget" is really just an abstract variant of button. 10672 class MouseActivatedWidget : Widget { 10673 @property bool isChecked() { 10674 assert(hwnd); 10675 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 10676 10677 } 10678 @property void isChecked(bool state) { 10679 assert(hwnd); 10680 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 10681 10682 } 10683 10684 override void handleWmCommand(ushort cmd, ushort id) { 10685 if(cmd == 0) { 10686 auto event = new Event(EventType.triggered, this); 10687 event.dispatch(); 10688 } 10689 } 10690 10691 this(Widget parent) { 10692 super(parent); 10693 } 10694 } 10695 else version(custom_widgets) 10696 /// ditto 10697 class MouseActivatedWidget : Widget { 10698 @property bool isChecked() { return isChecked_; } 10699 @property bool isChecked(bool b) { return isChecked_ = b; } 10700 10701 private bool isChecked_; 10702 10703 this(Widget parent) { 10704 super(parent); 10705 10706 addEventListener((MouseDownEvent ev) { 10707 if(ev.button == MouseButton.left) { 10708 setDynamicState(DynamicState.depressed, true); 10709 setDynamicState(DynamicState.hover, true); 10710 redraw(); 10711 } 10712 }); 10713 10714 addEventListener((MouseUpEvent ev) { 10715 if(ev.button == MouseButton.left) { 10716 setDynamicState(DynamicState.depressed, false); 10717 setDynamicState(DynamicState.hover, false); 10718 redraw(); 10719 } 10720 }); 10721 10722 addEventListener((MouseMoveEvent mme) { 10723 if(!(mme.state & ModifierState.leftButtonDown)) { 10724 if(dynamicState_ & DynamicState.depressed) { 10725 setDynamicState(DynamicState.depressed, false); 10726 redraw(); 10727 } 10728 } 10729 }); 10730 } 10731 10732 override void defaultEventHandler_focus(Event ev) { 10733 super.defaultEventHandler_focus(ev); 10734 this.redraw(); 10735 } 10736 override void defaultEventHandler_blur(Event ev) { 10737 super.defaultEventHandler_blur(ev); 10738 setDynamicState(DynamicState.depressed, false); 10739 this.redraw(); 10740 } 10741 override void defaultEventHandler_keydown(KeyDownEvent ev) { 10742 super.defaultEventHandler_keydown(ev); 10743 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 10744 setDynamicState(DynamicState.depressed, true); 10745 setDynamicState(DynamicState.hover, true); 10746 this.redraw(); 10747 } 10748 } 10749 override void defaultEventHandler_keyup(KeyUpEvent ev) { 10750 super.defaultEventHandler_keyup(ev); 10751 if(!(dynamicState & DynamicState.depressed)) 10752 return; 10753 setDynamicState(DynamicState.depressed, false); 10754 setDynamicState(DynamicState.hover, false); 10755 this.redraw(); 10756 10757 auto event = new Event(EventType.triggered, this); 10758 event.sendDirectly(); 10759 } 10760 override void defaultEventHandler_click(ClickEvent ev) { 10761 super.defaultEventHandler_click(ev); 10762 if(ev.button == MouseButton.left) { 10763 auto event = new Event(EventType.triggered, this); 10764 event.sendDirectly(); 10765 } 10766 } 10767 10768 } 10769 else static assert(false); 10770 10771 /* 10772 /++ 10773 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 10774 10775 Basically the same as a checkbox. 10776 +/ 10777 class OnOffSwitch : MouseActivatedWidget { 10778 10779 } 10780 */ 10781 10782 /++ 10783 History: 10784 Added June 15, 2021 (dub v10.1) 10785 +/ 10786 struct ImageLabel { 10787 /++ 10788 Defines a label+image combo used by some widgets. 10789 10790 If you provide just a text label, that is all the widget will try to 10791 display. Or just an image will display just that. If you provide both, 10792 it may display both text and image side by side or display the image 10793 and offer text on an input event depending on the widget. 10794 10795 History: 10796 The `alignment` parameter was added on September 27, 2021 10797 +/ 10798 this(string label, TextAlignment alignment = TextAlignment.Center) { 10799 this.label = label; 10800 this.displayFlags = DisplayFlags.displayText; 10801 this.alignment = alignment; 10802 } 10803 10804 /// ditto 10805 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 10806 this.label = label; 10807 this.image = image; 10808 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 10809 this.alignment = alignment; 10810 } 10811 10812 /// ditto 10813 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 10814 this.image = image; 10815 this.displayFlags = DisplayFlags.displayImage; 10816 this.alignment = alignment; 10817 } 10818 10819 /// ditto 10820 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 10821 this.label = label; 10822 this.image = image; 10823 this.alignment = alignment; 10824 this.displayFlags = displayFlags; 10825 } 10826 10827 string label; 10828 MemoryImage image; 10829 10830 enum DisplayFlags { 10831 displayText = 1 << 0, 10832 displayImage = 1 << 1, 10833 } 10834 10835 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 10836 10837 TextAlignment alignment; 10838 } 10839 10840 /++ 10841 A basic checked or not checked box with an attached label. 10842 10843 10844 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 10845 10846 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10847 10848 History: 10849 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. 10850 +/ 10851 class Checkbox : MouseActivatedWidget { 10852 version(win32_widgets) { 10853 override int maxHeight() { return scaleWithDpi(16); } 10854 override int minHeight() { return scaleWithDpi(16); } 10855 } else version(custom_widgets) { 10856 override int maxHeight() { return defaultLineHeight; } 10857 override int minHeight() { return defaultLineHeight; } 10858 } else static assert(0); 10859 10860 override int marginLeft() { return 4; } 10861 10862 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 10863 10864 /++ 10865 Just an alias because I keep typing checked out of web habit. 10866 10867 History: 10868 Added May 31, 2021 10869 +/ 10870 alias checked = isChecked; 10871 10872 private string label; 10873 private dchar accelerator; 10874 10875 /++ 10876 +/ 10877 this(string label, Widget parent) { 10878 this(ImageLabel(label), Appearance.checkbox, parent); 10879 } 10880 10881 /// ditto 10882 this(string label, Appearance appearance, Widget parent) { 10883 this(ImageLabel(label), appearance, parent); 10884 } 10885 10886 /++ 10887 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 10888 10889 History: 10890 Added June 29, 2021 (dub v10.2) 10891 +/ 10892 enum Appearance { 10893 checkbox, /// a normal checkbox 10894 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 10895 //sliderswitch, 10896 } 10897 private Appearance appearance; 10898 10899 /// ditto 10900 private this(ImageLabel label, Appearance appearance, Widget parent) { 10901 super(parent); 10902 version(win32_widgets) { 10903 this.label = label.label; 10904 10905 uint extraStyle; 10906 final switch(appearance) { 10907 case Appearance.checkbox: 10908 break; 10909 case Appearance.pushbutton: 10910 extraStyle |= BS_PUSHLIKE; 10911 break; 10912 } 10913 10914 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 10915 } else version(custom_widgets) { 10916 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 10917 } else static assert(0); 10918 } 10919 10920 version(custom_widgets) 10921 override void paint(WidgetPainter painter) { 10922 auto cs = getComputedStyle(); 10923 if(isFocused()) { 10924 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 10925 painter.fillColor = cs.windowBackgroundColor; 10926 painter.drawRectangle(Point(0, 0), width, height); 10927 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 10928 } else { 10929 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 10930 painter.fillColor = cs.windowBackgroundColor; 10931 painter.drawRectangle(Point(0, 0), width, height); 10932 } 10933 10934 10935 enum buttonSize = 16; 10936 10937 painter.outlineColor = Color.black; 10938 painter.fillColor = Color.white; 10939 painter.drawRectangle(scaleWithDpi(Point(2, 2)), scaleWithDpi(buttonSize - 2), scaleWithDpi(buttonSize - 2)); 10940 10941 if(isChecked) { 10942 painter.pen = Pen(Color.black, 2); 10943 // I'm using height so the checkbox is square 10944 enum padding = 5; 10945 painter.drawLine(scaleWithDpi(Point(padding, padding)), scaleWithDpi(Point(buttonSize - (padding-2), buttonSize - (padding-2)))); 10946 painter.drawLine(scaleWithDpi(Point(buttonSize-(padding-2), padding)), scaleWithDpi(Point(padding, buttonSize - (padding-2)))); 10947 10948 painter.pen = Pen(Color.black, 1); 10949 } 10950 10951 if(label !is null) { 10952 painter.outlineColor = cs.foregroundColor(); 10953 painter.fillColor = cs.foregroundColor(); 10954 10955 // FIXME: should prolly just align the baseline or something 10956 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 2)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 10957 } 10958 } 10959 10960 override void defaultEventHandler_triggered(Event ev) { 10961 isChecked = !isChecked; 10962 10963 this.emit!(ChangeEvent!bool)(&isChecked); 10964 10965 redraw(); 10966 } 10967 10968 /// Emits a change event with the checked state 10969 mixin Emits!(ChangeEvent!bool); 10970 } 10971 10972 /// Adds empty space to a layout. 10973 class VerticalSpacer : Widget { 10974 /// 10975 this(Widget parent) { 10976 super(parent); 10977 } 10978 } 10979 10980 /// ditto 10981 class HorizontalSpacer : Widget { 10982 /// 10983 this(Widget parent) { 10984 super(parent); 10985 this.tabStop = false; 10986 } 10987 } 10988 10989 10990 /++ 10991 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 10992 10993 10994 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 10995 10996 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10997 10998 History: 10999 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. 11000 +/ 11001 class Radiobox : MouseActivatedWidget { 11002 11003 version(win32_widgets) { 11004 override int maxHeight() { return scaleWithDpi(16); } 11005 override int minHeight() { return scaleWithDpi(16); } 11006 } else version(custom_widgets) { 11007 override int maxHeight() { return defaultLineHeight; } 11008 override int minHeight() { return defaultLineHeight; } 11009 } else static assert(0); 11010 11011 override int marginLeft() { return 4; } 11012 11013 private string label; 11014 private dchar accelerator; 11015 11016 version(win32_widgets) 11017 this(string label, Widget parent) { 11018 super(parent); 11019 this.label = label; 11020 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 11021 } 11022 else version(custom_widgets) 11023 this(string label, Widget parent) { 11024 super(parent); 11025 label.extractWindowsStyleLabel(this.label, this.accelerator); 11026 height = 16; 11027 width = height + 4 + cast(int) label.length * 16; 11028 } 11029 else static assert(false); 11030 11031 version(custom_widgets) 11032 override void paint(WidgetPainter painter) { 11033 auto cs = getComputedStyle(); 11034 if(isFocused) { 11035 painter.fillColor = cs.windowBackgroundColor; 11036 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11037 } else { 11038 painter.fillColor = cs.windowBackgroundColor; 11039 painter.outlineColor = cs.windowBackgroundColor; 11040 } 11041 painter.drawRectangle(Point(0, 0), width, height); 11042 11043 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11044 11045 enum buttonSize = 16; 11046 11047 painter.outlineColor = Color.black; 11048 painter.fillColor = Color.white; 11049 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 11050 if(isChecked) { 11051 painter.outlineColor = Color.black; 11052 painter.fillColor = Color.black; 11053 // I'm using height so the checkbox is square 11054 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5))); 11055 } 11056 11057 painter.outlineColor = cs.foregroundColor(); 11058 painter.fillColor = cs.foregroundColor(); 11059 11060 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11061 } 11062 11063 11064 override void defaultEventHandler_triggered(Event ev) { 11065 isChecked = true; 11066 11067 if(this.parent) { 11068 foreach(child; this.parent.children) { 11069 if(child is this) continue; 11070 if(auto rb = cast(Radiobox) child) { 11071 rb.isChecked = false; 11072 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 11073 rb.redraw(); 11074 } 11075 } 11076 } 11077 11078 this.emit!(ChangeEvent!bool)(&this.isChecked); 11079 11080 redraw(); 11081 } 11082 11083 /// 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. 11084 mixin Emits!(ChangeEvent!bool); 11085 } 11086 11087 11088 /++ 11089 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 11090 11091 11092 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 11093 11094 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11095 11096 History: 11097 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. 11098 +/ 11099 class Button : MouseActivatedWidget { 11100 override int heightStretchiness() { return 3; } 11101 override int widthStretchiness() { return 3; } 11102 11103 /++ 11104 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. 11105 11106 History: 11107 Added July 2, 2021 11108 +/ 11109 public bool triggersOnMultiClick; 11110 11111 private string label_; 11112 private TextAlignment alignment; 11113 private dchar accelerator; 11114 11115 /// 11116 string label() { return label_; } 11117 /// 11118 void label(string l) { 11119 label_ = l; 11120 version(win32_widgets) { 11121 WCharzBuffer bfr = WCharzBuffer(l); 11122 SetWindowTextW(hwnd, bfr.ptr); 11123 } else version(custom_widgets) { 11124 redraw(); 11125 } 11126 } 11127 11128 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 11129 super.defaultEventHandler_dblclick(ev); 11130 if(triggersOnMultiClick) { 11131 if(ev.button == MouseButton.left) { 11132 auto event = new Event(EventType.triggered, this); 11133 event.sendDirectly(); 11134 } 11135 } 11136 } 11137 11138 private Sprite sprite; 11139 private int displayFlags; 11140 11141 /++ 11142 Creates a push button with the given label, which may be an image or some text. 11143 11144 Bugs: 11145 If the image is bigger than the button, it may not be displayed in the right position on Linux. 11146 11147 History: 11148 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 11149 11150 The button with label and image will respect requests to show both on Windows as 11151 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 11152 +/ 11153 this(ImageLabel label, Widget parent) { 11154 version(win32_widgets) { 11155 // FIXME: use ideal button size instead 11156 width = 50; 11157 height = 30; 11158 super(parent); 11159 11160 // BS_BITMAP is set when we want image only, so checking for exactly that combination 11161 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 11162 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 11163 11164 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 11165 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 11166 11167 if(label.image) { 11168 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 11169 11170 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 11171 } 11172 11173 this.label = label.label; 11174 } else version(custom_widgets) { 11175 width = 50; 11176 height = 30; 11177 super(parent); 11178 11179 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 11180 11181 if(label.image) { 11182 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 11183 this.displayFlags = label.displayFlags; 11184 } 11185 11186 this.alignment = label.alignment; 11187 } 11188 } 11189 11190 /// 11191 this(string label, Widget parent) { 11192 this(ImageLabel(label), parent); 11193 } 11194 11195 override int minHeight() { return defaultLineHeight + 4; } 11196 11197 static class Style : Widget.Style { 11198 override WidgetBackground background() { 11199 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 11200 11201 auto pressed = DynamicState.depressed | DynamicState.hover; 11202 if((widget.dynamicState & pressed) == pressed) { 11203 return WidgetBackground(cs.depressedButtonColor()); 11204 } else if(widget.dynamicState & DynamicState.hover) { 11205 return WidgetBackground(cs.hoveringColor()); 11206 } else { 11207 return WidgetBackground(cs.buttonColor()); 11208 } 11209 } 11210 11211 override FrameStyle borderStyle() { 11212 auto pressed = DynamicState.depressed | DynamicState.hover; 11213 if((widget.dynamicState & pressed) == pressed) { 11214 return FrameStyle.sunk; 11215 } else { 11216 return FrameStyle.risen; 11217 } 11218 11219 } 11220 11221 override bool variesWithState(ulong dynamicStateFlags) { 11222 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11223 } 11224 } 11225 mixin OverrideStyle!Style; 11226 11227 version(custom_widgets) 11228 override void paint(WidgetPainter painter) { 11229 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 11230 if(sprite) { 11231 sprite.drawAt( 11232 painter, 11233 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 11234 Point(0, 0), 11235 bounds.size 11236 ); 11237 } else { 11238 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 11239 } 11240 return bounds; 11241 }); 11242 } 11243 11244 override int flexBasisWidth() { 11245 version(win32_widgets) { 11246 SIZE size; 11247 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11248 if(size.cx == 0) 11249 goto fallback; 11250 return size.cx + scaleWithDpi(16); 11251 } 11252 fallback: 11253 return scaleWithDpi(cast(int) label.length * 8 + 16); 11254 } 11255 11256 override int flexBasisHeight() { 11257 version(win32_widgets) { 11258 SIZE size; 11259 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11260 if(size.cy == 0) 11261 goto fallback; 11262 return size.cy + scaleWithDpi(6); 11263 } 11264 fallback: 11265 return defaultLineHeight + 4; 11266 } 11267 } 11268 11269 /++ 11270 A button with a consistent size, suitable for user commands like OK and cANCEL. 11271 +/ 11272 class CommandButton : Button { 11273 this(string label, Widget parent) { 11274 super(label, parent); 11275 } 11276 11277 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 11278 11279 override int maxHeight() { 11280 return defaultLineHeight + 4; 11281 } 11282 11283 override int maxWidth() { 11284 return defaultLineHeight * 4; 11285 } 11286 11287 override int marginLeft() { return 12; } 11288 override int marginRight() { return 12; } 11289 override int marginTop() { return 12; } 11290 override int marginBottom() { return 12; } 11291 } 11292 11293 /// 11294 enum ArrowDirection { 11295 left, /// 11296 right, /// 11297 up, /// 11298 down /// 11299 } 11300 11301 /// 11302 version(custom_widgets) 11303 class ArrowButton : Button { 11304 /// 11305 this(ArrowDirection direction, Widget parent) { 11306 super("", parent); 11307 this.direction = direction; 11308 triggersOnMultiClick = true; 11309 } 11310 11311 private ArrowDirection direction; 11312 11313 override int minHeight() { return scaleWithDpi(16); } 11314 override int maxHeight() { return scaleWithDpi(16); } 11315 override int minWidth() { return scaleWithDpi(16); } 11316 override int maxWidth() { return scaleWithDpi(16); } 11317 11318 override void paint(WidgetPainter painter) { 11319 super.paint(painter); 11320 11321 auto cs = getComputedStyle(); 11322 11323 painter.outlineColor = cs.foregroundColor; 11324 painter.fillColor = cs.foregroundColor; 11325 11326 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 11327 11328 final switch(direction) { 11329 case ArrowDirection.up: 11330 painter.drawPolygon( 11331 scaleWithDpi(Point(2, 10) + offset), 11332 scaleWithDpi(Point(7, 5) + offset), 11333 scaleWithDpi(Point(12, 10) + offset), 11334 scaleWithDpi(Point(2, 10) + offset) 11335 ); 11336 break; 11337 case ArrowDirection.down: 11338 painter.drawPolygon( 11339 scaleWithDpi(Point(2, 6) + offset), 11340 scaleWithDpi(Point(7, 11) + offset), 11341 scaleWithDpi(Point(12, 6) + offset), 11342 scaleWithDpi(Point(2, 6) + offset) 11343 ); 11344 break; 11345 case ArrowDirection.left: 11346 painter.drawPolygon( 11347 scaleWithDpi(Point(10, 2) + offset), 11348 scaleWithDpi(Point(5, 7) + offset), 11349 scaleWithDpi(Point(10, 12) + offset), 11350 scaleWithDpi(Point(10, 2) + offset) 11351 ); 11352 break; 11353 case ArrowDirection.right: 11354 painter.drawPolygon( 11355 scaleWithDpi(Point(6, 2) + offset), 11356 scaleWithDpi(Point(11, 7) + offset), 11357 scaleWithDpi(Point(6, 12) + offset), 11358 scaleWithDpi(Point(6, 2) + offset) 11359 ); 11360 break; 11361 } 11362 } 11363 } 11364 11365 private 11366 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 11367 int x, y; 11368 Widget par = c; 11369 while(par) { 11370 x += par.x; 11371 y += par.y; 11372 par = par.parent; 11373 } 11374 return [x, y]; 11375 } 11376 11377 version(win32_widgets) 11378 private 11379 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 11380 // MapWindowPoints? 11381 int x, y; 11382 Widget par = c; 11383 while(par) { 11384 x += par.x; 11385 y += par.y; 11386 par = par.parent; 11387 if(par !is null && par.useNativeDrawing()) 11388 break; 11389 } 11390 return [x, y]; 11391 } 11392 11393 /// 11394 class ImageBox : Widget { 11395 private MemoryImage image_; 11396 11397 override int widthStretchiness() { return 1; } 11398 override int heightStretchiness() { return 1; } 11399 override int widthShrinkiness() { return 1; } 11400 override int heightShrinkiness() { return 1; } 11401 11402 override int flexBasisHeight() { 11403 return image_.height; 11404 } 11405 11406 override int flexBasisWidth() { 11407 return image_.width; 11408 } 11409 11410 /// 11411 public void setImage(MemoryImage image){ 11412 this.image_ = image; 11413 if(this.parentWindow && this.parentWindow.win) { 11414 if(sprite) 11415 sprite.dispose(); 11416 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11417 } 11418 redraw(); 11419 } 11420 11421 /// How to fit the image in the box if they aren't an exact match in size? 11422 enum HowToFit { 11423 center, /// centers the image, cropping around all the edges as needed 11424 crop, /// always draws the image in the upper left, cropping the lower right if needed 11425 // stretch, /// not implemented 11426 } 11427 11428 private Sprite sprite; 11429 private HowToFit howToFit_; 11430 11431 private Color backgroundColor_; 11432 11433 /// 11434 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 11435 this.image_ = image; 11436 this.tabStop = false; 11437 this.howToFit_ = howToFit; 11438 this.backgroundColor_ = backgroundColor; 11439 super(parent); 11440 updateSprite(); 11441 } 11442 11443 /// ditto 11444 this(MemoryImage image, HowToFit howToFit, Widget parent) { 11445 this(image, howToFit, Color.transparent, parent); 11446 } 11447 11448 private void updateSprite() { 11449 if(sprite is null && this.parentWindow && this.parentWindow.win) { 11450 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11451 } 11452 } 11453 11454 override void paint(WidgetPainter painter) { 11455 updateSprite(); 11456 if(backgroundColor_.a) { 11457 painter.fillColor = backgroundColor_; 11458 painter.drawRectangle(Point(0, 0), width, height); 11459 } 11460 if(howToFit_ == HowToFit.crop) 11461 sprite.drawAt(painter, Point(0, 0)); 11462 else if(howToFit_ == HowToFit.center) { 11463 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 11464 } 11465 } 11466 } 11467 11468 /// 11469 class TextLabel : Widget { 11470 override int maxHeight() { return defaultLineHeight; } 11471 override int minHeight() { return defaultLineHeight; } 11472 override int minWidth() { return 32; } 11473 11474 override int flexBasisHeight() { return minHeight(); } 11475 override int flexBasisWidth() { return defaultTextWidth(label); } 11476 11477 string label_; 11478 11479 /++ 11480 Indicates which other control this label is here for. Similar to HTML `for` attribute. 11481 11482 In practice this means a click on the label will focus the `labelFor`. In future versions 11483 it will also set screen reader hints but that is not yet implemented. 11484 11485 History: 11486 Added October 3, 2021 (dub v10.4) 11487 +/ 11488 Widget labelFor; 11489 11490 /// 11491 @scriptable 11492 string label() { return label_; } 11493 11494 /// 11495 @scriptable 11496 void label(string l) { 11497 label_ = l; 11498 version(win32_widgets) { 11499 WCharzBuffer bfr = WCharzBuffer(l); 11500 SetWindowTextW(hwnd, bfr.ptr); 11501 } else version(custom_widgets) 11502 redraw(); 11503 } 11504 11505 /// 11506 this(string label, TextAlignment alignment, Widget parent) { 11507 this.label_ = label; 11508 this.alignment = alignment; 11509 this.tabStop = false; 11510 super(parent); 11511 11512 version(win32_widgets) 11513 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 11514 } 11515 11516 override void defaultEventHandler_click(scope ClickEvent ce) { 11517 if(this.labelFor !is null) 11518 this.labelFor.focus(); 11519 } 11520 11521 /++ 11522 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 11523 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 11524 +/ 11525 this(string label, Widget parent) { 11526 this(label, TextAlignment.Right, parent); 11527 } 11528 11529 11530 TextAlignment alignment; 11531 11532 version(custom_widgets) 11533 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 11534 painter.outlineColor = getComputedStyle().foregroundColor; 11535 painter.drawText(Point(0, 0), this.label, Point(width, height), alignment); 11536 return bounds; 11537 } 11538 11539 } 11540 11541 version(custom_widgets) 11542 private struct etc { 11543 mixin ExperimentalTextComponent; 11544 } 11545 11546 version(win32_widgets) 11547 alias EditableTextWidgetParent = Widget; /// 11548 else version(custom_widgets) 11549 alias EditableTextWidgetParent = ScrollableWidget; /// 11550 else static assert(0); 11551 11552 /+ 11553 This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too 11554 +/ 11555 11556 /// Contains the implementation of text editing 11557 abstract class EditableTextWidget : EditableTextWidgetParent { 11558 this(Widget parent) { 11559 super(parent); 11560 } 11561 11562 bool wordWrapEnabled_ = false; 11563 void wordWrapEnabled(bool enabled) { 11564 version(win32_widgets) { 11565 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 11566 } else version(custom_widgets) { 11567 wordWrapEnabled_ = enabled; // FIXME 11568 } else static assert(false); 11569 } 11570 11571 override int minWidth() { return scaleWithDpi(16); } 11572 override int minHeight() { return defaultLineHeight + 0; } // the +0 is to leave room for the padding 11573 override int widthStretchiness() { return 7; } 11574 11575 void selectAll() { 11576 version(win32_widgets) 11577 SendMessage(hwnd, EM_SETSEL, 0, -1); 11578 else version(custom_widgets) { 11579 textLayout.selectAll(); 11580 redraw(); 11581 } 11582 } 11583 11584 @property string content() { 11585 version(win32_widgets) { 11586 wchar[4096] bufferstack; 11587 wchar[] buffer; 11588 auto len = GetWindowTextLength(hwnd); 11589 if(len < bufferstack.length) 11590 buffer = bufferstack[0 .. len + 1]; 11591 else 11592 buffer = new wchar[](len + 1); 11593 11594 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 11595 if(l >= 0) 11596 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 11597 else 11598 return null; 11599 } else version(custom_widgets) { 11600 return textLayout.getPlainText(); 11601 } else static assert(false); 11602 } 11603 @property void content(string s) { 11604 version(win32_widgets) { 11605 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 11606 SetWindowTextW(hwnd, bfr.ptr); 11607 } else version(custom_widgets) { 11608 textLayout.clear(); 11609 textLayout.addText(s); 11610 11611 { 11612 // FIXME: it should be able to get this info easier 11613 auto painter = draw(); 11614 textLayout.redoLayout(painter); 11615 } 11616 auto cbb = textLayout.contentBoundingBox(); 11617 setContentSize(cbb.width, cbb.height); 11618 /* 11619 textLayout.addText(ForegroundColor.red, s); 11620 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 11621 textLayout.addText(" is the best!"); 11622 */ 11623 redraw(); 11624 } 11625 else static assert(false); 11626 } 11627 11628 void addText(string txt) { 11629 version(custom_widgets) { 11630 11631 textLayout.addText(txt); 11632 11633 { 11634 // FIXME: it should be able to get this info easier 11635 auto painter = draw(); 11636 textLayout.redoLayout(painter); 11637 } 11638 auto cbb = textLayout.contentBoundingBox(); 11639 setContentSize(cbb.width, cbb.height); 11640 11641 } else version(win32_widgets) { 11642 // get the current selection 11643 DWORD StartPos, EndPos; 11644 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 11645 11646 // move the caret to the end of the text 11647 int outLength = GetWindowTextLengthW(hwnd); 11648 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 11649 11650 // insert the text at the new caret position 11651 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 11652 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 11653 11654 // restore the previous selection 11655 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 11656 } else static assert(0); 11657 } 11658 11659 version(custom_widgets) 11660 override void paintFrameAndBackground(WidgetPainter painter) { 11661 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 11662 } 11663 11664 version(win32_widgets) { /* will do it with Windows calls in the classes */ } 11665 else version(custom_widgets) { 11666 // FIXME 11667 11668 static if(SimpledisplayTimerAvailable) 11669 Timer caretTimer; 11670 etc.TextLayout textLayout; 11671 11672 void setupCustomTextEditing() { 11673 textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4)); 11674 textLayout.selectionXorColor = getComputedStyle().activeListXorColor; 11675 } 11676 11677 override void paint(WidgetPainter painter) { 11678 if(parentWindow.win.closed) return; 11679 11680 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 11681 11682 /* 11683 painter.outlineColor = Color.white; 11684 painter.fillColor = Color.white; 11685 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 11686 */ 11687 11688 painter.outlineColor = Color.black; 11689 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 11690 11691 textLayout.caretShowingOnScreen = false; 11692 11693 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 11694 } 11695 11696 static class Style : Widget.Style { 11697 override MouseCursor cursor() { 11698 return GenericCursor.Text; 11699 } 11700 } 11701 mixin OverrideStyle!Style; 11702 } 11703 else static assert(false); 11704 11705 11706 11707 version(custom_widgets) 11708 override void defaultEventHandler_mousedown(MouseDownEvent ev) { 11709 super.defaultEventHandler_mousedown(ev); 11710 if(parentWindow.win.closed) return; 11711 if(ev.button == MouseButton.left) { 11712 if(textLayout.selectNone()) 11713 redraw(); 11714 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 11715 this.focus(); 11716 //this.parentWindow.win.grabInput(); 11717 } else if(ev.button == MouseButton.middle) { 11718 static if(UsingSimpledisplayX11) { 11719 getPrimarySelection(parentWindow.win, (txt) { 11720 textLayout.insert(txt); 11721 redraw(); 11722 11723 auto cbb = textLayout.contentBoundingBox(); 11724 setContentSize(cbb.width, cbb.height); 11725 }); 11726 } 11727 } 11728 } 11729 11730 version(custom_widgets) 11731 override void defaultEventHandler_mouseup(MouseUpEvent ev) { 11732 //this.parentWindow.win.releaseInputGrab(); 11733 super.defaultEventHandler_mouseup(ev); 11734 } 11735 11736 version(custom_widgets) 11737 override void defaultEventHandler_mousemove(MouseMoveEvent ev) { 11738 super.defaultEventHandler_mousemove(ev); 11739 if(ev.state & ModifierState.leftButtonDown) { 11740 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 11741 redraw(); 11742 } 11743 } 11744 11745 version(custom_widgets) 11746 override void defaultEventHandler_focus(Event ev) { 11747 super.defaultEventHandler_focus(ev); 11748 if(parentWindow.win.closed) return; 11749 auto painter = this.draw(); 11750 textLayout.drawCaret(painter); 11751 11752 static if(SimpledisplayTimerAvailable) 11753 if(caretTimer) { 11754 caretTimer.destroy(); 11755 caretTimer = null; 11756 } 11757 11758 bool blinkingCaret = true; 11759 static if(UsingSimpledisplayX11) 11760 if(!Image.impl.xshmAvailable) 11761 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 11762 11763 if(blinkingCaret) 11764 static if(SimpledisplayTimerAvailable) 11765 caretTimer = new Timer(500, { 11766 if(parentWindow.win.closed) { 11767 caretTimer.destroy(); 11768 return; 11769 } 11770 if(isFocused()) { 11771 auto painter = this.draw(); 11772 textLayout.drawCaret(painter); 11773 } else if(textLayout.caretShowingOnScreen) { 11774 auto painter = this.draw(); 11775 textLayout.eraseCaret(painter); 11776 } 11777 }); 11778 } 11779 11780 private string lastContentBlur; 11781 11782 override void defaultEventHandler_blur(Event ev) { 11783 super.defaultEventHandler_blur(ev); 11784 if(parentWindow.win.closed) return; 11785 version(custom_widgets) { 11786 auto painter = this.draw(); 11787 textLayout.eraseCaret(painter); 11788 static if(SimpledisplayTimerAvailable) 11789 if(caretTimer) { 11790 caretTimer.destroy(); 11791 caretTimer = null; 11792 } 11793 } 11794 11795 if(this.content != lastContentBlur) { 11796 auto evt = new ChangeEvent!string(this, &this.content); 11797 evt.dispatch(); 11798 lastContentBlur = this.content; 11799 } 11800 } 11801 11802 version(custom_widgets) 11803 override void defaultEventHandler_char(CharEvent ev) { 11804 super.defaultEventHandler_char(ev); 11805 textLayout.insert(ev.character); 11806 redraw(); 11807 11808 // FIXME: too inefficient 11809 auto cbb = textLayout.contentBoundingBox(); 11810 setContentSize(cbb.width, cbb.height); 11811 } 11812 version(custom_widgets) 11813 override void defaultEventHandler_keydown(KeyDownEvent ev) { 11814 //super.defaultEventHandler_keydown(ev); 11815 switch(ev.key) { 11816 case Key.Delete: 11817 textLayout.delete_(); 11818 redraw(); 11819 break; 11820 case Key.Left: 11821 textLayout.moveLeft(); 11822 redraw(); 11823 break; 11824 case Key.Right: 11825 textLayout.moveRight(); 11826 redraw(); 11827 break; 11828 case Key.Up: 11829 textLayout.moveUp(); 11830 redraw(); 11831 break; 11832 case Key.Down: 11833 textLayout.moveDown(); 11834 redraw(); 11835 break; 11836 case Key.Home: 11837 textLayout.moveHome(); 11838 redraw(); 11839 break; 11840 case Key.End: 11841 textLayout.moveEnd(); 11842 redraw(); 11843 break; 11844 case Key.PageUp: 11845 foreach(i; 0 .. 32) 11846 textLayout.moveUp(); 11847 redraw(); 11848 break; 11849 case Key.PageDown: 11850 foreach(i; 0 .. 32) 11851 textLayout.moveDown(); 11852 redraw(); 11853 break; 11854 11855 default: 11856 {} // intentionally blank, let "char" handle it 11857 } 11858 /* 11859 if(ev.key == Key.Backspace) { 11860 textLayout.backspace(); 11861 redraw(); 11862 } 11863 */ 11864 ensureVisibleInScroll(textLayout.caretBoundingBox()); 11865 } 11866 11867 11868 } 11869 11870 /// 11871 class LineEdit : EditableTextWidget { 11872 // FIXME: hack 11873 version(custom_widgets) { 11874 override bool showingVerticalScroll() { return false; } 11875 override bool showingHorizontalScroll() { return false; } 11876 } 11877 11878 override int flexBasisWidth() { return 250; } 11879 11880 /// 11881 this(Widget parent) { 11882 super(parent); 11883 version(win32_widgets) { 11884 createWin32Window(this, "edit"w, "", 11885 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 11886 } else version(custom_widgets) { 11887 setupCustomTextEditing(); 11888 addEventListener(delegate(CharEvent ev) { 11889 if(ev.character == '\n') 11890 ev.preventDefault(); 11891 }); 11892 } else static assert(false); 11893 } 11894 override int maxHeight() { return defaultLineHeight + 4; } 11895 override int minHeight() { return defaultLineHeight + 4; } 11896 11897 /+ 11898 @property void passwordMode(bool p) { 11899 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 11900 } 11901 +/ 11902 } 11903 11904 /++ 11905 A [LineEdit] that displays `*` in place of the actual characters. 11906 11907 Alas, Windows requires the window to be created differently to use this style, 11908 so it had to be a new class instead of a toggle on and off on an existing object. 11909 11910 FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now. 11911 11912 History: 11913 Added January 24, 2021 11914 +/ 11915 class PasswordEdit : EditableTextWidget { 11916 version(custom_widgets) { 11917 override bool showingVerticalScroll() { return false; } 11918 override bool showingHorizontalScroll() { return false; } 11919 } 11920 11921 override int flexBasisWidth() { return 250; } 11922 11923 /// 11924 this(Widget parent) { 11925 super(parent); 11926 version(win32_widgets) { 11927 createWin32Window(this, "edit"w, "", 11928 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 11929 } else version(custom_widgets) { 11930 setupCustomTextEditing(); 11931 addEventListener(delegate(CharEvent ev) { 11932 if(ev.character == '\n') 11933 ev.preventDefault(); 11934 }); 11935 } else static assert(false); 11936 } 11937 override int maxHeight() { return defaultLineHeight + 4; } 11938 override int minHeight() { return defaultLineHeight + 4; } 11939 } 11940 11941 11942 /// 11943 class TextEdit : EditableTextWidget { 11944 /// 11945 this(Widget parent) { 11946 super(parent); 11947 version(win32_widgets) { 11948 createWin32Window(this, "edit"w, "", 11949 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 11950 } else version(custom_widgets) { 11951 setupCustomTextEditing(); 11952 } else static assert(false); 11953 } 11954 override int maxHeight() { return int.max; } 11955 override int heightStretchiness() { return 7; } 11956 11957 override int flexBasisWidth() { return 250; } 11958 override int flexBasisHeight() { return 250; } 11959 } 11960 11961 11962 /++ 11963 11964 +/ 11965 version(none) 11966 class RichTextDisplay : Widget { 11967 @property void content(string c) {} 11968 void appendContent(string c) {} 11969 } 11970 11971 /// 11972 class MessageBox : Window { 11973 private string message; 11974 MessageBoxButton buttonPressed = MessageBoxButton.None; 11975 /// 11976 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 11977 super(300, 100); 11978 11979 assert(buttons.length); 11980 assert(buttons.length == buttonIds.length); 11981 11982 this.message = message; 11983 11984 int buttonsWidth = cast(int) buttons.length * 50 + (cast(int) buttons.length - 1) * 16; 11985 buttonsWidth = scaleWithDpi(buttonsWidth); 11986 11987 int x = this.width / 2 - buttonsWidth / 2; 11988 11989 foreach(idx, buttonText; buttons) { 11990 auto button = new Button(buttonText, this); 11991 button.x = x; 11992 button.y = height - (button.height + 10); 11993 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 11994 this.buttonPressed = buttonIds[idx]; 11995 win.close(); 11996 }; })(idx)); 11997 11998 button.registerMovement(); 11999 x += button.width; 12000 x += scaleWithDpi(16); 12001 if(idx == 0) 12002 button.focus(); 12003 } 12004 12005 win.show(); 12006 redraw(); 12007 } 12008 12009 override void paint(WidgetPainter painter) { 12010 super.paint(painter); 12011 12012 auto cs = getComputedStyle(); 12013 12014 painter.outlineColor = cs.foregroundColor(); 12015 painter.fillColor = cs.foregroundColor(); 12016 12017 painter.drawText(Point(0, 0), message, Point(width, height / 2), TextAlignment.Center | TextAlignment.VerticalCenter); 12018 } 12019 12020 // this one is all fixed position 12021 override void recomputeChildLayout() {} 12022 } 12023 12024 /// 12025 enum MessageBoxStyle { 12026 OK, /// 12027 OKCancel, /// 12028 RetryCancel, /// 12029 YesNo, /// 12030 YesNoCancel, /// 12031 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. 12032 } 12033 12034 /// 12035 enum MessageBoxIcon { 12036 None, /// 12037 Info, /// 12038 Warning, /// 12039 Error /// 12040 } 12041 12042 /// Identifies the button the user pressed on a message box. 12043 enum MessageBoxButton { 12044 None, /// The user closed the message box without clicking any of the buttons. 12045 OK, /// 12046 Cancel, /// 12047 Retry, /// 12048 Yes, /// 12049 No, /// 12050 Continue /// 12051 } 12052 12053 12054 /++ 12055 Displays a modal message box, blocking until the user dismisses it. 12056 12057 Returns: the button pressed. 12058 +/ 12059 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 12060 version(win32_widgets) { 12061 WCharzBuffer t = WCharzBuffer(title); 12062 WCharzBuffer m = WCharzBuffer(message); 12063 UINT type; 12064 with(MessageBoxStyle) 12065 final switch(style) { 12066 case OK: type |= MB_OK; break; 12067 case OKCancel: type |= MB_OKCANCEL; break; 12068 case RetryCancel: type |= MB_RETRYCANCEL; break; 12069 case YesNo: type |= MB_YESNO; break; 12070 case YesNoCancel: type |= MB_YESNOCANCEL; break; 12071 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 12072 } 12073 with(MessageBoxIcon) 12074 final switch(icon) { 12075 case None: break; 12076 case Info: type |= MB_ICONINFORMATION; break; 12077 case Warning: type |= MB_ICONWARNING; break; 12078 case Error: type |= MB_ICONERROR; break; 12079 } 12080 switch(MessageBoxW(null, m.ptr, t.ptr, type)) { 12081 case IDOK: return MessageBoxButton.OK; 12082 case IDCANCEL: return MessageBoxButton.Cancel; 12083 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 12084 case IDYES: return MessageBoxButton.Yes; 12085 case IDNO: return MessageBoxButton.No; 12086 case IDCONTINUE: return MessageBoxButton.Continue; 12087 default: return MessageBoxButton.None; 12088 } 12089 } else { 12090 string[] buttons; 12091 MessageBoxButton[] buttonIds; 12092 with(MessageBoxStyle) 12093 final switch(style) { 12094 case OK: 12095 buttons = ["OK"]; 12096 buttonIds = [MessageBoxButton.OK]; 12097 break; 12098 case OKCancel: 12099 buttons = ["OK", "Cancel"]; 12100 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 12101 break; 12102 case RetryCancel: 12103 buttons = ["Retry", "Cancel"]; 12104 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 12105 break; 12106 case YesNo: 12107 buttons = ["Yes", "No"]; 12108 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 12109 break; 12110 case YesNoCancel: 12111 buttons = ["Yes", "No", "Cancel"]; 12112 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 12113 break; 12114 case RetryCancelContinue: 12115 buttons = ["Try Again", "Cancel", "Continue"]; 12116 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 12117 break; 12118 } 12119 auto mb = new MessageBox(message, buttons, buttonIds); 12120 EventLoop el = EventLoop.get; 12121 el.run(() { return !mb.win.closed; }); 12122 return mb.buttonPressed; 12123 } 12124 } 12125 12126 /// ditto 12127 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 12128 return messageBox(null, message, style, icon); 12129 } 12130 12131 12132 12133 /// 12134 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 12135 12136 /++ 12137 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 12138 12139 History: 12140 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. 12141 +/ 12142 struct EventListener { 12143 private Widget widget; 12144 private string event; 12145 private EventHandler handler; 12146 private bool useCapture; 12147 12148 /// 12149 void disconnect() { 12150 widget.removeEventListener(this); 12151 } 12152 } 12153 12154 /++ 12155 The purpose of this enum was to give a compile-time checked version of various standard event strings. 12156 12157 Now, I recommend you use a statically typed event object instead. 12158 12159 See_Also: [Event] 12160 +/ 12161 enum EventType : string { 12162 click = "click", /// 12163 12164 mouseenter = "mouseenter", /// 12165 mouseleave = "mouseleave", /// 12166 mousein = "mousein", /// 12167 mouseout = "mouseout", /// 12168 mouseup = "mouseup", /// 12169 mousedown = "mousedown", /// 12170 mousemove = "mousemove", /// 12171 12172 keydown = "keydown", /// 12173 keyup = "keyup", /// 12174 char_ = "char", /// 12175 12176 focus = "focus", /// 12177 blur = "blur", /// 12178 12179 triggered = "triggered", /// 12180 12181 change = "change", /// 12182 } 12183 12184 /++ 12185 Represents an event that is currently being processed. 12186 12187 12188 Minigui's event model is based on the web browser. An event has a name, a target, 12189 and an associated data object. It starts from the window and works its way down through 12190 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 12191 then goes back up again all the way back to the window, triggering bubble phase handlers. At 12192 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 12193 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 12194 was called; that just stops it from calling other handlers in the widget tree, but the default happens 12195 whenever propagation is done, not only if it gets to the end of the chain). 12196 12197 This model has several nice points: 12198 12199 $(LIST 12200 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 12201 with event handlers set, then add/remove children as much as you want without needing 12202 to manage the event handlers on them - the parent alone can manage everything. 12203 12204 * It is easy to create new custom events in your application. 12205 12206 * It is familiar to many web developers. 12207 ) 12208 12209 There's a few downsides though: 12210 12211 $(LIST 12212 * There's not a lot of type safety. 12213 12214 * You don't get a static list of what events a widget can emit. 12215 12216 * Tracing where an event got cancelled along the chain can get difficult; the downside of 12217 the central delegation benefit is it can be lead to debugging of action at a distance. 12218 ) 12219 12220 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 12221 while keeping the benefits - and most compatibility with - the existing model. The main idea is 12222 to simply use a D object type which provides a static interface as well as a built-in event name. 12223 Then, a new static interface allows you to see what an event can emit and attach handlers to it 12224 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 12225 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 12226 to having a little more help from the D compiler and documentation generator. 12227 12228 Your code would change like this: 12229 12230 --- 12231 // old 12232 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 12233 12234 // new 12235 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 12236 --- 12237 12238 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. 12239 12240 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 12241 12242 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 12243 12244 Thus the family of functions are: 12245 12246 [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. 12247 12248 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 12249 12250 [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. 12251 12252 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 12253 12254 --- 12255 class MyCheckbox : Widget { 12256 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 12257 /// It is NOT actually required but should be used whenever possible. 12258 mixin Emits!(ChangeEvent!bool); 12259 12260 this(Widget parent) { 12261 super(parent); 12262 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 12263 } 12264 12265 private bool _checked; 12266 @property bool checked() { return _checked; } 12267 @property void checked(bool set) { 12268 _checked = set; 12269 emit!(ChangeEvent!bool)(&checked); 12270 } 12271 } 12272 --- 12273 12274 ## Creating Your Own Events 12275 12276 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. 12277 12278 --- 12279 class MyEvent : Event { 12280 this(Widget target) { super(EventString, target); } 12281 mixin Register; // adds EventString and other reflection information 12282 } 12283 --- 12284 12285 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 12286 12287 History: 12288 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. 12289 12290 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. 12291 +/ 12292 /+ 12293 12294 ## General Conventions 12295 12296 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. 12297 12298 12299 ## Qt-style signals and slots 12300 12301 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. 12302 12303 The intention is for events to be used when 12304 12305 --- 12306 class Demo : Widget { 12307 this() { 12308 myPropertyChanged = Signal!int(this); 12309 } 12310 @property myProperty(int v) { 12311 myPropertyChanged.emit(v); 12312 } 12313 12314 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 12315 // but it can just genuinely not care about `this` since that's not really passed. 12316 } 12317 12318 class Foo : Widget { 12319 // the slot uda is not necessary, but it helps the script and ui builder find it. 12320 @slot void setValue(int v) { ... } 12321 } 12322 12323 demo.myPropertyChanged.connect(&foo.setValue); 12324 --- 12325 12326 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 12327 12328 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 12329 12330 class StringChangeEvent : ChangeEvent, Signal!string { 12331 mixin SignalImpl 12332 } 12333 12334 +/ 12335 class Event : ReflectableProperties { 12336 /// Creates an event without populating any members and without sending it. See [dispatch] 12337 this(string eventName, Widget emittedBy) { 12338 this.eventName = eventName; 12339 this.srcElement = emittedBy; 12340 } 12341 12342 12343 /// Implementations for the [ReflectableProperties] interface/ 12344 void getPropertiesList(scope void delegate(string name) sink) const {} 12345 /// ditto 12346 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 12347 /// ditto 12348 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 12349 return SetPropertyResult.notPermitted; 12350 } 12351 12352 12353 /+ 12354 /++ 12355 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 12356 12357 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. 12358 +/ 12359 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 12360 if(value.length == 0) { 12361 finalSink(memberName, `""`); 12362 return; 12363 } 12364 12365 char[1024] bufferBacking; 12366 char[] buffer = bufferBacking; 12367 int bufferPosition; 12368 12369 void sink(char ch) { 12370 if(bufferPosition >= buffer.length) 12371 buffer.length = buffer.length + 1024; 12372 buffer[bufferPosition++] = ch; 12373 } 12374 12375 sink('"'); 12376 12377 foreach(ch; value) { 12378 switch(ch) { 12379 case '\\': 12380 sink('\\'); sink('\\'); 12381 break; 12382 case '"': 12383 sink('\\'); sink('"'); 12384 break; 12385 case '\n': 12386 sink('\\'); sink('n'); 12387 break; 12388 case '\r': 12389 sink('\\'); sink('r'); 12390 break; 12391 case '\t': 12392 sink('\\'); sink('t'); 12393 break; 12394 default: 12395 sink(ch); 12396 } 12397 } 12398 12399 sink('"'); 12400 12401 finalSink(memberName, buffer[0 .. bufferPosition]); 12402 } 12403 +/ 12404 12405 /+ 12406 enum EventInitiator { 12407 system, 12408 minigui, 12409 user 12410 } 12411 12412 immutable EventInitiator; initiatedBy; 12413 +/ 12414 12415 /++ 12416 Events should generally follow the propagation model, but there's some exceptions 12417 to that rule. If so, they should override this to return false. In that case, only 12418 bubbling event handlers on the target itself and capturing event handlers on the containing 12419 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 12420 capture -> target -> bubble process.) 12421 12422 History: 12423 Added May 12, 2021 12424 +/ 12425 bool propagates() const pure nothrow @nogc @safe { 12426 return true; 12427 } 12428 12429 /++ 12430 hints as to whether preventDefault will actually do anything. not entirely reliable. 12431 12432 History: 12433 Added May 14, 2021 12434 +/ 12435 bool cancelable() const pure nothrow @nogc @safe { 12436 return true; 12437 } 12438 12439 /++ 12440 You can mix this into child class to register some boilerplate. It includes the `EventString` 12441 member, a constructor, and implementations of the dynamic get data interfaces. 12442 12443 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 12444 12445 12446 You can override the default EventString by simply providing your own in the form of 12447 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 12448 which provides some namespace protection against conflicts in other libraries while still being fairly 12449 easy to use. 12450 12451 If you provide your own constructor, it will override the default constructor provided here. A constructor 12452 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 12453 first argument to your constructor. 12454 12455 History: 12456 Added May 13, 2021. 12457 +/ 12458 protected static mixin template Register() { 12459 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 12460 this(Widget target) { super(EventString, target); } 12461 12462 mixin ReflectableProperties.RegisterGetters; 12463 } 12464 12465 /++ 12466 This is the widget that emitted the event. 12467 12468 12469 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 12470 12471 History: 12472 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 12473 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 12474 so I don't intend to remove these aliases. 12475 +/ 12476 Widget source; 12477 /// ditto 12478 alias source target; 12479 /// ditto 12480 alias source srcElement; 12481 12482 Widget relatedTarget; /// Note: likely to be deprecated at some point. 12483 12484 /// Prevents the default event handler (if there is one) from being called 12485 void preventDefault() { 12486 lastDefaultPrevented = true; 12487 defaultPrevented = true; 12488 } 12489 12490 /// Stops the event propagation immediately. 12491 void stopPropagation() { 12492 propagationStopped = true; 12493 } 12494 12495 private bool defaultPrevented; 12496 private bool propagationStopped; 12497 private string eventName; 12498 12499 private bool isBubbling; 12500 12501 /// 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. 12502 protected void adjustScrolling() { } 12503 /// ditto 12504 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 12505 12506 /++ 12507 this sends it only to the target. If you want propagation, use dispatch() instead. 12508 12509 This should be made private!!! 12510 12511 +/ 12512 void sendDirectly() { 12513 if(srcElement is null) 12514 return; 12515 12516 // 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. 12517 12518 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 12519 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 12520 12521 adjustScrolling(); 12522 12523 if(auto e = target.parentWindow) { 12524 if(auto handlers = "*" in e.capturingEventHandlers) 12525 foreach(handler; *handlers) 12526 if(handler) handler(e, this); 12527 if(auto handlers = eventName in e.capturingEventHandlers) 12528 foreach(handler; *handlers) 12529 if(handler) handler(e, this); 12530 } 12531 12532 auto e = srcElement; 12533 12534 if(auto handlers = eventName in e.bubblingEventHandlers) 12535 foreach(handler; *handlers) 12536 if(handler) handler(e, this); 12537 12538 if(auto handlers = "*" in e.bubblingEventHandlers) 12539 foreach(handler; *handlers) 12540 if(handler) handler(e, this); 12541 12542 // there's never a default for a catch-all event 12543 if(!defaultPrevented) 12544 if(eventName in e.defaultEventHandlers) 12545 e.defaultEventHandlers[eventName](e, this); 12546 } 12547 12548 /// this dispatches the element using the capture -> target -> bubble process 12549 void dispatch() { 12550 if(srcElement is null) 12551 return; 12552 12553 if(!propagates) { 12554 sendDirectly; 12555 return; 12556 } 12557 12558 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 12559 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 12560 12561 adjustScrolling(); 12562 // first capture, then bubble 12563 12564 Widget[] chain; 12565 Widget curr = srcElement; 12566 while(curr) { 12567 auto l = curr; 12568 chain ~= l; 12569 curr = curr.parent; 12570 } 12571 12572 isBubbling = false; 12573 12574 foreach_reverse(e; chain) { 12575 if(auto handlers = "*" in e.capturingEventHandlers) 12576 foreach(handler; *handlers) if(handler !is null) handler(e, this); 12577 12578 if(propagationStopped) 12579 break; 12580 12581 if(auto handlers = eventName in e.capturingEventHandlers) 12582 foreach(handler; *handlers) if(handler !is null) handler(e, this); 12583 12584 // the default on capture should really be to always do nothing 12585 12586 //if(!defaultPrevented) 12587 // if(eventName in e.defaultEventHandlers) 12588 // e.defaultEventHandlers[eventName](e.element, this); 12589 12590 if(propagationStopped) 12591 break; 12592 } 12593 12594 int adjustX; 12595 int adjustY; 12596 12597 isBubbling = true; 12598 if(!propagationStopped) 12599 foreach(e; chain) { 12600 if(auto handlers = eventName in e.bubblingEventHandlers) 12601 foreach(handler; *handlers) if(handler !is null) handler(e, this); 12602 12603 if(propagationStopped) 12604 break; 12605 12606 if(auto handlers = "*" in e.bubblingEventHandlers) 12607 foreach(handler; *handlers) if(handler !is null) handler(e, this); 12608 12609 if(propagationStopped) 12610 break; 12611 12612 if(e.encapsulatedChildren()) { 12613 adjustClientCoordinates(adjustX, adjustY); 12614 target = e; 12615 } else { 12616 adjustX += e.x; 12617 adjustY += e.y; 12618 } 12619 } 12620 12621 if(!defaultPrevented) 12622 foreach(e; chain) { 12623 if(eventName in e.defaultEventHandlers) 12624 e.defaultEventHandlers[eventName](e, this); 12625 } 12626 } 12627 12628 12629 /* old compatibility things */ 12630 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 12631 final @property { 12632 Key key() { return (cast(KeyEventBase) this).key; } 12633 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 12634 12635 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 12636 bool altKey() { return (cast(KeyEventBase) this).altKey; } 12637 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 12638 } 12639 12640 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 12641 final @property { 12642 int clientX() { return (cast(MouseEventBase) this).clientX; } 12643 int clientY() { return (cast(MouseEventBase) this).clientY; } 12644 12645 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 12646 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 12647 12648 int button() { return (cast(MouseEventBase) this).button; } 12649 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 12650 } 12651 12652 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 12653 final @property { 12654 int state() { 12655 if(auto meb = cast(MouseEventBase) this) 12656 return meb.state; 12657 if(auto keb = cast(KeyEventBase) this) 12658 return keb.state; 12659 assert(0); 12660 } 12661 } 12662 12663 deprecated("Use a CharEvent instead of Event in your handler going forward") 12664 final @property { 12665 dchar character() { 12666 if(auto ce = cast(CharEvent) this) 12667 return ce.character; 12668 return dchar.init; 12669 } 12670 } 12671 12672 // for change events 12673 @property { 12674 /// 12675 int intValue() { return 0; } 12676 /// 12677 string stringValue() { return null; } 12678 } 12679 } 12680 12681 /++ 12682 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 12683 12684 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 12685 dynamic and custom events, but the static list helps ensure you get them right. 12686 12687 If this is declared, you can use [Widget.emit] to send the event. 12688 12689 All events work the same way though, following the capture->widget->bubble model described under [Event]. 12690 12691 History: 12692 Added May 4, 2021 12693 +/ 12694 mixin template Emits(EventType) { 12695 import arsd.minigui : EventString; 12696 static if(is(EventType : Event) && !is(EventType == Event)) 12697 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 12698 else 12699 static assert(0, "You can only emit subclasses of Event"); 12700 } 12701 12702 /// ditto 12703 mixin template Emits(string eventString) { 12704 mixin("private Event[0] emits_" ~ eventString ~";"); 12705 } 12706 12707 /* 12708 class SignalEvent(string name) : Event { 12709 12710 } 12711 */ 12712 12713 /++ 12714 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". 12715 12716 12717 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. 12718 12719 History: 12720 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 12721 +/ 12722 class CommandEvent : Event { 12723 enum EventString = "command"; 12724 this(Widget source, string CommandString = EventString) { 12725 super(CommandString, source); 12726 } 12727 } 12728 12729 /++ 12730 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 12731 +/ 12732 class CommandEventWithArgs(Args...) : CommandEvent { 12733 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 12734 Args args; 12735 } 12736 12737 /++ 12738 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. 12739 12740 See [CommandEvent] for more information. 12741 12742 Returns: 12743 The [EventListener] you can use to remove the handler. 12744 +/ 12745 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 12746 return w.addEventListener(CommandString, (Event ev) { 12747 if(ev.target is w) 12748 return; // it does not consume its own commands! 12749 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 12750 handler(cev.args); 12751 ev.stopPropagation(); 12752 } 12753 }); 12754 } 12755 12756 /++ 12757 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. 12758 +/ 12759 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 12760 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 12761 event.dispatch(); 12762 } 12763 12764 class ResizeEvent : Event { 12765 enum EventString = "resize"; 12766 12767 this(Widget target) { super(EventString, target); } 12768 12769 override bool propagates() const { return false; } 12770 } 12771 12772 /++ 12773 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 12774 12775 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. 12776 12777 History: 12778 Added June 21, 2021 (dub v10.1) 12779 +/ 12780 class ClosingEvent : Event { 12781 enum EventString = "closing"; 12782 12783 this(Widget target) { super(EventString, target); } 12784 12785 override bool propagates() const { return false; } 12786 override bool cancelable() const { return true; } 12787 } 12788 12789 /// ditto 12790 class ClosedEvent : Event { 12791 enum EventString = "closed"; 12792 12793 this(Widget target) { super(EventString, target); } 12794 12795 override bool propagates() const { return false; } 12796 override bool cancelable() const { return false; } 12797 } 12798 12799 /// 12800 class BlurEvent : Event { 12801 enum EventString = "blur"; 12802 12803 // FIXME: related target? 12804 this(Widget target) { super(EventString, target); } 12805 12806 override bool propagates() const { return false; } 12807 } 12808 12809 /// 12810 class FocusEvent : Event { 12811 enum EventString = "focus"; 12812 12813 // FIXME: related target? 12814 this(Widget target) { super(EventString, target); } 12815 12816 override bool propagates() const { return false; } 12817 } 12818 12819 /++ 12820 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 12821 12822 History: 12823 Added July 3, 2021 12824 +/ 12825 class FocusInEvent : Event { 12826 enum EventString = "focusin"; 12827 12828 // FIXME: related target? 12829 this(Widget target) { super(EventString, target); } 12830 12831 override bool cancelable() const { return false; } 12832 } 12833 12834 /// ditto 12835 class FocusOutEvent : Event { 12836 enum EventString = "focusout"; 12837 12838 // FIXME: related target? 12839 this(Widget target) { super(EventString, target); } 12840 12841 override bool cancelable() const { return false; } 12842 } 12843 12844 /// 12845 class ScrollEvent : Event { 12846 enum EventString = "scroll"; 12847 this(Widget target) { super(EventString, target); } 12848 12849 override bool cancelable() const { return false; } 12850 } 12851 12852 /++ 12853 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 12854 12855 History: 12856 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 12857 +/ 12858 class CharEvent : Event { 12859 enum EventString = "char"; 12860 this(Widget target, dchar ch) { 12861 character = ch; 12862 super(EventString, target); 12863 } 12864 12865 immutable dchar character; 12866 } 12867 12868 /++ 12869 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 12870 +/ 12871 abstract class ChangeEventBase : Event { 12872 enum EventString = "change"; 12873 this(Widget target) { 12874 super(EventString, target); 12875 } 12876 12877 /+ 12878 // idk where or how exactly i want to do this. 12879 // i might come back to it later. 12880 12881 // If a widget itself broadcasts one of theses itself, it stops propagation going down 12882 // this way the source doesn't get too confused (think of a nested scroll widget) 12883 // 12884 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 12885 // then you consume that command and change you scroll x position to whatever. then you do 12886 // some kind of change event that is broadcast back to the children and any horizontal scroll 12887 // listeners are now able to update, without having an explicit connection between them. 12888 void broadcastToChildren(string fieldName) { 12889 12890 } 12891 +/ 12892 } 12893 12894 /++ 12895 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. 12896 12897 12898 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). 12899 12900 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);` 12901 12902 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 12903 12904 History: 12905 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. 12906 +/ 12907 class ChangeEvent(T) : ChangeEventBase { 12908 this(Widget target, T delegate() getNewValue) { 12909 assert(getNewValue !is null); 12910 this.getNewValue = getNewValue; 12911 super(target); 12912 } 12913 12914 private T delegate() getNewValue; 12915 12916 /++ 12917 Gets the new value that just changed. 12918 +/ 12919 @property T value() { 12920 return getNewValue(); 12921 } 12922 12923 /// compatibility method for old generic Events 12924 static if(is(immutable T == immutable int)) 12925 override int intValue() { return value; } 12926 /// ditto 12927 static if(is(immutable T == immutable string)) 12928 override string stringValue() { return value; } 12929 } 12930 12931 /++ 12932 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 12933 12934 12935 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 12936 12937 History: 12938 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 12939 +/ 12940 abstract class KeyEventBase : Event { 12941 this(string name, Widget target) { 12942 super(name, target); 12943 } 12944 12945 // for key events 12946 Key key; /// 12947 12948 KeyEvent originalKeyEvent; 12949 12950 /++ 12951 Indicates the current state of the given keyboard modifier keys. 12952 12953 History: 12954 Added to events on April 15, 2020. 12955 +/ 12956 bool ctrlKey; 12957 12958 /// ditto 12959 bool altKey; 12960 12961 /// ditto 12962 bool shiftKey; 12963 12964 /++ 12965 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 12966 12967 See [arsd.simpledisplay.ModifierState] for other possible flags. 12968 +/ 12969 int state; 12970 12971 mixin Register; 12972 } 12973 12974 /++ 12975 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]. 12976 12977 12978 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 12979 12980 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. 12981 12982 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. 12983 12984 See_Also: [KeyUpEvent], [CharEvent] 12985 12986 History: 12987 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 12988 +/ 12989 class KeyDownEvent : KeyEventBase { 12990 enum EventString = "keydown"; 12991 this(Widget target) { super(EventString, target); } 12992 } 12993 12994 /++ 12995 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 12996 12997 12998 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 12999 13000 See_Also: [KeyDownEvent], [CharEvent] 13001 13002 History: 13003 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 13004 +/ 13005 class KeyUpEvent : KeyEventBase { 13006 enum EventString = "keyup"; 13007 this(Widget target) { super(EventString, target); } 13008 } 13009 13010 /++ 13011 Contains shared properties for various mouse events; 13012 13013 13014 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 13015 13016 History: 13017 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 13018 +/ 13019 abstract class MouseEventBase : Event { 13020 this(string name, Widget target) { 13021 super(name, target); 13022 } 13023 13024 // for mouse events 13025 int clientX; /// The mouse event location relative to the target widget 13026 int clientY; /// ditto 13027 13028 int viewportX; /// The mouse event location relative to the window origin 13029 int viewportY; /// ditto 13030 13031 int button; /// See: [MouseEvent.button] 13032 int buttonLinear; /// See: [MouseEvent.buttonLinear] 13033 13034 /++ 13035 Indicates the current state of the given keyboard modifier keys. 13036 13037 History: 13038 Added to mouse events on September 28, 2010. 13039 +/ 13040 bool ctrlKey; 13041 13042 /// ditto 13043 bool altKey; 13044 13045 /// ditto 13046 bool shiftKey; 13047 13048 13049 13050 int state; /// 13051 13052 /++ 13053 for consistent names with key event. 13054 13055 History: 13056 Added September 28, 2021 (dub v10.3) 13057 +/ 13058 alias modifierState = state; 13059 13060 /++ 13061 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 13062 13063 History: 13064 Added May 15, 2021 13065 +/ 13066 bool isMouseWheel() { 13067 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 13068 } 13069 13070 // private 13071 override void adjustClientCoordinates(int deltaX, int deltaY) { 13072 clientX += deltaX; 13073 clientY += deltaY; 13074 } 13075 13076 override void adjustScrolling() { 13077 version(custom_widgets) { // TEMP 13078 viewportX = clientX; 13079 viewportY = clientY; 13080 if(auto se = cast(ScrollableWidget) srcElement) { 13081 clientX += se.scrollOrigin.x; 13082 clientY += se.scrollOrigin.y; 13083 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 13084 //clientX += se.scrollX_; 13085 //clientY += se.scrollY_; 13086 } 13087 } 13088 } 13089 13090 mixin Register; 13091 } 13092 13093 /++ 13094 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 13095 13096 13097 $(WARNING 13098 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 13099 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 13100 behavior. 13101 ) 13102 13103 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 13104 13105 [MouseUpEvent] is sent when the user releases a mouse button. 13106 13107 [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.) 13108 13109 [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. 13110 13111 [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. 13112 13113 [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. 13114 13115 [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. 13116 13117 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 13118 13119 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 13120 13121 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 13122 13123 Rationale: 13124 13125 If you only want to do drag, mousedown/up works just fine being consistently sent. 13126 13127 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). 13128 13129 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. 13130 13131 History: 13132 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. 13133 +/ 13134 class MouseUpEvent : MouseEventBase { 13135 enum EventString = "mouseup"; /// 13136 this(Widget target) { super(EventString, target); } 13137 } 13138 /// ditto 13139 class MouseDownEvent : MouseEventBase { 13140 enum EventString = "mousedown"; /// 13141 this(Widget target) { super(EventString, target); } 13142 } 13143 /// ditto 13144 class MouseMoveEvent : MouseEventBase { 13145 enum EventString = "mousemove"; /// 13146 this(Widget target) { super(EventString, target); } 13147 } 13148 /// ditto 13149 class ClickEvent : MouseEventBase { 13150 enum EventString = "click"; /// 13151 this(Widget target) { super(EventString, target); } 13152 } 13153 /// ditto 13154 class DoubleClickEvent : MouseEventBase { 13155 enum EventString = "dblclick"; /// 13156 this(Widget target) { super(EventString, target); } 13157 } 13158 /// ditto 13159 class MouseOverEvent : Event { 13160 enum EventString = "mouseover"; /// 13161 this(Widget target) { super(EventString, target); } 13162 } 13163 /// ditto 13164 class MouseOutEvent : Event { 13165 enum EventString = "mouseout"; /// 13166 this(Widget target) { super(EventString, target); } 13167 } 13168 /// ditto 13169 class MouseEnterEvent : Event { 13170 enum EventString = "mouseenter"; /// 13171 this(Widget target) { super(EventString, target); } 13172 13173 override bool propagates() const { return false; } 13174 } 13175 /// ditto 13176 class MouseLeaveEvent : Event { 13177 enum EventString = "mouseleave"; /// 13178 this(Widget target) { super(EventString, target); } 13179 13180 override bool propagates() const { return false; } 13181 } 13182 13183 private bool isAParentOf(Widget a, Widget b) { 13184 if(a is null || b is null) 13185 return false; 13186 13187 while(b !is null) { 13188 if(a is b) 13189 return true; 13190 b = b.parent; 13191 } 13192 13193 return false; 13194 } 13195 13196 private struct WidgetAtPointResponse { 13197 Widget widget; 13198 13199 // x, y relative to the widget in the response. 13200 int x; 13201 int y; 13202 } 13203 13204 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 13205 assert(starting !is null); 13206 13207 starting.addScrollPosition(x, y); 13208 13209 auto child = starting.getChildAtPosition(x, y); 13210 while(child) { 13211 if(child.hidden) 13212 continue; 13213 starting = child; 13214 x -= child.x; 13215 y -= child.y; 13216 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 13217 child = r.widget; 13218 if(child is starting) 13219 break; 13220 } 13221 return WidgetAtPointResponse(starting, x, y); 13222 } 13223 13224 version(win32_widgets) { 13225 private: 13226 import core.sys.windows.commctrl; 13227 13228 pragma(lib, "comctl32"); 13229 shared static this() { 13230 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 13231 INITCOMMONCONTROLSEX ic; 13232 ic.dwSize = cast(DWORD) ic.sizeof; 13233 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 13234 if(!InitCommonControlsEx(&ic)) { 13235 //import std.stdio; writeln("ICC failed"); 13236 } 13237 } 13238 13239 13240 // everything from here is just win32 headers copy pasta 13241 private: 13242 extern(Windows): 13243 13244 alias HANDLE HMENU; 13245 HMENU CreateMenu(); 13246 bool SetMenu(HWND, HMENU); 13247 HMENU CreatePopupMenu(); 13248 enum MF_POPUP = 0x10; 13249 enum MF_STRING = 0; 13250 13251 13252 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 13253 struct INITCOMMONCONTROLSEX { 13254 DWORD dwSize; 13255 DWORD dwICC; 13256 } 13257 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 13258 enum { 13259 IDB_STD_SMALL_COLOR, 13260 IDB_STD_LARGE_COLOR, 13261 IDB_VIEW_SMALL_COLOR = 4, 13262 IDB_VIEW_LARGE_COLOR = 5 13263 } 13264 enum { 13265 STD_CUT, 13266 STD_COPY, 13267 STD_PASTE, 13268 STD_UNDO, 13269 STD_REDOW, 13270 STD_DELETE, 13271 STD_FILENEW, 13272 STD_FILEOPEN, 13273 STD_FILESAVE, 13274 STD_PRINTPRE, 13275 STD_PROPERTIES, 13276 STD_HELP, 13277 STD_FIND, 13278 STD_REPLACE, 13279 STD_PRINT // = 14 13280 } 13281 13282 alias HANDLE HIMAGELIST; 13283 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 13284 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 13285 BOOL ImageList_Destroy(HIMAGELIST); 13286 13287 uint MAKELONG(ushort a, ushort b) { 13288 return cast(uint) ((b << 16) | a); 13289 } 13290 13291 13292 struct TBBUTTON { 13293 int iBitmap; 13294 int idCommand; 13295 BYTE fsState; 13296 BYTE fsStyle; 13297 version(Win64) 13298 BYTE[6] bReserved; 13299 else 13300 BYTE[2] bReserved; 13301 DWORD dwData; 13302 INT_PTR iString; 13303 } 13304 13305 enum { 13306 TB_ADDBUTTONSA = WM_USER + 20, 13307 TB_INSERTBUTTONA = WM_USER + 21, 13308 TB_GETIDEALSIZE = WM_USER + 99, 13309 } 13310 13311 struct SIZE { 13312 LONG cx; 13313 LONG cy; 13314 } 13315 13316 13317 enum { 13318 TBSTATE_CHECKED = 1, 13319 TBSTATE_PRESSED = 2, 13320 TBSTATE_ENABLED = 4, 13321 TBSTATE_HIDDEN = 8, 13322 TBSTATE_INDETERMINATE = 16, 13323 TBSTATE_WRAP = 32 13324 } 13325 13326 13327 13328 enum { 13329 ILC_COLOR = 0, 13330 ILC_COLOR4 = 4, 13331 ILC_COLOR8 = 8, 13332 ILC_COLOR16 = 16, 13333 ILC_COLOR24 = 24, 13334 ILC_COLOR32 = 32, 13335 ILC_COLORDDB = 254, 13336 ILC_MASK = 1, 13337 ILC_PALETTE = 2048 13338 } 13339 13340 13341 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 13342 13343 13344 enum { 13345 TB_ENABLEBUTTON = WM_USER + 1, 13346 TB_CHECKBUTTON, 13347 TB_PRESSBUTTON, 13348 TB_HIDEBUTTON, 13349 TB_INDETERMINATE, // = WM_USER + 5, 13350 TB_ISBUTTONENABLED = WM_USER + 9, 13351 TB_ISBUTTONCHECKED, 13352 TB_ISBUTTONPRESSED, 13353 TB_ISBUTTONHIDDEN, 13354 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 13355 TB_SETSTATE = WM_USER + 17, 13356 TB_GETSTATE = WM_USER + 18, 13357 TB_ADDBITMAP = WM_USER + 19, 13358 TB_DELETEBUTTON = WM_USER + 22, 13359 TB_GETBUTTON, 13360 TB_BUTTONCOUNT, 13361 TB_COMMANDTOINDEX, 13362 TB_SAVERESTOREA, 13363 TB_CUSTOMIZE, 13364 TB_ADDSTRINGA, 13365 TB_GETITEMRECT, 13366 TB_BUTTONSTRUCTSIZE, 13367 TB_SETBUTTONSIZE, 13368 TB_SETBITMAPSIZE, 13369 TB_AUTOSIZE, // = WM_USER + 33, 13370 TB_GETTOOLTIPS = WM_USER + 35, 13371 TB_SETTOOLTIPS = WM_USER + 36, 13372 TB_SETPARENT = WM_USER + 37, 13373 TB_SETROWS = WM_USER + 39, 13374 TB_GETROWS, 13375 TB_GETBITMAPFLAGS, 13376 TB_SETCMDID, 13377 TB_CHANGEBITMAP, 13378 TB_GETBITMAP, 13379 TB_GETBUTTONTEXTA, 13380 TB_REPLACEBITMAP, // = WM_USER + 46, 13381 TB_GETBUTTONSIZE = WM_USER + 58, 13382 TB_SETBUTTONWIDTH = WM_USER + 59, 13383 TB_GETBUTTONTEXTW = WM_USER + 75, 13384 TB_SAVERESTOREW = WM_USER + 76, 13385 TB_ADDSTRINGW = WM_USER + 77, 13386 } 13387 13388 extern(Windows) 13389 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 13390 13391 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 13392 13393 13394 enum { 13395 TB_SETINDENT = WM_USER + 47, 13396 TB_SETIMAGELIST, 13397 TB_GETIMAGELIST, 13398 TB_LOADIMAGES, 13399 TB_GETRECT, 13400 TB_SETHOTIMAGELIST, 13401 TB_GETHOTIMAGELIST, 13402 TB_SETDISABLEDIMAGELIST, 13403 TB_GETDISABLEDIMAGELIST, 13404 TB_SETSTYLE, 13405 TB_GETSTYLE, 13406 //TB_GETBUTTONSIZE, 13407 //TB_SETBUTTONWIDTH, 13408 TB_SETMAXTEXTROWS, 13409 TB_GETTEXTROWS // = WM_USER + 61 13410 } 13411 13412 enum { 13413 CCM_FIRST = 0x2000, 13414 CCM_LAST = CCM_FIRST + 0x200, 13415 CCM_SETBKCOLOR = 8193, 13416 CCM_SETCOLORSCHEME = 8194, 13417 CCM_GETCOLORSCHEME = 8195, 13418 CCM_GETDROPTARGET = 8196, 13419 CCM_SETUNICODEFORMAT = 8197, 13420 CCM_GETUNICODEFORMAT = 8198, 13421 CCM_SETVERSION = 0x2007, 13422 CCM_GETVERSION = 0x2008, 13423 CCM_SETNOTIFYWINDOW = 0x2009 13424 } 13425 13426 13427 enum { 13428 PBM_SETRANGE = WM_USER + 1, 13429 PBM_SETPOS, 13430 PBM_DELTAPOS, 13431 PBM_SETSTEP, 13432 PBM_STEPIT, // = WM_USER + 5 13433 PBM_SETRANGE32 = 1030, 13434 PBM_GETRANGE, 13435 PBM_GETPOS, 13436 PBM_SETBARCOLOR, // = 1033 13437 PBM_SETBKCOLOR = CCM_SETBKCOLOR 13438 } 13439 13440 enum { 13441 PBS_SMOOTH = 1, 13442 PBS_VERTICAL = 4 13443 } 13444 13445 enum { 13446 ICC_LISTVIEW_CLASSES = 1, 13447 ICC_TREEVIEW_CLASSES = 2, 13448 ICC_BAR_CLASSES = 4, 13449 ICC_TAB_CLASSES = 8, 13450 ICC_UPDOWN_CLASS = 16, 13451 ICC_PROGRESS_CLASS = 32, 13452 ICC_HOTKEY_CLASS = 64, 13453 ICC_ANIMATE_CLASS = 128, 13454 ICC_WIN95_CLASSES = 255, 13455 ICC_DATE_CLASSES = 256, 13456 ICC_USEREX_CLASSES = 512, 13457 ICC_COOL_CLASSES = 1024, 13458 ICC_STANDARD_CLASSES = 0x00004000, 13459 } 13460 13461 enum WM_USER = 1024; 13462 } 13463 13464 version(win32_widgets) 13465 pragma(lib, "comdlg32"); 13466 13467 13468 /// 13469 enum GenericIcons : ushort { 13470 None, /// 13471 // these happen to match the win32 std icons numerically if you just subtract one from the value 13472 Cut, /// 13473 Copy, /// 13474 Paste, /// 13475 Undo, /// 13476 Redo, /// 13477 Delete, /// 13478 New, /// 13479 Open, /// 13480 Save, /// 13481 PrintPreview, /// 13482 Properties, /// 13483 Help, /// 13484 Find, /// 13485 Replace, /// 13486 Print, /// 13487 } 13488 13489 enum FileDialogType { 13490 Automatic, 13491 Open, 13492 Save 13493 } 13494 string previousFileReferenced; 13495 13496 /++ 13497 Used in automatic menu functions to indicate that the user should be able to browse for a file. 13498 13499 Params: 13500 storage = an alias to a `static string` variable that stores the last file referenced. It will 13501 use this to pre-fill the dialog with a suggestion. 13502 13503 Please note that it MUST be `static` or you will get compile errors. 13504 13505 filters = the filters param to [getFileName] 13506 13507 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 13508 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 13509 a save dialog box. Otherwise, it will show an open dialog box. 13510 +/ 13511 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 13512 string name; 13513 alias name this; 13514 } 13515 13516 /++ 13517 History: 13518 onCancel was added November 6, 2021. 13519 13520 The dialog itself on Linux was modified on December 2, 2021 to include 13521 a directory picker in addition to the command line completion view. 13522 13523 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 13524 Future_directions: 13525 I want to add some kind of custom preview and maybe thumbnail thing in the future, 13526 at least on Linux, maybe on Windows too. 13527 +/ 13528 void getOpenFileName( 13529 void delegate(string) onOK, 13530 string prefilledName = null, 13531 string[] filters = null, 13532 void delegate() onCancel = null, 13533 string initialDirectory = null, 13534 ) 13535 { 13536 return getFileName(true, onOK, prefilledName, filters, onCancel, initialDirectory); 13537 } 13538 13539 /// ditto 13540 void getSaveFileName( 13541 void delegate(string) onOK, 13542 string prefilledName = null, 13543 string[] filters = null, 13544 void delegate() onCancel = null, 13545 string initialDirectory = null, 13546 ) 13547 { 13548 return getFileName(false, onOK, prefilledName, filters, onCancel, initialDirectory); 13549 } 13550 13551 void getFileName( 13552 bool openOrSave, 13553 void delegate(string) onOK, 13554 string prefilledName = null, 13555 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 13556 void delegate() onCancel = null, 13557 string initialDirectory = null, 13558 ) 13559 { 13560 13561 version(win32_widgets) { 13562 import core.sys.windows.commdlg; 13563 /* 13564 Ofn.lStructSize = sizeof(OPENFILENAME); 13565 Ofn.hwndOwner = hWnd; 13566 Ofn.lpstrFilter = szFilter; 13567 Ofn.lpstrFile= szFile; 13568 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 13569 Ofn.lpstrFileTitle = szFileTitle; 13570 Ofn.nMaxFileTitle = sizeof(szFileTitle); 13571 Ofn.lpstrInitialDir = (LPSTR)NULL; 13572 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 13573 Ofn.lpstrTitle = szTitle; 13574 */ 13575 13576 13577 wchar[1024] file = 0; 13578 wchar[1024] filterBuffer = 0; 13579 makeWindowsString(prefilledName, file[]); 13580 OPENFILENAME ofn; 13581 ofn.lStructSize = ofn.sizeof; 13582 if(filters.length) { 13583 string filter; 13584 foreach(i, f; filters) { 13585 filter ~= f; 13586 filter ~= "\0"; 13587 } 13588 filter ~= "\0"; 13589 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 13590 } 13591 ofn.lpstrFile = file.ptr; 13592 ofn.nMaxFile = file.length; 13593 13594 wchar[1024] initialDir = 0; 13595 if(initialDirectory !is null) { 13596 makeWindowsString(initialDirectory, initialDir[]); 13597 ofn.lpstrInitialDir = file.ptr; 13598 } 13599 13600 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 13601 { 13602 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 13603 if(okString.length && okString[$-1] == '\0') 13604 okString = okString[0..$-1]; 13605 onOK(okString); 13606 } else { 13607 if(onCancel) 13608 onCancel(); 13609 } 13610 } else version(custom_widgets) { 13611 if(filters.length == 0) 13612 filters = ["All Files\0*.*"]; 13613 auto picker = new FilePicker(prefilledName, filters, initialDirectory); 13614 picker.onOK = onOK; 13615 picker.onCancel = onCancel; 13616 picker.show(); 13617 } 13618 } 13619 13620 version(custom_widgets) 13621 private 13622 class FilePicker : Dialog { 13623 void delegate(string) onOK; 13624 void delegate() onCancel; 13625 LineEdit lineEdit; 13626 13627 enum GetFilesResult { 13628 success, 13629 fileNotFound 13630 } 13631 static GetFilesResult getFiles(string cwd, scope void delegate(string name, bool isDirectory) dg) { 13632 version(Windows) { 13633 WIN32_FIND_DATA data; 13634 WCharzBuffer search = WCharzBuffer(cwd ~ "/*"); 13635 auto handle = FindFirstFileW(search.ptr, &data); 13636 scope(exit) if(handle !is INVALID_HANDLE_VALUE) FindClose(handle); 13637 if(handle is INVALID_HANDLE_VALUE) { 13638 if(GetLastError() == ERROR_FILE_NOT_FOUND) 13639 return GetFilesResult.fileNotFound; 13640 throw new WindowsApiException("FindFirstFileW"); 13641 } 13642 13643 try_more: 13644 13645 string name = makeUtf8StringFromWindowsString(data.cFileName[0 .. findIndexOfZero(data.cFileName[])]); 13646 13647 dg(name, (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? true : false); 13648 13649 auto ret = FindNextFileW(handle, &data); 13650 if(ret == 0) { 13651 if(GetLastError() == ERROR_NO_MORE_FILES) 13652 return GetFilesResult.success; 13653 throw new WindowsApiException("FindNextFileW"); 13654 } 13655 13656 goto try_more; 13657 13658 } else version(Posix) { 13659 import core.sys.posix.dirent; 13660 auto dir = opendir((cwd ~ "\0").ptr); 13661 scope(exit) 13662 if(dir) closedir(dir); 13663 if(dir is null) 13664 throw new ErrnoApiException("opendir [" ~ cwd ~ "]"); 13665 13666 auto dirent = readdir(dir); 13667 if(dirent is null) 13668 return GetFilesResult.fileNotFound; 13669 13670 try_more: 13671 13672 string name = dirent.d_name[0 .. findIndexOfZero(dirent.d_name[])].idup; 13673 13674 dg(name, dirent.d_type == DT_DIR); 13675 13676 dirent = readdir(dir); 13677 if(dirent is null) 13678 return GetFilesResult.success; 13679 13680 goto try_more; 13681 } else static assert(0); 13682 } 13683 13684 // returns common prefix 13685 string loadFiles(string cwd, string[] filters...) { 13686 string[] files; 13687 string[] dirs; 13688 13689 string commonPrefix; 13690 13691 getFiles(cwd, (string name, bool isDirectory) { 13692 if(name == ".") 13693 return; // skip this as unnecessary 13694 if(isDirectory) 13695 dirs ~= name; 13696 else { 13697 foreach(filter; filters) 13698 if( 13699 filter.length <= 1 || 13700 filter == "*.*" || 13701 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 13702 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 13703 ) 13704 { 13705 files ~= name; 13706 13707 if(filter.length > 0 && filter[$-1] == '*') { 13708 if(commonPrefix is null) { 13709 commonPrefix = name; 13710 } else { 13711 foreach(idx, char i; name) { 13712 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 13713 commonPrefix = commonPrefix[0 .. idx]; 13714 break; 13715 } 13716 } 13717 } 13718 } 13719 13720 break; 13721 } 13722 } 13723 }); 13724 13725 extern(C) static int comparator(scope const void* a, scope const void* b) { 13726 auto sa = *cast(string*) a; 13727 auto sb = *cast(string*) b; 13728 13729 for(int i = 0; i < sa.length; i++) { 13730 if(i == sb.length) 13731 return 1; 13732 return sa[i] - sb[i]; 13733 } 13734 13735 return 0; 13736 } 13737 13738 nonPhobosSort(files, &comparator); 13739 nonPhobosSort(dirs, &comparator); 13740 13741 listWidget.clear(); 13742 dirWidget.clear(); 13743 foreach(name; dirs) 13744 dirWidget.addOption(name); 13745 foreach(name; files) 13746 listWidget.addOption(name); 13747 13748 return commonPrefix; 13749 } 13750 13751 ListWidget listWidget; 13752 ListWidget dirWidget; 13753 13754 string currentDirectory; 13755 string[] processedFilters; 13756 13757 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"] 13758 this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) { 13759 super(300, 200, "Choose File..."); // owner); 13760 13761 foreach(filter; filters) { 13762 while(filter.length && filter[0] != 0) { 13763 filter = filter[1 .. $]; 13764 } 13765 if(filter.length) 13766 filter = filter[1 .. $]; // trim off the 0 13767 13768 while(filter.length) { 13769 int idx = 0; 13770 while(idx < filter.length && filter[idx] != ';') { 13771 idx++; 13772 } 13773 13774 processedFilters ~= filter[0 .. idx]; 13775 if(idx < filter.length) 13776 idx++; // skip the ; 13777 filter = filter[idx .. $]; 13778 } 13779 } 13780 13781 currentDirectory = initialDirectory is null ? "." : initialDirectory; 13782 13783 { 13784 auto hl = new HorizontalLayout(this); 13785 dirWidget = new ListWidget(hl); 13786 listWidget = new ListWidget(hl); 13787 13788 // double click events normally trigger something else but 13789 // here user might be clicking kinda fast and we'd rather just 13790 // keep it 13791 dirWidget.addEventListener((scope DoubleClickEvent dev) { 13792 auto ce = new ChangeEvent!void(dirWidget, () {}); 13793 ce.dispatch(); 13794 }); 13795 13796 dirWidget.addEventListener((scope ChangeEvent!void sce) { 13797 string v; 13798 foreach(o; dirWidget.options) 13799 if(o.selected) { 13800 v = o.label; 13801 break; 13802 } 13803 if(v.length) { 13804 currentDirectory ~= "/" ~ v; 13805 loadFiles(currentDirectory, processedFilters); 13806 } 13807 }); 13808 13809 // double click here, on the other hand, selects the file 13810 // and moves on 13811 listWidget.addEventListener((scope DoubleClickEvent dev) { 13812 OK(); 13813 }); 13814 } 13815 13816 lineEdit = new LineEdit(this); 13817 lineEdit.focus(); 13818 lineEdit.addEventListener(delegate(CharEvent event) { 13819 if(event.character == '\t' || event.character == '\n') 13820 event.preventDefault(); 13821 }); 13822 13823 listWidget.addEventListener(EventType.change, () { 13824 foreach(o; listWidget.options) 13825 if(o.selected) 13826 lineEdit.content = o.label; 13827 }); 13828 13829 loadFiles(currentDirectory, processedFilters); 13830 13831 lineEdit.addEventListener((KeyDownEvent event) { 13832 if(event.key == Key.Tab) { 13833 13834 auto current = lineEdit.content; 13835 if(current.length >= 2 && current[0 ..2] == "./") 13836 current = current[2 .. $]; 13837 13838 auto commonPrefix = loadFiles(".", current ~ "*"); 13839 13840 if(commonPrefix.length) 13841 lineEdit.content = commonPrefix; 13842 13843 // FIXME: if that is a directory, add the slash? or even go inside? 13844 13845 event.preventDefault(); 13846 } 13847 }); 13848 13849 lineEdit.content = prefilledName; 13850 13851 auto hl = new HorizontalLayout(60, this); 13852 auto cancelButton = new Button("Cancel", hl); 13853 auto okButton = new Button("OK", hl); 13854 13855 cancelButton.addEventListener(EventType.triggered, &Cancel); 13856 okButton.addEventListener(EventType.triggered, &OK); 13857 13858 this.addEventListener((KeyDownEvent event) { 13859 if(event.key == Key.Enter || event.key == Key.PadEnter) { 13860 event.preventDefault(); 13861 OK(); 13862 } 13863 if(event.key == Key.Escape) 13864 Cancel(); 13865 }); 13866 13867 } 13868 13869 override void OK() { 13870 if(lineEdit.content.length) { 13871 string accepted; 13872 auto c = lineEdit.content; 13873 if(c.length && c[0] == '/') 13874 accepted = c; 13875 else 13876 accepted = currentDirectory ~ "/" ~ lineEdit.content; 13877 13878 if(isDir(accepted)) { 13879 // FIXME: would be kinda nice to support ~ and collapse these paths too 13880 // FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later. 13881 currentDirectory = accepted; 13882 loadFiles(currentDirectory, processedFilters); 13883 lineEdit.content = ""; 13884 return; 13885 } 13886 13887 if(onOK) 13888 onOK(accepted); 13889 } 13890 close(); 13891 } 13892 13893 override void Cancel() { 13894 if(onCancel) 13895 onCancel(); 13896 close(); 13897 } 13898 } 13899 13900 private bool isDir(string name) { 13901 version(Windows) { 13902 auto ws = WCharzBuffer(name); 13903 auto ret = GetFileAttributesW(ws.ptr); 13904 if(ret == INVALID_FILE_ATTRIBUTES) 13905 return false; 13906 return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0; 13907 } else version(Posix) { 13908 import core.sys.posix.sys.stat; 13909 stat_t buf; 13910 auto ret = stat((name ~ '\0').ptr, &buf); 13911 if(ret == -1) 13912 return false; // I could probably check more specific errors tbh 13913 return (buf.st_mode & S_IFMT) == S_IFDIR; 13914 } else return false; 13915 } 13916 13917 /* 13918 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 13919 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 13920 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 13921 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 13922 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 13923 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 13924 http://www.sbin.org/doc/Xlib/chapt_03.html 13925 13926 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 13927 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 13928 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 13929 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 13930 */ 13931 13932 13933 // These are all for setMenuAndToolbarFromAnnotatedCode 13934 /// This item in the menu will be preceded by a separator line 13935 /// Group: generating_from_code 13936 struct separator {} 13937 deprecated("It was misspelled, use separator instead") alias seperator = separator; 13938 /// Program-wide keyboard shortcut to trigger the action 13939 /// Group: generating_from_code 13940 struct accelerator { string keyString; } 13941 /// tells which menu the action will be on 13942 /// Group: generating_from_code 13943 struct menu { string name; } 13944 /// Describes which toolbar section the action appears on 13945 /// Group: generating_from_code 13946 struct toolbar { string groupName; } 13947 /// 13948 /// Group: generating_from_code 13949 struct icon { ushort id; } 13950 /// 13951 /// Group: generating_from_code 13952 struct label { string label; } 13953 /// 13954 /// Group: generating_from_code 13955 struct hotkey { dchar ch; } 13956 /// 13957 /// Group: generating_from_code 13958 struct tip { string tip; } 13959 13960 13961 /++ 13962 Observes and allows inspection of an object via automatic gui 13963 +/ 13964 /// Group: generating_from_code 13965 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 13966 return new ObjectInspectionWindowImpl!(T)(t); 13967 } 13968 13969 class ObjectInspectionWindow : Window { 13970 this(int a, int b, string c) { 13971 super(a, b, c); 13972 } 13973 13974 abstract void readUpdatesFromObject(); 13975 } 13976 13977 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 13978 T t; 13979 this(T t) { 13980 this.t = t; 13981 13982 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 13983 13984 foreach(memberName; __traits(derivedMembers, T)) {{ 13985 alias member = I!(__traits(getMember, t, memberName))[0]; 13986 alias type = typeof(member); 13987 static if(is(type == int)) { 13988 auto le = new LabeledLineEdit(memberName ~ ": ", this); 13989 //le.addEventListener("char", (Event ev) { 13990 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 13991 //ev.preventDefault(); 13992 //}); 13993 le.addEventListener(EventType.change, (Event ev) { 13994 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 13995 }); 13996 13997 updateMemberDelegates[memberName] = () { 13998 le.content = toInternal!string(__traits(getMember, t, memberName)); 13999 }; 14000 } 14001 }} 14002 } 14003 14004 void delegate()[string] updateMemberDelegates; 14005 14006 override void readUpdatesFromObject() { 14007 foreach(k, v; updateMemberDelegates) 14008 v(); 14009 } 14010 } 14011 14012 /++ 14013 Creates a dialog based on a data structure. 14014 14015 --- 14016 dialog((YourStructure value) { 14017 // the user filled in the struct and clicked OK, 14018 // you can check the members now 14019 }); 14020 --- 14021 14022 Params: 14023 initialData = the initial value to show in the dialog. It will not modify this unless 14024 it is a class then it might, no promises. 14025 14026 History: 14027 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 14028 +/ 14029 /// Group: generating_from_code 14030 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 14031 dialog(T.init, onOK, onCancel, title); 14032 } 14033 /// ditto 14034 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 14035 auto dg = new AutomaticDialog!T(initialData, onOK, onCancel, title); 14036 dg.show(); 14037 } 14038 14039 private static template I(T...) { alias I = T; } 14040 14041 14042 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 14043 if(name == "id") 14044 return allLowerCase ? name : "ID"; 14045 14046 char[160] buffer; 14047 int bufferIndex = 0; 14048 bool shouldCap = true; 14049 bool shouldSpace; 14050 bool lastWasCap; 14051 foreach(idx, char ch; name) { 14052 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 14053 14054 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 14055 if(lastWasCap) { 14056 // two caps in a row, don't change. Prolly acronym. 14057 } else { 14058 if(idx) 14059 shouldSpace = true; // new word, add space 14060 } 14061 14062 lastWasCap = true; 14063 } else { 14064 lastWasCap = false; 14065 } 14066 14067 if(shouldSpace) { 14068 buffer[bufferIndex++] = space; 14069 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 14070 shouldSpace = false; 14071 } 14072 if(shouldCap) { 14073 if(ch >= 'a' && ch <= 'z') 14074 ch -= 32; 14075 shouldCap = false; 14076 } 14077 if(allLowerCase && ch >= 'A' && ch <= 'Z') 14078 ch += 32; 14079 buffer[bufferIndex++] = ch; 14080 } 14081 return buffer[0 .. bufferIndex].idup; 14082 } 14083 14084 /++ 14085 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. 14086 +/ 14087 class AutomaticDialog(T) : Dialog { 14088 T t; 14089 14090 void delegate(T) onOK; 14091 void delegate() onCancel; 14092 14093 override int paddingTop() { return defaultLineHeight; } 14094 override int paddingBottom() { return defaultLineHeight; } 14095 override int paddingRight() { return defaultLineHeight; } 14096 override int paddingLeft() { return defaultLineHeight; } 14097 14098 this(T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 14099 assert(onOK !is null); 14100 14101 t = initialData; 14102 14103 static if(is(T == class)) { 14104 if(t is null) 14105 t = new T(); 14106 } 14107 this.onOK = onOK; 14108 this.onCancel = onCancel; 14109 super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + 4 + 2) + Window.lineHeight + 56, title); 14110 14111 static if(is(T == class)) 14112 this.addDataControllerWidget(t); 14113 else 14114 this.addDataControllerWidget(&t); 14115 14116 auto hl = new HorizontalLayout(this); 14117 auto stretch = new HorizontalSpacer(hl); // to right align 14118 auto ok = new CommandButton("OK", hl); 14119 auto cancel = new CommandButton("Cancel", hl); 14120 ok.addEventListener(EventType.triggered, &OK); 14121 cancel.addEventListener(EventType.triggered, &Cancel); 14122 14123 this.addEventListener((KeyDownEvent ev) { 14124 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 14125 ok.focus(); 14126 OK(); 14127 ev.preventDefault(); 14128 } 14129 if(ev.key == Key.Escape) { 14130 Cancel(); 14131 ev.preventDefault(); 14132 } 14133 }); 14134 14135 //this.children[0].focus(); 14136 } 14137 14138 override void OK() { 14139 onOK(t); 14140 close(); 14141 } 14142 14143 override void Cancel() { 14144 if(onCancel) 14145 onCancel(); 14146 close(); 14147 } 14148 } 14149 14150 private template baseClassCount(Class) { 14151 private int helper() { 14152 int count = 0; 14153 static if(is(Class bases == super)) { 14154 foreach(base; bases) 14155 static if(is(base == class)) 14156 count += 1 + baseClassCount!base; 14157 } 14158 return count; 14159 } 14160 14161 enum int baseClassCount = helper(); 14162 } 14163 14164 private long stringToLong(string s) { 14165 long ret; 14166 if(s.length == 0) 14167 return ret; 14168 bool negative = s[0] == '-'; 14169 if(negative) 14170 s = s[1 .. $]; 14171 foreach(ch; s) { 14172 if(ch >= '0' && ch <= '9') { 14173 ret *= 10; 14174 ret += ch - '0'; 14175 } 14176 } 14177 if(negative) 14178 ret = -ret; 14179 return ret; 14180 } 14181 14182 14183 interface ReflectableProperties { 14184 /++ 14185 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 14186 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 14187 json in the current implementation. 14188 14189 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 14190 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 14191 as of the June 2, 2021 release. 14192 14193 History: 14194 Added June 2, 2021. 14195 14196 See_Also: [getPropertyAsString], [setPropertyFromString] 14197 +/ 14198 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 14199 /++ 14200 Requests a property to be delivered to you as a string, through your `sink` delegate. 14201 14202 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 14203 be interpreted as json, otherwise, it is just a plain string. 14204 14205 The sink should always be called exactly once for each call (it is basically a return value, but it might 14206 use a local buffer it maintains instead of allocating a return value). 14207 14208 History: 14209 Added June 2, 2021. 14210 14211 See_Also: [getPropertiesList], [setPropertyFromString] 14212 +/ 14213 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 14214 /++ 14215 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. 14216 14217 History: 14218 Added June 2, 2021. 14219 14220 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 14221 +/ 14222 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 14223 14224 /// [setPropertyFromString] possible return values 14225 enum SetPropertyResult { 14226 success = 0, /// the property has been successfully set to the request value 14227 notPermitted = -1, /// the property exists but it cannot be changed at this time 14228 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 14229 noSuchProperty = -3, /// there is no property by that name 14230 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 14231 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) 14232 } 14233 14234 /++ 14235 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 14236 14237 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 14238 14239 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 14240 rarely need to use these building blocks directly. 14241 +/ 14242 mixin template RegisterSetters() { 14243 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 14244 switch(name) { 14245 foreach(memberName; __traits(derivedMembers, typeof(this))) { 14246 case memberName: 14247 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 14248 if(value != "true" && value != "false") 14249 return SetPropertyResult.wrongFormat; 14250 __traits(getMember, this, memberName) = value == "true" ? true : false; 14251 return SetPropertyResult.success; 14252 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 14253 import core.stdc.stdlib; 14254 char[128] zero = 0; 14255 if(buffer.length + 1 >= zero.length) 14256 return SetPropertyResult.wrongFormat; 14257 zero[0 .. buffer.length] = buffer[]; 14258 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 14259 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 14260 import core.stdc.stdlib; 14261 char[128] zero = 0; 14262 if(buffer.length + 1 >= zero.length) 14263 return SetPropertyResult.wrongFormat; 14264 zero[0 .. buffer.length] = buffer[]; 14265 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 14266 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 14267 __traits(getMember, this, memberName) = value.idup; 14268 } else { 14269 return SetPropertyResult.notImplemented; 14270 } 14271 14272 } 14273 default: 14274 return super.setPropertyFromString(name, value, valueIsJson); 14275 } 14276 } 14277 } 14278 14279 /++ 14280 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 14281 14282 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 14283 14284 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 14285 rarely need to use these building blocks directly. 14286 +/ 14287 mixin template RegisterGetters() { 14288 override void getPropertiesList(scope void delegate(string name) sink) const { 14289 super.getPropertiesList(sink); 14290 14291 foreach(memberName; __traits(derivedMembers, typeof(this))) { 14292 sink(memberName); 14293 } 14294 } 14295 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 14296 switch(name) { 14297 foreach(memberName; __traits(derivedMembers, typeof(this))) { 14298 case memberName: 14299 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 14300 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 14301 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 14302 import core.stdc.stdio; 14303 char[32] buffer; 14304 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 14305 sink(name, buffer[0 .. len], true); 14306 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 14307 import core.stdc.stdio; 14308 char[32] buffer; 14309 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 14310 sink(name, buffer[0 .. len], true); 14311 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 14312 sink(name, __traits(getMember, this, memberName), false); 14313 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 14314 } else { 14315 sink(name, null, true); 14316 } 14317 14318 return; 14319 } 14320 default: 14321 return super.getPropertyAsString(name, sink); 14322 } 14323 } 14324 } 14325 } 14326 14327 private struct Stack(T) { 14328 this(int maxSize) { 14329 internalLength = 0; 14330 arr = initialBuffer[]; 14331 } 14332 14333 ///. 14334 void push(T t) { 14335 if(internalLength >= arr.length) { 14336 auto oldarr = arr; 14337 if(arr.length < 4096) 14338 arr = new T[arr.length * 2]; 14339 else 14340 arr = new T[arr.length + 4096]; 14341 arr[0 .. oldarr.length] = oldarr[]; 14342 } 14343 14344 arr[internalLength] = t; 14345 internalLength++; 14346 } 14347 14348 ///. 14349 T pop() { 14350 assert(internalLength); 14351 internalLength--; 14352 return arr[internalLength]; 14353 } 14354 14355 ///. 14356 T peek() { 14357 assert(internalLength); 14358 return arr[internalLength - 1]; 14359 } 14360 14361 ///. 14362 @property bool empty() { 14363 return internalLength ? false : true; 14364 } 14365 14366 ///. 14367 private T[] arr; 14368 private size_t internalLength; 14369 private T[64] initialBuffer; 14370 // 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), 14371 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 14372 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 14373 } 14374 14375 /// 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. 14376 private struct WidgetStream { 14377 14378 ///. 14379 @property Widget front() { 14380 return current.widget; 14381 } 14382 14383 /// Use Widget.tree instead. 14384 this(Widget start) { 14385 current.widget = start; 14386 current.childPosition = -1; 14387 isEmpty = false; 14388 stack = typeof(stack)(0); 14389 } 14390 14391 /* 14392 Handle it 14393 handle its children 14394 14395 */ 14396 14397 ///. 14398 void popFront() { 14399 more: 14400 if(isEmpty) return; 14401 14402 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 14403 14404 current.childPosition++; 14405 if(current.childPosition >= current.widget.children.length) { 14406 if(stack.empty()) 14407 isEmpty = true; 14408 else { 14409 current = stack.pop(); 14410 goto more; 14411 } 14412 } else { 14413 stack.push(current); 14414 current.widget = current.widget.children[current.childPosition]; 14415 current.childPosition = -1; 14416 } 14417 } 14418 14419 ///. 14420 @property bool empty() { 14421 return isEmpty; 14422 } 14423 14424 private: 14425 14426 struct Current { 14427 Widget widget; 14428 int childPosition; 14429 } 14430 14431 Current current; 14432 14433 Stack!(Current) stack; 14434 14435 bool isEmpty; 14436 } 14437 14438 14439 /+ 14440 14441 I could fix up the hierarchy kinda like this 14442 14443 class Widget { 14444 Widget[] children() { return null; } 14445 } 14446 interface WidgetContainer { 14447 Widget asWidget(); 14448 void addChild(Widget w); 14449 14450 // alias asWidget this; // but meh 14451 } 14452 14453 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 14454 14455 class Layout : Widget, WidgetContainer {} 14456 14457 class Window : WidgetContainer {} 14458 14459 14460 All constructors that previously took Widgets should now take WidgetContainers instead 14461 14462 14463 14464 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". 14465 +/ 14466 14467 /+ 14468 LAYOUTS 2.0 14469 14470 can just be assigned as a function. assigning a new one will cause it to be immediately called. 14471 14472 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 14473 14474 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 14475 14476 and even Paint can just use computedStyle... 14477 14478 background color 14479 font 14480 border color and style 14481 14482 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 14483 please note that many widgets and in some modes will completely ignore properties as they will. 14484 they are just hints you set, not promises. 14485 14486 14487 14488 14489 14490 So generally the existing virtual functions are just the default for the class. But individual objects 14491 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 14492 +/ 14493 14494 /++ 14495 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. 14496 14497 History: 14498 Added May 24, 2021. 14499 +/ 14500 struct WidgetBackground { 14501 /++ 14502 A background with the given solid color. 14503 +/ 14504 this(Color color) { 14505 this.color = color; 14506 } 14507 14508 this(WidgetBackground bg) { 14509 this = bg; 14510 } 14511 14512 /++ 14513 Creates a widget from the string. 14514 14515 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 14516 +/ 14517 static WidgetBackground fromString(string s) { 14518 return WidgetBackground(Color.fromString(s)); 14519 } 14520 14521 private Color color; 14522 } 14523 14524 /++ 14525 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!) 14526 14527 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. 14528 14529 You should not inherit from this directly, but instead use [VisualTheme]. 14530 14531 History: 14532 Added May 8, 2021 14533 +/ 14534 abstract class BaseVisualTheme { 14535 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 14536 abstract void doPaint(Widget widget, WidgetPainter painter); 14537 14538 /+ 14539 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 14540 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 14541 +/ 14542 14543 /++ 14544 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 14545 where the interpretation of the string varies for each property and may include things like measurement units. 14546 +/ 14547 abstract string getPropertyString(Widget widget, string propertyName); 14548 14549 /++ 14550 Default background color of the window. Widgets also use this to simulate transparency. 14551 14552 Probably some shade of grey. 14553 +/ 14554 abstract Color windowBackgroundColor(); 14555 abstract Color widgetBackgroundColor(); 14556 abstract Color foregroundColor(); 14557 abstract Color lightAccentColor(); 14558 abstract Color darkAccentColor(); 14559 14560 /++ 14561 Color used to indicate active selections in lists and text boxes, etc. 14562 +/ 14563 abstract Color selectionColor(); 14564 14565 abstract OperatingSystemFont defaultFont(); 14566 14567 private OperatingSystemFont defaultFontCache_; 14568 private bool defaultFontCachePopulated; 14569 private OperatingSystemFont defaultFontCached() { 14570 if(!defaultFontCachePopulated) { 14571 // FIXME: set this to false if X disconnect or if visual theme changes 14572 defaultFontCache_ = defaultFont(); 14573 defaultFontCachePopulated = true; 14574 } 14575 return defaultFontCache_; 14576 } 14577 } 14578 14579 /+ 14580 A widget should have: 14581 classList 14582 dataset 14583 attributes 14584 computedStyles 14585 state (persistent) 14586 dynamic state (focused, hover, etc) 14587 +/ 14588 14589 // visualTheme.computedStyle(this).paddingLeft 14590 14591 14592 /++ 14593 This is your entry point to create your own visual theme for custom widgets. 14594 +/ 14595 abstract class VisualTheme(CRTP) : BaseVisualTheme { 14596 override string getPropertyString(Widget widget, string propertyName) { 14597 return null; 14598 } 14599 14600 /+ 14601 mixin StyleOverride!Widget 14602 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 14603 w.useStyleProperties(dg); 14604 } 14605 +/ 14606 14607 final override void doPaint(Widget widget, WidgetPainter painter) { 14608 auto derived = cast(CRTP) cast(void*) this; 14609 14610 scope void delegate(Widget, WidgetPainter) bestMatch; 14611 int bestMatchScore; 14612 14613 static if(__traits(hasMember, CRTP, "paint")) 14614 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 14615 static if(is(typeof(overload) Params == __parameters)) { 14616 static assert(Params.length == 2); 14617 static assert(is(Params[0] : Widget)); 14618 static assert(is(Params[1] == WidgetPainter)); 14619 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); 14620 14621 alias type = Params[0]; 14622 if(cast(type) widget) { 14623 auto score = baseClassCount!type; 14624 14625 if(score > bestMatchScore) { 14626 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 14627 bestMatchScore = score; 14628 } 14629 } 14630 } else static assert(0, "paint should be a method."); 14631 } 14632 14633 if(bestMatch) 14634 bestMatch(widget, painter); 14635 else 14636 widget.paint(painter); 14637 } 14638 14639 // 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 14640 override Color windowBackgroundColor() { return Color(212, 212, 212); } 14641 override Color widgetBackgroundColor() { return Color.white; } 14642 override Color foregroundColor() { return Color.black; } 14643 override Color darkAccentColor() { return Color(172, 172, 172); } 14644 override Color lightAccentColor() { return Color(223, 223, 223); } 14645 override Color selectionColor() { return Color(0, 0, 128); } 14646 override OperatingSystemFont defaultFont() { return null; } // will just use the default out of simpledisplay's xfontstr 14647 14648 private static struct Cached { 14649 // i prolly want to do this 14650 } 14651 } 14652 14653 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 14654 /+ 14655 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 14656 Color windowBackgroundColor() { return Color(242, 242, 242); } 14657 Color darkAccentColor() { return windowBackgroundColor; } 14658 Color lightAccentColor() { return windowBackgroundColor; } 14659 +/ 14660 } 14661 14662 /++ 14663 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 14664 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 14665 14666 History: 14667 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 14668 +/ 14669 class StateChanged(alias field) : Event { 14670 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 14671 override bool cancelable() const { return false; } 14672 this(Widget target, typeof(field) newValue) { 14673 this.newValue = newValue; 14674 super(EventString, target); 14675 } 14676 14677 typeof(field) newValue; 14678 } 14679 14680 /++ 14681 Convenience function to add a `triggered` event listener. 14682 14683 Its implementation is simply `w.addEventListener("triggered", dg);` 14684 14685 History: 14686 Added November 27, 2021 (dub v10.4) 14687 +/ 14688 void addWhenTriggered(Widget w, void delegate() dg) { 14689 w.addEventListener("triggered", dg); 14690 } 14691 14692 /++ 14693 Observable varables can be added to widgets and when they are changed, it fires 14694 off a [StateChanged] event so you can react to it. 14695 14696 It is implemented as a getter and setter property, along with another helper you 14697 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 14698 event through the usual means. Just give the name of the variable. See [StateChanged] for an 14699 example. 14700 14701 History: 14702 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 14703 +/ 14704 mixin template Observable(T, string name) { 14705 private T backing; 14706 14707 mixin(q{ 14708 void } ~ name ~ q{_changed (void delegate(T) dg) { 14709 this.addEventListener((StateChanged!this_thing ev) { 14710 dg(ev.newValue); 14711 }); 14712 } 14713 14714 @property T } ~ name ~ q{ () { 14715 return backing; 14716 } 14717 14718 @property void } ~ name ~ q{ (T t) { 14719 backing = t; 14720 auto event = new StateChanged!this_thing(this, t); 14721 event.dispatch(); 14722 } 14723 }); 14724 14725 mixin("private alias this_thing = " ~ name ~ ";"); 14726 } 14727 14728 14729 private bool startsWith(string test, string thing) { 14730 if(test.length < thing.length) 14731 return false; 14732 return test[0 .. thing.length] == thing; 14733 } 14734 14735 private bool endsWith(string test, string thing) { 14736 if(test.length < thing.length) 14737 return false; 14738 return test[$ - thing.length .. $] == thing; 14739 } 14740 14741 // still do layout delegation 14742 // and... split off Window from Widget. 14743 14744 version(minigui_screenshots) 14745 struct Screenshot { 14746 string name; 14747 } 14748 14749 version(minigui_screenshots) 14750 static if(__VERSION__ > 2092) 14751 mixin(q{ 14752 shared static this() { 14753 import core.runtime; 14754 14755 static UnitTestResult screenshotMagic() { 14756 string name; 14757 14758 import arsd.png; 14759 14760 auto results = new Window(); 14761 auto button = new Button("do it", results); 14762 14763 Window.newWindowCreated = delegate(Window w) { 14764 Timer timer; 14765 timer = new Timer(250, { 14766 auto img = w.win.takeScreenshot(); 14767 timer.destroy(); 14768 14769 version(Windows) 14770 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 14771 else 14772 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 14773 14774 w.close(); 14775 }); 14776 }; 14777 14778 button.addWhenTriggered( { 14779 14780 foreach(test; __traits(getUnitTests, mixin(__MODULE__))) { 14781 name = null; 14782 static foreach(attr; __traits(getAttributes, test)) { 14783 static if(is(typeof(attr) == Screenshot)) 14784 name = attr.name; 14785 } 14786 if(name.length) { 14787 test(); 14788 } 14789 } 14790 14791 }); 14792 14793 results.loop(); 14794 14795 return UnitTestResult(0, 0, false, false); 14796 } 14797 14798 14799 Runtime.extendedModuleUnitTester = &screenshotMagic; 14800 } 14801 }); 14802 version(minigui_screenshots) { 14803 version(unittest) 14804 void main() {} 14805 else static assert(0, "dont forget the -unittest flag to dmd"); 14806 } 14807 14808 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 14809 // FIXME: make multiple accelerators disambiguate based ona rgs 14810 // FIXME: MainWindow ctor should have same arg order as Window 14811 // FIXME: mainwindow ctor w/ client area size instead of total size. 14812 // 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. 14813 // FIXME: tri-state checkbox 14814 // FIXME: subordinate controls grouping...