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 ScreenPainter screenPainter; 3490 /// Forward to the screen painter for other methods 3491 alias screenPainter this; 3492 3493 private Widget drawingUpon; 3494 3495 /++ 3496 This is the list of rectangles that actually need to be redrawn. 3497 3498 Not actually implemented yet. 3499 +/ 3500 Rectangle[] invalidatedRectangles; 3501 3502 private static BaseVisualTheme _visualTheme; 3503 3504 /++ 3505 Functions to access the visual theme and helpers to easily use it. 3506 3507 These are aware of the current widget's computed style out of the theme. 3508 +/ 3509 static @property BaseVisualTheme visualTheme() { 3510 if(_visualTheme is null) 3511 _visualTheme = new DefaultVisualTheme(); 3512 return _visualTheme; 3513 } 3514 3515 /// ditto 3516 static @property void visualTheme(BaseVisualTheme theme) { 3517 _visualTheme = theme; 3518 } 3519 3520 /// ditto 3521 Color themeForeground() { 3522 return drawingUpon.getComputedStyle().foregroundColor(); 3523 } 3524 3525 /// ditto 3526 Color themeBackground() { 3527 return drawingUpon.getComputedStyle().background.color; 3528 } 3529 3530 int isDarkTheme() { 3531 return 0; // unspecified, yes, no as enum. FIXME 3532 } 3533 3534 /++ 3535 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. 3536 3537 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 3538 3539 If you change teh clip rectangle, you should change it back before you return. 3540 3541 3542 The sequence it uses is: 3543 background 3544 content (delegated to you) 3545 border 3546 focused outline 3547 selected overlay 3548 3549 Example code: 3550 3551 --- 3552 void paint(WidgetPainter painter) { 3553 painter.drawThemed((bounds) { 3554 return bounds; // if the selection overlay should be contained, you can return it here. 3555 }); 3556 } 3557 --- 3558 +/ 3559 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 3560 drawThemed((WidgetPainter painter, const Rectangle bounds) { 3561 return drawBody(bounds); 3562 }); 3563 } 3564 // this overload is actually mroe for setting the delegate to a virtual function 3565 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 3566 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 3567 3568 auto cs = drawingUpon.getComputedStyle(); 3569 3570 auto bg = cs.background.color; 3571 3572 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 3573 3574 rect.left += borderWidth; 3575 rect.right -= borderWidth; 3576 rect.top += borderWidth; 3577 rect.bottom -= borderWidth; 3578 3579 auto insideBorderRect = rect; 3580 3581 rect.left += cs.paddingLeft; 3582 rect.right -= cs.paddingRight; 3583 rect.top += cs.paddingTop; 3584 rect.bottom += cs.paddingBottom; 3585 3586 this.outlineColor = this.themeForeground; 3587 this.fillColor = bg; 3588 3589 auto widgetFont = cs.fontCached; 3590 if(widgetFont !is null) 3591 this.setFont(widgetFont); 3592 3593 rect = drawBody(this, rect); 3594 3595 if(widgetFont !is null) { 3596 if(auto vtFont = visualTheme.defaultFontCached) 3597 this.setFont(vtFont); 3598 else 3599 this.setFont(null); 3600 } 3601 3602 if(auto os = cs.outlineStyle()) { 3603 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 3604 this.fillColor = Color.transparent; 3605 this.drawRectangle(insideBorderRect); 3606 } 3607 } 3608 3609 /++ 3610 First, draw the background. 3611 Then draw your content. 3612 Next, draw the border. 3613 And the focused indicator. 3614 And the is-selected box. 3615 3616 If it is focused i can draw the outline too... 3617 3618 If selected i can even do the xor action but that's at the end. 3619 +/ 3620 void drawThemeBackground() { 3621 3622 } 3623 3624 void drawThemeBorder() { 3625 3626 } 3627 3628 // all this stuff is a dangerous experiment.... 3629 static class ScriptableVersion { 3630 ScreenPainterImplementation* p; 3631 int originX, originY; 3632 3633 @scriptable: 3634 void drawRectangle(int x, int y, int width, int height) { 3635 p.drawRectangle(x + originX, y + originY, width, height); 3636 } 3637 void drawLine(int x1, int y1, int x2, int y2) { 3638 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 3639 } 3640 void drawText(int x, int y, string text) { 3641 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 3642 } 3643 void setOutlineColor(int r, int g, int b) { 3644 p.pen = Pen(Color(r,g,b), 1); 3645 } 3646 void setFillColor(int r, int g, int b) { 3647 p.fillColor = Color(r,g,b); 3648 } 3649 } 3650 3651 ScriptableVersion toArsdJsvar() { 3652 auto sv = new ScriptableVersion; 3653 sv.p = this.screenPainter.impl; 3654 sv.originX = this.screenPainter.originX; 3655 sv.originY = this.screenPainter.originY; 3656 return sv; 3657 } 3658 3659 static WidgetPainter fromJsVar(T)(T t) { 3660 return WidgetPainter.init; 3661 } 3662 // done.......... 3663 } 3664 3665 3666 struct Style { 3667 static struct helper(string m, T) { 3668 enum method = m; 3669 T v; 3670 3671 mixin template MethodOverride(typeof(this) v) { 3672 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 3673 } 3674 } 3675 3676 static auto opDispatch(string method, T)(T value) { 3677 return helper!(method, T)(value); 3678 } 3679 } 3680 3681 /++ 3682 Implementation detail of the [ControlledBy] UDA. 3683 3684 History: 3685 Added Oct 28, 2020 3686 +/ 3687 struct ControlledBy_(T, Args...) { 3688 Args args; 3689 3690 static if(Args.length) 3691 this(Args args) { 3692 this.args = args; 3693 } 3694 3695 private T construct(Widget parent) { 3696 return new T(args, parent); 3697 } 3698 } 3699 3700 /++ 3701 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 3702 3703 History: 3704 Added Oct 28, 2020 3705 +/ 3706 auto ControlledBy(T, Args...)(Args args) { 3707 return ControlledBy_!(T, Args)(args); 3708 } 3709 3710 struct ContainerMeta { 3711 string name; 3712 ContainerMeta[] children; 3713 Widget function(Widget parent) factory; 3714 3715 Widget instantiate(Widget parent) { 3716 auto n = factory(parent); 3717 n.name = name; 3718 foreach(child; children) 3719 child.instantiate(n); 3720 return n; 3721 } 3722 } 3723 3724 /++ 3725 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 3726 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 3727 3728 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 3729 structures. It works fine on structs declared inside functions though. 3730 3731 See: https://issues.dlang.org/show_bug.cgi?id=21984 3732 +/ 3733 template Container(CArgs...) { 3734 static if(CArgs.length && is(CArgs[0] : Widget)) { 3735 private alias Super = CArgs[0]; 3736 private alias CArgs2 = CArgs[1 .. $]; 3737 } else { 3738 private alias Super = Layout; 3739 private alias CArgs2 = CArgs; 3740 } 3741 3742 class Container : Super { 3743 this(Widget parent) { super(parent); } 3744 3745 // just to partially support old gdc versions 3746 version(GNU) { 3747 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 3748 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 3749 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 3750 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 3751 } else mixin(q{ 3752 static foreach(Arg; CArgs2) { 3753 mixin Arg.MethodOverride!(Arg); 3754 } 3755 }); 3756 3757 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 3758 return ContainerMeta( 3759 name, 3760 children.dup, 3761 function (Widget parent) { return new typeof(this)(parent); } 3762 ); 3763 } 3764 3765 static ContainerMeta opCall(ContainerMeta[] children...) { 3766 return opCall(null, children); 3767 } 3768 } 3769 } 3770 3771 /++ 3772 The data controller widget is created by reflecting over the given 3773 data type. You can use [ControlledBy] as a UDA on a struct or 3774 just let it create things automatically. 3775 3776 Unlike [dialog], this uses real-time updating of the data and 3777 you add it to another window yourself. 3778 3779 --- 3780 struct Test { 3781 int x; 3782 int y; 3783 } 3784 3785 auto window = new Window(); 3786 auto dcw = new DataControllerWidget!Test(new Test, window); 3787 --- 3788 3789 The way it works is any public members are given a widget based 3790 on their data type, and public methods trigger an action button 3791 if no relevant parameters or a dialog action if it does have 3792 parameters, similar to the [menu] facility. 3793 3794 If you change data programmatically, without going through the 3795 DataControllerWidget methods, you will have to tell it something 3796 has changed and it needs to redraw. This is done with the `invalidate` 3797 method. 3798 3799 History: 3800 Added Oct 28, 2020 3801 +/ 3802 /// Group: generating_from_code 3803 class DataControllerWidget(T) : WidgetContainer { 3804 static if(is(T == class) || is(T : const E[], E)) 3805 private alias Tref = T; 3806 else 3807 private alias Tref = T*; 3808 3809 Tref datum; 3810 3811 /++ 3812 See_also: [addDataControllerWidget] 3813 +/ 3814 this(Tref datum, Widget parent) { 3815 this.datum = datum; 3816 3817 Widget cp = this; 3818 3819 super(parent); 3820 3821 foreach(attr; __traits(getAttributes, T)) 3822 static if(is(typeof(attr) == ContainerMeta)) { 3823 cp = attr.instantiate(this); 3824 } 3825 3826 auto def = this.getByName("default"); 3827 if(def !is null) 3828 cp = def; 3829 3830 Widget helper(string name) { 3831 auto maybe = this.getByName(name); 3832 if(maybe is null) 3833 return cp; 3834 return maybe; 3835 3836 } 3837 3838 foreach(member; __traits(allMembers, T)) 3839 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 3840 static if(is(typeof(__traits(getMember, this.datum, member)))) 3841 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 3842 void delegate() update; 3843 3844 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 3845 3846 if(update) 3847 updaters ~= update; 3848 3849 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 3850 w.addEventListener("triggered", delegate() { 3851 makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))(); 3852 notifyDataUpdated(); 3853 }); 3854 } else static if(is(typeof(w.isChecked) == bool)) { 3855 w.addEventListener(EventType.change, (Event ev) { 3856 __traits(getMember, this.datum, member) = w.isChecked; 3857 }); 3858 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 3859 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 3860 } else static if(is(typeof(w.value) == int)) { 3861 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 3862 } else static if(is(typeof(w) == DropDownSelection)) { 3863 // special case for this to kinda support enums and such. coudl be better though 3864 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 3865 } else { 3866 static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 3867 } 3868 } 3869 } 3870 3871 /++ 3872 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 3873 3874 History: 3875 Added May 28, 2021 3876 +/ 3877 void notifyDataUpdated() { 3878 foreach(updater; updaters) 3879 updater(); 3880 3881 this.emit!(ChangeEvent!void)(delegate{}); 3882 } 3883 3884 private Widget[string] memberWidgets; 3885 private void delegate()[] updaters; 3886 3887 mixin Emits!(ChangeEvent!void); 3888 } 3889 3890 private int saturatedSum(int[] values...) { 3891 int sum; 3892 foreach(value; values) { 3893 if(value == int.max) 3894 return int.max; 3895 sum += value; 3896 } 3897 return sum; 3898 } 3899 3900 void genericSetValue(T, W)(T* where, W what) { 3901 import std.conv; 3902 *where = to!T(what); 3903 //*where = cast(T) stringToLong(what); 3904 } 3905 3906 /++ 3907 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 3908 3909 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 3910 3911 Note that this creates the widget but does not attach any event handlers to it. 3912 +/ 3913 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 3914 3915 string displayName = __traits(identifier, tt).beautify; 3916 3917 static if(controlledByCount!tt == 1) { 3918 foreach(i, attr; __traits(getAttributes, tt)) { 3919 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 3920 auto w = attr.construct(parent); 3921 static if(__traits(compiles, w.setPosition(*valptr))) 3922 update = () { w.setPosition(*valptr); }; 3923 else static if(__traits(compiles, w.setValue(*valptr))) 3924 update = () { w.setValue(*valptr); }; 3925 3926 if(update) 3927 update(); 3928 return w; 3929 } 3930 } 3931 } else static if(controlledByCount!tt == 0) { 3932 static if(is(typeof(tt) == enum)) { 3933 // FIXME: update 3934 auto dds = new DropDownSelection(parent); 3935 foreach(idx, option; __traits(allMembers, typeof(tt))) { 3936 dds.addOption(option); 3937 if(__traits(getMember, typeof(tt), option) == *valptr) 3938 dds.setSelection(cast(int) idx); 3939 } 3940 return dds; 3941 } else static if(is(typeof(tt) == bool)) { 3942 auto box = new Checkbox(displayName, parent); 3943 update = () { box.isChecked = *valptr; }; 3944 update(); 3945 return box; 3946 } else static if(is(typeof(tt) : const long)) { 3947 auto le = new LabeledLineEdit(displayName, parent); 3948 update = () { le.content = toInternal!string(*valptr); }; 3949 update(); 3950 return le; 3951 } else static if(is(typeof(tt) : const string)) { 3952 auto le = new LabeledLineEdit(displayName, parent); 3953 update = () { le.content = *valptr; }; 3954 update(); 3955 return le; 3956 } else static if(is(typeof(tt) == function)) { 3957 auto w = new Button(displayName, parent); 3958 return w; 3959 } 3960 } else static assert(0, "multiple controllers not yet supported"); 3961 } 3962 3963 private template controlledByCount(alias tt) { 3964 static int helper() { 3965 int count; 3966 foreach(i, attr; __traits(getAttributes, tt)) 3967 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 3968 count++; 3969 return count; 3970 } 3971 3972 enum controlledByCount = helper; 3973 } 3974 3975 /++ 3976 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 3977 3978 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 3979 3980 History: 3981 The `redrawOnChange` parameter was added on May 28, 2021. 3982 +/ 3983 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class)) { 3984 auto dcw = new DataControllerWidget!T(t, parent); 3985 initializeDataControllerWidget(dcw, redrawOnChange); 3986 return dcw; 3987 } 3988 3989 /// ditto 3990 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 3991 auto dcw = new DataControllerWidget!T(t, parent); 3992 initializeDataControllerWidget(dcw, redrawOnChange); 3993 return dcw; 3994 } 3995 3996 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 3997 if(redrawOnChange !is null) 3998 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 3999 } 4000 4001 /++ 4002 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. 4003 4004 History: 4005 Finalized on June 3, 2021 for the dub v10.0 release 4006 +/ 4007 struct StyleInformation { 4008 private Widget w; 4009 private BaseVisualTheme visualTheme; 4010 4011 private this(Widget w) { 4012 this.w = w; 4013 this.visualTheme = WidgetPainter.visualTheme; 4014 } 4015 4016 /++ 4017 Forwards to [Widget.Style] 4018 4019 Bugs: 4020 It is supposed to fall back to the [VisualTheme] if 4021 the style doesn't override the default, but that is 4022 not generally implemented. Many of them may end up 4023 being explicit overloads instead of the generic 4024 opDispatch fallback, like [font] is now. 4025 +/ 4026 public @property opDispatch(string name)() { 4027 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4028 w.useStyleProperties((scope Widget.Style props) { 4029 //visualTheme.useStyleProperties(w, (props) { 4030 prop = __traits(getMember, props, name); 4031 }); 4032 return prop; 4033 } 4034 4035 /++ 4036 Returns the cached font object associated with the widget, 4037 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 4038 4039 History: 4040 Prior to March 21, 2022 (dub v10.7), `font` went through 4041 [opDispatch], which did not use the cache. You can now call it 4042 repeatedly without guilt. 4043 +/ 4044 public @property OperatingSystemFont font() { 4045 OperatingSystemFont prop; 4046 w.useStyleProperties((scope Widget.Style props) { 4047 prop = props.fontCached; 4048 }); 4049 if(prop is null) 4050 prop = visualTheme.defaultFontCached; 4051 return prop; 4052 } 4053 4054 @property { 4055 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 4056 /** */ int paddingLeft() { return w.paddingLeft(); } 4057 /** */ int paddingRight() { return w.paddingRight(); } 4058 /** */ int paddingTop() { return w.paddingTop(); } 4059 /** */ int paddingBottom() { return w.paddingBottom(); } 4060 4061 /** */ int marginLeft() { return w.marginLeft(); } 4062 /** */ int marginRight() { return w.marginRight(); } 4063 /** */ int marginTop() { return w.marginTop(); } 4064 /** */ int marginBottom() { return w.marginBottom(); } 4065 4066 /** */ int maxHeight() { return w.maxHeight(); } 4067 /** */ int minHeight() { return w.minHeight(); } 4068 4069 /** */ int maxWidth() { return w.maxWidth(); } 4070 /** */ int minWidth() { return w.minWidth(); } 4071 4072 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 4073 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 4074 4075 /** */ int heightStretchiness() { return w.heightStretchiness(); } 4076 /** */ int widthStretchiness() { return w.widthStretchiness(); } 4077 4078 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 4079 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 4080 4081 // Global helpers some of these are unstable. 4082 static: 4083 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4084 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4085 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4086 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4087 4088 /** */ Color activeTabColor() { return lightAccentColor; } 4089 /** */ Color buttonColor() { return windowBackgroundColor; } 4090 /** */ Color depressedButtonColor() { return darkAccentColor; } 4091 /** */ Color hoveringColor() { return Color(228, 228, 228); } 4092 /** */ Color activeListXorColor() { 4093 auto c = WidgetPainter.visualTheme.selectionColor(); 4094 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4095 } 4096 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4097 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4098 } 4099 4100 4101 4102 /+ 4103 4104 private static auto extractStyleProperty(string name)(Widget w) { 4105 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4106 w.useStyleProperties((props) { 4107 prop = __traits(getMember, props, name); 4108 }); 4109 return prop; 4110 } 4111 4112 // FIXME: clear this upon a X server disconnect 4113 private static OperatingSystemFont[string] fontCache; 4114 4115 T getProperty(T)(string name, lazy T default_) { 4116 if(visualTheme !is null) { 4117 auto str = visualTheme.getPropertyString(w, name); 4118 if(str is null) 4119 return default_; 4120 static if(is(T == Color)) 4121 return Color.fromString(str); 4122 else static if(is(T == Measurement)) 4123 return Measurement(cast(int) toInternal!int(str)); 4124 else static if(is(T == WidgetBackground)) 4125 return WidgetBackground.fromString(str); 4126 else static if(is(T == OperatingSystemFont)) { 4127 if(auto f = str in fontCache) 4128 return *f; 4129 else 4130 return fontCache[str] = new OperatingSystemFont(str); 4131 } else static if(is(T == FrameStyle)) { 4132 switch(str) { 4133 default: 4134 return FrameStyle.none; 4135 foreach(style; __traits(allMembers, FrameStyle)) 4136 case style: 4137 return __traits(getMember, FrameStyle, style); 4138 } 4139 } else static assert(0); 4140 } else 4141 return default_; 4142 } 4143 4144 static struct Measurement { 4145 int value; 4146 alias value this; 4147 } 4148 4149 @property: 4150 4151 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 4152 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 4153 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 4154 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 4155 4156 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 4157 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 4158 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 4159 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 4160 4161 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 4162 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 4163 4164 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 4165 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 4166 4167 4168 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 4169 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 4170 4171 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 4172 4173 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 4174 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 4175 4176 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 4177 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 4178 4179 4180 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4181 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4182 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4183 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4184 4185 Color activeTabColor() { return lightAccentColor; } 4186 Color buttonColor() { return windowBackgroundColor; } 4187 Color depressedButtonColor() { return darkAccentColor; } 4188 Color hoveringColor() { return Color(228, 228, 228); } 4189 Color activeListXorColor() { 4190 auto c = WidgetPainter.visualTheme.selectionColor(); 4191 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4192 } 4193 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4194 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4195 +/ 4196 } 4197 4198 4199 4200 // pragma(msg, __traits(classInstanceSize, Widget)); 4201 4202 /*private*/ template EventString(E) { 4203 static if(is(typeof(E.EventString))) 4204 enum EventString = E.EventString; 4205 else 4206 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 4207 } 4208 4209 /*private*/ template EventStringIdentifier(E) { 4210 string helper() { 4211 auto es = EventString!E; 4212 char[] id = new char[](es.length * 2); 4213 size_t idx; 4214 foreach(char ch; es) { 4215 id[idx++] = cast(char)('a' + (ch >> 4)); 4216 id[idx++] = cast(char)('a' + (ch & 0x0f)); 4217 } 4218 return cast(string) id; 4219 } 4220 4221 enum EventStringIdentifier = helper(); 4222 } 4223 4224 4225 template classStaticallyEmits(This, EventType) { 4226 static if(is(This Base == super)) 4227 static if(is(Base : Widget)) 4228 enum baseEmits = classStaticallyEmits!(Base, EventType); 4229 else 4230 enum baseEmits = false; 4231 else 4232 enum baseEmits = false; 4233 4234 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 4235 4236 enum classStaticallyEmits = thisEmits || baseEmits; 4237 } 4238 4239 /++ 4240 A helper to make widgets out of other native windows. 4241 4242 History: 4243 Factored out of OpenGlWidget on November 5, 2021 4244 +/ 4245 class NestedChildWindowWidget : Widget { 4246 SimpleWindow win; 4247 4248 /++ 4249 Used on X to send focus to the appropriate child window when requested by the window manager. 4250 4251 Normally returns its own nested window. Can also return another child or null to revert to the parent 4252 if you override it in a child class. 4253 4254 History: 4255 Added April 2, 2022 (dub v10.8) 4256 +/ 4257 SimpleWindow focusableWindow() { 4258 return win; 4259 } 4260 4261 /// 4262 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4263 this(SimpleWindow win, Widget parent) { 4264 this.parentWindow = parent.parentWindow; 4265 this.win = win; 4266 4267 super(parent); 4268 windowsetup(win); 4269 } 4270 4271 static protected SimpleWindow getParentWindow(Widget parent) { 4272 assert(parent !is null); 4273 SimpleWindow pwin = parent.parentWindow.win; 4274 4275 version(win32_widgets) { 4276 HWND phwnd; 4277 auto wtf = parent; 4278 while(wtf) { 4279 if(wtf.hwnd) { 4280 phwnd = wtf.hwnd; 4281 break; 4282 } 4283 wtf = wtf.parent; 4284 } 4285 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 4286 if(phwnd) 4287 pwin = new SimpleWindow(phwnd); 4288 } 4289 4290 return pwin; 4291 } 4292 4293 /++ 4294 Called upon the nested window being destroyed. 4295 Remember the window has already been destroyed at 4296 this point, so don't use the native handle for anything. 4297 4298 History: 4299 Added April 3, 2022 (dub v10.8) 4300 +/ 4301 protected void dispose() { 4302 4303 } 4304 4305 protected void windowsetup(SimpleWindow w) { 4306 /* 4307 win.onFocusChange = (bool getting) { 4308 if(getting) 4309 this.focus(); 4310 }; 4311 */ 4312 4313 /+ 4314 win.onFocusChange = (bool getting) { 4315 if(getting) { 4316 this.parentWindow.focusedWidget = this; 4317 this.emit!FocusEvent(); 4318 this.emit!FocusInEvent(); 4319 } else { 4320 this.emit!BlurEvent(); 4321 this.emit!FocusOutEvent(); 4322 } 4323 }; 4324 +/ 4325 4326 win.onDestroyed = () { 4327 this.dispose(); 4328 }; 4329 4330 version(win32_widgets) { 4331 Widget.nativeMapping[win.hwnd] = this; 4332 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4333 } else { 4334 win.setEventHandlers( 4335 (MouseEvent e) { 4336 Widget p = this; 4337 while(p ! is parentWindow) { 4338 e.x += p.x; 4339 e.y += p.y; 4340 p = p.parent; 4341 } 4342 parentWindow.dispatchMouseEvent(e); 4343 }, 4344 (KeyEvent e) { 4345 //import std.stdio; writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 4346 parentWindow.dispatchKeyEvent(e); 4347 }, 4348 (dchar e) { 4349 parentWindow.dispatchCharEvent(e); 4350 }, 4351 ); 4352 } 4353 4354 } 4355 4356 override void showing(bool s, bool recalc) { 4357 auto cur = hidden; 4358 win.hidden = !s; 4359 if(cur != s && s) 4360 redraw(); 4361 } 4362 4363 /// OpenGL widgets cannot have child widgets. Do not call this. 4364 /* @disable */ final override void addChild(Widget, int) { 4365 throw new Error("cannot add children to OpenGL widgets"); 4366 } 4367 4368 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 4369 /// Keep in mind that events like mouse coordinates are still relative to your size. 4370 override void registerMovement() { 4371 //import std.stdio; writefln("%d %d %d %d", x,y,width,height); 4372 version(win32_widgets) 4373 auto pos = getChildPositionRelativeToParentHwnd(this); 4374 else 4375 auto pos = getChildPositionRelativeToParentOrigin(this); 4376 win.moveResize(pos[0], pos[1], width, height); 4377 4378 registerMovementAdditionalWork(); 4379 sendResizeEvent(); 4380 } 4381 4382 abstract void registerMovementAdditionalWork(); 4383 } 4384 4385 /++ 4386 Nests an opengl capable window inside this window as a widget. 4387 4388 You may also just want to create an additional [SimpleWindow] with 4389 [OpenGlOptions.yes] yourself. 4390 4391 An OpenGL widget cannot have child widgets. It will throw if you try. 4392 +/ 4393 static if(OpenGlEnabled) 4394 class OpenGlWidget : NestedChildWindowWidget { 4395 4396 override void registerMovementAdditionalWork() { 4397 win.setAsCurrentOpenGlContext(); 4398 } 4399 4400 /// 4401 this(Widget parent) { 4402 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4403 super(win, parent); 4404 } 4405 4406 override void paint(WidgetPainter painter) { 4407 win.setAsCurrentOpenGlContext(); 4408 glViewport(0, 0, this.width, this.height); 4409 win.redrawOpenGlSceneNow(); 4410 } 4411 4412 void redrawOpenGlScene(void delegate() dg) { 4413 win.redrawOpenGlScene = dg; 4414 } 4415 } 4416 4417 /++ 4418 This demo shows how to draw text in an opengl scene. 4419 +/ 4420 unittest { 4421 import arsd.minigui; 4422 import arsd.ttf; 4423 4424 void main() { 4425 auto window = new Window(); 4426 4427 auto widget = new OpenGlWidget(window); 4428 4429 // old means non-shader code so compatible with glBegin etc. 4430 // tbh I haven't implemented new one in font yet... 4431 // anyway, declaring here, will construct soon. 4432 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 4433 4434 // this is a little bit awkward, calling some methods through 4435 // the underlying SimpleWindow `win` method, and you can't do this 4436 // on a nanovega widget due to conflicts so I should probably fix 4437 // the api to be a bit easier. But here it will work. 4438 // 4439 // Alternatively, you could load the font on the first draw, inside 4440 // the redrawOpenGlScene, and keep a flag so you don't do it every 4441 // time. That'd be a bit easier since the lib sets up the context 4442 // by then guaranteed. 4443 // 4444 // But still, I wanna show this. 4445 widget.win.visibleForTheFirstTime = delegate { 4446 // must set the opengl context 4447 widget.win.setAsCurrentOpenGlContext(); 4448 4449 // if you were doing a OpenGL 3+ shader, this 4450 // gets especially important to do in order. With 4451 // old-style opengl, I think you can even do it 4452 // in main(), but meh, let's show it more correctly. 4453 4454 // Anyway, now it is time to load the font from the 4455 // OS (you can alternatively load one from a .ttf file 4456 // you bundle with the application), then load the 4457 // font into texture for drawing. 4458 4459 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 4460 4461 assert(!osfont.isNull()); // make sure it actually loaded 4462 4463 // using typeof to avoid repeating the long name lol 4464 glfont = new typeof(glfont)( 4465 // get the raw data from the font for loading in here 4466 // since it doesn't use the OS function to draw the 4467 // text, we gotta treat it more as a file than as 4468 // a drawing api. 4469 osfont.getTtfBytes(), 4470 18, // need to respecify size since opengl world is different coordinate system 4471 4472 // these last two numbers are why it is called 4473 // "Limited" font. It only loads the characters 4474 // in the given range, since the texture atlas 4475 // it references is all a big image generated ahead 4476 // of time. You could maybe do the whole thing but 4477 // idk how much memory that is. 4478 // 4479 // But here, 0-128 represents the ASCII range, so 4480 // good enough for most English things, numeric labels, 4481 // etc. 4482 0, 4483 128 4484 ); 4485 }; 4486 4487 widget.redrawOpenGlScene = () { 4488 // now we can use the glfont's drawString function 4489 4490 // first some opengl setup. You can do this in one place 4491 // on window first visible too in many cases, just showing 4492 // here cuz it is easier for me. 4493 4494 // gonna need some alpha blending or it just looks awful 4495 glEnable(GL_BLEND); 4496 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 4497 glClearColor(0,0,0,0); 4498 glDepthFunc(GL_LEQUAL); 4499 4500 // Also need to enable 2d textures, since it draws the 4501 // font characters as images baked in 4502 glMatrixMode(GL_MODELVIEW); 4503 glLoadIdentity(); 4504 glDisable(GL_DEPTH_TEST); 4505 glEnable(GL_TEXTURE_2D); 4506 4507 // the orthographic matrix is best for 2d things like text 4508 // so let's set that up. This matrix makes the coordinates 4509 // in the opengl scene be one-to-one with the actual pixels 4510 // on screen. (Not necessarily best, you may wish to scale 4511 // things, but it does help keep fonts looking normal.) 4512 glMatrixMode(GL_PROJECTION); 4513 glLoadIdentity(); 4514 glOrtho(0, widget.width, widget.height, 0, 0, 1); 4515 4516 // you can do other glScale, glRotate, glTranslate, etc 4517 // to the matrix here of course if you want. 4518 4519 // note the x,y coordinates here are for the text baseline 4520 // NOT the upper-left corner. The baseline is like the line 4521 // in the notebook you write on. Most the letters are actually 4522 // above it, but some, like p and q, dip a bit below it. 4523 // 4524 // So if you're used to the upper left coordinate like the 4525 // rest of simpledisplay/minigui usually do, do the 4526 // y + glfont.ascent to bring it down a little. So this 4527 // example puts the string in the upper left of the window. 4528 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 4529 4530 // re color btw: the function sets a solid color internally, 4531 // but you actually COULD do your own thing for rainbow effects 4532 // and the sort if you wanted too, by pulling its guts out. 4533 // Just view its source for an idea of how it actually draws: 4534 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 4535 4536 // it gets a bit complicated with the character positioning, 4537 // but the opengl parts are fairly simple: bind a texture, 4538 // set the color, draw a quad for each letter. 4539 4540 4541 // the last optional argument there btw is a bounding box 4542 // it will/ use to word wrap and return an object you can 4543 // use to implement scrolling or pagination; it tells how 4544 // much of the string didn't fit in the box. But for simple 4545 // labels we can just ignore that. 4546 4547 4548 // I'd suggest drawing text as the last step, after you 4549 // do your other drawing. You might use the push/pop matrix 4550 // stuff to keep your place. You, in theory, should be able 4551 // to do text in a 3d space but I've never actually tried 4552 // that.... 4553 }; 4554 4555 window.loop(); 4556 } 4557 } 4558 4559 version(custom_widgets) 4560 private alias ListWidgetBase = ScrollableWidget; 4561 else 4562 private alias ListWidgetBase = Widget; 4563 4564 /++ 4565 A list widget contains a list of strings that the user can examine and select. 4566 4567 4568 In the future, items in the list may be possible to be more than just strings. 4569 4570 See_Also: 4571 [TableView] 4572 +/ 4573 class ListWidget : ListWidgetBase { 4574 /// 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. 4575 mixin Emits!(ChangeEvent!void); 4576 4577 static struct Option { 4578 string label; 4579 bool selected; 4580 void* tag; 4581 } 4582 4583 /++ 4584 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 4585 +/ 4586 void setSelection(int y) { 4587 if(!multiSelect) 4588 foreach(ref opt; options) 4589 opt.selected = false; 4590 if(y >= 0 && y < options.length) 4591 options[y].selected = !options[y].selected; 4592 4593 this.emit!(ChangeEvent!void)(delegate {}); 4594 4595 version(custom_widgets) 4596 redraw(); 4597 } 4598 4599 /++ 4600 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 4601 Returns -1 if nothing is selected. 4602 +/ 4603 int getSelection() 4604 { 4605 foreach(i, opt; options) { 4606 if (opt.selected) 4607 return cast(int) i; 4608 } 4609 return -1; 4610 } 4611 4612 version(custom_widgets) 4613 override void defaultEventHandler_click(ClickEvent event) { 4614 this.focus(); 4615 if(event.button == MouseButton.left) { 4616 auto y = (event.clientY - 4) / defaultLineHeight; 4617 if(y >= 0 && y < options.length) { 4618 setSelection(y); 4619 } 4620 } 4621 super.defaultEventHandler_click(event); 4622 } 4623 4624 this(Widget parent) { 4625 tabStop = false; 4626 super(parent); 4627 version(win32_widgets) 4628 createWin32Window(this, WC_LISTBOX, "", 4629 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 4630 } 4631 4632 version(win32_widgets) 4633 override void handleWmCommand(ushort code, ushort id) { 4634 switch(code) { 4635 case LBN_SELCHANGE: 4636 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 4637 setSelection(cast(int) sel); 4638 break; 4639 default: 4640 } 4641 } 4642 4643 4644 version(custom_widgets) 4645 override void paintFrameAndBackground(WidgetPainter painter) { 4646 draw3dFrame(this, painter, FrameStyle.sunk, Color.white); 4647 } 4648 4649 version(custom_widgets) 4650 override void paint(WidgetPainter painter) { 4651 auto cs = getComputedStyle(); 4652 auto pos = Point(4, 4); 4653 foreach(idx, option; options) { 4654 painter.fillColor = Color.white; 4655 painter.outlineColor = Color.white; 4656 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4657 painter.outlineColor = cs.foregroundColor; 4658 painter.drawText(pos, option.label); 4659 if(option.selected) { 4660 painter.rasterOp = RasterOp.xor; 4661 painter.outlineColor = Color.white; 4662 painter.fillColor = cs.activeListXorColor; 4663 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4664 painter.rasterOp = RasterOp.normal; 4665 } 4666 pos.y += defaultLineHeight; 4667 } 4668 } 4669 4670 static class Style : Widget.Style { 4671 override WidgetBackground background() { 4672 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 4673 } 4674 } 4675 mixin OverrideStyle!Style; 4676 //mixin Padding!q{2}; 4677 4678 void addOption(string text, void* tag = null) { 4679 options ~= Option(text, false, tag); 4680 version(win32_widgets) { 4681 WCharzBuffer buffer = WCharzBuffer(text); 4682 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 4683 } 4684 version(custom_widgets) { 4685 setContentSize(width, cast(int) (options.length * defaultLineHeight)); 4686 redraw(); 4687 } 4688 } 4689 4690 void clear() { 4691 options = null; 4692 version(win32_widgets) { 4693 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 4694 {} 4695 4696 } else version(custom_widgets) { 4697 scrollTo(Point(0, 0)); 4698 redraw(); 4699 } 4700 } 4701 4702 Option[] options; 4703 version(win32_widgets) 4704 enum multiSelect = false; /// not implemented yet 4705 else 4706 bool multiSelect; 4707 4708 override int heightStretchiness() { return 6; } 4709 } 4710 4711 4712 4713 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 4714 enum ScrollBarShowPolicy { 4715 automatic, /// automatically show the scroll bar if it is necessary 4716 never, /// never show the scroll bar (scrolling must be done programmatically) 4717 always /// always show the scroll bar, even if it is disabled 4718 } 4719 4720 /++ 4721 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 4722 4723 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 4724 +/ 4725 // FIXME ScrollBarShowPolicy 4726 // FIXME: use the ScrollMessageWidget in here now that it exists 4727 class ScrollableWidget : Widget { 4728 // FIXME: make line size configurable 4729 // FIXME: add keyboard controls 4730 version(win32_widgets) { 4731 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 4732 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 4733 auto pos = HIWORD(wParam); 4734 auto m = LOWORD(wParam); 4735 4736 // FIXME: I can reintroduce the 4737 // scroll bars now by using this 4738 // in the top-level window handler 4739 // to forward comamnds 4740 auto scrollbarHwnd = lParam; 4741 switch(m) { 4742 case SB_BOTTOM: 4743 if(msg == WM_HSCROLL) 4744 horizontalScrollTo(contentWidth_); 4745 else 4746 verticalScrollTo(contentHeight_); 4747 break; 4748 case SB_TOP: 4749 if(msg == WM_HSCROLL) 4750 horizontalScrollTo(0); 4751 else 4752 verticalScrollTo(0); 4753 break; 4754 case SB_ENDSCROLL: 4755 // idk 4756 break; 4757 case SB_LINEDOWN: 4758 if(msg == WM_HSCROLL) 4759 horizontalScroll(scaleWithDpi(16)); 4760 else 4761 verticalScroll(scaleWithDpi(16)); 4762 break; 4763 case SB_LINEUP: 4764 if(msg == WM_HSCROLL) 4765 horizontalScroll(scaleWithDpi(-16)); 4766 else 4767 verticalScroll(scaleWithDpi(-16)); 4768 break; 4769 case SB_PAGEDOWN: 4770 if(msg == WM_HSCROLL) 4771 horizontalScroll(scaleWithDpi(100)); 4772 else 4773 verticalScroll(scaleWithDpi(100)); 4774 break; 4775 case SB_PAGEUP: 4776 if(msg == WM_HSCROLL) 4777 horizontalScroll(scaleWithDpi(-100)); 4778 else 4779 verticalScroll(scaleWithDpi(-100)); 4780 break; 4781 case SB_THUMBPOSITION: 4782 case SB_THUMBTRACK: 4783 if(msg == WM_HSCROLL) 4784 horizontalScrollTo(pos); 4785 else 4786 verticalScrollTo(pos); 4787 4788 if(m == SB_THUMBTRACK) { 4789 // the event loop doesn't seem to carry on with a requested redraw.. 4790 // so we request it to get our dirty bit set... 4791 redraw(); 4792 4793 // then we need to immediately actually redraw it too for instant feedback to user 4794 4795 SimpleWindow.processAllCustomEvents(); 4796 //if(parentWindow) 4797 //parentWindow.actualRedraw(); 4798 } 4799 break; 4800 default: 4801 } 4802 } 4803 return super.hookedWndProc(msg, wParam, lParam); 4804 } 4805 } 4806 /// 4807 this(Widget parent) { 4808 this.parentWindow = parent.parentWindow; 4809 4810 version(win32_widgets) { 4811 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 4812 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 4813 super(parent); 4814 } else version(custom_widgets) { 4815 outerContainer = new InternalScrollableContainerWidget(this, parent); 4816 super(outerContainer); 4817 } else static assert(0); 4818 } 4819 4820 version(custom_widgets) 4821 InternalScrollableContainerWidget outerContainer; 4822 4823 override void defaultEventHandler_click(ClickEvent event) { 4824 if(event.button == MouseButton.wheelUp) 4825 verticalScroll(scaleWithDpi(-16)); 4826 if(event.button == MouseButton.wheelDown) 4827 verticalScroll(scaleWithDpi(16)); 4828 super.defaultEventHandler_click(event); 4829 } 4830 4831 override void defaultEventHandler_keydown(KeyDownEvent event) { 4832 switch(event.key) { 4833 case Key.Left: 4834 horizontalScroll(scaleWithDpi(-16)); 4835 break; 4836 case Key.Right: 4837 horizontalScroll(scaleWithDpi(16)); 4838 break; 4839 case Key.Up: 4840 verticalScroll(scaleWithDpi(-16)); 4841 break; 4842 case Key.Down: 4843 verticalScroll(scaleWithDpi(16)); 4844 break; 4845 case Key.Home: 4846 verticalScrollTo(0); 4847 break; 4848 case Key.End: 4849 verticalScrollTo(contentHeight); 4850 break; 4851 case Key.PageUp: 4852 verticalScroll(scaleWithDpi(-160)); 4853 break; 4854 case Key.PageDown: 4855 verticalScroll(scaleWithDpi(160)); 4856 break; 4857 default: 4858 } 4859 super.defaultEventHandler_keydown(event); 4860 } 4861 4862 4863 version(win32_widgets) 4864 override void recomputeChildLayout() { 4865 super.recomputeChildLayout(); 4866 SCROLLINFO info; 4867 info.cbSize = info.sizeof; 4868 info.nPage = viewportHeight; 4869 info.fMask = SIF_PAGE | SIF_RANGE; 4870 info.nMin = 0; 4871 info.nMax = contentHeight_; 4872 SetScrollInfo(hwnd, SB_VERT, &info, true); 4873 4874 info.cbSize = info.sizeof; 4875 info.nPage = viewportWidth; 4876 info.fMask = SIF_PAGE | SIF_RANGE; 4877 info.nMin = 0; 4878 info.nMax = contentWidth_; 4879 SetScrollInfo(hwnd, SB_HORZ, &info, true); 4880 } 4881 4882 /* 4883 Scrolling 4884 ------------ 4885 4886 You are assigned a width and a height by the layout engine, which 4887 is your viewport box. However, you may draw more than that by setting 4888 a contentWidth and contentHeight. 4889 4890 If these can be contained by the viewport, no scrollbar is displayed. 4891 If they cannot fit though, it will automatically show scroll as necessary. 4892 4893 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 4894 is zero, no vertical scrolling is performed. 4895 4896 If scrolling is necessary, the lib will automatically work with the bars. 4897 When you redraw, the origin and clipping info in the painter is set so if 4898 you just draw everything, it will work, but you can be more efficient by checking 4899 the viewportWidth, viewportHeight, and scrollOrigin members. 4900 */ 4901 4902 /// 4903 final @property int viewportWidth() { 4904 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 4905 } 4906 /// 4907 final @property int viewportHeight() { 4908 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 4909 } 4910 4911 // FIXME property 4912 Point scrollOrigin_; 4913 4914 /// 4915 final const(Point) scrollOrigin() { 4916 return scrollOrigin_; 4917 } 4918 4919 // the user sets these two 4920 private int contentWidth_ = 0; 4921 private int contentHeight_ = 0; 4922 4923 /// 4924 int contentWidth() { return contentWidth_; } 4925 /// 4926 int contentHeight() { return contentHeight_; } 4927 4928 /// 4929 void setContentSize(int width, int height) { 4930 contentWidth_ = width; 4931 contentHeight_ = height; 4932 4933 version(custom_widgets) { 4934 if(showingVerticalScroll || showingHorizontalScroll) { 4935 outerContainer.recomputeChildLayout(); 4936 } 4937 4938 if(showingVerticalScroll()) 4939 outerContainer.verticalScrollBar.redraw(); 4940 if(showingHorizontalScroll()) 4941 outerContainer.horizontalScrollBar.redraw(); 4942 } else version(win32_widgets) { 4943 recomputeChildLayout(); 4944 } else static assert(0); 4945 } 4946 4947 /// 4948 void verticalScroll(int delta) { 4949 verticalScrollTo(scrollOrigin.y + delta); 4950 } 4951 /// 4952 void verticalScrollTo(int pos) { 4953 scrollOrigin_.y = pos; 4954 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 4955 scrollOrigin_.y = contentHeight - viewportHeight; 4956 4957 if(scrollOrigin_.y < 0) 4958 scrollOrigin_.y = 0; 4959 4960 version(win32_widgets) { 4961 SCROLLINFO info; 4962 info.cbSize = info.sizeof; 4963 info.fMask = SIF_POS; 4964 info.nPos = scrollOrigin_.y; 4965 SetScrollInfo(hwnd, SB_VERT, &info, true); 4966 } else version(custom_widgets) { 4967 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 4968 } else static assert(0); 4969 4970 redraw(); 4971 } 4972 4973 /// 4974 void horizontalScroll(int delta) { 4975 horizontalScrollTo(scrollOrigin.x + delta); 4976 } 4977 /// 4978 void horizontalScrollTo(int pos) { 4979 scrollOrigin_.x = pos; 4980 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 4981 scrollOrigin_.x = contentWidth - viewportWidth; 4982 4983 if(scrollOrigin_.x < 0) 4984 scrollOrigin_.x = 0; 4985 4986 version(win32_widgets) { 4987 SCROLLINFO info; 4988 info.cbSize = info.sizeof; 4989 info.fMask = SIF_POS; 4990 info.nPos = scrollOrigin_.x; 4991 SetScrollInfo(hwnd, SB_HORZ, &info, true); 4992 } else version(custom_widgets) { 4993 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 4994 } else static assert(0); 4995 4996 redraw(); 4997 } 4998 /// 4999 void scrollTo(Point p) { 5000 verticalScrollTo(p.y); 5001 horizontalScrollTo(p.x); 5002 } 5003 5004 /// 5005 void ensureVisibleInScroll(Point p) { 5006 auto rect = viewportRectangle(); 5007 if(rect.contains(p)) 5008 return; 5009 if(p.x < rect.left) 5010 horizontalScroll(p.x - rect.left); 5011 else if(p.x > rect.right) 5012 horizontalScroll(p.x - rect.right); 5013 5014 if(p.y < rect.top) 5015 verticalScroll(p.y - rect.top); 5016 else if(p.y > rect.bottom) 5017 verticalScroll(p.y - rect.bottom); 5018 } 5019 5020 /// 5021 void ensureVisibleInScroll(Rectangle rect) { 5022 ensureVisibleInScroll(rect.upperLeft); 5023 ensureVisibleInScroll(rect.lowerRight); 5024 } 5025 5026 /// 5027 Rectangle viewportRectangle() { 5028 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 5029 } 5030 5031 /// 5032 bool showingHorizontalScroll() { 5033 return contentWidth > width; 5034 } 5035 /// 5036 bool showingVerticalScroll() { 5037 return contentHeight > height; 5038 } 5039 5040 /// This is called before the ordinary paint delegate, 5041 /// giving you a chance to draw the window frame, etc, 5042 /// before the scroll clip takes effect 5043 void paintFrameAndBackground(WidgetPainter painter) { 5044 version(win32_widgets) { 5045 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5046 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5047 // since the pen is null, to fill the whole space, we need the +1 on both. 5048 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5049 SelectObject(painter.impl.hdc, p); 5050 SelectObject(painter.impl.hdc, b); 5051 } 5052 5053 } 5054 5055 // make space for the scroll bar, and that's it. 5056 final override int paddingRight() { return scaleWithDpi(16); } 5057 final override int paddingBottom() { return scaleWithDpi(16); } 5058 5059 /* 5060 END SCROLLING 5061 */ 5062 5063 override WidgetPainter draw() { 5064 int x = this.x, y = this.y; 5065 auto parent = this.parent; 5066 while(parent) { 5067 x += parent.x; 5068 y += parent.y; 5069 parent = parent.parent; 5070 } 5071 5072 //version(win32_widgets) { 5073 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5074 //} else { 5075 auto painter = parentWindow.win.draw(true); 5076 //} 5077 painter.originX = x; 5078 painter.originY = y; 5079 5080 painter.originX = painter.originX - scrollOrigin.x; 5081 painter.originY = painter.originY - scrollOrigin.y; 5082 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 5083 5084 return WidgetPainter(painter, this); 5085 } 5086 5087 mixin ScrollableChildren; 5088 } 5089 5090 // you need to have a Point scrollOrigin in the class somewhere 5091 // and a paintFrameAndBackground 5092 private mixin template ScrollableChildren() { 5093 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5094 if(hidden) 5095 return; 5096 5097 //version(win32_widgets) 5098 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5099 5100 painter.originX = lox + x; 5101 painter.originY = loy + y; 5102 5103 bool actuallyPainted = false; 5104 5105 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 5106 if(clip == Rectangle.init) 5107 return; 5108 5109 if(force || redrawRequested) { 5110 //painter.setClipRectangle(scrollOrigin, width, height); 5111 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5112 paintFrameAndBackground(painter); 5113 } 5114 5115 painter.originX = painter.originX - scrollOrigin.x; 5116 painter.originY = painter.originY - scrollOrigin.y; 5117 if(force || redrawRequested) { 5118 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 5119 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 5120 5121 //erase(painter); // we paintFrameAndBackground above so no need 5122 if(painter.visualTheme) 5123 painter.visualTheme.doPaint(this, painter); 5124 else 5125 paint(painter); 5126 5127 if(invalidate) { 5128 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5129 // children are contained inside this, so no need to do extra work 5130 invalidate = false; 5131 } 5132 5133 5134 actuallyPainted = true; 5135 redrawRequested = false; 5136 } 5137 foreach(child; children) { 5138 if(cast(FixedPosition) child) 5139 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5140 else 5141 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5142 } 5143 } 5144 } 5145 5146 private class InternalScrollableContainerInsideWidget : ContainerWidget { 5147 ScrollableContainerWidget scw; 5148 5149 this(ScrollableContainerWidget parent) { 5150 scw = parent; 5151 super(parent); 5152 } 5153 5154 version(custom_widgets) 5155 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5156 if(hidden) 5157 return; 5158 5159 bool actuallyPainted = false; 5160 5161 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 5162 5163 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 5164 if(clip == Rectangle.init) 5165 return; 5166 5167 painter.originX = lox + x - scrollOrigin.x; 5168 painter.originY = loy + y - scrollOrigin.y; 5169 if(force || redrawRequested) { 5170 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5171 5172 erase(painter); 5173 if(painter.visualTheme) 5174 painter.visualTheme.doPaint(this, painter); 5175 else 5176 paint(painter); 5177 5178 if(invalidate) { 5179 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5180 // children are contained inside this, so no need to do extra work 5181 invalidate = false; 5182 } 5183 5184 actuallyPainted = true; 5185 redrawRequested = false; 5186 } 5187 foreach(child; children) { 5188 if(cast(FixedPosition) child) 5189 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5190 else 5191 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5192 } 5193 } 5194 5195 version(custom_widgets) 5196 override protected void addScrollPosition(ref int x, ref int y) { 5197 x += scw.scrollX_; 5198 y += scw.scrollY_; 5199 } 5200 } 5201 5202 /++ 5203 A widget meant to contain other widgets that may need to scroll. 5204 5205 Currently buggy. 5206 5207 History: 5208 Added July 1, 2021 (dub v10.2) 5209 5210 On January 3, 2022, I tried to use it in a few other cases 5211 and found it only worked well in the original test case. Since 5212 it still sucks, I think I'm going to rewrite it again. 5213 +/ 5214 class ScrollableContainerWidget : ContainerWidget { 5215 /// 5216 this(Widget parent) { 5217 super(parent); 5218 5219 container = new InternalScrollableContainerInsideWidget(this); 5220 hsb = new HorizontalScrollbar(this); 5221 vsb = new VerticalScrollbar(this); 5222 5223 tabStop = false; 5224 container.tabStop = false; 5225 magic = true; 5226 5227 5228 vsb.addEventListener("scrolltonextline", () { 5229 scrollBy(0, scaleWithDpi(16)); 5230 }); 5231 vsb.addEventListener("scrolltopreviousline", () { 5232 scrollBy(0,scaleWithDpi( -16)); 5233 }); 5234 vsb.addEventListener("scrolltonextpage", () { 5235 scrollBy(0, container.height); 5236 }); 5237 vsb.addEventListener("scrolltopreviouspage", () { 5238 scrollBy(0, -container.height); 5239 }); 5240 vsb.addEventListener((scope ScrollToPositionEvent spe) { 5241 scrollTo(scrollX_, spe.value); 5242 }); 5243 5244 this.addEventListener(delegate (scope ClickEvent e) { 5245 if(e.button == MouseButton.wheelUp) { 5246 if(!e.defaultPrevented) 5247 scrollBy(0, scaleWithDpi(-16)); 5248 e.stopPropagation(); 5249 } else if(e.button == MouseButton.wheelDown) { 5250 if(!e.defaultPrevented) 5251 scrollBy(0, scaleWithDpi(16)); 5252 e.stopPropagation(); 5253 } 5254 }); 5255 } 5256 5257 /+ 5258 override void defaultEventHandler_click(ClickEvent e) { 5259 } 5260 +/ 5261 5262 override void removeAllChildren() { 5263 container.removeAllChildren(); 5264 } 5265 5266 void scrollTo(int x, int y) { 5267 scrollBy(x - scrollX_, y - scrollY_); 5268 } 5269 5270 void scrollBy(int x, int y) { 5271 auto ox = scrollX_; 5272 auto oy = scrollY_; 5273 5274 auto nx = ox + x; 5275 auto ny = oy + y; 5276 5277 if(nx < 0) 5278 nx = 0; 5279 if(ny < 0) 5280 ny = 0; 5281 5282 auto maxX = hsb.max - container.width; 5283 if(maxX < 0) maxX = 0; 5284 auto maxY = vsb.max - container.height; 5285 if(maxY < 0) maxY = 0; 5286 5287 if(nx > maxX) 5288 nx = maxX; 5289 if(ny > maxY) 5290 ny = maxY; 5291 5292 auto dx = nx - ox; 5293 auto dy = ny - oy; 5294 5295 if(dx || dy) { 5296 version(win32_widgets) 5297 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 5298 else { 5299 redraw(); 5300 } 5301 5302 hsb.setPosition = nx; 5303 vsb.setPosition = ny; 5304 5305 scrollX_ = nx; 5306 scrollY_ = ny; 5307 } 5308 } 5309 5310 private int scrollX_; 5311 private int scrollY_; 5312 5313 void setTotalArea(int width, int height) { 5314 hsb.setMax(width); 5315 vsb.setMax(height); 5316 } 5317 5318 /// 5319 void setViewableArea(int width, int height) { 5320 hsb.setViewableArea(width); 5321 vsb.setViewableArea(height); 5322 } 5323 5324 private bool magic; 5325 override void addChild(Widget w, int position = int.max) { 5326 if(magic) 5327 container.addChild(w, position); 5328 else 5329 super.addChild(w, position); 5330 } 5331 5332 override void recomputeChildLayout() { 5333 if(hsb is null || vsb is null || container is null) return; 5334 5335 /+ 5336 import std.stdio; writeln(x, " ", y , " ", width, " ", height); 5337 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 5338 +/ 5339 5340 registerMovement(); 5341 5342 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 5343 hsb.x = 0; 5344 hsb.y = this.height - hsb.height; 5345 hsb.width = this.width - scaleWithDpi(16); 5346 hsb.recomputeChildLayout(); 5347 5348 vsb.width = scaleWithDpi(16); // FIXME? 5349 vsb.x = this.width - vsb.width; 5350 vsb.y = 0; 5351 vsb.height = this.height - scaleWithDpi(16); 5352 vsb.recomputeChildLayout(); 5353 5354 container.x = 0; 5355 container.y = 0; 5356 container.width = this.width - vsb.width; 5357 container.height = this.height - hsb.height; 5358 container.recomputeChildLayout(); 5359 5360 scrollX_ = 0; 5361 scrollY_ = 0; 5362 5363 hsb.setPosition(0); 5364 vsb.setPosition(0); 5365 5366 int mw, mh; 5367 Widget c = container; 5368 // FIXME: hack here to handle a layout inside... 5369 if(c.children.length == 1 && cast(Layout) c.children[0]) 5370 c = c.children[0]; 5371 foreach(child; c.children) { 5372 auto w = child.x + child.width; 5373 auto h = child.y + child.height; 5374 5375 if(w > mw) mw = w; 5376 if(h > mh) mh = h; 5377 } 5378 5379 setTotalArea(mw, mh); 5380 setViewableArea(width, height); 5381 } 5382 5383 override int minHeight() { return scaleWithDpi(64); } 5384 5385 HorizontalScrollbar hsb; 5386 VerticalScrollbar vsb; 5387 ContainerWidget container; 5388 } 5389 5390 5391 version(custom_widgets) 5392 private class InternalScrollableContainerWidget : Widget { 5393 5394 ScrollableWidget sw; 5395 5396 VerticalScrollbar verticalScrollBar; 5397 HorizontalScrollbar horizontalScrollBar; 5398 5399 this(ScrollableWidget sw, Widget parent) { 5400 this.sw = sw; 5401 5402 this.tabStop = false; 5403 5404 horizontalScrollBar = new HorizontalScrollbar(this); 5405 verticalScrollBar = new VerticalScrollbar(this); 5406 5407 horizontalScrollBar.showing_ = false; 5408 verticalScrollBar.showing_ = false; 5409 5410 horizontalScrollBar.addEventListener("scrolltonextline", { 5411 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 5412 sw.horizontalScrollTo(horizontalScrollBar.position); 5413 }); 5414 horizontalScrollBar.addEventListener("scrolltopreviousline", { 5415 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 5416 sw.horizontalScrollTo(horizontalScrollBar.position); 5417 }); 5418 verticalScrollBar.addEventListener("scrolltonextline", { 5419 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 5420 sw.verticalScrollTo(verticalScrollBar.position); 5421 }); 5422 verticalScrollBar.addEventListener("scrolltopreviousline", { 5423 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 5424 sw.verticalScrollTo(verticalScrollBar.position); 5425 }); 5426 horizontalScrollBar.addEventListener("scrolltonextpage", { 5427 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 5428 sw.horizontalScrollTo(horizontalScrollBar.position); 5429 }); 5430 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 5431 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 5432 sw.horizontalScrollTo(horizontalScrollBar.position); 5433 }); 5434 verticalScrollBar.addEventListener("scrolltonextpage", { 5435 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 5436 sw.verticalScrollTo(verticalScrollBar.position); 5437 }); 5438 verticalScrollBar.addEventListener("scrolltopreviouspage", { 5439 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 5440 sw.verticalScrollTo(verticalScrollBar.position); 5441 }); 5442 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 5443 horizontalScrollBar.setPosition(event.intValue); 5444 sw.horizontalScrollTo(horizontalScrollBar.position); 5445 }); 5446 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 5447 verticalScrollBar.setPosition(event.intValue); 5448 sw.verticalScrollTo(verticalScrollBar.position); 5449 }); 5450 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 5451 horizontalScrollBar.setPosition(event.intValue); 5452 sw.horizontalScrollTo(horizontalScrollBar.position); 5453 }); 5454 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 5455 verticalScrollBar.setPosition(event.intValue); 5456 }); 5457 5458 super(parent); 5459 } 5460 5461 // this is supposed to be basically invisible... 5462 override int minWidth() { return sw.minWidth; } 5463 override int minHeight() { return sw.minHeight; } 5464 override int maxWidth() { return sw.maxWidth; } 5465 override int maxHeight() { return sw.maxHeight; } 5466 override int widthStretchiness() { return sw.widthStretchiness; } 5467 override int heightStretchiness() { return sw.heightStretchiness; } 5468 override int marginLeft() { return sw.marginLeft; } 5469 override int marginRight() { return sw.marginRight; } 5470 override int marginTop() { return sw.marginTop; } 5471 override int marginBottom() { return sw.marginBottom; } 5472 override int paddingLeft() { return sw.paddingLeft; } 5473 override int paddingRight() { return sw.paddingRight; } 5474 override int paddingTop() { return sw.paddingTop; } 5475 override int paddingBottom() { return sw.paddingBottom; } 5476 override void focus() { sw.focus(); } 5477 5478 5479 override void recomputeChildLayout() { 5480 // The stupid thing needs to calculate if a scroll bar is needed... 5481 recomputeChildLayoutHelper(); 5482 // then running it again will position things correctly if the bar is NOT needed 5483 recomputeChildLayoutHelper(); 5484 5485 // this sucks but meh it barely works 5486 } 5487 5488 private void recomputeChildLayoutHelper() { 5489 if(sw is null) return; 5490 5491 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 5492 if(horizontalScrollBar && verticalScrollBar) { 5493 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 5494 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 5495 horizontalScrollBar.x = 0; 5496 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 5497 5498 verticalScrollBar.width = verticalScrollBar.minWidth(); 5499 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 5500 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 5501 verticalScrollBar.y = 0 + 2; 5502 5503 sw.x = 0; 5504 sw.y = 0; 5505 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 5506 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 5507 5508 if(sw.contentWidth_ <= this.width) 5509 sw.scrollOrigin_.x = 0; 5510 if(sw.contentHeight_ <= this.height) 5511 sw.scrollOrigin_.y = 0; 5512 5513 horizontalScrollBar.recomputeChildLayout(); 5514 verticalScrollBar.recomputeChildLayout(); 5515 sw.recomputeChildLayout(); 5516 } 5517 5518 if(sw.contentWidth_ <= this.width) 5519 sw.scrollOrigin_.x = 0; 5520 if(sw.contentHeight_ <= this.height) 5521 sw.scrollOrigin_.y = 0; 5522 5523 if(sw.showingHorizontalScroll()) 5524 horizontalScrollBar.showing(true, false); 5525 else 5526 horizontalScrollBar.showing(false, false); 5527 if(sw.showingVerticalScroll()) 5528 verticalScrollBar.showing(true, false); 5529 else 5530 verticalScrollBar.showing(false, false); 5531 5532 verticalScrollBar.setViewableArea(sw.viewportHeight()); 5533 verticalScrollBar.setMax(sw.contentHeight); 5534 verticalScrollBar.setPosition(sw.scrollOrigin.y); 5535 5536 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 5537 horizontalScrollBar.setMax(sw.contentWidth); 5538 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 5539 } 5540 } 5541 5542 /* 5543 class ScrollableClientWidget : Widget { 5544 this(Widget parent) { 5545 super(parent); 5546 } 5547 override void paint(WidgetPainter p) { 5548 parent.paint(p); 5549 } 5550 } 5551 */ 5552 5553 /++ 5554 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. 5555 +/ 5556 abstract class Slider : Widget { 5557 this(int min, int max, int step, Widget parent) { 5558 min_ = min; 5559 max_ = max; 5560 step_ = step; 5561 page_ = step; 5562 super(parent); 5563 } 5564 5565 private int min_; 5566 private int max_; 5567 private int step_; 5568 private int position_; 5569 private int page_; 5570 5571 // selection start and selection end 5572 // tics 5573 // tooltip? 5574 // some way to see and just type the value 5575 // win32 buddy controls are labels 5576 5577 /// 5578 void setMin(int a) { 5579 min_ = a; 5580 version(custom_widgets) 5581 redraw(); 5582 version(win32_widgets) 5583 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 5584 } 5585 /// 5586 int min() { 5587 return min_; 5588 } 5589 /// 5590 void setMax(int a) { 5591 max_ = a; 5592 version(custom_widgets) 5593 redraw(); 5594 version(win32_widgets) 5595 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 5596 } 5597 /// 5598 int max() { 5599 return max_; 5600 } 5601 /// 5602 void setPosition(int a) { 5603 if(a > max) 5604 a = max; 5605 if(a < min) 5606 a = min; 5607 position_ = a; 5608 version(custom_widgets) 5609 setPositionCustom(a); 5610 5611 version(win32_widgets) 5612 setPositionWindows(a); 5613 } 5614 version(win32_widgets) { 5615 protected abstract void setPositionWindows(int a); 5616 } 5617 5618 protected abstract int win32direction(); 5619 5620 /++ 5621 Alias for [position] for better compatibility with generic code. 5622 5623 History: 5624 Added October 5, 2021 5625 +/ 5626 @property int value() { 5627 return position; 5628 } 5629 5630 /// 5631 int position() { 5632 return position_; 5633 } 5634 /// 5635 void setStep(int a) { 5636 step_ = a; 5637 version(win32_widgets) 5638 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 5639 } 5640 /// 5641 int step() { 5642 return step_; 5643 } 5644 /// 5645 void setPageSize(int a) { 5646 page_ = a; 5647 version(win32_widgets) 5648 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 5649 } 5650 /// 5651 int pageSize() { 5652 return page_; 5653 } 5654 5655 private void notify() { 5656 auto event = new ChangeEvent!int(this, &this.position); 5657 event.dispatch(); 5658 } 5659 5660 version(win32_widgets) 5661 void win32Setup(int style) { 5662 createWin32Window(this, TRACKBAR_CLASS, "", 5663 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 5664 5665 // the trackbar sends the same messages as scroll, which 5666 // our other layer sends as these... just gonna translate 5667 // here 5668 this.addDirectEventListener("scrolltoposition", (Event event) { 5669 event.stopPropagation(); 5670 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 5671 notify(); 5672 }); 5673 this.addDirectEventListener("scrolltonextline", (Event event) { 5674 event.stopPropagation(); 5675 this.setPosition(this.position + this.step_ * this.win32direction); 5676 notify(); 5677 }); 5678 this.addDirectEventListener("scrolltopreviousline", (Event event) { 5679 event.stopPropagation(); 5680 this.setPosition(this.position - this.step_ * this.win32direction); 5681 notify(); 5682 }); 5683 this.addDirectEventListener("scrolltonextpage", (Event event) { 5684 event.stopPropagation(); 5685 this.setPosition(this.position + this.page_ * this.win32direction); 5686 notify(); 5687 }); 5688 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 5689 event.stopPropagation(); 5690 this.setPosition(this.position - this.page_ * this.win32direction); 5691 notify(); 5692 }); 5693 5694 setMin(min_); 5695 setMax(max_); 5696 setStep(step_); 5697 setPageSize(page_); 5698 } 5699 5700 version(custom_widgets) { 5701 protected MouseTrackingWidget thumb; 5702 5703 protected abstract void setPositionCustom(int a); 5704 5705 override void defaultEventHandler_keydown(KeyDownEvent event) { 5706 switch(event.key) { 5707 case Key.Up: 5708 case Key.Right: 5709 setPosition(position() - step() * win32direction); 5710 changed(); 5711 break; 5712 case Key.Down: 5713 case Key.Left: 5714 setPosition(position() + step() * win32direction); 5715 changed(); 5716 break; 5717 case Key.Home: 5718 setPosition(win32direction > 0 ? min() : max()); 5719 changed(); 5720 break; 5721 case Key.End: 5722 setPosition(win32direction > 0 ? max() : min()); 5723 changed(); 5724 break; 5725 case Key.PageUp: 5726 setPosition(position() - pageSize() * win32direction); 5727 changed(); 5728 break; 5729 case Key.PageDown: 5730 setPosition(position() + pageSize() * win32direction); 5731 changed(); 5732 break; 5733 default: 5734 } 5735 super.defaultEventHandler_keydown(event); 5736 } 5737 5738 protected void changed() { 5739 auto ev = new ChangeEvent!int(this, &position); 5740 ev.dispatch(); 5741 } 5742 } 5743 } 5744 5745 /++ 5746 5747 +/ 5748 class VerticalSlider : Slider { 5749 this(int min, int max, int step, Widget parent) { 5750 version(custom_widgets) 5751 initialize(); 5752 5753 super(min, max, step, parent); 5754 5755 version(win32_widgets) 5756 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 5757 } 5758 5759 protected override int win32direction() { 5760 return -1; 5761 } 5762 5763 version(win32_widgets) 5764 protected override void setPositionWindows(int a) { 5765 // the windows thing makes the top 0 and i don't like that. 5766 SendMessage(hwnd, TBM_SETPOS, true, max - a); 5767 } 5768 5769 version(custom_widgets) 5770 private void initialize() { 5771 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 5772 5773 thumb.tabStop = false; 5774 5775 thumb.thumbWidth = width; 5776 thumb.thumbHeight = scaleWithDpi(16); 5777 5778 thumb.addEventListener(EventType.change, () { 5779 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 5780 sx = max - sx; 5781 //informProgramThatUserChangedPosition(sx); 5782 5783 position_ = sx; 5784 5785 changed(); 5786 }); 5787 } 5788 5789 version(custom_widgets) 5790 override void recomputeChildLayout() { 5791 thumb.thumbWidth = this.width; 5792 super.recomputeChildLayout(); 5793 setPositionCustom(position_); 5794 } 5795 5796 version(custom_widgets) 5797 protected override void setPositionCustom(int a) { 5798 if(max()) 5799 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 5800 redraw(); 5801 } 5802 } 5803 5804 /++ 5805 5806 +/ 5807 class HorizontalSlider : Slider { 5808 this(int min, int max, int step, Widget parent) { 5809 version(custom_widgets) 5810 initialize(); 5811 5812 super(min, max, step, parent); 5813 5814 version(win32_widgets) 5815 win32Setup(TBS_HORZ); 5816 } 5817 5818 version(win32_widgets) 5819 protected override void setPositionWindows(int a) { 5820 SendMessage(hwnd, TBM_SETPOS, true, a); 5821 } 5822 5823 protected override int win32direction() { 5824 return 1; 5825 } 5826 5827 version(custom_widgets) 5828 private void initialize() { 5829 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 5830 5831 thumb.tabStop = false; 5832 5833 thumb.thumbWidth = scaleWithDpi(16); 5834 thumb.thumbHeight = height; 5835 5836 thumb.addEventListener(EventType.change, () { 5837 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 5838 //informProgramThatUserChangedPosition(sx); 5839 5840 position_ = sx; 5841 5842 changed(); 5843 }); 5844 } 5845 5846 version(custom_widgets) 5847 override void recomputeChildLayout() { 5848 thumb.thumbHeight = this.height; 5849 super.recomputeChildLayout(); 5850 setPositionCustom(position_); 5851 } 5852 5853 version(custom_widgets) 5854 protected override void setPositionCustom(int a) { 5855 if(max()) 5856 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 5857 redraw(); 5858 } 5859 } 5860 5861 5862 /// 5863 abstract class ScrollbarBase : Widget { 5864 /// 5865 this(Widget parent) { 5866 super(parent); 5867 tabStop = false; 5868 step_ = scaleWithDpi(16); 5869 } 5870 5871 private int viewableArea_; 5872 private int max_; 5873 private int step_;// = 16; 5874 private int position_; 5875 5876 /// 5877 bool atEnd() { 5878 return position_ + viewableArea_ >= max_; 5879 } 5880 5881 /// 5882 bool atStart() { 5883 return position_ == 0; 5884 } 5885 5886 /// 5887 void setViewableArea(int a) { 5888 viewableArea_ = a; 5889 version(custom_widgets) 5890 redraw(); 5891 } 5892 /// 5893 void setMax(int a) { 5894 max_ = a; 5895 version(custom_widgets) 5896 redraw(); 5897 } 5898 /// 5899 int max() { 5900 return max_; 5901 } 5902 /// 5903 void setPosition(int a) { 5904 if(a == int.max) 5905 a = max; 5906 position_ = max ? a : 0; 5907 if(position_ + viewableArea_ > max) 5908 position_ = max - viewableArea_; 5909 if(position_ < 0) 5910 position_ = 0; 5911 version(custom_widgets) 5912 redraw(); 5913 } 5914 /// 5915 int position() { 5916 return position_; 5917 } 5918 /// 5919 void setStep(int a) { 5920 step_ = a; 5921 } 5922 /// 5923 int step() { 5924 return step_; 5925 } 5926 5927 // FIXME: remove this.... maybe 5928 /+ 5929 protected void informProgramThatUserChangedPosition(int n) { 5930 position_ = n; 5931 auto evt = new Event(EventType.change, this); 5932 evt.intValue = n; 5933 evt.dispatch(); 5934 } 5935 +/ 5936 5937 version(custom_widgets) { 5938 abstract protected int getBarDim(); 5939 int thumbSize() { 5940 if(viewableArea_ >= max_) 5941 return getBarDim(); 5942 5943 int res; 5944 if(max_) { 5945 res = getBarDim() * viewableArea_ / max_; 5946 } 5947 if(res < 6) 5948 res = 6; 5949 5950 return res; 5951 } 5952 5953 int thumbPosition() { 5954 /* 5955 viewableArea_ is the viewport height/width 5956 position_ is where we are 5957 */ 5958 if(max_) { 5959 if(position_ + viewableArea_ >= max_) 5960 return getBarDim - thumbSize; 5961 return getBarDim * position_ / max_; 5962 } 5963 return 0; 5964 } 5965 } 5966 } 5967 5968 //public import mgt; 5969 5970 /++ 5971 A mouse tracking widget is one that follows the mouse when dragged inside it. 5972 5973 Concrete subclasses may include a scrollbar thumb and a volume control. 5974 +/ 5975 //version(custom_widgets) 5976 class MouseTrackingWidget : Widget { 5977 5978 /// 5979 int positionX() { return positionX_; } 5980 /// 5981 int positionY() { return positionY_; } 5982 5983 /// 5984 void positionX(int p) { positionX_ = p; } 5985 /// 5986 void positionY(int p) { positionY_ = p; } 5987 5988 private int positionX_; 5989 private int positionY_; 5990 5991 /// 5992 enum Orientation { 5993 horizontal, /// 5994 vertical, /// 5995 twoDimensional, /// 5996 } 5997 5998 private int thumbWidth_; 5999 private int thumbHeight_; 6000 6001 /// 6002 int thumbWidth() { return thumbWidth_; } 6003 /// 6004 int thumbHeight() { return thumbHeight_; } 6005 /// 6006 int thumbWidth(int a) { return thumbWidth_ = a; } 6007 /// 6008 int thumbHeight(int a) { return thumbHeight_ = a; } 6009 6010 private bool dragging; 6011 private bool hovering; 6012 private int startMouseX, startMouseY; 6013 6014 /// 6015 this(Orientation orientation, Widget parent) { 6016 super(parent); 6017 6018 //assert(parentWindow !is null); 6019 6020 addEventListener((MouseDownEvent event) { 6021 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6022 dragging = true; 6023 startMouseX = event.clientX - positionX; 6024 startMouseY = event.clientY - positionY; 6025 parentWindow.captureMouse(this); 6026 } else { 6027 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6028 positionX = event.clientX - thumbWidth / 2; 6029 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6030 positionY = event.clientY - thumbHeight / 2; 6031 6032 if(positionX + thumbWidth > this.width) 6033 positionX = this.width - thumbWidth; 6034 if(positionY + thumbHeight > this.height) 6035 positionY = this.height - thumbHeight; 6036 6037 if(positionX < 0) 6038 positionX = 0; 6039 if(positionY < 0) 6040 positionY = 0; 6041 6042 6043 // this.emit!(ChangeEvent!void)(); 6044 auto evt = new Event(EventType.change, this); 6045 evt.sendDirectly(); 6046 6047 redraw(); 6048 6049 } 6050 }); 6051 6052 addEventListener(EventType.mouseup, (Event event) { 6053 dragging = false; 6054 parentWindow.releaseMouseCapture(); 6055 }); 6056 6057 addEventListener(EventType.mouseout, (Event event) { 6058 if(!hovering) 6059 return; 6060 hovering = false; 6061 redraw(); 6062 }); 6063 6064 int lpx, lpy; 6065 6066 addEventListener((MouseMoveEvent event) { 6067 auto oh = hovering; 6068 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6069 hovering = true; 6070 } else { 6071 hovering = false; 6072 } 6073 if(!dragging) { 6074 if(hovering != oh) 6075 redraw(); 6076 return; 6077 } 6078 6079 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6080 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 6081 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6082 positionY = event.clientY - startMouseY; 6083 6084 if(positionX + thumbWidth > this.width) 6085 positionX = this.width - thumbWidth; 6086 if(positionY + thumbHeight > this.height) 6087 positionY = this.height - thumbHeight; 6088 6089 if(positionX < 0) 6090 positionX = 0; 6091 if(positionY < 0) 6092 positionY = 0; 6093 6094 if(positionX != lpx || positionY != lpy) { 6095 auto evt = new Event(EventType.change, this); 6096 evt.sendDirectly(); 6097 6098 lpx = positionX; 6099 lpy = positionY; 6100 } 6101 6102 redraw(); 6103 }); 6104 } 6105 6106 version(custom_widgets) 6107 override void paint(WidgetPainter painter) { 6108 auto cs = getComputedStyle(); 6109 auto c = darken(cs.windowBackgroundColor, 0.2); 6110 painter.outlineColor = c; 6111 painter.fillColor = c; 6112 painter.drawRectangle(Point(0, 0), this.width, this.height); 6113 6114 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 6115 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 6116 } 6117 } 6118 6119 //version(custom_widgets) 6120 //private 6121 class HorizontalScrollbar : ScrollbarBase { 6122 6123 version(custom_widgets) { 6124 private MouseTrackingWidget thumb; 6125 6126 override int getBarDim() { 6127 return thumb.width; 6128 } 6129 } 6130 6131 override void setViewableArea(int a) { 6132 super.setViewableArea(a); 6133 6134 version(win32_widgets) { 6135 SCROLLINFO info; 6136 info.cbSize = info.sizeof; 6137 info.nPage = a + 1; 6138 info.fMask = SIF_PAGE; 6139 SetScrollInfo(hwnd, SB_CTL, &info, true); 6140 } else version(custom_widgets) { 6141 thumb.positionX = thumbPosition; 6142 thumb.thumbWidth = thumbSize; 6143 thumb.redraw(); 6144 } else static assert(0); 6145 6146 } 6147 6148 override void setMax(int a) { 6149 super.setMax(a); 6150 version(win32_widgets) { 6151 SCROLLINFO info; 6152 info.cbSize = info.sizeof; 6153 info.nMin = 0; 6154 info.nMax = max; 6155 info.fMask = SIF_RANGE; 6156 SetScrollInfo(hwnd, SB_CTL, &info, true); 6157 } else version(custom_widgets) { 6158 thumb.positionX = thumbPosition; 6159 thumb.thumbWidth = thumbSize; 6160 thumb.redraw(); 6161 } 6162 } 6163 6164 override void setPosition(int a) { 6165 super.setPosition(a); 6166 version(win32_widgets) { 6167 SCROLLINFO info; 6168 info.cbSize = info.sizeof; 6169 info.fMask = SIF_POS; 6170 info.nPos = position; 6171 SetScrollInfo(hwnd, SB_CTL, &info, true); 6172 } else version(custom_widgets) { 6173 thumb.positionX = thumbPosition(); 6174 thumb.thumbWidth = thumbSize; 6175 thumb.redraw(); 6176 } else static assert(0); 6177 } 6178 6179 this(Widget parent) { 6180 super(parent); 6181 6182 version(win32_widgets) { 6183 createWin32Window(this, "Scrollbar"w, "", 6184 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 6185 } else version(custom_widgets) { 6186 auto vl = new HorizontalLayout(this); 6187 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 6188 leftButton.setClickRepeat(scrollClickRepeatInterval); 6189 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 6190 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 6191 rightButton.setClickRepeat(scrollClickRepeatInterval); 6192 6193 leftButton.tabStop = false; 6194 rightButton.tabStop = false; 6195 thumb.tabStop = false; 6196 6197 leftButton.addEventListener(EventType.triggered, () { 6198 this.emitCommand!"scrolltopreviousline"(); 6199 //informProgramThatUserChangedPosition(position - step()); 6200 }); 6201 rightButton.addEventListener(EventType.triggered, () { 6202 this.emitCommand!"scrolltonextline"(); 6203 //informProgramThatUserChangedPosition(position + step()); 6204 }); 6205 6206 thumb.thumbWidth = this.minWidth; 6207 thumb.thumbHeight = scaleWithDpi(16); 6208 6209 thumb.addEventListener(EventType.change, () { 6210 auto sx = thumb.positionX * max() / thumb.width; 6211 //informProgramThatUserChangedPosition(sx); 6212 6213 auto ev = new ScrollToPositionEvent(this, sx); 6214 ev.dispatch(); 6215 }); 6216 } 6217 } 6218 6219 override int minHeight() { return scaleWithDpi(16); } 6220 override int maxHeight() { return scaleWithDpi(16); } 6221 override int minWidth() { return scaleWithDpi(48); } 6222 } 6223 6224 class ScrollToPositionEvent : Event { 6225 enum EventString = "scrolltoposition"; 6226 6227 this(Widget target, int value) { 6228 this.value = value; 6229 super(EventString, target); 6230 } 6231 6232 immutable int value; 6233 6234 override @property int intValue() { 6235 return value; 6236 } 6237 } 6238 6239 //version(custom_widgets) 6240 //private 6241 class VerticalScrollbar : ScrollbarBase { 6242 6243 version(custom_widgets) { 6244 override int getBarDim() { 6245 return thumb.height; 6246 } 6247 6248 private MouseTrackingWidget thumb; 6249 } 6250 6251 override void setViewableArea(int a) { 6252 super.setViewableArea(a); 6253 6254 version(win32_widgets) { 6255 SCROLLINFO info; 6256 info.cbSize = info.sizeof; 6257 info.nPage = a + 1; 6258 info.fMask = SIF_PAGE; 6259 SetScrollInfo(hwnd, SB_CTL, &info, true); 6260 } else version(custom_widgets) { 6261 thumb.positionY = thumbPosition; 6262 thumb.thumbHeight = thumbSize; 6263 thumb.redraw(); 6264 } else static assert(0); 6265 6266 } 6267 6268 override void setMax(int a) { 6269 super.setMax(a); 6270 version(win32_widgets) { 6271 SCROLLINFO info; 6272 info.cbSize = info.sizeof; 6273 info.nMin = 0; 6274 info.nMax = max; 6275 info.fMask = SIF_RANGE; 6276 SetScrollInfo(hwnd, SB_CTL, &info, true); 6277 } else version(custom_widgets) { 6278 thumb.positionY = thumbPosition; 6279 thumb.thumbHeight = thumbSize; 6280 thumb.redraw(); 6281 } 6282 } 6283 6284 override void setPosition(int a) { 6285 super.setPosition(a); 6286 version(win32_widgets) { 6287 SCROLLINFO info; 6288 info.cbSize = info.sizeof; 6289 info.fMask = SIF_POS; 6290 info.nPos = position; 6291 SetScrollInfo(hwnd, SB_CTL, &info, true); 6292 } else version(custom_widgets) { 6293 thumb.positionY = thumbPosition; 6294 thumb.thumbHeight = thumbSize; 6295 thumb.redraw(); 6296 } else static assert(0); 6297 } 6298 6299 this(Widget parent) { 6300 super(parent); 6301 6302 version(win32_widgets) { 6303 createWin32Window(this, "Scrollbar"w, "", 6304 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 6305 } else version(custom_widgets) { 6306 auto vl = new VerticalLayout(this); 6307 auto upButton = new ArrowButton(ArrowDirection.up, vl); 6308 upButton.setClickRepeat(scrollClickRepeatInterval); 6309 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 6310 auto downButton = new ArrowButton(ArrowDirection.down, vl); 6311 downButton.setClickRepeat(scrollClickRepeatInterval); 6312 6313 upButton.addEventListener(EventType.triggered, () { 6314 this.emitCommand!"scrolltopreviousline"(); 6315 //informProgramThatUserChangedPosition(position - step()); 6316 }); 6317 downButton.addEventListener(EventType.triggered, () { 6318 this.emitCommand!"scrolltonextline"(); 6319 //informProgramThatUserChangedPosition(position + step()); 6320 }); 6321 6322 thumb.thumbWidth = this.minWidth; 6323 thumb.thumbHeight = scaleWithDpi(16); 6324 6325 thumb.addEventListener(EventType.change, () { 6326 auto sy = thumb.positionY * max() / thumb.height; 6327 6328 auto ev = new ScrollToPositionEvent(this, sy); 6329 ev.dispatch(); 6330 6331 //informProgramThatUserChangedPosition(sy); 6332 }); 6333 6334 upButton.tabStop = false; 6335 downButton.tabStop = false; 6336 thumb.tabStop = false; 6337 } 6338 } 6339 6340 override int minWidth() { return scaleWithDpi(16); } 6341 override int maxWidth() { return scaleWithDpi(16); } 6342 override int minHeight() { return scaleWithDpi(48); } 6343 } 6344 6345 6346 /++ 6347 EXPERIMENTAL 6348 6349 A widget specialized for being a container for other widgets. 6350 6351 History: 6352 Added May 29, 2021. Not stabilized at this time. 6353 +/ 6354 class WidgetContainer : Widget { 6355 this(Widget parent) { 6356 tabStop = false; 6357 super(parent); 6358 } 6359 6360 override int maxHeight() { 6361 if(this.children.length == 1) { 6362 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 6363 } else { 6364 return int.max; 6365 } 6366 } 6367 6368 override int maxWidth() { 6369 if(this.children.length == 1) { 6370 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 6371 } else { 6372 return int.max; 6373 } 6374 } 6375 6376 /+ 6377 6378 override int minHeight() { 6379 int largest = 0; 6380 int margins = 0; 6381 int lastMargin = 0; 6382 foreach(child; children) { 6383 auto mh = child.minHeight(); 6384 if(mh > largest) 6385 largest = mh; 6386 margins += mymax(lastMargin, child.marginTop()); 6387 lastMargin = child.marginBottom(); 6388 } 6389 return largest + margins; 6390 } 6391 6392 override int maxHeight() { 6393 int largest = 0; 6394 int margins = 0; 6395 int lastMargin = 0; 6396 foreach(child; children) { 6397 auto mh = child.maxHeight(); 6398 if(mh == int.max) 6399 return int.max; 6400 if(mh > largest) 6401 largest = mh; 6402 margins += mymax(lastMargin, child.marginTop()); 6403 lastMargin = child.marginBottom(); 6404 } 6405 return largest + margins; 6406 } 6407 6408 override int minWidth() { 6409 int min; 6410 foreach(child; children) { 6411 auto cm = child.minWidth; 6412 if(cm > min) 6413 min = cm; 6414 } 6415 return min + paddingLeft + paddingRight; 6416 } 6417 6418 override int minHeight() { 6419 int min; 6420 foreach(child; children) { 6421 auto cm = child.minHeight; 6422 if(cm > min) 6423 min = cm; 6424 } 6425 return min + paddingTop + paddingBottom; 6426 } 6427 6428 override int maxHeight() { 6429 int largest = 0; 6430 int margins = 0; 6431 int lastMargin = 0; 6432 foreach(child; children) { 6433 auto mh = child.maxHeight(); 6434 if(mh == int.max) 6435 return int.max; 6436 if(mh > largest) 6437 largest = mh; 6438 margins += mymax(lastMargin, child.marginTop()); 6439 lastMargin = child.marginBottom(); 6440 } 6441 return largest + margins; 6442 } 6443 6444 override int heightStretchiness() { 6445 int max; 6446 foreach(child; children) { 6447 auto c = child.heightStretchiness; 6448 if(c > max) 6449 max = c; 6450 } 6451 return max; 6452 } 6453 6454 override int marginTop() { 6455 if(this.children.length) 6456 return this.children[0].marginTop; 6457 return 0; 6458 } 6459 +/ 6460 } 6461 6462 /// 6463 abstract class Layout : Widget { 6464 this(Widget parent) { 6465 tabStop = false; 6466 super(parent); 6467 } 6468 } 6469 6470 /++ 6471 Makes all children minimum width and height, placing them down 6472 left to right, top to bottom. 6473 6474 Useful if you want to make a list of buttons that automatically 6475 wrap to a new line when necessary. 6476 +/ 6477 class InlineBlockLayout : Layout { 6478 /// 6479 this(Widget parent) { super(parent); } 6480 6481 override void recomputeChildLayout() { 6482 registerMovement(); 6483 6484 int x = this.paddingLeft, y = this.paddingTop; 6485 6486 int lineHeight; 6487 int previousMargin = 0; 6488 int previousMarginBottom = 0; 6489 6490 foreach(child; children) { 6491 if(child.hidden) 6492 continue; 6493 if(cast(FixedPosition) child) { 6494 child.recomputeChildLayout(); 6495 continue; 6496 } 6497 child.width = child.flexBasisWidth(); 6498 if(child.width == 0) 6499 child.width = child.minWidth(); 6500 if(child.width == 0) 6501 child.width = 32; 6502 6503 child.height = child.flexBasisHeight(); 6504 if(child.height == 0) 6505 child.height = child.minHeight(); 6506 if(child.height == 0) 6507 child.height = 32; 6508 6509 if(x + child.width + paddingRight > this.width) { 6510 x = this.paddingLeft; 6511 y += lineHeight; 6512 lineHeight = 0; 6513 previousMargin = 0; 6514 previousMarginBottom = 0; 6515 } 6516 6517 auto margin = child.marginLeft; 6518 if(previousMargin > margin) 6519 margin = previousMargin; 6520 6521 x += margin; 6522 6523 child.x = x; 6524 child.y = y; 6525 6526 int marginTopApplied; 6527 if(child.marginTop > previousMarginBottom) { 6528 child.y += child.marginTop; 6529 marginTopApplied = child.marginTop; 6530 } 6531 6532 x += child.width; 6533 previousMargin = child.marginRight; 6534 6535 if(child.marginBottom > previousMarginBottom) 6536 previousMarginBottom = child.marginBottom; 6537 6538 auto h = child.height + previousMarginBottom + marginTopApplied; 6539 if(h > lineHeight) 6540 lineHeight = h; 6541 6542 child.recomputeChildLayout(); 6543 } 6544 6545 } 6546 6547 override int minWidth() { 6548 int min; 6549 foreach(child; children) { 6550 auto cm = child.minWidth; 6551 if(cm > min) 6552 min = cm; 6553 } 6554 return min + paddingLeft + paddingRight; 6555 } 6556 6557 override int minHeight() { 6558 int min; 6559 foreach(child; children) { 6560 auto cm = child.minHeight; 6561 if(cm > min) 6562 min = cm; 6563 } 6564 return min + paddingTop + paddingBottom; 6565 } 6566 } 6567 6568 /++ 6569 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 6570 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 6571 the [TabWidget] will automatically change pages of child widgets. 6572 6573 This allows you to react to it however you see fit rather than having to 6574 be tied to just the new sets of child widgets. 6575 6576 It sends the message in the form of `this.emitCommand!"changetab"();`. 6577 6578 History: 6579 Added December 24, 2021 (dub v10.5) 6580 +/ 6581 class TabMessageWidget : Widget { 6582 6583 protected void tabIndexClicked(int item) { 6584 this.emitCommand!"changetab"(); 6585 } 6586 6587 /++ 6588 Adds the a new tab to the control with the given title. 6589 6590 Returns: 6591 The index of the newly added tab. You will need to know 6592 this index to refer to it later and to know which tab to 6593 change to when you get a changetab message. 6594 +/ 6595 int addTab(string title, int pos = int.max) { 6596 version(win32_widgets) { 6597 TCITEM item; 6598 item.mask = TCIF_TEXT; 6599 WCharzBuffer buf = WCharzBuffer(title); 6600 item.pszText = buf.ptr; 6601 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 6602 } else version(custom_widgets) { 6603 if(pos >= tabs.length) { 6604 tabs ~= title; 6605 redraw(); 6606 return cast(int) tabs.length - 1; 6607 } else if(pos <= 0) { 6608 tabs = title ~ tabs; 6609 redraw(); 6610 return 0; 6611 } else { 6612 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 6613 redraw(); 6614 return pos; 6615 } 6616 } 6617 } 6618 6619 override void addChild(Widget child, int pos = int.max) { 6620 if(container) 6621 container.addChild(child, pos); 6622 else 6623 super.addChild(child, pos); 6624 } 6625 6626 protected Widget makeContainer() { 6627 return new Widget(this); 6628 } 6629 6630 private Widget container; 6631 6632 override void recomputeChildLayout() { 6633 version(win32_widgets) { 6634 this.registerMovement(); 6635 6636 RECT rect; 6637 GetWindowRect(hwnd, &rect); 6638 6639 auto left = rect.left; 6640 auto top = rect.top; 6641 6642 TabCtrl_AdjustRect(hwnd, false, &rect); 6643 foreach(child; children) { 6644 if(!child.showing) continue; 6645 child.x = rect.left - left; 6646 child.y = rect.top - top; 6647 child.width = rect.right - rect.left; 6648 child.height = rect.bottom - rect.top; 6649 child.recomputeChildLayout(); 6650 } 6651 } else version(custom_widgets) { 6652 this.registerMovement(); 6653 foreach(child; children) { 6654 if(!child.showing) continue; 6655 child.x = 2; 6656 child.y = tabBarHeight + 2; // for the border 6657 child.width = width - 4; // for the border 6658 child.height = height - tabBarHeight - 2 - 2; // for the border 6659 child.recomputeChildLayout(); 6660 } 6661 } else static assert(0); 6662 } 6663 6664 version(custom_widgets) 6665 string[] tabs; 6666 6667 this(Widget parent) { 6668 super(parent); 6669 6670 tabStop = false; 6671 6672 version(win32_widgets) { 6673 createWin32Window(this, WC_TABCONTROL, "", 0); 6674 } else version(custom_widgets) { 6675 addEventListener((ClickEvent event) { 6676 if(event.target !is this && this.container !is null && event.target !is this.container) return; 6677 if(event.clientY < tabBarHeight) { 6678 auto t = (event.clientX / tabWidth); 6679 if(t >= 0 && t < tabs.length) { 6680 currentTab_ = t; 6681 tabIndexClicked(t); 6682 redraw(); 6683 } 6684 } 6685 }); 6686 } else static assert(0); 6687 6688 this.container = makeContainer(); 6689 } 6690 6691 override int marginTop() { return 4; } 6692 override int paddingBottom() { return 4; } 6693 6694 override int minHeight() { 6695 int max = 0; 6696 foreach(child; children) 6697 max = mymax(child.minHeight, max); 6698 6699 6700 version(win32_widgets) { 6701 RECT rect; 6702 rect.right = this.width; 6703 rect.bottom = max; 6704 TabCtrl_AdjustRect(hwnd, true, &rect); 6705 6706 max = rect.bottom; 6707 } else { 6708 max += defaultLineHeight + 4; 6709 } 6710 6711 6712 return max; 6713 } 6714 6715 version(win32_widgets) 6716 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 6717 switch(code) { 6718 case TCN_SELCHANGE: 6719 auto sel = TabCtrl_GetCurSel(hwnd); 6720 tabIndexClicked(sel); 6721 break; 6722 default: 6723 } 6724 return 0; 6725 } 6726 6727 version(custom_widgets) { 6728 private int currentTab_; 6729 private int tabBarHeight() { return defaultLineHeight; } 6730 int tabWidth = 80; 6731 } 6732 6733 version(win32_widgets) 6734 override void paint(WidgetPainter painter) {} 6735 6736 version(custom_widgets) 6737 override void paint(WidgetPainter painter) { 6738 auto cs = getComputedStyle(); 6739 6740 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 6741 6742 int posX = 0; 6743 foreach(idx, title; tabs) { 6744 auto isCurrent = idx == getCurrentTab(); 6745 6746 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 6747 6748 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 6749 painter.outlineColor = cs.foregroundColor; 6750 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 6751 6752 if(isCurrent) { 6753 painter.outlineColor = cs.windowBackgroundColor; 6754 painter.fillColor = Color.transparent; 6755 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 6756 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 6757 6758 painter.outlineColor = Color.white; 6759 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 6760 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 6761 painter.outlineColor = cs.activeTabColor; 6762 painter.drawPixel(Point(posX, tabBarHeight - 1)); 6763 } 6764 6765 posX += tabWidth - 2; 6766 } 6767 } 6768 6769 /// 6770 @scriptable 6771 void setCurrentTab(int item) { 6772 version(win32_widgets) 6773 TabCtrl_SetCurSel(hwnd, item); 6774 else version(custom_widgets) 6775 currentTab_ = item; 6776 else static assert(0); 6777 6778 tabIndexClicked(item); 6779 } 6780 6781 /// 6782 @scriptable 6783 int getCurrentTab() { 6784 version(win32_widgets) 6785 return TabCtrl_GetCurSel(hwnd); 6786 else version(custom_widgets) 6787 return currentTab_; // FIXME 6788 else static assert(0); 6789 } 6790 6791 /// 6792 @scriptable 6793 void removeTab(int item) { 6794 if(item && item == getCurrentTab()) 6795 setCurrentTab(item - 1); 6796 6797 version(win32_widgets) { 6798 TabCtrl_DeleteItem(hwnd, item); 6799 } 6800 6801 for(int a = item; a < children.length - 1; a++) 6802 this._children[a] = this._children[a + 1]; 6803 this._children = this._children[0 .. $-1]; 6804 } 6805 6806 } 6807 6808 6809 /++ 6810 A tab widget is a set of clickable tab buttons followed by a content area. 6811 6812 6813 Tabs can change existing content or can be new pages. 6814 6815 When the user picks a different tab, a `change` message is generated. 6816 +/ 6817 class TabWidget : TabMessageWidget { 6818 this(Widget parent) { 6819 super(parent); 6820 } 6821 6822 override protected Widget makeContainer() { 6823 return null; 6824 } 6825 6826 override void addChild(Widget child, int pos = int.max) { 6827 if(auto twp = cast(TabWidgetPage) child) { 6828 Widget.addChild(child, pos); 6829 if(pos == int.max) 6830 pos = cast(int) this.children.length - 1; 6831 6832 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 6833 6834 if(pos != getCurrentTab) { 6835 child.showing = false; 6836 } 6837 } else { 6838 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 6839 } 6840 } 6841 6842 // FIXME: add tab icons at some point, Windows supports them 6843 /++ 6844 Adds a page and its associated tab with the given label to the widget. 6845 6846 Returns: 6847 The added page object, to which you can add other widgets. 6848 +/ 6849 @scriptable 6850 TabWidgetPage addPage(string title) { 6851 return new TabWidgetPage(title, this); 6852 } 6853 6854 /++ 6855 Gets the page at the given tab index, or `null` if the index is bad. 6856 6857 History: 6858 Added December 24, 2021. 6859 +/ 6860 TabWidgetPage getPage(int index) { 6861 if(index < this.children.length) 6862 return null; 6863 return cast(TabWidgetPage) this.children[index]; 6864 } 6865 6866 /++ 6867 While you can still use the addTab from the parent class, 6868 *strongly* recommend you use [addPage] insteaad. 6869 6870 History: 6871 Added December 24, 2021 to fulful the interface 6872 requirement that came from adding [TabMessageWidget]. 6873 6874 You should not use it though since the [addPage] function 6875 is much easier to use here. 6876 +/ 6877 override int addTab(string title, int pos = int.max) { 6878 auto p = addPage(title); 6879 foreach(idx, child; this.children) 6880 if(child is p) 6881 return cast(int) idx; 6882 return -1; 6883 } 6884 6885 protected override void tabIndexClicked(int item) { 6886 foreach(idx, child; children) { 6887 child.showing(false, false); // batch the recalculates for the end 6888 } 6889 6890 foreach(idx, child; children) { 6891 if(idx == item) { 6892 child.showing(true, false); 6893 if(parentWindow) { 6894 auto f = parentWindow.getFirstFocusable(child); 6895 if(f) 6896 f.focus(); 6897 } 6898 recomputeChildLayout(); 6899 } 6900 } 6901 6902 version(win32_widgets) { 6903 InvalidateRect(hwnd, null, true); 6904 } else version(custom_widgets) { 6905 this.redraw(); 6906 } 6907 } 6908 6909 } 6910 6911 /++ 6912 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 6913 6914 You add [TabWidgetPage]s to it. 6915 +/ 6916 class PageWidget : Widget { 6917 this(Widget parent) { 6918 super(parent); 6919 } 6920 6921 override int minHeight() { 6922 int max = 0; 6923 foreach(child; children) 6924 max = mymax(child.minHeight, max); 6925 6926 return max; 6927 } 6928 6929 6930 override void addChild(Widget child, int pos = int.max) { 6931 if(auto twp = cast(TabWidgetPage) child) { 6932 super.addChild(child, pos); 6933 if(pos == int.max) 6934 pos = cast(int) this.children.length - 1; 6935 6936 if(pos != getCurrentTab) { 6937 child.showing = false; 6938 } 6939 } else { 6940 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 6941 } 6942 } 6943 6944 override void recomputeChildLayout() { 6945 this.registerMovement(); 6946 foreach(child; children) { 6947 child.x = 0; 6948 child.y = 0; 6949 child.width = width; 6950 child.height = height; 6951 child.recomputeChildLayout(); 6952 } 6953 } 6954 6955 private int currentTab_; 6956 6957 /// 6958 @scriptable 6959 void setCurrentTab(int item) { 6960 currentTab_ = item; 6961 6962 showOnly(item); 6963 } 6964 6965 /// 6966 @scriptable 6967 int getCurrentTab() { 6968 return currentTab_; 6969 } 6970 6971 /// 6972 @scriptable 6973 void removeTab(int item) { 6974 if(item && item == getCurrentTab()) 6975 setCurrentTab(item - 1); 6976 6977 for(int a = item; a < children.length - 1; a++) 6978 this._children[a] = this._children[a + 1]; 6979 this._children = this._children[0 .. $-1]; 6980 } 6981 6982 /// 6983 @scriptable 6984 TabWidgetPage addPage(string title) { 6985 return new TabWidgetPage(title, this); 6986 } 6987 6988 private void showOnly(int item) { 6989 foreach(idx, child; children) 6990 if(idx == item) { 6991 child.show(); 6992 child.recomputeChildLayout(); 6993 } else { 6994 child.hide(); 6995 } 6996 } 6997 6998 } 6999 7000 /++ 7001 7002 +/ 7003 class TabWidgetPage : Widget { 7004 string title; 7005 this(string title, Widget parent) { 7006 this.title = title; 7007 this.tabStop = false; 7008 super(parent); 7009 7010 ///* 7011 version(win32_widgets) { 7012 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 7013 } 7014 //*/ 7015 } 7016 7017 override int minHeight() { 7018 int sum = 0; 7019 foreach(child; children) 7020 sum += child.minHeight(); 7021 return sum; 7022 } 7023 } 7024 7025 version(none) 7026 /++ 7027 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 7028 7029 I think I need to modify the layout algorithms to support this. 7030 +/ 7031 class CollapsableSidebar : Widget { 7032 7033 } 7034 7035 /// Stacks the widgets vertically, taking all the available width for each child. 7036 class VerticalLayout : Layout { 7037 // most of this is intentionally blank - widget's default is vertical layout right now 7038 /// 7039 this(Widget parent) { super(parent); } 7040 7041 /++ 7042 Sets a max width for the layout so you don't have to subclass. The max width 7043 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7044 7045 History: 7046 Added November 29, 2021 (dub v10.5) 7047 +/ 7048 this(int maxWidth, Widget parent) { 7049 this.mw = maxWidth; 7050 super(parent); 7051 } 7052 7053 private int mw = int.max; 7054 7055 override int maxWidth() { return scaleWithDpi(mw); } 7056 } 7057 7058 /// Stacks the widgets horizontally, taking all the available height for each child. 7059 class HorizontalLayout : Layout { 7060 /// 7061 this(Widget parent) { super(parent); } 7062 7063 /++ 7064 Sets a max height for the layout so you don't have to subclass. The max height 7065 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7066 7067 History: 7068 Added November 29, 2021 (dub v10.5) 7069 +/ 7070 this(int maxHeight, Widget parent) { 7071 this.mh = maxHeight; 7072 super(parent); 7073 } 7074 7075 private int mh = 0; 7076 7077 7078 7079 override void recomputeChildLayout() { 7080 .recomputeChildLayout!"width"(this); 7081 } 7082 7083 override int minHeight() { 7084 int largest = 0; 7085 int margins = 0; 7086 int lastMargin = 0; 7087 foreach(child; children) { 7088 auto mh = child.minHeight(); 7089 if(mh > largest) 7090 largest = mh; 7091 margins += mymax(lastMargin, child.marginTop()); 7092 lastMargin = child.marginBottom(); 7093 } 7094 return largest + margins; 7095 } 7096 7097 override int maxHeight() { 7098 if(mh != 0) 7099 return mymax(minHeight, scaleWithDpi(mh)); 7100 7101 int largest = 0; 7102 int margins = 0; 7103 int lastMargin = 0; 7104 foreach(child; children) { 7105 auto mh = child.maxHeight(); 7106 if(mh == int.max) 7107 return int.max; 7108 if(mh > largest) 7109 largest = mh; 7110 margins += mymax(lastMargin, child.marginTop()); 7111 lastMargin = child.marginBottom(); 7112 } 7113 return largest + margins; 7114 } 7115 7116 override int heightStretchiness() { 7117 int max; 7118 foreach(child; children) { 7119 auto c = child.heightStretchiness; 7120 if(c > max) 7121 max = c; 7122 } 7123 return max; 7124 } 7125 7126 } 7127 7128 version(win32_widgets) 7129 private 7130 extern(Windows) 7131 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 7132 Widget* pwin = hwnd in Widget.nativeMapping; 7133 if(pwin is null) 7134 return DefWindowProc(hwnd, message, wparam, lparam); 7135 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 7136 if(win is null) 7137 return DefWindowProc(hwnd, message, wparam, lparam); 7138 7139 switch(message) { 7140 case WM_SIZE: 7141 auto width = LOWORD(lparam); 7142 auto height = HIWORD(lparam); 7143 7144 // FIXME: could this be more efficient? it never relinquishes a large bitmap 7145 if(width > win.bmpWidth || height > win.bmpHeight) { 7146 auto hdc = GetDC(hwnd); 7147 auto oldBuffer = win.buffer; 7148 win.buffer = CreateCompatibleBitmap(hdc, width, height); 7149 7150 auto hdcBmp = CreateCompatibleDC(hdc); 7151 7152 auto oldBmp = SelectObject(hdcBmp, win.buffer); 7153 7154 if(oldBuffer) { 7155 auto hdcOldBmp = CreateCompatibleDC(hdc); 7156 auto oldOldBmp = SelectObject(hdcOldBmp, oldBuffer); 7157 7158 BitBlt(hdcBmp, 0, 0, win.bmpWidth, win.bmpHeight, hdcOldBmp, 0, 0, SRCCOPY); 7159 7160 SelectObject(hdcOldBmp, oldOldBmp); 7161 DeleteDC(hdcOldBmp); 7162 } 7163 7164 auto brush = GetSysColorBrush(COLOR_3DFACE); 7165 RECT r; 7166 r.left = win.bmpWidth; 7167 r.top = 0; 7168 r.right = width; 7169 r.bottom = height; 7170 FillRect(hdcBmp, &r, brush); 7171 7172 r.left = 0; 7173 r.top = win.bmpHeight; 7174 r.right = width; 7175 r.bottom = height; 7176 FillRect(hdcBmp, &r, brush); 7177 7178 SelectObject(hdcBmp, oldBmp); 7179 DeleteDC(hdcBmp); 7180 ReleaseDC(hwnd, hdc); 7181 7182 if(oldBuffer) 7183 DeleteObject(oldBuffer); 7184 7185 win.bmpWidth = width; 7186 win.bmpHeight = height; 7187 } 7188 break; 7189 case WM_PAINT: 7190 if(win.buffer is null) 7191 goto default; 7192 7193 BITMAP bm; 7194 PAINTSTRUCT ps; 7195 7196 HDC hdc = BeginPaint(hwnd, &ps); 7197 7198 HDC hdcMem = CreateCompatibleDC(hdc); 7199 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 7200 7201 GetObject(win.buffer, bm.sizeof, &bm); 7202 7203 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 7204 7205 SelectObject(hdcMem, hbmOld); 7206 DeleteDC(hdcMem); 7207 EndPaint(hwnd, &ps); 7208 break; 7209 default: 7210 return DefWindowProc(hwnd, message, wparam, lparam); 7211 } 7212 7213 return 0; 7214 } 7215 7216 private wstring Win32Class(wstring name)() { 7217 static bool classRegistered; 7218 if(!classRegistered) { 7219 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 7220 WNDCLASSEX wc; 7221 wc.cbSize = wc.sizeof; 7222 wc.hInstance = hInstance; 7223 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 7224 wc.lpfnWndProc = &DoubleBufferWndProc; 7225 wc.lpszClassName = name.ptr; 7226 if(!RegisterClassExW(&wc)) 7227 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 7228 classRegistered = true; 7229 } 7230 7231 return name; 7232 } 7233 7234 /+ 7235 version(win32_widgets) 7236 extern(Windows) 7237 private 7238 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 7239 switch(iMessage) { 7240 case WM_PAINT: 7241 if(auto te = hWnd in Widget.nativeMapping) { 7242 try { 7243 //te.redraw(); 7244 import std.stdio; writeln(te, " drawing"); 7245 } catch(Exception) {} 7246 } 7247 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7248 default: 7249 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7250 } 7251 } 7252 +/ 7253 7254 7255 /++ 7256 A widget specifically designed to hold other widgets. 7257 7258 History: 7259 Added July 1, 2021 7260 +/ 7261 class ContainerWidget : Widget { 7262 this(Widget parent) { 7263 super(parent); 7264 this.tabStop = false; 7265 7266 version(win32_widgets) { 7267 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 7268 } 7269 } 7270 } 7271 7272 /++ 7273 A widget that takes your widget, puts scroll bars around it, and sends 7274 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 7275 no effort to automatically scroll or clip its child widgets - it just sends 7276 the messages. 7277 7278 7279 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 7280 The scroll coordinates are all given in a unit you interpret as you wish. One 7281 of these units is moved on each press of the arrow buttons and represents the 7282 smallest amount the user can scroll. The intention is for this to be one line, 7283 one item in a list, one row in a table, etc. Whatever makes sense for your widget 7284 in each direction that the user might be interested in. 7285 7286 You can set a "page size" with the [step] property. (Yes, I regret the name...) 7287 This is the amount it jumps when the user pressed page up and page down, or clicks 7288 in the exposed part of the scroll bar. 7289 7290 You should add child content to the ScrollMessageWidget. However, it is important to 7291 note that the coordinates are always independent of the scroll position! It is YOUR 7292 responsibility to do any necessary transforms, clipping, etc., while drawing the 7293 content and interpreting mouse events if they are supposed to change with the scroll. 7294 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 7295 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 7296 you more control (which can be considerably more efficient and adapted to your actual data) 7297 at the expense of you also needing to be aware of its reality. 7298 7299 Please note that it does NOT react to mouse wheel events or various keyboard events as of 7300 version 10.3. Maybe this will change in the future.... 7301 +/ 7302 class ScrollMessageWidget : Widget { 7303 this(Widget parent) { 7304 super(parent); 7305 7306 container = new Widget(this); 7307 hsb = new HorizontalScrollbar(this); 7308 vsb = new VerticalScrollbar(this); 7309 7310 hsb.addEventListener("scrolltonextline", { 7311 hsb.setPosition(hsb.position + 1); 7312 notify(); 7313 }); 7314 hsb.addEventListener("scrolltopreviousline", { 7315 hsb.setPosition(hsb.position - 1); 7316 notify(); 7317 }); 7318 vsb.addEventListener("scrolltonextline", { 7319 vsb.setPosition(vsb.position + 1); 7320 notify(); 7321 }); 7322 vsb.addEventListener("scrolltopreviousline", { 7323 vsb.setPosition(vsb.position - 1); 7324 notify(); 7325 }); 7326 hsb.addEventListener("scrolltonextpage", { 7327 hsb.setPosition(hsb.position + hsb.step_); 7328 notify(); 7329 }); 7330 hsb.addEventListener("scrolltopreviouspage", { 7331 hsb.setPosition(hsb.position - hsb.step_); 7332 notify(); 7333 }); 7334 vsb.addEventListener("scrolltonextpage", { 7335 vsb.setPosition(vsb.position + vsb.step_); 7336 notify(); 7337 }); 7338 vsb.addEventListener("scrolltopreviouspage", { 7339 vsb.setPosition(vsb.position - vsb.step_); 7340 notify(); 7341 }); 7342 hsb.addEventListener("scrolltoposition", (Event event) { 7343 hsb.setPosition(event.intValue); 7344 notify(); 7345 }); 7346 vsb.addEventListener("scrolltoposition", (Event event) { 7347 vsb.setPosition(event.intValue); 7348 notify(); 7349 }); 7350 7351 7352 tabStop = false; 7353 container.tabStop = false; 7354 magic = true; 7355 } 7356 7357 /++ 7358 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 7359 7360 7361 The defaults for [addDefaultWheelListeners] are: 7362 7363 $(LIST 7364 * Mouse wheel scrolls vertically 7365 * Alt key + mouse wheel scrolls horiontally 7366 * Shift + mouse wheel scrolls faster. 7367 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 7368 ) 7369 7370 The defaults for [addDefaultKeyboardListeners] are: 7371 7372 $(LIST 7373 * Arrow keys scroll by the given amounts 7374 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 7375 * Page up and down scroll by the vertical viewable area 7376 * Home and end scroll to the start and end of the verticle viewable area. 7377 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 7378 ) 7379 7380 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 7381 7382 Params: 7383 horizontalArrowScrollAmount = 7384 verticalArrowScrollAmount = 7385 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 7386 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 7387 shiftMultiplier = multiplies the scroll amount by this when shift is held 7388 +/ 7389 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 7390 auto _this = this; 7391 7392 container.addEventListener((scope KeyDownEvent ke) { 7393 switch(ke.key) { 7394 case Key.Left: 7395 _this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7396 break; 7397 case Key.Right: 7398 _this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7399 break; 7400 case Key.Up: 7401 _this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7402 break; 7403 case Key.Down: 7404 _this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7405 break; 7406 case Key.PageUp: 7407 if(ke.altKey) 7408 _this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7409 else 7410 _this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7411 break; 7412 case Key.PageDown: 7413 if(ke.altKey) 7414 _this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7415 else 7416 _this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7417 break; 7418 case Key.Home: 7419 if(ke.altKey) 7420 _this.scrollLeft(short.max * 16); 7421 else 7422 _this.scrollUp(short.max * 16); 7423 break; 7424 case Key.End: 7425 if(ke.altKey) 7426 _this.scrollRight(short.max * 16); 7427 else 7428 _this.scrollDown(short.max * 16); 7429 break; 7430 7431 default: 7432 // ignore, not for us. 7433 } 7434 7435 }); 7436 } 7437 7438 /// ditto 7439 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 7440 auto _this = this; 7441 container.addEventListener((scope ClickEvent ce) { 7442 7443 if(ce.target && ce.target.tabStop) 7444 ce.target.focus(); 7445 7446 // ctrl is reserved for the application 7447 if(ce.ctrlKey) 7448 return; 7449 7450 if(horizontalWheelScrollAmount == 0 && ce.altKey) 7451 return; 7452 7453 if(shiftMultiplier == 0 && ce.shiftKey) 7454 return; 7455 7456 if(ce.button == MouseButton.wheelDown) { 7457 if(ce.altKey) 7458 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7459 else 7460 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7461 } else if(ce.button == MouseButton.wheelUp) { 7462 if(ce.altKey) 7463 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7464 else 7465 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7466 } 7467 }); 7468 } 7469 7470 /++ 7471 Scrolls the given amount. 7472 7473 History: 7474 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. 7475 +/ 7476 void scrollUp(int amount = 1) { 7477 vsb.setPosition(vsb.position - amount); 7478 notify(); 7479 } 7480 /// ditto 7481 void scrollDown(int amount = 1) { 7482 vsb.setPosition(vsb.position + amount); 7483 notify(); 7484 } 7485 /// ditto 7486 void scrollLeft(int amount = 1) { 7487 hsb.setPosition(hsb.position - amount); 7488 notify(); 7489 } 7490 /// ditto 7491 void scrollRight(int amount = 1) { 7492 hsb.setPosition(hsb.position + amount); 7493 notify(); 7494 } 7495 7496 /// 7497 VerticalScrollbar verticalScrollBar() { return vsb; } 7498 /// 7499 HorizontalScrollbar horizontalScrollBar() { return hsb; } 7500 7501 void notify() { 7502 static bool insideNotify; 7503 7504 if(insideNotify) 7505 return; // avoid the recursive call, even if it isn't strictly correct 7506 7507 insideNotify = true; 7508 scope(exit) insideNotify = false; 7509 7510 this.emit!ScrollEvent(); 7511 } 7512 7513 mixin Emits!ScrollEvent; 7514 7515 /// 7516 Point position() { 7517 return Point(hsb.position, vsb.position); 7518 } 7519 7520 /// 7521 void setPosition(int x, int y) { 7522 hsb.setPosition(x); 7523 vsb.setPosition(y); 7524 } 7525 7526 /// 7527 void setPageSize(int unitsX, int unitsY) { 7528 hsb.setStep(unitsX); 7529 vsb.setStep(unitsY); 7530 } 7531 7532 /// 7533 void setTotalArea(int width, int height) { 7534 hsb.setMax(width); 7535 vsb.setMax(height); 7536 } 7537 7538 /// Always set the viewable area AFTER setitng the total area if you are going to change both. 7539 /// NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 7540 /// If you need to do that, use [queueRecomputeChildLayout]. 7541 void setViewableArea(int width, int height) { 7542 7543 // actually there IS A need to dothis cuz the max might have changed since then 7544 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 7545 //return; // no need to do what is already done 7546 hsb.setViewableArea(width); 7547 vsb.setViewableArea(height); 7548 7549 bool needsNotify = false; 7550 7551 // FIXME: if at any point the rhs is outside the scrollbar, we need 7552 // to reset to 0. but it should remember the old position in case the 7553 // window resizes again, so it can kinda return ot where it was. 7554 // 7555 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 7556 if(width >= hsb.max) { 7557 // there's plenty of room to display it all so we need to reset to zero 7558 // FIXME: adjust so it matches the note above 7559 hsb.setPosition(0); 7560 needsNotify = true; 7561 } 7562 if(height >= vsb.max) { 7563 // there's plenty of room to display it all so we need to reset to zero 7564 // FIXME: adjust so it matches the note above 7565 vsb.setPosition(0); 7566 needsNotify = true; 7567 } 7568 if(needsNotify) 7569 notify(); 7570 } 7571 7572 private bool magic; 7573 override void addChild(Widget w, int position = int.max) { 7574 if(magic) 7575 container.addChild(w, position); 7576 else 7577 super.addChild(w, position); 7578 } 7579 7580 override void recomputeChildLayout() { 7581 if(hsb is null || vsb is null || container is null) return; 7582 7583 registerMovement(); 7584 7585 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 7586 hsb.x = 0; 7587 hsb.y = this.height - hsb.height; 7588 hsb.width = this.width - scaleWithDpi(16); 7589 hsb.recomputeChildLayout(); 7590 7591 vsb.width = scaleWithDpi(16); // FIXME? 7592 vsb.x = this.width - vsb.width; 7593 vsb.y = 0; 7594 vsb.height = this.height - scaleWithDpi(16); 7595 vsb.recomputeChildLayout(); 7596 7597 if(this.header is null) { 7598 container.x = 0; 7599 container.y = 0; 7600 container.width = this.width - vsb.width; 7601 container.height = this.height - hsb.height; 7602 container.recomputeChildLayout(); 7603 } else { 7604 header.x = 0; 7605 header.y = 0; 7606 header.width = this.width - vsb.width; 7607 header.height = scaleWithDpi(16); // size of the button 7608 header.recomputeChildLayout(); 7609 7610 container.x = 0; 7611 container.y = scaleWithDpi(16); 7612 container.width = this.width - vsb.width; 7613 container.height = this.height - hsb.height - scaleWithDpi(16); 7614 container.recomputeChildLayout(); 7615 } 7616 } 7617 7618 HorizontalScrollbar hsb; 7619 VerticalScrollbar vsb; 7620 Widget container; 7621 private Widget header; 7622 7623 /++ 7624 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 7625 7626 History: 7627 Added September 27, 2021 (dub v10.3) 7628 +/ 7629 Widget getHeader() { 7630 if(this.header is null) { 7631 magic = false; 7632 scope(exit) magic = true; 7633 this.header = new Widget(this); 7634 recomputeChildLayout(); 7635 } 7636 return this.header; 7637 } 7638 } 7639 7640 /++ 7641 $(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") 7642 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 7643 +/ 7644 version(minigui_screenshots) 7645 @Screenshot("ScrollMessageWidget") 7646 unittest { 7647 auto window = new Window("ScrollMessageWidget"); 7648 7649 auto smw = new ScrollMessageWidget(window); 7650 smw.addDefaultKeyboardListeners(); 7651 smw.addDefaultWheelListeners(); 7652 7653 window.loop(); 7654 } 7655 7656 /++ 7657 Bypasses automatic layout for its children, using manual positioning and sizing only. 7658 While you need to manually position them, you must ensure they are inside the StaticLayout's 7659 bounding box to avoid undefined behavior. 7660 7661 You should almost never use this. 7662 +/ 7663 class StaticLayout : Layout { 7664 /// 7665 this(Widget parent) { super(parent); } 7666 override void recomputeChildLayout() { 7667 registerMovement(); 7668 foreach(child; children) 7669 child.recomputeChildLayout(); 7670 } 7671 } 7672 7673 /++ 7674 Bypasses automatic positioning when being laid out. It is your responsibility to make 7675 room for this widget in the parent layout. 7676 7677 Its children are laid out normally, unless there is exactly one, in which case it takes 7678 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 7679 can do that with `padding`). 7680 +/ 7681 class StaticPosition : Layout { 7682 /// 7683 this(Widget parent) { super(parent); } 7684 7685 override void recomputeChildLayout() { 7686 registerMovement(); 7687 if(this.children.length == 1) { 7688 auto child = children[0]; 7689 child.x = 0; 7690 child.y = 0; 7691 child.width = this.width; 7692 child.height = this.height; 7693 child.recomputeChildLayout(); 7694 } else 7695 foreach(child; children) 7696 child.recomputeChildLayout(); 7697 } 7698 7699 alias width = typeof(super).width; 7700 alias height = typeof(super).height; 7701 7702 @property int width(int w) @nogc pure @safe nothrow { 7703 return this._width = w; 7704 } 7705 7706 @property int height(int w) @nogc pure @safe nothrow { 7707 return this._height = w; 7708 } 7709 7710 } 7711 7712 /++ 7713 FixedPosition is like [StaticPosition], but its coordinates 7714 are always relative to the viewport, meaning they do not scroll with 7715 the parent content. 7716 +/ 7717 class FixedPosition : StaticPosition { 7718 /// 7719 this(Widget parent) { super(parent); } 7720 } 7721 7722 version(win32_widgets) 7723 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 7724 if(true) { 7725 // cmd == 0 = menu, cmd == 1 = accelerator 7726 if(auto item = idm in Action.mapping) { 7727 foreach(handler; (*item).triggered) 7728 handler(); 7729 /* 7730 auto event = new Event("triggered", *item); 7731 event.button = idm; 7732 event.dispatch(); 7733 */ 7734 return 0; 7735 } 7736 } 7737 if(handle) 7738 if(auto widgetp = handle in Widget.nativeMapping) { 7739 (*widgetp).handleWmCommand(cmd, idm); 7740 return 0; 7741 } 7742 return 1; 7743 } 7744 7745 7746 /// 7747 class Window : Widget { 7748 int mouseCaptureCount = 0; 7749 Widget mouseCapturedBy; 7750 void captureMouse(Widget byWhom) { 7751 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 7752 mouseCaptureCount++; 7753 mouseCapturedBy = byWhom; 7754 win.grabInput(); 7755 } 7756 void releaseMouseCapture() { 7757 mouseCaptureCount--; 7758 mouseCapturedBy = null; 7759 win.releaseInputGrab(); 7760 } 7761 7762 /++ 7763 Sets the window icon which is often seen in title bars and taskbars. 7764 7765 History: 7766 Added April 5, 2022 (dub v10.8) 7767 +/ 7768 @property void icon(MemoryImage icon) { 7769 if(win && icon) 7770 win.icon = icon; 7771 } 7772 7773 /// 7774 @scriptable 7775 @property bool focused() { 7776 return win.focused; 7777 } 7778 7779 static class Style : Widget.Style { 7780 override WidgetBackground background() { 7781 version(custom_widgets) 7782 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 7783 else version(win32_widgets) 7784 return WidgetBackground(Color.transparent); 7785 else static assert(0); 7786 } 7787 } 7788 mixin OverrideStyle!Style; 7789 7790 /++ 7791 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. 7792 +/ 7793 static int lineHeight() { 7794 OperatingSystemFont font; 7795 if(auto vt = WidgetPainter.visualTheme) { 7796 font = vt.defaultFontCached(); 7797 } 7798 7799 if(font is null) { 7800 static int defaultHeightCache; 7801 if(defaultHeightCache == 0) { 7802 font = new OperatingSystemFont; 7803 font.loadDefault; 7804 defaultHeightCache = font.height() * 5 / 4; 7805 } 7806 return defaultHeightCache; 7807 } 7808 7809 return font.height() * 5 / 4; 7810 } 7811 7812 Widget focusedWidget; 7813 7814 private SimpleWindow win_; 7815 7816 @property { 7817 /++ 7818 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 7819 7820 History: 7821 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 7822 +/ 7823 public SimpleWindow win() { 7824 return win_; 7825 } 7826 /// 7827 protected void win(SimpleWindow w) { 7828 win_ = w; 7829 } 7830 } 7831 7832 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 7833 this(Widget p) { 7834 tabStop = false; 7835 super(p); 7836 } 7837 7838 private void actualRedraw() { 7839 if(recomputeChildLayoutRequired) 7840 recomputeChildLayoutEntry(); 7841 if(!showing) return; 7842 7843 assert(parentWindow !is null); 7844 7845 auto w = drawableWindow; 7846 if(w is null) 7847 w = parentWindow.win; 7848 7849 if(w.closed()) 7850 return; 7851 7852 auto ugh = this.parent; 7853 int lox, loy; 7854 while(ugh) { 7855 lox += ugh.x; 7856 loy += ugh.y; 7857 ugh = ugh.parent; 7858 } 7859 auto painter = w.draw(true); 7860 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 7861 // RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN); 7862 } 7863 7864 7865 private bool skipNextChar = false; 7866 7867 /++ 7868 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 7869 7870 This constructor is intended primarily for internal use and may be changed to `protected` later. 7871 +/ 7872 this(SimpleWindow win) { 7873 7874 static if(UsingSimpledisplayX11) { 7875 win.discardAdditionalConnectionState = &discardXConnectionState; 7876 win.recreateAdditionalConnectionState = &recreateXConnectionState; 7877 } 7878 7879 tabStop = false; 7880 super(null); 7881 this.win = win; 7882 7883 win.addEventListener((Widget.RedrawEvent) { 7884 if(win.eventQueued!RecomputeEvent) { 7885 // import std.stdio; writeln("skipping"); 7886 return; // let the recompute event do the actual redraw 7887 } 7888 this.actualRedraw(); 7889 }); 7890 7891 win.addEventListener((Widget.RecomputeEvent) { 7892 recomputeChildLayoutEntry(); 7893 if(win.eventQueued!RedrawEvent) 7894 return; // let the queued one do it 7895 else { 7896 // import std.stdio; writeln("drawing"); 7897 this.actualRedraw(); // if not queued, it needs to be done now anyway 7898 } 7899 }); 7900 7901 this.width = win.width; 7902 this.height = win.height; 7903 this.parentWindow = this; 7904 7905 win.closeQuery = () { 7906 if(this.emit!ClosingEvent()) 7907 win.close(); 7908 }; 7909 win.onClosing = () { 7910 this.emit!ClosedEvent(); 7911 }; 7912 7913 win.windowResized = (int w, int h) { 7914 this.width = w; 7915 this.height = h; 7916 recomputeChildLayout(); 7917 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 7918 //version(win32_widgets) 7919 //InvalidateRect(hwnd, null, true); 7920 redraw(); 7921 }; 7922 7923 win.onFocusChange = (bool getting) { 7924 if(this.focusedWidget) { 7925 if(getting) { 7926 this.focusedWidget.emit!FocusEvent(); 7927 this.focusedWidget.emit!FocusInEvent(); 7928 } else { 7929 this.focusedWidget.emit!BlurEvent(); 7930 this.focusedWidget.emit!FocusOutEvent(); 7931 } 7932 } 7933 7934 if(getting) { 7935 this.emit!FocusEvent(); 7936 this.emit!FocusInEvent(); 7937 } else { 7938 this.emit!BlurEvent(); 7939 this.emit!FocusOutEvent(); 7940 } 7941 }; 7942 7943 win.onDpiChanged = { 7944 this.queueRecomputeChildLayout(); 7945 auto event = new DpiChangedEvent(this); 7946 event.sendDirectly(); 7947 7948 privateDpiChanged(); 7949 }; 7950 7951 win.setEventHandlers( 7952 (MouseEvent e) { 7953 dispatchMouseEvent(e); 7954 }, 7955 (KeyEvent e) { 7956 //import std.stdio; 7957 //writefln("%x %s", cast(uint) e.key, e.key); 7958 dispatchKeyEvent(e); 7959 }, 7960 (dchar e) { 7961 if(e == 13) e = 10; // hack? 7962 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 7963 dispatchCharEvent(e); 7964 }, 7965 ); 7966 7967 addEventListener("char", (Widget, Event ev) { 7968 if(skipNextChar) { 7969 ev.preventDefault(); 7970 skipNextChar = false; 7971 } 7972 }); 7973 7974 version(win32_widgets) 7975 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 7976 if(hwnd !is this.win.impl.hwnd) 7977 return 1; // we don't care... pass it on 7978 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 7979 if(mustReturn) 7980 return ret; 7981 return 1; // pass it on 7982 }; 7983 7984 if(Window.newWindowCreated) 7985 Window.newWindowCreated(this); 7986 } 7987 7988 version(custom_widgets) 7989 override void defaultEventHandler_click(ClickEvent event) { 7990 if(event.target && event.target.tabStop) 7991 event.target.focus(); 7992 } 7993 7994 private static void delegate(Window) newWindowCreated; 7995 7996 version(win32_widgets) 7997 override void paint(WidgetPainter painter) { 7998 /* 7999 RECT rect; 8000 rect.right = this.width; 8001 rect.bottom = this.height; 8002 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 8003 */ 8004 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 8005 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 8006 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 8007 // since the pen is null, to fill the whole space, we need the +1 on both. 8008 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 8009 SelectObject(painter.impl.hdc, p); 8010 SelectObject(painter.impl.hdc, b); 8011 } 8012 version(custom_widgets) 8013 override void paint(WidgetPainter painter) { 8014 auto cs = getComputedStyle(); 8015 painter.fillColor = cs.windowBackgroundColor; 8016 painter.outlineColor = cs.windowBackgroundColor; 8017 painter.drawRectangle(Point(0, 0), this.width, this.height); 8018 } 8019 8020 8021 override void defaultEventHandler_keydown(KeyDownEvent event) { 8022 Widget _this = event.target; 8023 8024 if(event.key == Key.Tab) { 8025 /* Window tab ordering is a recursive thingy with each group */ 8026 8027 // FIXME inefficient 8028 Widget[] helper(Widget p) { 8029 if(p.hidden) 8030 return null; 8031 Widget[] childOrdering; 8032 8033 auto children = p.children.dup; 8034 8035 while(true) { 8036 // UIs should be generally small, so gonna brute force it a little 8037 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 8038 8039 Widget smallestTab; 8040 foreach(ref c; children) { 8041 if(c is null) continue; 8042 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 8043 smallestTab = c; 8044 c = null; 8045 } 8046 } 8047 if(smallestTab !is null) { 8048 if(smallestTab.tabStop && !smallestTab.hidden) 8049 childOrdering ~= smallestTab; 8050 if(!smallestTab.hidden) 8051 childOrdering ~= helper(smallestTab); 8052 } else 8053 break; 8054 8055 } 8056 8057 return childOrdering; 8058 } 8059 8060 Widget[] tabOrdering = helper(this); 8061 8062 Widget recipient; 8063 8064 if(tabOrdering.length) { 8065 bool seenThis = false; 8066 Widget previous; 8067 foreach(idx, child; tabOrdering) { 8068 if(child is focusedWidget) { 8069 8070 if(event.shiftKey) { 8071 if(idx == 0) 8072 recipient = tabOrdering[$-1]; 8073 else 8074 recipient = tabOrdering[idx - 1]; 8075 break; 8076 } 8077 8078 seenThis = true; 8079 if(idx + 1 == tabOrdering.length) { 8080 // we're at the end, either move to the next group 8081 // or start back over 8082 recipient = tabOrdering[0]; 8083 } 8084 continue; 8085 } 8086 if(seenThis) { 8087 recipient = child; 8088 break; 8089 } 8090 previous = child; 8091 } 8092 } 8093 8094 if(recipient !is null) { 8095 // import std.stdio; writeln(typeid(recipient)); 8096 recipient.focus(); 8097 8098 skipNextChar = true; 8099 } 8100 } 8101 8102 debug if(event.key == Key.F12) { 8103 if(devTools) { 8104 devTools.close(); 8105 devTools = null; 8106 } else { 8107 devTools = new DevToolWindow(this); 8108 devTools.show(); 8109 } 8110 } 8111 } 8112 8113 debug DevToolWindow devTools; 8114 8115 8116 /++ 8117 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 8118 8119 History: 8120 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 8121 8122 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 8123 +/ 8124 this(int width = 500, int height = 500, string title = null) { 8125 if(title is null) { 8126 import core.runtime; 8127 if(Runtime.args.length) 8128 title = Runtime.args[0]; 8129 } 8130 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus); 8131 8132 static if(UsingSimpledisplayX11) { 8133 ///+ 8134 // for input proxy 8135 auto display = XDisplayConnection.get; 8136 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 8137 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask); 8138 XMapWindow(display, inputProxy); 8139 //import std.stdio; writefln("input proxy: 0x%0x", inputProxy); 8140 this.inputProxy = new SimpleWindow(inputProxy); 8141 8142 XEvent lastEvent; 8143 this.inputProxy.handleNativeEvent = (XEvent ev) { 8144 lastEvent = ev; 8145 return 1; 8146 }; 8147 this.inputProxy.setEventHandlers( 8148 (MouseEvent e) { 8149 dispatchMouseEvent(e); 8150 }, 8151 (KeyEvent e) { 8152 //import std.stdio; 8153 //writefln("%x %s", cast(uint) e.key, e.key); 8154 if(dispatchKeyEvent(e)) { 8155 // FIXME: i should trap error 8156 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 8157 auto thing = nw.focusableWindow(); 8158 if(thing && thing.window) { 8159 lastEvent.xkey.window = thing.window; 8160 // import std.stdio; writeln("sending event ", lastEvent.xkey); 8161 trapXErrors( { 8162 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 8163 }); 8164 } 8165 } 8166 } 8167 }, 8168 (dchar e) { 8169 if(e == 13) e = 10; // hack? 8170 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8171 dispatchCharEvent(e); 8172 }, 8173 ); 8174 // done 8175 //+/ 8176 } 8177 8178 8179 8180 win.setRequestedInputFocus = &this.setRequestedInputFocus; 8181 8182 this(win); 8183 } 8184 8185 SimpleWindow inputProxy; 8186 8187 private SimpleWindow setRequestedInputFocus() { 8188 return inputProxy; 8189 } 8190 8191 /// ditto 8192 this(string title, int width = 500, int height = 500) { 8193 this(width, height, title); 8194 } 8195 8196 /// 8197 @property string title() { return parentWindow.win.title; } 8198 /// 8199 @property void title(string title) { parentWindow.win.title = title; } 8200 8201 /// 8202 @scriptable 8203 void close() { 8204 win.close(); 8205 // I synchronize here upon window closing to ensure all child windows 8206 // get updated too before the event loop. This avoids some random X errors. 8207 static if(UsingSimpledisplayX11) { 8208 runInGuiThread( { 8209 XSync(XDisplayConnection.get, false); 8210 }); 8211 } 8212 } 8213 8214 bool dispatchKeyEvent(KeyEvent ev) { 8215 auto wid = focusedWidget; 8216 if(wid is null) 8217 wid = this; 8218 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 8219 event.originalKeyEvent = ev; 8220 event.key = ev.key; 8221 event.state = ev.modifierState; 8222 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8223 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8224 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8225 event.dispatch(); 8226 8227 return !event.propagationStopped; 8228 } 8229 8230 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 8231 bool dispatchCharEvent(dchar ch) { 8232 if(focusedWidget) { 8233 auto event = new CharEvent(focusedWidget, ch); 8234 event.dispatch(); 8235 return !event.propagationStopped; 8236 } 8237 return true; 8238 } 8239 8240 Widget mouseLastOver; 8241 Widget mouseLastDownOn; 8242 bool lastWasDoubleClick; 8243 bool dispatchMouseEvent(MouseEvent ev) { 8244 auto eleR = widgetAtPoint(this, ev.x, ev.y); 8245 auto ele = eleR.widget; 8246 8247 auto captureEle = ele; 8248 8249 if(mouseCapturedBy !is null) { 8250 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 8251 captureEle = mouseCapturedBy; 8252 } 8253 8254 // a hack to get it relative to the widget. 8255 eleR.x = ev.x; 8256 eleR.y = ev.y; 8257 auto pain = captureEle; 8258 while(pain) { 8259 eleR.x -= pain.x; 8260 eleR.y -= pain.y; 8261 pain.addScrollPosition(eleR.x, eleR.y); 8262 pain = pain.parent; 8263 } 8264 8265 void populateMouseEventBase(MouseEventBase event) { 8266 event.button = ev.button; 8267 event.buttonLinear = ev.buttonLinear; 8268 event.state = ev.modifierState; 8269 event.clientX = eleR.x; 8270 event.clientY = eleR.y; 8271 8272 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8273 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8274 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8275 } 8276 8277 if(ev.type == MouseEventType.buttonPressed) { 8278 { 8279 auto event = new MouseDownEvent(captureEle); 8280 populateMouseEventBase(event); 8281 event.dispatch(); 8282 } 8283 8284 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 8285 auto event = new DoubleClickEvent(captureEle); 8286 populateMouseEventBase(event); 8287 event.dispatch(); 8288 lastWasDoubleClick = ev.doubleClick; 8289 } else { 8290 lastWasDoubleClick = false; 8291 } 8292 8293 mouseLastDownOn = ele; 8294 } else if(ev.type == MouseEventType.buttonReleased) { 8295 { 8296 auto event = new MouseUpEvent(captureEle); 8297 populateMouseEventBase(event); 8298 event.dispatch(); 8299 } 8300 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 8301 auto event = new ClickEvent(captureEle); 8302 populateMouseEventBase(event); 8303 event.dispatch(); 8304 } 8305 } else if(ev.type == MouseEventType.motion) { 8306 // motion 8307 { 8308 auto event = new MouseMoveEvent(captureEle); 8309 populateMouseEventBase(event); // fills in button which is meaningless but meh 8310 event.dispatch(); 8311 } 8312 8313 if(mouseLastOver !is ele) { 8314 if(ele !is null) { 8315 if(!isAParentOf(ele, mouseLastOver)) { 8316 ele.setDynamicState(DynamicState.hover, true); 8317 auto event = new MouseEnterEvent(ele); 8318 event.relatedTarget = mouseLastOver; 8319 event.sendDirectly(); 8320 8321 ele.useStyleProperties((scope Widget.Style s) { 8322 ele.parentWindow.win.cursor = s.cursor; 8323 }); 8324 } 8325 } 8326 8327 if(mouseLastOver !is null) { 8328 if(!isAParentOf(mouseLastOver, ele)) { 8329 mouseLastOver.setDynamicState(DynamicState.hover, false); 8330 auto event = new MouseLeaveEvent(mouseLastOver); 8331 event.relatedTarget = ele; 8332 event.sendDirectly(); 8333 } 8334 } 8335 8336 if(ele !is null) { 8337 auto event = new MouseOverEvent(ele); 8338 event.relatedTarget = mouseLastOver; 8339 event.dispatch(); 8340 } 8341 8342 if(mouseLastOver !is null) { 8343 auto event = new MouseOutEvent(mouseLastOver); 8344 event.relatedTarget = ele; 8345 event.dispatch(); 8346 } 8347 8348 mouseLastOver = ele; 8349 } 8350 } 8351 8352 return true; // FIXME: the event default prevented? 8353 } 8354 8355 /++ 8356 Shows the window and runs the application event loop. 8357 8358 Blocks until this window is closed. 8359 8360 History: 8361 The [BlockingMode] parameter was added on December 8, 2021. 8362 The default behavior is to block until the application quits 8363 (so all windows have been closed), unless another minigui or 8364 simpledisplay event loop is already running, in which case it 8365 will block until this window closes specifically. 8366 +/ 8367 @scriptable 8368 void loop(BlockingMode bm = BlockingMode.automatic) { 8369 if(win.closed) 8370 return; // otherwise show will throw 8371 show(); 8372 win.eventLoopWithBlockingMode(bm, 0); 8373 } 8374 8375 private bool firstShow = true; 8376 8377 @scriptable 8378 override void show() { 8379 bool rd = false; 8380 if(firstShow) { 8381 firstShow = false; 8382 recomputeChildLayout(); 8383 auto f = getFirstFocusable(this); // FIXME: autofocus? 8384 if(f) 8385 f.focus(); 8386 redraw(); 8387 } 8388 win.show(); 8389 super.show(); 8390 } 8391 @scriptable 8392 override void hide() { 8393 win.hide(); 8394 super.hide(); 8395 } 8396 8397 static Widget getFirstFocusable(Widget start) { 8398 if(start is null) 8399 return null; 8400 8401 foreach(widget; &start.focusableWidgets) { 8402 return widget; 8403 } 8404 8405 return null; 8406 } 8407 8408 static Widget getLastFocusable(Widget start) { 8409 if(start is null) 8410 return null; 8411 8412 Widget last; 8413 foreach(widget; &start.focusableWidgets) { 8414 last = widget; 8415 } 8416 8417 return last; 8418 } 8419 8420 8421 mixin Emits!ClosingEvent; 8422 mixin Emits!ClosedEvent; 8423 } 8424 8425 /++ 8426 History: 8427 Added January 12, 2022 8428 +/ 8429 class DpiChangedEvent : Event { 8430 enum EventString = "dpichanged"; 8431 8432 this(Widget target) { 8433 super(EventString, target); 8434 } 8435 } 8436 8437 debug private class DevToolWindow : Window { 8438 Window p; 8439 8440 TextEdit parentList; 8441 TextEdit logWindow; 8442 TextLabel clickX, clickY; 8443 8444 this(Window p) { 8445 this.p = p; 8446 super(400, 300, "Developer Toolbox"); 8447 8448 logWindow = new TextEdit(this); 8449 parentList = new TextEdit(this); 8450 8451 auto hl = new HorizontalLayout(this); 8452 clickX = new TextLabel("", TextAlignment.Right, hl); 8453 clickY = new TextLabel("", TextAlignment.Right, hl); 8454 8455 parentListeners ~= p.addEventListener("*", (Event ev) { 8456 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 8457 }); 8458 8459 parentListeners ~= p.addEventListener((ClickEvent ev) { 8460 auto s = ev.srcElement; 8461 string list = s.toString(); 8462 s = s.parent; 8463 while(s) { 8464 list ~= "\n"; 8465 list ~= s.toString(); 8466 s = s.parent; 8467 } 8468 parentList.content = list; 8469 8470 clickX.label = toInternal!string(ev.clientX); 8471 clickY.label = toInternal!string(ev.clientY); 8472 }); 8473 } 8474 8475 EventListener[] parentListeners; 8476 8477 override void close() { 8478 assert(p !is null); 8479 foreach(p; parentListeners) 8480 p.disconnect(); 8481 parentListeners = null; 8482 p.devTools = null; 8483 p = null; 8484 super.close(); 8485 } 8486 8487 override void defaultEventHandler_keydown(KeyDownEvent ev) { 8488 if(ev.key == Key.F12) { 8489 this.close(); 8490 if(p) 8491 p.devTools = null; 8492 } else { 8493 super.defaultEventHandler_keydown(ev); 8494 } 8495 } 8496 8497 void log(T...)(T t) { 8498 string str; 8499 import std.conv; 8500 foreach(i; t) 8501 str ~= to!string(i); 8502 str ~= "\n"; 8503 logWindow.addText(str); 8504 8505 version(custom_widgets) 8506 logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 8507 } 8508 } 8509 8510 /++ 8511 A dialog is a transient window that intends to get information from 8512 the user before being dismissed. 8513 +/ 8514 abstract class Dialog : Window { 8515 /// 8516 this(int width, int height, string title = null) { 8517 super(width, height, title); 8518 } 8519 8520 /// 8521 abstract void OK(); 8522 8523 /// 8524 void Cancel() { 8525 this.close(); 8526 } 8527 } 8528 8529 /++ 8530 A custom widget similar to the HTML5 <details> tag. 8531 +/ 8532 version(none) 8533 class DetailsView : Widget { 8534 8535 } 8536 8537 // FIXME: maybe i should expose the other list views Windows offers too 8538 8539 /++ 8540 A TableView is a widget made to display a table of data strings. 8541 8542 8543 Future_Directions: 8544 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 8545 8546 I will add a selection changed event at some point, as well as item clicked events. 8547 History: 8548 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 8549 See_Also: 8550 [ListWidget] which displays a list of strings without additional columns. 8551 +/ 8552 class TableView : Widget { 8553 /++ 8554 8555 +/ 8556 this(Widget parent) { 8557 super(parent); 8558 8559 version(win32_widgets) { 8560 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 8561 } else version(custom_widgets) { 8562 auto smw = new ScrollMessageWidget(this); 8563 smw.addDefaultKeyboardListeners(); 8564 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 8565 tvwi = new TableViewWidgetInner(this, smw); 8566 } 8567 } 8568 8569 // FIXME: auto-size columns on double click of header thing like in Windows 8570 // it need only make the currently displayed things fit well. 8571 8572 8573 private ColumnInfo[] columns; 8574 private int itemCount; 8575 8576 version(custom_widgets) private { 8577 TableViewWidgetInner tvwi; 8578 } 8579 8580 /// Passed to [setColumnInfo] 8581 static struct ColumnInfo { 8582 const(char)[] name; /// the name displayed in the header 8583 /++ 8584 The default width, in pixels. As a special case, you can set this to -1 8585 if you want the system to try to automatically size the width to fit visible 8586 content. If it can't, it will try to pick a sensible default size. 8587 8588 Any other negative value is not allowed and may lead to unpredictable results. 8589 8590 History: 8591 The -1 behavior was specified on December 3, 2021. It actually worked before 8592 anyway on Win32 but now it is a formal feature with partial Linux support. 8593 8594 Bugs: 8595 It doesn't actually attempt to calculate a best-fit width on Linux as of 8596 December 3, 2021. I do plan to fix this in the future, but Windows is the 8597 priority right now. At least it doesn't break things when you use it now. 8598 +/ 8599 int width; 8600 8601 /++ 8602 Alignment of the text in the cell. Applies to the header as well as all data in this 8603 column. 8604 8605 Bugs: 8606 On Windows, the first column ignores this member and is always left aligned. 8607 You can work around this by inserting a dummy first column with width = 0 8608 then putting your actual data in the second column, which does respect the 8609 alignment. 8610 8611 This is a quirk of the operating system's implementation going back a very 8612 long time and is unlikely to ever be fixed. 8613 +/ 8614 TextAlignment alignment; 8615 8616 /++ 8617 After all the pixel widths have been assigned, any left over 8618 space is divided up among all columns and distributed to according 8619 to the widthPercent field. 8620 8621 8622 For example, if you have two fields, both with width 50 and one with 8623 widthPercent of 25 and the other with widthPercent of 75, and the 8624 container is 200 pixels wide, first both get their width of 50. 8625 then the 100 remaining pixels are split up, so the one gets a total 8626 of 75 pixels and the other gets a total of 125. 8627 8628 This is automatically applied as the window is resized. 8629 8630 If there is not enough space - that is, when a horizontal scrollbar 8631 needs to appear - there are 0 pixels divided up, and thus everyone 8632 gets 0. This can cause a column to shrink out of proportion when 8633 passing the scroll threshold. 8634 8635 It is important to still set a fixed width (that is, to populate the 8636 `width` field) even if you use the percents because that will be the 8637 default minimum in the event of a scroll bar appearing. 8638 8639 The percents total in the column can never exceed 100 or be less than 0. 8640 Doing this will trigger an assert error. 8641 8642 Implementation note: 8643 8644 Please note that percentages are only recalculated 1) upon original 8645 construction and 2) upon resizing the control. If the user adjusts the 8646 width of a column, the percentage items will not be updated. 8647 8648 On the other hand, if the user adjusts the width of a percentage column 8649 then resizes the window, it is recalculated, meaning their hand adjustment 8650 is discarded. This specific behavior may change in the future as it is 8651 arguably a bug, but I'm not certain yet. 8652 8653 History: 8654 Added November 10, 2021 (dub v10.4) 8655 +/ 8656 int widthPercent; 8657 8658 8659 private int calculatedWidth; 8660 } 8661 /++ 8662 Sets the number of columns along with information about the headers. 8663 8664 Please note: on Windows, the first column ignores your alignment preference 8665 and is always left aligned. 8666 +/ 8667 void setColumnInfo(ColumnInfo[] columns...) { 8668 8669 foreach(ref c; columns) { 8670 c.name = c.name.idup; 8671 } 8672 this.columns = columns.dup; 8673 8674 updateCalculatedWidth(false); 8675 8676 version(custom_widgets) { 8677 tvwi.header.updateHeaders(); 8678 tvwi.updateScrolls(); 8679 } else version(win32_widgets) 8680 foreach(i, column; this.columns) { 8681 LVCOLUMN lvColumn; 8682 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 8683 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 8684 8685 auto bfr = WCharzBuffer(column.name); 8686 lvColumn.pszText = bfr.ptr; 8687 8688 if(column.alignment & TextAlignment.Center) 8689 lvColumn.fmt = LVCFMT_CENTER; 8690 else if(column.alignment & TextAlignment.Right) 8691 lvColumn.fmt = LVCFMT_RIGHT; 8692 else 8693 lvColumn.fmt = LVCFMT_LEFT; 8694 8695 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 8696 throw new WindowsApiException("Insert Column Fail"); 8697 } 8698 } 8699 8700 private int getActualSetSize(size_t i, bool askWindows) { 8701 version(win32_widgets) 8702 if(askWindows) 8703 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 8704 auto w = columns[i].width; 8705 if(w == -1) 8706 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 8707 return w; 8708 } 8709 8710 private void updateCalculatedWidth(bool informWindows) { 8711 int padding; 8712 version(win32_widgets) 8713 padding = 4; 8714 int remaining = this.width; 8715 foreach(i, column; columns) 8716 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 8717 remaining -= padding; 8718 if(remaining < 0) 8719 remaining = 0; 8720 8721 int percentTotal; 8722 foreach(i, ref column; columns) { 8723 percentTotal += column.widthPercent; 8724 8725 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 8726 8727 column.calculatedWidth = c; 8728 8729 version(win32_widgets) 8730 if(informWindows) 8731 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 8732 } 8733 8734 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 8735 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)."); 8736 8737 8738 } 8739 8740 override void registerMovement() { 8741 super.registerMovement(); 8742 8743 updateCalculatedWidth(true); 8744 } 8745 8746 /++ 8747 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. 8748 +/ 8749 void setItemCount(int count) { 8750 this.itemCount = count; 8751 version(custom_widgets) { 8752 tvwi.updateScrolls(); 8753 redraw(); 8754 } else version(win32_widgets) { 8755 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 8756 } 8757 } 8758 8759 /++ 8760 Clears all items; 8761 +/ 8762 void clear() { 8763 this.itemCount = 0; 8764 this.columns = null; 8765 version(custom_widgets) { 8766 tvwi.header.updateHeaders(); 8767 tvwi.updateScrolls(); 8768 redraw(); 8769 } else version(win32_widgets) { 8770 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 8771 } 8772 } 8773 8774 /+ 8775 version(win32_widgets) 8776 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 8777 auto itemId = dis.itemID; 8778 auto hdc = dis.hDC; 8779 auto rect = dis.rcItem; 8780 switch(dis.itemAction) { 8781 case ODA_DRAWENTIRE: 8782 8783 // FIXME: do other items 8784 // FIXME: do the focus rectangle i guess 8785 // FIXME: alignment 8786 // FIXME: column width 8787 // FIXME: padding left 8788 // FIXME: check dpi scaling 8789 // FIXME: don't owner draw unless it is necessary. 8790 8791 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 8792 RECT itemRect; 8793 itemRect.top = 1; // subitem idx, 1-based 8794 itemRect.left = LVIR_BOUNDS; 8795 8796 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 8797 itemRect.left += padding; 8798 8799 getData(itemId, 0, (in char[] data) { 8800 auto wdata = WCharzBuffer(data); 8801 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 8802 8803 }); 8804 goto case; 8805 case ODA_FOCUS: 8806 if(dis.itemState & ODS_FOCUS) 8807 DrawFocusRect(hdc, &rect); 8808 break; 8809 case ODA_SELECT: 8810 // itemState & ODS_SELECTED 8811 break; 8812 default: 8813 } 8814 return 1; 8815 } 8816 +/ 8817 8818 version(win32_widgets) { 8819 CellStyle last; 8820 COLORREF defaultColor; 8821 COLORREF defaultBackground; 8822 } 8823 8824 version(win32_widgets) 8825 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 8826 switch(code) { 8827 case NM_CUSTOMDRAW: 8828 auto s = cast(NMLVCUSTOMDRAW*) hdr; 8829 switch(s.nmcd.dwDrawStage) { 8830 case CDDS_PREPAINT: 8831 if(getCellStyle is null) 8832 return 0; 8833 8834 mustReturn = true; 8835 return CDRF_NOTIFYITEMDRAW; 8836 case CDDS_ITEMPREPAINT: 8837 mustReturn = true; 8838 return CDRF_NOTIFYSUBITEMDRAW; 8839 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 8840 mustReturn = true; 8841 8842 if(getCellStyle is null) // this SHOULD never happen... 8843 return 0; 8844 8845 if(s.iSubItem == 0) { 8846 // Windows resets it per row so we'll use item 0 as a chance 8847 // to capture these for later 8848 defaultColor = s.clrText; 8849 defaultBackground = s.clrTextBk; 8850 } 8851 8852 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 8853 // if no special style and no reset needed... 8854 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 8855 return 0; // allow default processing to continue 8856 8857 last = style; 8858 8859 // might still need to reset or use the preference. 8860 8861 if(style.flags & CellStyle.Flags.textColorSet) 8862 s.clrText = style.textColor.asWindowsColorRef; 8863 else 8864 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 8865 if(style.flags & CellStyle.Flags.backgroundColorSet) 8866 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 8867 else 8868 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 8869 8870 return CDRF_NEWFONT; 8871 default: 8872 return 0; 8873 8874 } 8875 case NM_RETURN: // no need since i subclass keydown 8876 break; 8877 case LVN_COLUMNCLICK: 8878 auto info = cast(LPNMLISTVIEW) hdr; 8879 this.emit!HeaderClickedEvent(info.iSubItem); 8880 break; 8881 case NM_CLICK: 8882 case NM_DBLCLK: 8883 case NM_RCLICK: 8884 case NM_RDBLCLK: 8885 // the item/subitem is set here and that can be a useful notification 8886 // even beyond the normal click notification 8887 break; 8888 case LVN_GETDISPINFO: 8889 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 8890 if(info.item.mask & LVIF_TEXT) { 8891 if(getData) { 8892 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 8893 auto bfr = WCharzBuffer(dataReceived); 8894 auto len = info.item.cchTextMax; 8895 if(bfr.length < len) 8896 len = cast(typeof(len)) bfr.length; 8897 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 8898 info.item.pszText[len] = 0; 8899 }); 8900 } else { 8901 info.item.pszText[0] = 0; 8902 } 8903 //info.item.iItem 8904 //if(info.item.iSubItem) 8905 } 8906 break; 8907 default: 8908 } 8909 return 0; 8910 } 8911 8912 override bool encapsulatedChildren() { 8913 return true; 8914 } 8915 8916 /++ 8917 Informs the control that content has changed. 8918 8919 History: 8920 Added November 10, 2021 (dub v10.4) 8921 +/ 8922 void update() { 8923 version(custom_widgets) 8924 redraw(); 8925 else { 8926 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 8927 UpdateWindow(hwnd); 8928 } 8929 8930 8931 } 8932 8933 /++ 8934 Called by the system to request the text content of an individual cell. You 8935 should pass the text into the provided `sink` delegate. This function will be 8936 called for each visible cell as-needed when drawing. 8937 +/ 8938 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 8939 8940 /++ 8941 Available per-cell style customization options. Use one of the constructors 8942 provided to set the values conveniently, or default construct it and set individual 8943 values yourself. Just remember to set the `flags` so your values are actually used. 8944 If the flag isn't set, the field is ignored and the system default is used instead. 8945 8946 This is returned by the [getCellStyle] delegate. 8947 8948 Examples: 8949 --- 8950 // assumes you have a variables called `my_data` which is an array of arrays of numbers 8951 auto table = new TableView(window); 8952 // snip: you would set up columns here 8953 8954 // this is how you provide data to the table view class 8955 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 8956 import std.conv; 8957 sink(to!string(my_data[row][column])); 8958 }; 8959 8960 // and this is how you customize the colors 8961 table.getCellStyle = delegate(int row, int column) { 8962 return (my_data[row][column] < 0) ? 8963 TableView.CellStyle(Color.red); // make negative numbers red 8964 : TableView.CellStyle.init; // leave the rest alone 8965 }; 8966 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 8967 --- 8968 8969 History: 8970 Added November 27, 2021 (dub v10.4) 8971 +/ 8972 struct CellStyle { 8973 /// 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. 8974 this(Color textColor) { 8975 this.textColor = textColor; 8976 this.flags |= Flags.textColorSet; 8977 } 8978 /// Sets a custom text and background color. 8979 this(Color textColor, Color backgroundColor) { 8980 this.textColor = textColor; 8981 this.backgroundColor = backgroundColor; 8982 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 8983 } 8984 8985 Color textColor; 8986 Color backgroundColor; 8987 int flags; /// bitmask of [Flags] 8988 /// available options to combine into [flags] 8989 enum Flags { 8990 textColorSet = 1 << 0, 8991 backgroundColorSet = 1 << 1, 8992 } 8993 } 8994 /++ 8995 Companion delegate to [getData] that allows you to custom style each 8996 cell of the table. 8997 8998 Returns: 8999 A [CellStyle] structure that describes the desired style for the 9000 given cell. `return CellStyle.init` if you want the default style. 9001 9002 History: 9003 Added November 27, 2021 (dub v10.4) 9004 +/ 9005 CellStyle delegate(int row, int column) getCellStyle; 9006 9007 // i want to be able to do things like draw little colored things to show red for negative numbers 9008 // or background color indicators or even in-cell charts 9009 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 9010 9011 /++ 9012 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 9013 +/ 9014 mixin Emits!HeaderClickedEvent; 9015 } 9016 9017 /++ 9018 This is emitted by the [TableView] when a user clicks on a column header. 9019 9020 Its member `columnIndex` has the zero-based index of the column that was clicked. 9021 9022 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 9023 9024 History: 9025 Added November 27, 2021 (dub v10.4) 9026 +/ 9027 class HeaderClickedEvent : Event { 9028 enum EventString = "HeaderClicked"; 9029 this(Widget target, int columnIndex) { 9030 this.columnIndex = columnIndex; 9031 super(EventString, target); 9032 } 9033 9034 /// The index of the column 9035 int columnIndex; 9036 9037 /// 9038 override @property int intValue() { 9039 return columnIndex; 9040 } 9041 } 9042 9043 version(custom_widgets) 9044 private class TableViewWidgetInner : Widget { 9045 9046 // wrap this thing in a ScrollMessageWidget 9047 9048 TableView tvw; 9049 ScrollMessageWidget smw; 9050 HeaderWidget header; 9051 9052 this(TableView tvw, ScrollMessageWidget smw) { 9053 this.tvw = tvw; 9054 this.smw = smw; 9055 super(smw); 9056 9057 this.tabStop = true; 9058 9059 header = new HeaderWidget(this, smw.getHeader()); 9060 9061 smw.addEventListener("scroll", () { 9062 this.redraw(); 9063 header.redraw(); 9064 }); 9065 9066 9067 // I need headers outside the scroll area but rendered on the same line as the up arrow 9068 // FIXME: add a fixed header to the SMW 9069 } 9070 9071 enum padding = 3; 9072 9073 void updateScrolls() { 9074 int w; 9075 foreach(idx, column; tvw.columns) { 9076 if(column.width == 0) continue; 9077 w += tvw.getActualSetSize(idx, false);// + padding; 9078 } 9079 smw.setTotalArea(w, tvw.itemCount); 9080 columnsWidth = w; 9081 } 9082 9083 private int columnsWidth; 9084 9085 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 9086 9087 override void registerMovement() { 9088 super.registerMovement(); 9089 // FIXME: actual column width. it might need to be done per-pixel instead of per-colum 9090 smw.setViewableArea(this.width, this.height / lh); 9091 } 9092 9093 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 9094 int x; 9095 int y; 9096 9097 int row = smw.position.y; 9098 9099 foreach(lol; 0 .. this.height / lh) { 9100 if(row >= tvw.itemCount) 9101 break; 9102 x = 0; 9103 foreach(columnNumber, column; tvw.columns) { 9104 auto x2 = x + column.calculatedWidth; 9105 auto smwx = smw.position.x; 9106 9107 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 9108 auto startX = x; 9109 auto endX = x + column.calculatedWidth; 9110 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 9111 case TextAlignment.Left: startX += padding; break; 9112 case TextAlignment.Center: startX += padding; endX -= padding; break; 9113 case TextAlignment.Right: endX -= padding; break; 9114 default: /* broken */ break; 9115 } 9116 if(column.width != 0) // no point drawing an invisible column 9117 tvw.getData(row, cast(int) columnNumber, (info) { 9118 // auto clip = painter.setClipRectangle( 9119 9120 void dotext(WidgetPainter painter) { 9121 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 9122 } 9123 9124 if(tvw.getCellStyle !is null) { 9125 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 9126 9127 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 9128 auto tempPainter = painter; 9129 tempPainter.fillColor = style.backgroundColor; 9130 tempPainter.outlineColor = style.backgroundColor; 9131 9132 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 9133 Point(endX - smw.position.x, y + lh)); 9134 } 9135 auto tempPainter = painter; 9136 if(style.flags & TableView.CellStyle.Flags.textColorSet) 9137 tempPainter.outlineColor = style.textColor; 9138 9139 dotext(tempPainter); 9140 } else { 9141 dotext(painter); 9142 } 9143 }); 9144 } 9145 9146 x += column.calculatedWidth; 9147 } 9148 row++; 9149 y += lh; 9150 } 9151 return bounds; 9152 } 9153 9154 static class Style : Widget.Style { 9155 override WidgetBackground background() { 9156 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 9157 } 9158 } 9159 mixin OverrideStyle!Style; 9160 9161 private static class HeaderWidget : Widget { 9162 this(TableViewWidgetInner tvw, Widget parent) { 9163 super(parent); 9164 this.tvw = tvw; 9165 9166 this.remainder = new Button("", this); 9167 9168 this.addEventListener((scope ClickEvent ev) { 9169 int header = -1; 9170 foreach(idx, child; this.children[1 .. $]) { 9171 if(child is ev.target) { 9172 header = cast(int) idx; 9173 break; 9174 } 9175 } 9176 9177 if(header != -1) { 9178 auto hce = new HeaderClickedEvent(tvw.tvw, header); 9179 hce.dispatch(); 9180 } 9181 9182 }); 9183 } 9184 9185 void updateHeaders() { 9186 foreach(child; children[1 .. $]) 9187 child.removeWidget(); 9188 9189 foreach(column; tvw.tvw.columns) { 9190 // the cast is ok because I dup it above, just the type is never changed. 9191 // all this is private so it should never get messed up. 9192 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 9193 } 9194 } 9195 9196 Button remainder; 9197 TableViewWidgetInner tvw; 9198 9199 override void recomputeChildLayout() { 9200 registerMovement(); 9201 int pos; 9202 foreach(idx, child; children[1 .. $]) { 9203 if(idx >= tvw.tvw.columns.length) 9204 continue; 9205 child.x = pos; 9206 child.y = 0; 9207 child.width = tvw.tvw.columns[idx].calculatedWidth; 9208 child.height = scaleWithDpi(16);// this.height; 9209 pos += child.width; 9210 9211 child.recomputeChildLayout(); 9212 } 9213 9214 if(remainder is null) 9215 return; 9216 9217 remainder.x = pos; 9218 remainder.y = 0; 9219 if(pos < this.width) 9220 remainder.width = this.width - pos;// + 4; 9221 else 9222 remainder.width = 0; 9223 remainder.height = scaleWithDpi(16); 9224 9225 remainder.recomputeChildLayout(); 9226 } 9227 9228 // for the scrollable children mixin 9229 Point scrollOrigin() { 9230 return Point(tvw.smw.position.x, 0); 9231 } 9232 void paintFrameAndBackground(WidgetPainter painter) { } 9233 9234 mixin ScrollableChildren; 9235 } 9236 } 9237 9238 /+ 9239 9240 // given struct / array / number / string / etc, make it viewable and editable 9241 class DataViewerWidget : Widget { 9242 9243 } 9244 +/ 9245 9246 /++ 9247 A line edit box with an associated label. 9248 9249 History: 9250 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 9251 9252 ``` 9253 Old: ________ 9254 9255 New: 9256 ____________ 9257 ``` 9258 9259 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 9260 9261 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 9262 horizontal label but left aligned. You may also consider a [GridLayout]. 9263 +/ 9264 alias LabeledLineEdit = Labeled!LineEdit; 9265 9266 /++ 9267 History: 9268 Added May 19, 2021 9269 +/ 9270 class Labeled(T) : Widget { 9271 /// 9272 this(string label, Widget parent) { 9273 super(parent); 9274 initialize!VerticalLayout(label, TextAlignment.Left, parent); 9275 } 9276 9277 /++ 9278 History: 9279 The alignment parameter was added May 17, 2021 9280 +/ 9281 this(string label, TextAlignment alignment, Widget parent) { 9282 super(parent); 9283 initialize!HorizontalLayout(label, alignment, parent); 9284 } 9285 9286 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 9287 tabStop = false; 9288 horizontal = is(L == HorizontalLayout); 9289 auto hl = new L(this); 9290 this.label = new TextLabel(label, alignment, hl); 9291 this.lineEdit = new T(hl); 9292 9293 this.label.labelFor = this.lineEdit; 9294 } 9295 9296 private bool horizontal; 9297 9298 TextLabel label; /// 9299 T lineEdit; /// 9300 9301 override int flexBasisWidth() { return 250; } 9302 9303 override int minHeight() { return (horizontal ? 1 : 2) * defaultLineHeight + 4; } 9304 override int maxHeight() { return (horizontal ? 1 : 2) * defaultLineHeight + 4; } 9305 override int marginTop() { return 4; } 9306 override int marginBottom() { return 4; } 9307 9308 // FIXME: i should prolly call it value as well as content tbh 9309 9310 /// 9311 @property string content() { 9312 return lineEdit.content; 9313 } 9314 /// 9315 @property void content(string c) { 9316 return lineEdit.content(c); 9317 } 9318 9319 /// 9320 void selectAll() { 9321 lineEdit.selectAll(); 9322 } 9323 9324 override void focus() { 9325 lineEdit.focus(); 9326 } 9327 } 9328 9329 /++ 9330 A labeled password edit. 9331 9332 History: 9333 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 9334 9335 The default parameters for the constructors were also removed on May 19, 2021 9336 +/ 9337 alias LabeledPasswordEdit = Labeled!PasswordEdit; 9338 9339 private string toMenuLabel(string s) { 9340 string n; 9341 n.reserve(s.length); 9342 foreach(c; s) 9343 if(c == '_') 9344 n ~= ' '; 9345 else 9346 n ~= c; 9347 return n; 9348 } 9349 9350 private void autoExceptionHandler(Exception e) { 9351 messageBox(e.msg); 9352 } 9353 9354 private void delegate() makeAutomaticHandler(alias fn, T)(T t) { 9355 static if(is(T : void delegate())) { 9356 return () { 9357 try 9358 t(); 9359 catch(Exception e) 9360 autoExceptionHandler(e); 9361 }; 9362 } else static if(is(typeof(fn) Params == __parameters)) { 9363 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 9364 return () { 9365 void onOK(string s) { 9366 member = s; 9367 try 9368 t(Params[0](s)); 9369 catch(Exception e) 9370 autoExceptionHandler(e); 9371 } 9372 9373 if( 9374 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 9375 || type == FileDialogType.Save) 9376 { 9377 getSaveFileName(&onOK, member, filters, null); 9378 } else 9379 getOpenFileName(&onOK, member, filters, null); 9380 }; 9381 } else { 9382 struct S { 9383 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 9384 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 9385 } else mixin(q{ 9386 static foreach(idx, ignore; Params) { 9387 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 9388 } 9389 }); 9390 } 9391 return () { 9392 dialog((S s) { 9393 try 9394 cast(void) t(s.tupleof); 9395 catch(Exception e) 9396 autoExceptionHandler(e); 9397 }, null, __traits(identifier, fn)); 9398 }; 9399 } 9400 } 9401 } 9402 9403 private template hasAnyRelevantAnnotations(a...) { 9404 bool helper() { 9405 bool any; 9406 foreach(attr; a) { 9407 static if(is(typeof(attr) == .menu)) 9408 any = true; 9409 else static if(is(typeof(attr) == .toolbar)) 9410 any = true; 9411 else static if(is(attr == .separator)) 9412 any = true; 9413 else static if(is(typeof(attr) == .accelerator)) 9414 any = true; 9415 else static if(is(typeof(attr) == .hotkey)) 9416 any = true; 9417 else static if(is(typeof(attr) == .icon)) 9418 any = true; 9419 else static if(is(typeof(attr) == .label)) 9420 any = true; 9421 else static if(is(typeof(attr) == .tip)) 9422 any = true; 9423 } 9424 return any; 9425 } 9426 9427 enum bool hasAnyRelevantAnnotations = helper(); 9428 } 9429 9430 /++ 9431 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. 9432 +/ 9433 class MainWindow : Window { 9434 /// 9435 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 9436 super(initialWidth, initialHeight, title); 9437 9438 _clientArea = new ClientAreaWidget(); 9439 _clientArea.x = 0; 9440 _clientArea.y = 0; 9441 _clientArea.width = this.width; 9442 _clientArea.height = this.height; 9443 _clientArea.tabStop = false; 9444 9445 super.addChild(_clientArea); 9446 9447 statusBar = new StatusBar(this); 9448 } 9449 9450 /++ 9451 Adds a menu and toolbar from annotated functions. 9452 9453 --- 9454 struct Commands { 9455 @menu("File") { 9456 void New() {} 9457 void Open() {} 9458 void Save() {} 9459 @separator 9460 void Exit() @accelerator("Alt+F4") @hotkey('x') { 9461 window.close(); 9462 } 9463 } 9464 9465 @menu("Edit") { 9466 void Undo() { 9467 undo(); 9468 } 9469 @separator 9470 void Cut() {} 9471 void Copy() {} 9472 void Paste() {} 9473 } 9474 9475 @menu("Help") { 9476 void About() {} 9477 } 9478 } 9479 9480 Commands commands; 9481 9482 window.setMenuAndToolbarFromAnnotatedCode(commands); 9483 --- 9484 9485 Note that you can call this function multiple times and it will add the items in order to the given items. 9486 9487 +/ 9488 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 9489 setMenuAndToolbarFromAnnotatedCode_internal(t); 9490 } 9491 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 9492 setMenuAndToolbarFromAnnotatedCode_internal(t); 9493 } 9494 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 9495 Action[] toolbarActions; 9496 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 9497 Menu[string] mcs; 9498 9499 foreach(menu; menuBar.subMenus) { 9500 mcs[menu.label] = menu; 9501 } 9502 9503 foreach(memberName; __traits(derivedMembers, T)) { 9504 static if(memberName != "this") 9505 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 9506 .menu menu; 9507 .toolbar toolbar; 9508 bool separator; 9509 .accelerator accelerator; 9510 .hotkey hotkey; 9511 .icon icon; 9512 string label; 9513 string tip; 9514 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 9515 static if(is(typeof(attr) == .menu)) 9516 menu = attr; 9517 else static if(is(typeof(attr) == .toolbar)) 9518 toolbar = attr; 9519 else static if(is(attr == .separator)) 9520 separator = true; 9521 else static if(is(typeof(attr) == .accelerator)) 9522 accelerator = attr; 9523 else static if(is(typeof(attr) == .hotkey)) 9524 hotkey = attr; 9525 else static if(is(typeof(attr) == .icon)) 9526 icon = attr; 9527 else static if(is(typeof(attr) == .label)) 9528 label = attr.label; 9529 else static if(is(typeof(attr) == .tip)) 9530 tip = attr.tip; 9531 } 9532 9533 if(menu !is .menu.init || toolbar !is .toolbar.init) { 9534 ushort correctIcon = icon.id; // FIXME 9535 if(label.length == 0) 9536 label = memberName.toMenuLabel; 9537 9538 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName)); 9539 9540 auto action = new Action(label, correctIcon, handler); 9541 9542 if(accelerator.keyString.length) { 9543 auto ke = KeyEvent.parse(accelerator.keyString); 9544 action.accelerator = ke; 9545 accelerators[ke.toStr] = handler; 9546 } 9547 9548 if(toolbar !is .toolbar.init) 9549 toolbarActions ~= action; 9550 if(menu !is .menu.init) { 9551 Menu mc; 9552 if(menu.name in mcs) { 9553 mc = mcs[menu.name]; 9554 } else { 9555 mc = new Menu(menu.name, this); 9556 menuBar.addItem(mc); 9557 mcs[menu.name] = mc; 9558 } 9559 9560 if(separator) 9561 mc.addSeparator(); 9562 mc.addItem(new MenuItem(action)); 9563 } 9564 } 9565 } 9566 } 9567 9568 this.menuBar = menuBar; 9569 9570 if(toolbarActions.length) { 9571 auto tb = new ToolBar(toolbarActions, this); 9572 } 9573 } 9574 9575 void delegate()[string] accelerators; 9576 9577 override void defaultEventHandler_keydown(KeyDownEvent event) { 9578 auto str = event.originalKeyEvent.toStr; 9579 if(auto acl = str in accelerators) 9580 (*acl)(); 9581 super.defaultEventHandler_keydown(event); 9582 } 9583 9584 override void defaultEventHandler_mouseover(MouseOverEvent event) { 9585 super.defaultEventHandler_mouseover(event); 9586 if(this.statusBar !is null && event.target.statusTip.length) 9587 this.statusBar.parts[0].content = event.target.statusTip; 9588 else if(this.statusBar !is null && this.statusTip.length) 9589 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 9590 } 9591 9592 override void addChild(Widget c, int position = int.max) { 9593 if(auto tb = cast(ToolBar) c) 9594 version(win32_widgets) 9595 super.addChild(c, 0); 9596 else version(custom_widgets) 9597 super.addChild(c, menuBar ? 1 : 0); 9598 else static assert(0); 9599 else 9600 clientArea.addChild(c, position); 9601 } 9602 9603 ToolBar _toolBar; 9604 /// 9605 ToolBar toolBar() { return _toolBar; } 9606 /// 9607 ToolBar toolBar(ToolBar t) { 9608 _toolBar = t; 9609 foreach(child; this.children) 9610 if(child is t) 9611 return t; 9612 version(win32_widgets) 9613 super.addChild(t, 0); 9614 else version(custom_widgets) 9615 super.addChild(t, menuBar ? 1 : 0); 9616 else static assert(0); 9617 return t; 9618 } 9619 9620 MenuBar _menu; 9621 /// 9622 MenuBar menuBar() { return _menu; } 9623 /// 9624 MenuBar menuBar(MenuBar m) { 9625 if(m is _menu) { 9626 version(custom_widgets) 9627 recomputeChildLayout(); 9628 return m; 9629 } 9630 9631 if(_menu !is null) { 9632 // make sure it is sanely removed 9633 // FIXME 9634 } 9635 9636 _menu = m; 9637 9638 version(win32_widgets) { 9639 SetMenu(parentWindow.win.impl.hwnd, m.handle); 9640 } else version(custom_widgets) { 9641 super.addChild(m, 0); 9642 9643 // clientArea.y = menu.height; 9644 // clientArea.height = this.height - menu.height; 9645 9646 recomputeChildLayout(); 9647 } else static assert(false); 9648 9649 return _menu; 9650 } 9651 private Widget _clientArea; 9652 /// 9653 @property Widget clientArea() { return _clientArea; } 9654 protected @property void clientArea(Widget wid) { 9655 _clientArea = wid; 9656 } 9657 9658 private StatusBar _statusBar; 9659 /++ 9660 Returns the window's [StatusBar]. Be warned it may be `null`. 9661 +/ 9662 @property StatusBar statusBar() { return _statusBar; } 9663 /// ditto 9664 @property void statusBar(StatusBar bar) { 9665 if(_statusBar !is null) 9666 _statusBar.removeWidget(); 9667 _statusBar = bar; 9668 if(bar !is null) 9669 super.addChild(_statusBar); 9670 } 9671 } 9672 9673 /+ 9674 This is really an implementation detail of [MainWindow] 9675 +/ 9676 private class ClientAreaWidget : Widget { 9677 this() { 9678 this.tabStop = false; 9679 super(null); 9680 //sa = new ScrollableWidget(this); 9681 } 9682 /* 9683 ScrollableWidget sa; 9684 override void addChild(Widget w, int position) { 9685 if(sa is null) 9686 super.addChild(w, position); 9687 else { 9688 sa.addChild(w, position); 9689 sa.setContentSize(this.minWidth + 1, this.minHeight); 9690 import std.stdio; writeln(sa.contentWidth, "x", sa.contentHeight); 9691 } 9692 } 9693 */ 9694 } 9695 9696 /** 9697 Toolbars are lists of buttons (typically icons) that appear under the menu. 9698 Each button ought to correspond to a menu item, represented by [Action] objects. 9699 */ 9700 class ToolBar : Widget { 9701 version(win32_widgets) { 9702 private const int idealHeight; 9703 override int minHeight() { return idealHeight; } 9704 override int maxHeight() { return idealHeight; } 9705 } else version(custom_widgets) { 9706 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 9707 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 9708 } else static assert(false); 9709 override int heightStretchiness() { return 0; } 9710 9711 version(win32_widgets) 9712 HIMAGELIST imageList; 9713 9714 this(Widget parent) { 9715 this(null, parent); 9716 } 9717 9718 /// 9719 this(Action[] actions, Widget parent) { 9720 super(parent); 9721 9722 tabStop = false; 9723 9724 version(win32_widgets) { 9725 // so i like how the flat thing looks on windows, but not on wine 9726 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 9727 // leave it commented 9728 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 9729 9730 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 9731 9732 imageList = ImageList_Create( 9733 // width, height 9734 16, 16, 9735 ILC_COLOR16 | ILC_MASK, 9736 16 /*numberOfButtons*/, 0); 9737 9738 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageList); 9739 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 9740 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 9741 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 9742 9743 TBBUTTON[] buttons; 9744 9745 // FIXME: I_IMAGENONE is if here is no icon 9746 foreach(action; actions) 9747 buttons ~= TBBUTTON( 9748 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 9749 action.id, 9750 TBSTATE_ENABLED, // state 9751 0, // style 9752 0, // reserved array, just zero it out 9753 0, // dwData 9754 cast(size_t) toWstringzInternal(action.label) // INT_PTR 9755 ); 9756 9757 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 9758 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 9759 9760 SIZE size; 9761 import core.sys.windows.commctrl; 9762 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 9763 idealHeight = size.cy + 4; // the plus 4 is a hack 9764 9765 /* 9766 RECT rect; 9767 GetWindowRect(hwnd, &rect); 9768 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 9769 */ 9770 9771 assert(idealHeight); 9772 } else version(custom_widgets) { 9773 foreach(action; actions) 9774 new ToolButton(action, this); 9775 } else static assert(false); 9776 } 9777 9778 override void recomputeChildLayout() { 9779 .recomputeChildLayout!"width"(this); 9780 } 9781 } 9782 9783 enum toolbarIconSize = 24; 9784 9785 /// 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. 9786 class ToolButton : Button { 9787 /// 9788 this(string label, Widget parent) { 9789 super(label, parent); 9790 tabStop = false; 9791 } 9792 /// 9793 this(Action action, Widget parent) { 9794 super(action.label, parent); 9795 tabStop = false; 9796 this.action = action; 9797 } 9798 9799 version(custom_widgets) 9800 override void defaultEventHandler_click(ClickEvent event) { 9801 foreach(handler; action.triggered) 9802 handler(); 9803 } 9804 9805 Action action; 9806 9807 override int maxWidth() { return toolbarIconSize; } 9808 override int minWidth() { return toolbarIconSize; } 9809 override int maxHeight() { return toolbarIconSize; } 9810 override int minHeight() { return toolbarIconSize; } 9811 9812 version(custom_widgets) 9813 override void paint(WidgetPainter painter) { 9814 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 9815 painter.outlineColor = Color.black; 9816 9817 // I want to get from 16 to 24. that's * 3 / 2 9818 static assert(toolbarIconSize >= 16); 9819 enum multiplier = toolbarIconSize / 8; 9820 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 9821 switch(action.iconId) { 9822 case GenericIcons.New: 9823 painter.fillColor = Color.white; 9824 painter.drawPolygon( 9825 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 9826 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 9827 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 9828 ); 9829 break; 9830 case GenericIcons.Save: 9831 painter.fillColor = Color.white; 9832 painter.outlineColor = Color.black; 9833 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 9834 9835 // the label 9836 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 9837 9838 // the slider 9839 painter.fillColor = Color.black; 9840 painter.outlineColor = Color.black; 9841 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 9842 9843 painter.fillColor = Color.white; 9844 painter.outlineColor = Color.white; 9845 // the disc window 9846 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 9847 break; 9848 case GenericIcons.Open: 9849 painter.fillColor = Color.white; 9850 painter.drawPolygon( 9851 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 9852 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 9853 painter.drawPolygon( 9854 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 9855 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 9856 Point(2, 6) * multiplier / divisor); 9857 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 9858 break; 9859 case GenericIcons.Copy: 9860 painter.fillColor = Color.white; 9861 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 9862 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 9863 break; 9864 case GenericIcons.Cut: 9865 painter.fillColor = Color.transparent; 9866 painter.outlineColor = getComputedStyle.foregroundColor(); 9867 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 9868 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 9869 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 9870 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 9871 break; 9872 case GenericIcons.Paste: 9873 painter.fillColor = Color.white; 9874 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 9875 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 9876 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 9877 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 9878 painter.fillColor = Color.black; 9879 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 9880 break; 9881 case GenericIcons.Help: 9882 painter.outlineColor = getComputedStyle.foregroundColor(); 9883 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 9884 break; 9885 case GenericIcons.Undo: 9886 painter.fillColor = Color.transparent; 9887 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 9888 painter.outlineColor = Color.black; 9889 painter.fillColor = Color.black; 9890 painter.drawPolygon( 9891 Point(4, 4) * multiplier / divisor, 9892 Point(8, 2) * multiplier / divisor, 9893 Point(8, 6) * multiplier / divisor, 9894 Point(4, 4) * multiplier / divisor, 9895 ); 9896 break; 9897 case GenericIcons.Redo: 9898 painter.fillColor = Color.transparent; 9899 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 9900 painter.outlineColor = Color.black; 9901 painter.fillColor = Color.black; 9902 painter.drawPolygon( 9903 Point(10, 4) * multiplier / divisor, 9904 Point(6, 2) * multiplier / divisor, 9905 Point(6, 6) * multiplier / divisor, 9906 Point(10, 4) * multiplier / divisor, 9907 ); 9908 break; 9909 default: 9910 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 9911 } 9912 return bounds; 9913 }); 9914 } 9915 9916 } 9917 9918 9919 /// 9920 class MenuBar : Widget { 9921 MenuItem[] items; 9922 Menu[] subMenus; 9923 9924 version(win32_widgets) { 9925 HMENU handle; 9926 /// 9927 this(Widget parent = null) { 9928 super(parent); 9929 9930 handle = CreateMenu(); 9931 tabStop = false; 9932 } 9933 } else version(custom_widgets) { 9934 /// 9935 this(Widget parent = null) { 9936 tabStop = false; // these are selected some other way 9937 super(parent); 9938 } 9939 9940 mixin Padding!q{2}; 9941 } else static assert(false); 9942 9943 version(custom_widgets) 9944 override void paint(WidgetPainter painter) { 9945 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 9946 } 9947 9948 /// 9949 MenuItem addItem(MenuItem item) { 9950 this.addChild(item); 9951 items ~= item; 9952 version(win32_widgets) { 9953 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 9954 } 9955 return item; 9956 } 9957 9958 9959 /// 9960 Menu addItem(Menu item) { 9961 9962 subMenus ~= item; 9963 9964 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 9965 9966 addChild(mbItem); 9967 items ~= mbItem; 9968 9969 version(win32_widgets) { 9970 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 9971 } else version(custom_widgets) { 9972 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 9973 item.popup(mbItem); 9974 }; 9975 } else static assert(false); 9976 9977 return item; 9978 } 9979 9980 override void recomputeChildLayout() { 9981 .recomputeChildLayout!"width"(this); 9982 } 9983 9984 override int maxHeight() { return defaultLineHeight + 4; } 9985 override int minHeight() { return defaultLineHeight + 4; } 9986 } 9987 9988 9989 /** 9990 Status bars appear at the bottom of a MainWindow. 9991 They are made out of Parts, with a width and content. 9992 9993 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 9994 9995 9996 sb.parts[0].content = "Status bar text!"; 9997 */ 9998 class StatusBar : Widget { 9999 private Part[] partsArray; 10000 /// 10001 struct Parts { 10002 @disable this(); 10003 this(StatusBar owner) { this.owner = owner; } 10004 //@disable this(this); 10005 /// 10006 @property int length() { return cast(int) owner.partsArray.length; } 10007 private StatusBar owner; 10008 private this(StatusBar owner, Part[] parts) { 10009 this.owner.partsArray = parts; 10010 this.owner = owner; 10011 } 10012 /// 10013 Part opIndex(int p) { 10014 if(owner.partsArray.length == 0) 10015 this ~= new StatusBar.Part(300); 10016 return owner.partsArray[p]; 10017 } 10018 10019 /// 10020 Part opOpAssign(string op : "~" )(Part p) { 10021 assert(owner.partsArray.length < 255); 10022 p.owner = this.owner; 10023 p.idx = cast(int) owner.partsArray.length; 10024 owner.partsArray ~= p; 10025 version(win32_widgets) { 10026 int[256] pos; 10027 int cpos = 0; 10028 foreach(idx, part; owner.partsArray) { 10029 if(part.width) 10030 cpos += part.width; 10031 else 10032 cpos += 100; 10033 10034 if(idx + 1 == owner.partsArray.length) 10035 pos[idx] = -1; 10036 else 10037 pos[idx] = cpos; 10038 } 10039 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 10040 } else version(custom_widgets) { 10041 owner.redraw(); 10042 } else static assert(false); 10043 10044 return p; 10045 } 10046 } 10047 10048 private Parts _parts; 10049 /// 10050 final @property Parts parts() { 10051 return _parts; 10052 } 10053 10054 /// 10055 static class Part { 10056 int width; 10057 StatusBar owner; 10058 10059 /// 10060 this(int w = 100) { width = w; } 10061 10062 private int idx; 10063 private string _content; 10064 /// 10065 @property string content() { return _content; } 10066 /// 10067 @property void content(string s) { 10068 version(win32_widgets) { 10069 _content = s; 10070 WCharzBuffer bfr = WCharzBuffer(s); 10071 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 10072 } else version(custom_widgets) { 10073 if(_content != s) { 10074 _content = s; 10075 owner.redraw(); 10076 } 10077 } else static assert(false); 10078 } 10079 } 10080 string simpleModeContent; 10081 bool inSimpleMode; 10082 10083 10084 /// 10085 this(Widget parent) { 10086 super(null); // FIXME 10087 _parts = Parts(this); 10088 tabStop = false; 10089 version(win32_widgets) { 10090 parentWindow = parent.parentWindow; 10091 createWin32Window(this, "msctls_statusbar32"w, "", 0); 10092 10093 RECT rect; 10094 GetWindowRect(hwnd, &rect); 10095 idealHeight = rect.bottom - rect.top; 10096 assert(idealHeight); 10097 } else version(custom_widgets) { 10098 } else static assert(false); 10099 } 10100 10101 version(win32_widgets) 10102 override protected void dpiChanged() { 10103 RECT rect; 10104 GetWindowRect(hwnd, &rect); 10105 idealHeight = rect.bottom - rect.top; 10106 assert(idealHeight); 10107 } 10108 10109 version(custom_widgets) 10110 override void paint(WidgetPainter painter) { 10111 auto cs = getComputedStyle(); 10112 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10113 int cpos = 0; 10114 int remainingLength = this.width; 10115 foreach(idx, part; this.partsArray) { 10116 auto partWidth = part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 10117 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 10118 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 10119 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 10120 10121 painter.outlineColor = cs.foregroundColor(); 10122 painter.fillColor = cs.foregroundColor(); 10123 10124 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 10125 cpos += partWidth; 10126 remainingLength -= partWidth; 10127 } 10128 } 10129 10130 10131 version(win32_widgets) { 10132 private int idealHeight; 10133 override int maxHeight() { return idealHeight; } 10134 override int minHeight() { return idealHeight; } 10135 } else version(custom_widgets) { 10136 override int maxHeight() { return defaultLineHeight + 4; } 10137 override int minHeight() { return defaultLineHeight + 4; } 10138 } else static assert(false); 10139 } 10140 10141 /// Displays an in-progress indicator without known values 10142 version(none) 10143 class IndefiniteProgressBar : Widget { 10144 version(win32_widgets) 10145 this(Widget parent) { 10146 super(parent); 10147 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 10148 tabStop = false; 10149 } 10150 override int minHeight() { return 10; } 10151 } 10152 10153 /// A progress bar with a known endpoint and completion amount 10154 class ProgressBar : Widget { 10155 /++ 10156 History: 10157 Added March 16, 2022 (dub v10.7) 10158 +/ 10159 this(int min, int max, Widget parent) { 10160 this(parent); 10161 setRange(cast(ushort) min, cast(ushort) max); // FIXME 10162 } 10163 this(Widget parent) { 10164 version(win32_widgets) { 10165 super(parent); 10166 createWin32Window(this, "msctls_progress32"w, "", 0); 10167 tabStop = false; 10168 } else version(custom_widgets) { 10169 super(parent); 10170 max = 100; 10171 step = 10; 10172 tabStop = false; 10173 } else static assert(0); 10174 } 10175 10176 version(custom_widgets) 10177 override void paint(WidgetPainter painter) { 10178 auto cs = getComputedStyle(); 10179 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10180 painter.fillColor = cs.progressBarColor; 10181 painter.drawRectangle(Point(0, 0), width * current / max, height); 10182 } 10183 10184 10185 version(custom_widgets) { 10186 int current; 10187 int max; 10188 int step; 10189 } 10190 10191 /// 10192 void advanceOneStep() { 10193 version(win32_widgets) 10194 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 10195 else version(custom_widgets) 10196 addToPosition(step); 10197 else static assert(false); 10198 } 10199 10200 /// 10201 void setStepIncrement(int increment) { 10202 version(win32_widgets) 10203 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 10204 else version(custom_widgets) 10205 step = increment; 10206 else static assert(false); 10207 } 10208 10209 /// 10210 void addToPosition(int amount) { 10211 version(win32_widgets) 10212 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 10213 else version(custom_widgets) 10214 setPosition(current + amount); 10215 else static assert(false); 10216 } 10217 10218 /// 10219 void setPosition(int pos) { 10220 version(win32_widgets) 10221 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 10222 else version(custom_widgets) { 10223 current = pos; 10224 if(current > max) 10225 current = max; 10226 redraw(); 10227 } 10228 else static assert(false); 10229 } 10230 10231 /// 10232 void setRange(ushort min, ushort max) { 10233 version(win32_widgets) 10234 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 10235 else version(custom_widgets) { 10236 this.max = max; 10237 } 10238 else static assert(false); 10239 } 10240 10241 override int minHeight() { return 10; } 10242 } 10243 10244 version(custom_widgets) 10245 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 10246 thisLabel.reserve(label.length); 10247 bool justSawAmpersand; 10248 foreach(ch; label) { 10249 if(justSawAmpersand) { 10250 justSawAmpersand = false; 10251 if(ch == '&') { 10252 goto plain; 10253 } 10254 thisAccelerator = ch; 10255 } else { 10256 if(ch == '&') { 10257 justSawAmpersand = true; 10258 continue; 10259 } 10260 plain: 10261 thisLabel ~= ch; 10262 } 10263 } 10264 } 10265 10266 /++ 10267 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. 10268 10269 10270 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 10271 10272 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10273 10274 History: 10275 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. 10276 +/ 10277 class Fieldset : Widget { 10278 // FIXME: on Windows,it doesn't draw the background on the label 10279 // on X, it doesn't fix the clipping rectangle for it 10280 version(win32_widgets) 10281 override int paddingTop() { return defaultLineHeight; } 10282 else version(custom_widgets) 10283 override int paddingTop() { return defaultLineHeight + 2; } 10284 else static assert(false); 10285 override int paddingBottom() { return 6; } 10286 override int paddingLeft() { return 6; } 10287 override int paddingRight() { return 6; } 10288 10289 override int marginLeft() { return 6; } 10290 override int marginRight() { return 6; } 10291 override int marginTop() { return 2; } 10292 override int marginBottom() { return 2; } 10293 10294 string legend; 10295 10296 version(custom_widgets) private dchar accelerator; 10297 10298 this(string legend, Widget parent) { 10299 version(win32_widgets) { 10300 super(parent); 10301 this.legend = legend; 10302 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 10303 tabStop = false; 10304 } else version(custom_widgets) { 10305 super(parent); 10306 tabStop = false; 10307 10308 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 10309 } else static assert(0); 10310 } 10311 10312 version(custom_widgets) 10313 override void paint(WidgetPainter painter) { 10314 painter.fillColor = Color.transparent; 10315 auto cs = getComputedStyle(); 10316 painter.pen = Pen(cs.foregroundColor, 1); 10317 painter.drawRectangle(Point(0, defaultLineHeight / 2), width, height - Window.lineHeight / 2); 10318 10319 auto tx = painter.textSize(legend); 10320 painter.outlineColor = Color.transparent; 10321 10322 static if(UsingSimpledisplayX11) { 10323 painter.fillColor = getComputedStyle().windowBackgroundColor; 10324 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 10325 } else version(Windows) { 10326 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 10327 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 10328 SelectObject(painter.impl.hdc, b); 10329 } else static assert(0); 10330 painter.outlineColor = cs.foregroundColor; 10331 painter.drawText(Point(8, 0), legend); 10332 } 10333 10334 override int maxHeight() { 10335 auto m = paddingTop() + paddingBottom(); 10336 foreach(child; children) { 10337 auto mh = child.maxHeight(); 10338 if(mh == int.max) 10339 return int.max; 10340 m += mh; 10341 m += child.marginBottom(); 10342 m += child.marginTop(); 10343 } 10344 m += 6; 10345 if(m < minHeight) 10346 return minHeight; 10347 return m; 10348 } 10349 10350 override int minHeight() { 10351 auto m = paddingTop() + paddingBottom(); 10352 foreach(child; children) { 10353 m += child.minHeight(); 10354 m += child.marginBottom(); 10355 m += child.marginTop(); 10356 } 10357 return m + 6; 10358 } 10359 } 10360 10361 /++ 10362 $(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") 10363 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 10364 +/ 10365 version(minigui_screenshots) 10366 @Screenshot("Fieldset") 10367 unittest { 10368 auto window = new Window(200, 100); 10369 auto set = new Fieldset("Baby will", window); 10370 auto option1 = new Radiobox("Eat", set); 10371 auto option2 = new Radiobox("Cry", set); 10372 auto option3 = new Radiobox("Sleep", set); 10373 window.loop(); 10374 } 10375 10376 /// Draws a line 10377 class HorizontalRule : Widget { 10378 mixin Margin!q{ 2 }; 10379 override int minHeight() { return 2; } 10380 override int maxHeight() { return 2; } 10381 10382 /// 10383 this(Widget parent) { 10384 super(parent); 10385 } 10386 10387 override void paint(WidgetPainter painter) { 10388 auto cs = getComputedStyle(); 10389 painter.outlineColor = cs.darkAccentColor; 10390 painter.drawLine(Point(0, 0), Point(width, 0)); 10391 painter.outlineColor = cs.lightAccentColor; 10392 painter.drawLine(Point(0, 1), Point(width, 1)); 10393 } 10394 } 10395 10396 version(minigui_screenshots) 10397 @Screenshot("HorizontalRule") 10398 /++ 10399 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 10400 10401 +/ 10402 unittest { 10403 auto window = new Window(200, 100); 10404 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 10405 new HorizontalRule(window); 10406 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 10407 window.loop(); 10408 } 10409 10410 /// ditto 10411 class VerticalRule : Widget { 10412 mixin Margin!q{ 2 }; 10413 override int minWidth() { return 2; } 10414 override int maxWidth() { return 2; } 10415 10416 /// 10417 this(Widget parent) { 10418 super(parent); 10419 } 10420 10421 override void paint(WidgetPainter painter) { 10422 auto cs = getComputedStyle(); 10423 painter.outlineColor = cs.darkAccentColor; 10424 painter.drawLine(Point(0, 0), Point(0, height)); 10425 painter.outlineColor = cs.lightAccentColor; 10426 painter.drawLine(Point(1, 0), Point(1, height)); 10427 } 10428 } 10429 10430 10431 /// 10432 class Menu : Window { 10433 void remove() { 10434 foreach(i, child; parentWindow.children) 10435 if(child is this) { 10436 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 10437 break; 10438 } 10439 parentWindow.redraw(); 10440 10441 parentWindow.releaseMouseCapture(); 10442 } 10443 10444 /// 10445 void addSeparator() { 10446 version(win32_widgets) 10447 AppendMenu(handle, MF_SEPARATOR, 0, null); 10448 else version(custom_widgets) 10449 auto hr = new HorizontalRule(this); 10450 else static assert(0); 10451 } 10452 10453 override int paddingTop() { return 4; } 10454 override int paddingBottom() { return 4; } 10455 override int paddingLeft() { return 2; } 10456 override int paddingRight() { return 2; } 10457 10458 version(win32_widgets) {} 10459 else version(custom_widgets) { 10460 SimpleWindow dropDown; 10461 Widget menuParent; 10462 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 10463 this.menuParent = parent; 10464 10465 int w = 150; 10466 int h = paddingTop + paddingBottom; 10467 if(this.children.length) { 10468 // hacking it to get the ideal height out of recomputeChildLayout 10469 this.width = w; 10470 this.height = h; 10471 this.recomputeChildLayout(); 10472 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 10473 h += paddingBottom; 10474 10475 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 10476 } 10477 10478 if(offsetY == int.min) 10479 offsetY = parent.defaultLineHeight; 10480 10481 auto coord = parent.globalCoordinates(); 10482 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 10483 this.x = 0; 10484 this.y = 0; 10485 this.width = dropDown.width; 10486 this.height = dropDown.height; 10487 this.drawableWindow = dropDown; 10488 this.recomputeChildLayout(); 10489 10490 static if(UsingSimpledisplayX11) 10491 XSync(XDisplayConnection.get, 0); 10492 10493 dropDown.visibilityChanged = (bool visible) { 10494 if(visible) { 10495 this.redraw(); 10496 dropDown.grabInput(); 10497 } else { 10498 dropDown.releaseInputGrab(); 10499 } 10500 }; 10501 10502 dropDown.show(); 10503 10504 clickListener = this.addEventListener((scope ClickEvent ev) { 10505 unpopup(); 10506 // need to unlock asap just in case other user handlers block... 10507 static if(UsingSimpledisplayX11) 10508 flushGui(); 10509 }, true /* again for asap action */); 10510 } 10511 10512 EventListener clickListener; 10513 } 10514 else static assert(false); 10515 10516 version(custom_widgets) 10517 void unpopup() { 10518 mouseLastOver = mouseLastDownOn = null; 10519 dropDown.hide(); 10520 if(!menuParent.parentWindow.win.closed) { 10521 if(auto maw = cast(MouseActivatedWidget) menuParent) { 10522 maw.setDynamicState(DynamicState.depressed, false); 10523 maw.setDynamicState(DynamicState.hover, false); 10524 maw.redraw(); 10525 } 10526 // menuParent.parentWindow.win.focus(); 10527 } 10528 clickListener.disconnect(); 10529 } 10530 10531 MenuItem[] items; 10532 10533 /// 10534 MenuItem addItem(MenuItem item) { 10535 addChild(item); 10536 items ~= item; 10537 version(win32_widgets) { 10538 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10539 } 10540 return item; 10541 } 10542 10543 string label; 10544 10545 version(win32_widgets) { 10546 HMENU handle; 10547 /// 10548 this(string label, Widget parent) { 10549 // not actually passing the parent since it effs up the drawing 10550 super(cast(Widget) null);// parent); 10551 this.label = label; 10552 handle = CreatePopupMenu(); 10553 } 10554 } else version(custom_widgets) { 10555 /// 10556 this(string label, Widget parent) { 10557 10558 if(dropDown) { 10559 dropDown.close(); 10560 } 10561 dropDown = new SimpleWindow( 10562 150, 4, 10563 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 10564 10565 this.label = label; 10566 10567 super(dropDown); 10568 } 10569 } else static assert(false); 10570 10571 override int maxHeight() { return defaultLineHeight; } 10572 override int minHeight() { return defaultLineHeight; } 10573 10574 version(custom_widgets) 10575 override void paint(WidgetPainter painter) { 10576 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 10577 } 10578 } 10579 10580 /++ 10581 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 10582 +/ 10583 class MenuItem : MouseActivatedWidget { 10584 Menu submenu; 10585 10586 Action action; 10587 string label; 10588 10589 override int paddingLeft() { return 4; } 10590 10591 override int maxHeight() { return defaultLineHeight + 4; } 10592 override int minHeight() { return defaultLineHeight + 4; } 10593 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 10594 override int maxWidth() { 10595 if(cast(MenuBar) parent) { 10596 return minWidth(); 10597 } 10598 return int.max; 10599 } 10600 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 10601 this(string lbl, Widget parent = null) { 10602 super(parent); 10603 //label = lbl; // FIXME 10604 foreach(char ch; lbl) // FIXME 10605 if(ch != '&') // FIXME 10606 label ~= ch; // FIXME 10607 tabStop = false; // these are selected some other way 10608 } 10609 10610 /// 10611 this(Action action, Widget parent = null) { 10612 assert(action !is null); 10613 this(action.label, parent); 10614 this.action = action; 10615 tabStop = false; // these are selected some other way 10616 } 10617 10618 version(custom_widgets) 10619 override void paint(WidgetPainter painter) { 10620 auto cs = getComputedStyle(); 10621 if(dynamicState & DynamicState.depressed) 10622 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10623 if(dynamicState & DynamicState.hover) 10624 painter.outlineColor = cs.activeMenuItemColor; 10625 else 10626 painter.outlineColor = cs.foregroundColor; 10627 painter.fillColor = Color.transparent; 10628 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 10629 if(action && action.accelerator !is KeyEvent.init) { 10630 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 10631 10632 } 10633 } 10634 10635 static class Style : Widget.Style { 10636 override bool variesWithState(ulong dynamicStateFlags) { 10637 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 10638 } 10639 } 10640 mixin OverrideStyle!Style; 10641 10642 override void defaultEventHandler_triggered(Event event) { 10643 if(action) 10644 foreach(handler; action.triggered) 10645 handler(); 10646 10647 if(auto pmenu = cast(Menu) this.parent) 10648 pmenu.remove(); 10649 10650 super.defaultEventHandler_triggered(event); 10651 } 10652 } 10653 10654 version(win32_widgets) 10655 /// A "mouse activiated widget" is really just an abstract variant of button. 10656 class MouseActivatedWidget : Widget { 10657 @property bool isChecked() { 10658 assert(hwnd); 10659 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 10660 10661 } 10662 @property void isChecked(bool state) { 10663 assert(hwnd); 10664 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 10665 10666 } 10667 10668 override void handleWmCommand(ushort cmd, ushort id) { 10669 if(cmd == 0) { 10670 auto event = new Event(EventType.triggered, this); 10671 event.dispatch(); 10672 } 10673 } 10674 10675 this(Widget parent) { 10676 super(parent); 10677 } 10678 } 10679 else version(custom_widgets) 10680 /// ditto 10681 class MouseActivatedWidget : Widget { 10682 @property bool isChecked() { return isChecked_; } 10683 @property bool isChecked(bool b) { return isChecked_ = b; } 10684 10685 private bool isChecked_; 10686 10687 this(Widget parent) { 10688 super(parent); 10689 10690 addEventListener((MouseDownEvent ev) { 10691 if(ev.button == MouseButton.left) { 10692 setDynamicState(DynamicState.depressed, true); 10693 setDynamicState(DynamicState.hover, true); 10694 redraw(); 10695 } 10696 }); 10697 10698 addEventListener((MouseUpEvent ev) { 10699 if(ev.button == MouseButton.left) { 10700 setDynamicState(DynamicState.depressed, false); 10701 setDynamicState(DynamicState.hover, false); 10702 redraw(); 10703 } 10704 }); 10705 10706 addEventListener((MouseMoveEvent mme) { 10707 if(!(mme.state & ModifierState.leftButtonDown)) { 10708 if(dynamicState_ & DynamicState.depressed) { 10709 setDynamicState(DynamicState.depressed, false); 10710 redraw(); 10711 } 10712 } 10713 }); 10714 } 10715 10716 override void defaultEventHandler_focus(Event ev) { 10717 super.defaultEventHandler_focus(ev); 10718 this.redraw(); 10719 } 10720 override void defaultEventHandler_blur(Event ev) { 10721 super.defaultEventHandler_blur(ev); 10722 setDynamicState(DynamicState.depressed, false); 10723 this.redraw(); 10724 } 10725 override void defaultEventHandler_keydown(KeyDownEvent ev) { 10726 super.defaultEventHandler_keydown(ev); 10727 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 10728 setDynamicState(DynamicState.depressed, true); 10729 setDynamicState(DynamicState.hover, true); 10730 this.redraw(); 10731 } 10732 } 10733 override void defaultEventHandler_keyup(KeyUpEvent ev) { 10734 super.defaultEventHandler_keyup(ev); 10735 if(!(dynamicState & DynamicState.depressed)) 10736 return; 10737 setDynamicState(DynamicState.depressed, false); 10738 setDynamicState(DynamicState.hover, false); 10739 this.redraw(); 10740 10741 auto event = new Event(EventType.triggered, this); 10742 event.sendDirectly(); 10743 } 10744 override void defaultEventHandler_click(ClickEvent ev) { 10745 super.defaultEventHandler_click(ev); 10746 if(ev.button == MouseButton.left) { 10747 auto event = new Event(EventType.triggered, this); 10748 event.sendDirectly(); 10749 } 10750 } 10751 10752 } 10753 else static assert(false); 10754 10755 /* 10756 /++ 10757 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 10758 10759 Basically the same as a checkbox. 10760 +/ 10761 class OnOffSwitch : MouseActivatedWidget { 10762 10763 } 10764 */ 10765 10766 /++ 10767 History: 10768 Added June 15, 2021 (dub v10.1) 10769 +/ 10770 struct ImageLabel { 10771 /++ 10772 Defines a label+image combo used by some widgets. 10773 10774 If you provide just a text label, that is all the widget will try to 10775 display. Or just an image will display just that. If you provide both, 10776 it may display both text and image side by side or display the image 10777 and offer text on an input event depending on the widget. 10778 10779 History: 10780 The `alignment` parameter was added on September 27, 2021 10781 +/ 10782 this(string label, TextAlignment alignment = TextAlignment.Center) { 10783 this.label = label; 10784 this.displayFlags = DisplayFlags.displayText; 10785 this.alignment = alignment; 10786 } 10787 10788 /// ditto 10789 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 10790 this.label = label; 10791 this.image = image; 10792 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 10793 this.alignment = alignment; 10794 } 10795 10796 /// ditto 10797 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 10798 this.image = image; 10799 this.displayFlags = DisplayFlags.displayImage; 10800 this.alignment = alignment; 10801 } 10802 10803 /// ditto 10804 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 10805 this.label = label; 10806 this.image = image; 10807 this.alignment = alignment; 10808 this.displayFlags = displayFlags; 10809 } 10810 10811 string label; 10812 MemoryImage image; 10813 10814 enum DisplayFlags { 10815 displayText = 1 << 0, 10816 displayImage = 1 << 1, 10817 } 10818 10819 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 10820 10821 TextAlignment alignment; 10822 } 10823 10824 /++ 10825 A basic checked or not checked box with an attached label. 10826 10827 10828 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 10829 10830 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10831 10832 History: 10833 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. 10834 +/ 10835 class Checkbox : MouseActivatedWidget { 10836 version(win32_widgets) { 10837 override int maxHeight() { return scaleWithDpi(16); } 10838 override int minHeight() { return scaleWithDpi(16); } 10839 } else version(custom_widgets) { 10840 override int maxHeight() { return defaultLineHeight; } 10841 override int minHeight() { return defaultLineHeight; } 10842 } else static assert(0); 10843 10844 override int marginLeft() { return 4; } 10845 10846 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 10847 10848 /++ 10849 Just an alias because I keep typing checked out of web habit. 10850 10851 History: 10852 Added May 31, 2021 10853 +/ 10854 alias checked = isChecked; 10855 10856 private string label; 10857 private dchar accelerator; 10858 10859 /++ 10860 +/ 10861 this(string label, Widget parent) { 10862 this(ImageLabel(label), Appearance.checkbox, parent); 10863 } 10864 10865 /// ditto 10866 this(string label, Appearance appearance, Widget parent) { 10867 this(ImageLabel(label), appearance, parent); 10868 } 10869 10870 /++ 10871 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 10872 10873 History: 10874 Added June 29, 2021 (dub v10.2) 10875 +/ 10876 enum Appearance { 10877 checkbox, /// a normal checkbox 10878 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 10879 //sliderswitch, 10880 } 10881 private Appearance appearance; 10882 10883 /// ditto 10884 private this(ImageLabel label, Appearance appearance, Widget parent) { 10885 super(parent); 10886 version(win32_widgets) { 10887 this.label = label.label; 10888 10889 uint extraStyle; 10890 final switch(appearance) { 10891 case Appearance.checkbox: 10892 break; 10893 case Appearance.pushbutton: 10894 extraStyle |= BS_PUSHLIKE; 10895 break; 10896 } 10897 10898 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 10899 } else version(custom_widgets) { 10900 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 10901 } else static assert(0); 10902 } 10903 10904 version(custom_widgets) 10905 override void paint(WidgetPainter painter) { 10906 auto cs = getComputedStyle(); 10907 if(isFocused()) { 10908 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 10909 painter.fillColor = cs.windowBackgroundColor; 10910 painter.drawRectangle(Point(0, 0), width, height); 10911 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 10912 } else { 10913 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 10914 painter.fillColor = cs.windowBackgroundColor; 10915 painter.drawRectangle(Point(0, 0), width, height); 10916 } 10917 10918 10919 enum buttonSize = 16; 10920 10921 painter.outlineColor = Color.black; 10922 painter.fillColor = Color.white; 10923 painter.drawRectangle(scaleWithDpi(Point(2, 2)), scaleWithDpi(buttonSize - 2), scaleWithDpi(buttonSize - 2)); 10924 10925 if(isChecked) { 10926 painter.pen = Pen(Color.black, 2); 10927 // I'm using height so the checkbox is square 10928 enum padding = 5; 10929 painter.drawLine(scaleWithDpi(Point(padding, padding)), scaleWithDpi(Point(buttonSize - (padding-2), buttonSize - (padding-2)))); 10930 painter.drawLine(scaleWithDpi(Point(buttonSize-(padding-2), padding)), scaleWithDpi(Point(padding, buttonSize - (padding-2)))); 10931 10932 painter.pen = Pen(Color.black, 1); 10933 } 10934 10935 if(label !is null) { 10936 painter.outlineColor = cs.foregroundColor(); 10937 painter.fillColor = cs.foregroundColor(); 10938 10939 // FIXME: should prolly just align the baseline or something 10940 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 2)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 10941 } 10942 } 10943 10944 override void defaultEventHandler_triggered(Event ev) { 10945 isChecked = !isChecked; 10946 10947 this.emit!(ChangeEvent!bool)(&isChecked); 10948 10949 redraw(); 10950 } 10951 10952 /// Emits a change event with the checked state 10953 mixin Emits!(ChangeEvent!bool); 10954 } 10955 10956 /// Adds empty space to a layout. 10957 class VerticalSpacer : Widget { 10958 /// 10959 this(Widget parent) { 10960 super(parent); 10961 } 10962 } 10963 10964 /// ditto 10965 class HorizontalSpacer : Widget { 10966 /// 10967 this(Widget parent) { 10968 super(parent); 10969 this.tabStop = false; 10970 } 10971 } 10972 10973 10974 /++ 10975 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 10976 10977 10978 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 10979 10980 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10981 10982 History: 10983 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. 10984 +/ 10985 class Radiobox : MouseActivatedWidget { 10986 10987 version(win32_widgets) { 10988 override int maxHeight() { return scaleWithDpi(16); } 10989 override int minHeight() { return scaleWithDpi(16); } 10990 } else version(custom_widgets) { 10991 override int maxHeight() { return defaultLineHeight; } 10992 override int minHeight() { return defaultLineHeight; } 10993 } else static assert(0); 10994 10995 override int marginLeft() { return 4; } 10996 10997 private string label; 10998 private dchar accelerator; 10999 11000 version(win32_widgets) 11001 this(string label, Widget parent) { 11002 super(parent); 11003 this.label = label; 11004 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 11005 } 11006 else version(custom_widgets) 11007 this(string label, Widget parent) { 11008 super(parent); 11009 label.extractWindowsStyleLabel(this.label, this.accelerator); 11010 height = 16; 11011 width = height + 4 + cast(int) label.length * 16; 11012 } 11013 else static assert(false); 11014 11015 version(custom_widgets) 11016 override void paint(WidgetPainter painter) { 11017 auto cs = getComputedStyle(); 11018 if(isFocused) { 11019 painter.fillColor = cs.windowBackgroundColor; 11020 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11021 } else { 11022 painter.fillColor = cs.windowBackgroundColor; 11023 painter.outlineColor = cs.windowBackgroundColor; 11024 } 11025 painter.drawRectangle(Point(0, 0), width, height); 11026 11027 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11028 11029 enum buttonSize = 16; 11030 11031 painter.outlineColor = Color.black; 11032 painter.fillColor = Color.white; 11033 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 11034 if(isChecked) { 11035 painter.outlineColor = Color.black; 11036 painter.fillColor = Color.black; 11037 // I'm using height so the checkbox is square 11038 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5))); 11039 } 11040 11041 painter.outlineColor = cs.foregroundColor(); 11042 painter.fillColor = cs.foregroundColor(); 11043 11044 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11045 } 11046 11047 11048 override void defaultEventHandler_triggered(Event ev) { 11049 isChecked = true; 11050 11051 if(this.parent) { 11052 foreach(child; this.parent.children) { 11053 if(child is this) continue; 11054 if(auto rb = cast(Radiobox) child) { 11055 rb.isChecked = false; 11056 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 11057 rb.redraw(); 11058 } 11059 } 11060 } 11061 11062 this.emit!(ChangeEvent!bool)(&this.isChecked); 11063 11064 redraw(); 11065 } 11066 11067 /// 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. 11068 mixin Emits!(ChangeEvent!bool); 11069 } 11070 11071 11072 /++ 11073 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 11074 11075 11076 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 11077 11078 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11079 11080 History: 11081 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. 11082 +/ 11083 class Button : MouseActivatedWidget { 11084 override int heightStretchiness() { return 3; } 11085 override int widthStretchiness() { return 3; } 11086 11087 /++ 11088 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. 11089 11090 History: 11091 Added July 2, 2021 11092 +/ 11093 public bool triggersOnMultiClick; 11094 11095 private string label_; 11096 private TextAlignment alignment; 11097 private dchar accelerator; 11098 11099 /// 11100 string label() { return label_; } 11101 /// 11102 void label(string l) { 11103 label_ = l; 11104 version(win32_widgets) { 11105 WCharzBuffer bfr = WCharzBuffer(l); 11106 SetWindowTextW(hwnd, bfr.ptr); 11107 } else version(custom_widgets) { 11108 redraw(); 11109 } 11110 } 11111 11112 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 11113 super.defaultEventHandler_dblclick(ev); 11114 if(triggersOnMultiClick) { 11115 if(ev.button == MouseButton.left) { 11116 auto event = new Event(EventType.triggered, this); 11117 event.sendDirectly(); 11118 } 11119 } 11120 } 11121 11122 private Sprite sprite; 11123 private int displayFlags; 11124 11125 /++ 11126 Creates a push button with the given label, which may be an image or some text. 11127 11128 Bugs: 11129 If the image is bigger than the button, it may not be displayed in the right position on Linux. 11130 11131 History: 11132 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 11133 11134 The button with label and image will respect requests to show both on Windows as 11135 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 11136 +/ 11137 this(ImageLabel label, Widget parent) { 11138 version(win32_widgets) { 11139 // FIXME: use ideal button size instead 11140 width = 50; 11141 height = 30; 11142 super(parent); 11143 11144 // BS_BITMAP is set when we want image only, so checking for exactly that combination 11145 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 11146 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 11147 11148 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 11149 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 11150 11151 if(label.image) { 11152 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 11153 11154 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 11155 } 11156 11157 this.label = label.label; 11158 } else version(custom_widgets) { 11159 width = 50; 11160 height = 30; 11161 super(parent); 11162 11163 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 11164 11165 if(label.image) { 11166 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 11167 this.displayFlags = label.displayFlags; 11168 } 11169 11170 this.alignment = label.alignment; 11171 } 11172 } 11173 11174 /// 11175 this(string label, Widget parent) { 11176 this(ImageLabel(label), parent); 11177 } 11178 11179 override int minHeight() { return defaultLineHeight + 4; } 11180 11181 static class Style : Widget.Style { 11182 override WidgetBackground background() { 11183 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 11184 11185 auto pressed = DynamicState.depressed | DynamicState.hover; 11186 if((widget.dynamicState & pressed) == pressed) { 11187 return WidgetBackground(cs.depressedButtonColor()); 11188 } else if(widget.dynamicState & DynamicState.hover) { 11189 return WidgetBackground(cs.hoveringColor()); 11190 } else { 11191 return WidgetBackground(cs.buttonColor()); 11192 } 11193 } 11194 11195 override FrameStyle borderStyle() { 11196 auto pressed = DynamicState.depressed | DynamicState.hover; 11197 if((widget.dynamicState & pressed) == pressed) { 11198 return FrameStyle.sunk; 11199 } else { 11200 return FrameStyle.risen; 11201 } 11202 11203 } 11204 11205 override bool variesWithState(ulong dynamicStateFlags) { 11206 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11207 } 11208 } 11209 mixin OverrideStyle!Style; 11210 11211 version(custom_widgets) 11212 override void paint(WidgetPainter painter) { 11213 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 11214 if(sprite) { 11215 sprite.drawAt( 11216 painter, 11217 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 11218 Point(0, 0), 11219 bounds.size 11220 ); 11221 } else { 11222 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 11223 } 11224 return bounds; 11225 }); 11226 } 11227 11228 override int flexBasisWidth() { 11229 version(win32_widgets) { 11230 SIZE size; 11231 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11232 if(size.cx == 0) 11233 goto fallback; 11234 return size.cx + scaleWithDpi(16); 11235 } 11236 fallback: 11237 return scaleWithDpi(cast(int) label.length * 8 + 16); 11238 } 11239 11240 override int flexBasisHeight() { 11241 version(win32_widgets) { 11242 SIZE size; 11243 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11244 if(size.cy == 0) 11245 goto fallback; 11246 return size.cy + scaleWithDpi(6); 11247 } 11248 fallback: 11249 return defaultLineHeight + 4; 11250 } 11251 } 11252 11253 /++ 11254 A button with a consistent size, suitable for user commands like OK and cANCEL. 11255 +/ 11256 class CommandButton : Button { 11257 this(string label, Widget parent) { 11258 super(label, parent); 11259 } 11260 11261 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 11262 11263 override int maxHeight() { 11264 return defaultLineHeight + 4; 11265 } 11266 11267 override int maxWidth() { 11268 return defaultLineHeight * 4; 11269 } 11270 11271 override int marginLeft() { return 12; } 11272 override int marginRight() { return 12; } 11273 override int marginTop() { return 12; } 11274 override int marginBottom() { return 12; } 11275 } 11276 11277 /// 11278 enum ArrowDirection { 11279 left, /// 11280 right, /// 11281 up, /// 11282 down /// 11283 } 11284 11285 /// 11286 version(custom_widgets) 11287 class ArrowButton : Button { 11288 /// 11289 this(ArrowDirection direction, Widget parent) { 11290 super("", parent); 11291 this.direction = direction; 11292 triggersOnMultiClick = true; 11293 } 11294 11295 private ArrowDirection direction; 11296 11297 override int minHeight() { return scaleWithDpi(16); } 11298 override int maxHeight() { return scaleWithDpi(16); } 11299 override int minWidth() { return scaleWithDpi(16); } 11300 override int maxWidth() { return scaleWithDpi(16); } 11301 11302 override void paint(WidgetPainter painter) { 11303 super.paint(painter); 11304 11305 auto cs = getComputedStyle(); 11306 11307 painter.outlineColor = cs.foregroundColor; 11308 painter.fillColor = cs.foregroundColor; 11309 11310 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 11311 11312 final switch(direction) { 11313 case ArrowDirection.up: 11314 painter.drawPolygon( 11315 scaleWithDpi(Point(2, 10) + offset), 11316 scaleWithDpi(Point(7, 5) + offset), 11317 scaleWithDpi(Point(12, 10) + offset), 11318 scaleWithDpi(Point(2, 10) + offset) 11319 ); 11320 break; 11321 case ArrowDirection.down: 11322 painter.drawPolygon( 11323 scaleWithDpi(Point(2, 6) + offset), 11324 scaleWithDpi(Point(7, 11) + offset), 11325 scaleWithDpi(Point(12, 6) + offset), 11326 scaleWithDpi(Point(2, 6) + offset) 11327 ); 11328 break; 11329 case ArrowDirection.left: 11330 painter.drawPolygon( 11331 scaleWithDpi(Point(10, 2) + offset), 11332 scaleWithDpi(Point(5, 7) + offset), 11333 scaleWithDpi(Point(10, 12) + offset), 11334 scaleWithDpi(Point(10, 2) + offset) 11335 ); 11336 break; 11337 case ArrowDirection.right: 11338 painter.drawPolygon( 11339 scaleWithDpi(Point(6, 2) + offset), 11340 scaleWithDpi(Point(11, 7) + offset), 11341 scaleWithDpi(Point(6, 12) + offset), 11342 scaleWithDpi(Point(6, 2) + offset) 11343 ); 11344 break; 11345 } 11346 } 11347 } 11348 11349 private 11350 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 11351 int x, y; 11352 Widget par = c; 11353 while(par) { 11354 x += par.x; 11355 y += par.y; 11356 par = par.parent; 11357 } 11358 return [x, y]; 11359 } 11360 11361 version(win32_widgets) 11362 private 11363 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 11364 // MapWindowPoints? 11365 int x, y; 11366 Widget par = c; 11367 while(par) { 11368 x += par.x; 11369 y += par.y; 11370 par = par.parent; 11371 if(par !is null && par.useNativeDrawing()) 11372 break; 11373 } 11374 return [x, y]; 11375 } 11376 11377 /// 11378 class ImageBox : Widget { 11379 private MemoryImage image_; 11380 11381 override int widthStretchiness() { return 1; } 11382 override int heightStretchiness() { return 1; } 11383 override int widthShrinkiness() { return 1; } 11384 override int heightShrinkiness() { return 1; } 11385 11386 override int flexBasisHeight() { 11387 return image_.height; 11388 } 11389 11390 override int flexBasisWidth() { 11391 return image_.width; 11392 } 11393 11394 /// 11395 public void setImage(MemoryImage image){ 11396 this.image_ = image; 11397 if(this.parentWindow && this.parentWindow.win) { 11398 if(sprite) 11399 sprite.dispose(); 11400 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11401 } 11402 redraw(); 11403 } 11404 11405 /// How to fit the image in the box if they aren't an exact match in size? 11406 enum HowToFit { 11407 center, /// centers the image, cropping around all the edges as needed 11408 crop, /// always draws the image in the upper left, cropping the lower right if needed 11409 // stretch, /// not implemented 11410 } 11411 11412 private Sprite sprite; 11413 private HowToFit howToFit_; 11414 11415 private Color backgroundColor_; 11416 11417 /// 11418 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 11419 this.image_ = image; 11420 this.tabStop = false; 11421 this.howToFit_ = howToFit; 11422 this.backgroundColor_ = backgroundColor; 11423 super(parent); 11424 updateSprite(); 11425 } 11426 11427 /// ditto 11428 this(MemoryImage image, HowToFit howToFit, Widget parent) { 11429 this(image, howToFit, Color.transparent, parent); 11430 } 11431 11432 private void updateSprite() { 11433 if(sprite is null && this.parentWindow && this.parentWindow.win) { 11434 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11435 } 11436 } 11437 11438 override void paint(WidgetPainter painter) { 11439 updateSprite(); 11440 if(backgroundColor_.a) { 11441 painter.fillColor = backgroundColor_; 11442 painter.drawRectangle(Point(0, 0), width, height); 11443 } 11444 if(howToFit_ == HowToFit.crop) 11445 sprite.drawAt(painter, Point(0, 0)); 11446 else if(howToFit_ == HowToFit.center) { 11447 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 11448 } 11449 } 11450 } 11451 11452 /// 11453 class TextLabel : Widget { 11454 override int maxHeight() { return defaultLineHeight; } 11455 override int minHeight() { return defaultLineHeight; } 11456 override int minWidth() { return 32; } 11457 11458 override int flexBasisHeight() { return minHeight(); } 11459 override int flexBasisWidth() { return defaultTextWidth(label); } 11460 11461 string label_; 11462 11463 /++ 11464 Indicates which other control this label is here for. Similar to HTML `for` attribute. 11465 11466 In practice this means a click on the label will focus the `labelFor`. In future versions 11467 it will also set screen reader hints but that is not yet implemented. 11468 11469 History: 11470 Added October 3, 2021 (dub v10.4) 11471 +/ 11472 Widget labelFor; 11473 11474 /// 11475 @scriptable 11476 string label() { return label_; } 11477 11478 /// 11479 @scriptable 11480 void label(string l) { 11481 label_ = l; 11482 version(win32_widgets) { 11483 WCharzBuffer bfr = WCharzBuffer(l); 11484 SetWindowTextW(hwnd, bfr.ptr); 11485 } else version(custom_widgets) 11486 redraw(); 11487 } 11488 11489 /// 11490 this(string label, TextAlignment alignment, Widget parent) { 11491 this.label_ = label; 11492 this.alignment = alignment; 11493 this.tabStop = false; 11494 super(parent); 11495 11496 version(win32_widgets) 11497 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 11498 } 11499 11500 override void defaultEventHandler_click(scope ClickEvent ce) { 11501 if(this.labelFor !is null) 11502 this.labelFor.focus(); 11503 } 11504 11505 /++ 11506 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 11507 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 11508 +/ 11509 this(string label, Widget parent) { 11510 this(label, TextAlignment.Right, parent); 11511 } 11512 11513 11514 TextAlignment alignment; 11515 11516 version(custom_widgets) 11517 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 11518 painter.outlineColor = getComputedStyle().foregroundColor; 11519 painter.drawText(Point(0, 0), this.label, Point(width, height), alignment); 11520 return bounds; 11521 } 11522 11523 } 11524 11525 version(custom_widgets) 11526 private struct etc { 11527 mixin ExperimentalTextComponent; 11528 } 11529 11530 version(win32_widgets) 11531 alias EditableTextWidgetParent = Widget; /// 11532 else version(custom_widgets) 11533 alias EditableTextWidgetParent = ScrollableWidget; /// 11534 else static assert(0); 11535 11536 /// Contains the implementation of text editing 11537 abstract class EditableTextWidget : EditableTextWidgetParent { 11538 this(Widget parent) { 11539 super(parent); 11540 } 11541 11542 bool wordWrapEnabled_ = false; 11543 void wordWrapEnabled(bool enabled) { 11544 version(win32_widgets) { 11545 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 11546 } else version(custom_widgets) { 11547 wordWrapEnabled_ = enabled; // FIXME 11548 } else static assert(false); 11549 } 11550 11551 override int minWidth() { return scaleWithDpi(16); } 11552 override int minHeight() { return defaultLineHeight + 0; } // the +0 is to leave room for the padding 11553 override int widthStretchiness() { return 7; } 11554 11555 void selectAll() { 11556 version(win32_widgets) 11557 SendMessage(hwnd, EM_SETSEL, 0, -1); 11558 else version(custom_widgets) { 11559 textLayout.selectAll(); 11560 redraw(); 11561 } 11562 } 11563 11564 @property string content() { 11565 version(win32_widgets) { 11566 wchar[4096] bufferstack; 11567 wchar[] buffer; 11568 auto len = GetWindowTextLength(hwnd); 11569 if(len < bufferstack.length) 11570 buffer = bufferstack[0 .. len + 1]; 11571 else 11572 buffer = new wchar[](len + 1); 11573 11574 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 11575 if(l >= 0) 11576 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 11577 else 11578 return null; 11579 } else version(custom_widgets) { 11580 return textLayout.getPlainText(); 11581 } else static assert(false); 11582 } 11583 @property void content(string s) { 11584 version(win32_widgets) { 11585 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 11586 SetWindowTextW(hwnd, bfr.ptr); 11587 } else version(custom_widgets) { 11588 textLayout.clear(); 11589 textLayout.addText(s); 11590 11591 { 11592 // FIXME: it should be able to get this info easier 11593 auto painter = draw(); 11594 textLayout.redoLayout(painter); 11595 } 11596 auto cbb = textLayout.contentBoundingBox(); 11597 setContentSize(cbb.width, cbb.height); 11598 /* 11599 textLayout.addText(ForegroundColor.red, s); 11600 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 11601 textLayout.addText(" is the best!"); 11602 */ 11603 redraw(); 11604 } 11605 else static assert(false); 11606 } 11607 11608 void addText(string txt) { 11609 version(custom_widgets) { 11610 11611 textLayout.addText(txt); 11612 11613 { 11614 // FIXME: it should be able to get this info easier 11615 auto painter = draw(); 11616 textLayout.redoLayout(painter); 11617 } 11618 auto cbb = textLayout.contentBoundingBox(); 11619 setContentSize(cbb.width, cbb.height); 11620 11621 } else version(win32_widgets) { 11622 // get the current selection 11623 DWORD StartPos, EndPos; 11624 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 11625 11626 // move the caret to the end of the text 11627 int outLength = GetWindowTextLengthW(hwnd); 11628 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 11629 11630 // insert the text at the new caret position 11631 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 11632 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 11633 11634 // restore the previous selection 11635 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 11636 } else static assert(0); 11637 } 11638 11639 version(custom_widgets) 11640 override void paintFrameAndBackground(WidgetPainter painter) { 11641 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 11642 } 11643 11644 version(win32_widgets) { /* will do it with Windows calls in the classes */ } 11645 else version(custom_widgets) { 11646 // FIXME 11647 11648 static if(SimpledisplayTimerAvailable) 11649 Timer caretTimer; 11650 etc.TextLayout textLayout; 11651 11652 void setupCustomTextEditing() { 11653 textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4)); 11654 textLayout.selectionXorColor = getComputedStyle().activeListXorColor; 11655 } 11656 11657 override void paint(WidgetPainter painter) { 11658 if(parentWindow.win.closed) return; 11659 11660 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 11661 11662 /* 11663 painter.outlineColor = Color.white; 11664 painter.fillColor = Color.white; 11665 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 11666 */ 11667 11668 painter.outlineColor = Color.black; 11669 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 11670 11671 textLayout.caretShowingOnScreen = false; 11672 11673 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 11674 } 11675 11676 static class Style : Widget.Style { 11677 override MouseCursor cursor() { 11678 return GenericCursor.Text; 11679 } 11680 } 11681 mixin OverrideStyle!Style; 11682 } 11683 else static assert(false); 11684 11685 11686 11687 version(custom_widgets) 11688 override void defaultEventHandler_mousedown(MouseDownEvent ev) { 11689 super.defaultEventHandler_mousedown(ev); 11690 if(parentWindow.win.closed) return; 11691 if(ev.button == MouseButton.left) { 11692 if(textLayout.selectNone()) 11693 redraw(); 11694 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 11695 this.focus(); 11696 //this.parentWindow.win.grabInput(); 11697 } else if(ev.button == MouseButton.middle) { 11698 static if(UsingSimpledisplayX11) { 11699 getPrimarySelection(parentWindow.win, (txt) { 11700 textLayout.insert(txt); 11701 redraw(); 11702 11703 auto cbb = textLayout.contentBoundingBox(); 11704 setContentSize(cbb.width, cbb.height); 11705 }); 11706 } 11707 } 11708 } 11709 11710 version(custom_widgets) 11711 override void defaultEventHandler_mouseup(MouseUpEvent ev) { 11712 //this.parentWindow.win.releaseInputGrab(); 11713 super.defaultEventHandler_mouseup(ev); 11714 } 11715 11716 version(custom_widgets) 11717 override void defaultEventHandler_mousemove(MouseMoveEvent ev) { 11718 super.defaultEventHandler_mousemove(ev); 11719 if(ev.state & ModifierState.leftButtonDown) { 11720 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 11721 redraw(); 11722 } 11723 } 11724 11725 version(custom_widgets) 11726 override void defaultEventHandler_focus(Event ev) { 11727 super.defaultEventHandler_focus(ev); 11728 if(parentWindow.win.closed) return; 11729 auto painter = this.draw(); 11730 textLayout.drawCaret(painter); 11731 11732 static if(SimpledisplayTimerAvailable) 11733 if(caretTimer) { 11734 caretTimer.destroy(); 11735 caretTimer = null; 11736 } 11737 11738 bool blinkingCaret = true; 11739 static if(UsingSimpledisplayX11) 11740 if(!Image.impl.xshmAvailable) 11741 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 11742 11743 if(blinkingCaret) 11744 static if(SimpledisplayTimerAvailable) 11745 caretTimer = new Timer(500, { 11746 if(parentWindow.win.closed) { 11747 caretTimer.destroy(); 11748 return; 11749 } 11750 if(isFocused()) { 11751 auto painter = this.draw(); 11752 textLayout.drawCaret(painter); 11753 } else if(textLayout.caretShowingOnScreen) { 11754 auto painter = this.draw(); 11755 textLayout.eraseCaret(painter); 11756 } 11757 }); 11758 } 11759 11760 private string lastContentBlur; 11761 11762 override void defaultEventHandler_blur(Event ev) { 11763 super.defaultEventHandler_blur(ev); 11764 if(parentWindow.win.closed) return; 11765 version(custom_widgets) { 11766 auto painter = this.draw(); 11767 textLayout.eraseCaret(painter); 11768 static if(SimpledisplayTimerAvailable) 11769 if(caretTimer) { 11770 caretTimer.destroy(); 11771 caretTimer = null; 11772 } 11773 } 11774 11775 if(this.content != lastContentBlur) { 11776 auto evt = new ChangeEvent!string(this, &this.content); 11777 evt.dispatch(); 11778 lastContentBlur = this.content; 11779 } 11780 } 11781 11782 version(custom_widgets) 11783 override void defaultEventHandler_char(CharEvent ev) { 11784 super.defaultEventHandler_char(ev); 11785 textLayout.insert(ev.character); 11786 redraw(); 11787 11788 // FIXME: too inefficient 11789 auto cbb = textLayout.contentBoundingBox(); 11790 setContentSize(cbb.width, cbb.height); 11791 } 11792 version(custom_widgets) 11793 override void defaultEventHandler_keydown(KeyDownEvent ev) { 11794 //super.defaultEventHandler_keydown(ev); 11795 switch(ev.key) { 11796 case Key.Delete: 11797 textLayout.delete_(); 11798 redraw(); 11799 break; 11800 case Key.Left: 11801 textLayout.moveLeft(); 11802 redraw(); 11803 break; 11804 case Key.Right: 11805 textLayout.moveRight(); 11806 redraw(); 11807 break; 11808 case Key.Up: 11809 textLayout.moveUp(); 11810 redraw(); 11811 break; 11812 case Key.Down: 11813 textLayout.moveDown(); 11814 redraw(); 11815 break; 11816 case Key.Home: 11817 textLayout.moveHome(); 11818 redraw(); 11819 break; 11820 case Key.End: 11821 textLayout.moveEnd(); 11822 redraw(); 11823 break; 11824 case Key.PageUp: 11825 foreach(i; 0 .. 32) 11826 textLayout.moveUp(); 11827 redraw(); 11828 break; 11829 case Key.PageDown: 11830 foreach(i; 0 .. 32) 11831 textLayout.moveDown(); 11832 redraw(); 11833 break; 11834 11835 default: 11836 {} // intentionally blank, let "char" handle it 11837 } 11838 /* 11839 if(ev.key == Key.Backspace) { 11840 textLayout.backspace(); 11841 redraw(); 11842 } 11843 */ 11844 ensureVisibleInScroll(textLayout.caretBoundingBox()); 11845 } 11846 11847 11848 } 11849 11850 /// 11851 class LineEdit : EditableTextWidget { 11852 // FIXME: hack 11853 version(custom_widgets) { 11854 override bool showingVerticalScroll() { return false; } 11855 override bool showingHorizontalScroll() { return false; } 11856 } 11857 11858 override int flexBasisWidth() { return 250; } 11859 11860 /// 11861 this(Widget parent) { 11862 super(parent); 11863 version(win32_widgets) { 11864 createWin32Window(this, "edit"w, "", 11865 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 11866 } else version(custom_widgets) { 11867 setupCustomTextEditing(); 11868 addEventListener(delegate(CharEvent ev) { 11869 if(ev.character == '\n') 11870 ev.preventDefault(); 11871 }); 11872 } else static assert(false); 11873 } 11874 override int maxHeight() { return defaultLineHeight + 4; } 11875 override int minHeight() { return defaultLineHeight + 4; } 11876 11877 /+ 11878 @property void passwordMode(bool p) { 11879 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 11880 } 11881 +/ 11882 } 11883 11884 /++ 11885 A [LineEdit] that displays `*` in place of the actual characters. 11886 11887 Alas, Windows requires the window to be created differently to use this style, 11888 so it had to be a new class instead of a toggle on and off on an existing object. 11889 11890 FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now. 11891 11892 History: 11893 Added January 24, 2021 11894 +/ 11895 class PasswordEdit : EditableTextWidget { 11896 version(custom_widgets) { 11897 override bool showingVerticalScroll() { return false; } 11898 override bool showingHorizontalScroll() { return false; } 11899 } 11900 11901 override int flexBasisWidth() { return 250; } 11902 11903 /// 11904 this(Widget parent) { 11905 super(parent); 11906 version(win32_widgets) { 11907 createWin32Window(this, "edit"w, "", 11908 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 11909 } else version(custom_widgets) { 11910 setupCustomTextEditing(); 11911 addEventListener(delegate(CharEvent ev) { 11912 if(ev.character == '\n') 11913 ev.preventDefault(); 11914 }); 11915 } else static assert(false); 11916 } 11917 override int maxHeight() { return defaultLineHeight + 4; } 11918 override int minHeight() { return defaultLineHeight + 4; } 11919 } 11920 11921 11922 /// 11923 class TextEdit : EditableTextWidget { 11924 /// 11925 this(Widget parent) { 11926 super(parent); 11927 version(win32_widgets) { 11928 createWin32Window(this, "edit"w, "", 11929 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 11930 } else version(custom_widgets) { 11931 setupCustomTextEditing(); 11932 } else static assert(false); 11933 } 11934 override int maxHeight() { return int.max; } 11935 override int heightStretchiness() { return 7; } 11936 11937 override int flexBasisWidth() { return 250; } 11938 override int flexBasisHeight() { return 250; } 11939 } 11940 11941 11942 /++ 11943 11944 +/ 11945 version(none) 11946 class RichTextDisplay : Widget { 11947 @property void content(string c) {} 11948 void appendContent(string c) {} 11949 } 11950 11951 /// 11952 class MessageBox : Window { 11953 private string message; 11954 MessageBoxButton buttonPressed = MessageBoxButton.None; 11955 /// 11956 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 11957 super(300, 100); 11958 11959 assert(buttons.length); 11960 assert(buttons.length == buttonIds.length); 11961 11962 this.message = message; 11963 11964 int buttonsWidth = cast(int) buttons.length * 50 + (cast(int) buttons.length - 1) * 16; 11965 buttonsWidth = scaleWithDpi(buttonsWidth); 11966 11967 int x = this.width / 2 - buttonsWidth / 2; 11968 11969 foreach(idx, buttonText; buttons) { 11970 auto button = new Button(buttonText, this); 11971 button.x = x; 11972 button.y = height - (button.height + 10); 11973 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 11974 this.buttonPressed = buttonIds[idx]; 11975 win.close(); 11976 }; })(idx)); 11977 11978 button.registerMovement(); 11979 x += button.width; 11980 x += scaleWithDpi(16); 11981 if(idx == 0) 11982 button.focus(); 11983 } 11984 11985 win.show(); 11986 redraw(); 11987 } 11988 11989 override void paint(WidgetPainter painter) { 11990 super.paint(painter); 11991 11992 auto cs = getComputedStyle(); 11993 11994 painter.outlineColor = cs.foregroundColor(); 11995 painter.fillColor = cs.foregroundColor(); 11996 11997 painter.drawText(Point(0, 0), message, Point(width, height / 2), TextAlignment.Center | TextAlignment.VerticalCenter); 11998 } 11999 12000 // this one is all fixed position 12001 override void recomputeChildLayout() {} 12002 } 12003 12004 /// 12005 enum MessageBoxStyle { 12006 OK, /// 12007 OKCancel, /// 12008 RetryCancel, /// 12009 YesNo, /// 12010 YesNoCancel, /// 12011 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. 12012 } 12013 12014 /// 12015 enum MessageBoxIcon { 12016 None, /// 12017 Info, /// 12018 Warning, /// 12019 Error /// 12020 } 12021 12022 /// Identifies the button the user pressed on a message box. 12023 enum MessageBoxButton { 12024 None, /// The user closed the message box without clicking any of the buttons. 12025 OK, /// 12026 Cancel, /// 12027 Retry, /// 12028 Yes, /// 12029 No, /// 12030 Continue /// 12031 } 12032 12033 12034 /++ 12035 Displays a modal message box, blocking until the user dismisses it. 12036 12037 Returns: the button pressed. 12038 +/ 12039 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 12040 version(win32_widgets) { 12041 WCharzBuffer t = WCharzBuffer(title); 12042 WCharzBuffer m = WCharzBuffer(message); 12043 UINT type; 12044 with(MessageBoxStyle) 12045 final switch(style) { 12046 case OK: type |= MB_OK; break; 12047 case OKCancel: type |= MB_OKCANCEL; break; 12048 case RetryCancel: type |= MB_RETRYCANCEL; break; 12049 case YesNo: type |= MB_YESNO; break; 12050 case YesNoCancel: type |= MB_YESNOCANCEL; break; 12051 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 12052 } 12053 with(MessageBoxIcon) 12054 final switch(icon) { 12055 case None: break; 12056 case Info: type |= MB_ICONINFORMATION; break; 12057 case Warning: type |= MB_ICONWARNING; break; 12058 case Error: type |= MB_ICONERROR; break; 12059 } 12060 switch(MessageBoxW(null, m.ptr, t.ptr, type)) { 12061 case IDOK: return MessageBoxButton.OK; 12062 case IDCANCEL: return MessageBoxButton.Cancel; 12063 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 12064 case IDYES: return MessageBoxButton.Yes; 12065 case IDNO: return MessageBoxButton.No; 12066 case IDCONTINUE: return MessageBoxButton.Continue; 12067 default: return MessageBoxButton.None; 12068 } 12069 } else { 12070 string[] buttons; 12071 MessageBoxButton[] buttonIds; 12072 with(MessageBoxStyle) 12073 final switch(style) { 12074 case OK: 12075 buttons = ["OK"]; 12076 buttonIds = [MessageBoxButton.OK]; 12077 break; 12078 case OKCancel: 12079 buttons = ["OK", "Cancel"]; 12080 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 12081 break; 12082 case RetryCancel: 12083 buttons = ["Retry", "Cancel"]; 12084 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 12085 break; 12086 case YesNo: 12087 buttons = ["Yes", "No"]; 12088 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 12089 break; 12090 case YesNoCancel: 12091 buttons = ["Yes", "No", "Cancel"]; 12092 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 12093 break; 12094 case RetryCancelContinue: 12095 buttons = ["Try Again", "Cancel", "Continue"]; 12096 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 12097 break; 12098 } 12099 auto mb = new MessageBox(message, buttons, buttonIds); 12100 EventLoop el = EventLoop.get; 12101 el.run(() { return !mb.win.closed; }); 12102 return mb.buttonPressed; 12103 } 12104 } 12105 12106 /// ditto 12107 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 12108 return messageBox(null, message, style, icon); 12109 } 12110 12111 12112 12113 /// 12114 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 12115 12116 /++ 12117 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 12118 12119 History: 12120 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. 12121 +/ 12122 struct EventListener { 12123 private Widget widget; 12124 private string event; 12125 private EventHandler handler; 12126 private bool useCapture; 12127 12128 /// 12129 void disconnect() { 12130 widget.removeEventListener(this); 12131 } 12132 } 12133 12134 /++ 12135 The purpose of this enum was to give a compile-time checked version of various standard event strings. 12136 12137 Now, I recommend you use a statically typed event object instead. 12138 12139 See_Also: [Event] 12140 +/ 12141 enum EventType : string { 12142 click = "click", /// 12143 12144 mouseenter = "mouseenter", /// 12145 mouseleave = "mouseleave", /// 12146 mousein = "mousein", /// 12147 mouseout = "mouseout", /// 12148 mouseup = "mouseup", /// 12149 mousedown = "mousedown", /// 12150 mousemove = "mousemove", /// 12151 12152 keydown = "keydown", /// 12153 keyup = "keyup", /// 12154 char_ = "char", /// 12155 12156 focus = "focus", /// 12157 blur = "blur", /// 12158 12159 triggered = "triggered", /// 12160 12161 change = "change", /// 12162 } 12163 12164 /++ 12165 Represents an event that is currently being processed. 12166 12167 12168 Minigui's event model is based on the web browser. An event has a name, a target, 12169 and an associated data object. It starts from the window and works its way down through 12170 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 12171 then goes back up again all the way back to the window, triggering bubble phase handlers. At 12172 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 12173 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 12174 was called; that just stops it from calling other handlers in the widget tree, but the default happens 12175 whenever propagation is done, not only if it gets to the end of the chain). 12176 12177 This model has several nice points: 12178 12179 $(LIST 12180 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 12181 with event handlers set, then add/remove children as much as you want without needing 12182 to manage the event handlers on them - the parent alone can manage everything. 12183 12184 * It is easy to create new custom events in your application. 12185 12186 * It is familiar to many web developers. 12187 ) 12188 12189 There's a few downsides though: 12190 12191 $(LIST 12192 * There's not a lot of type safety. 12193 12194 * You don't get a static list of what events a widget can emit. 12195 12196 * Tracing where an event got cancelled along the chain can get difficult; the downside of 12197 the central delegation benefit is it can be lead to debugging of action at a distance. 12198 ) 12199 12200 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 12201 while keeping the benefits - and most compatibility with - the existing model. The main idea is 12202 to simply use a D object type which provides a static interface as well as a built-in event name. 12203 Then, a new static interface allows you to see what an event can emit and attach handlers to it 12204 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 12205 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 12206 to having a little more help from the D compiler and documentation generator. 12207 12208 Your code would change like this: 12209 12210 --- 12211 // old 12212 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 12213 12214 // new 12215 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 12216 --- 12217 12218 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. 12219 12220 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 12221 12222 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 12223 12224 Thus the family of functions are: 12225 12226 [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. 12227 12228 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 12229 12230 [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. 12231 12232 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 12233 12234 --- 12235 class MyCheckbox : Widget { 12236 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 12237 /// It is NOT actually required but should be used whenever possible. 12238 mixin Emits!(ChangeEvent!bool); 12239 12240 this(Widget parent) { 12241 super(parent); 12242 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 12243 } 12244 12245 private bool _checked; 12246 @property bool checked() { return _checked; } 12247 @property void checked(bool set) { 12248 _checked = set; 12249 emit!(ChangeEvent!bool)(&checked); 12250 } 12251 } 12252 --- 12253 12254 ## Creating Your Own Events 12255 12256 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. 12257 12258 --- 12259 class MyEvent : Event { 12260 this(Widget target) { super(EventString, target); } 12261 mixin Register; // adds EventString and other reflection information 12262 } 12263 --- 12264 12265 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 12266 12267 History: 12268 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. 12269 12270 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. 12271 +/ 12272 /+ 12273 12274 ## General Conventions 12275 12276 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. 12277 12278 12279 ## Qt-style signals and slots 12280 12281 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. 12282 12283 The intention is for events to be used when 12284 12285 --- 12286 class Demo : Widget { 12287 this() { 12288 myPropertyChanged = Signal!int(this); 12289 } 12290 @property myProperty(int v) { 12291 myPropertyChanged.emit(v); 12292 } 12293 12294 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 12295 // but it can just genuinely not care about `this` since that's not really passed. 12296 } 12297 12298 class Foo : Widget { 12299 // the slot uda is not necessary, but it helps the script and ui builder find it. 12300 @slot void setValue(int v) { ... } 12301 } 12302 12303 demo.myPropertyChanged.connect(&foo.setValue); 12304 --- 12305 12306 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 12307 12308 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 12309 12310 class StringChangeEvent : ChangeEvent, Signal!string { 12311 mixin SignalImpl 12312 } 12313 12314 +/ 12315 class Event : ReflectableProperties { 12316 /// Creates an event without populating any members and without sending it. See [dispatch] 12317 this(string eventName, Widget emittedBy) { 12318 this.eventName = eventName; 12319 this.srcElement = emittedBy; 12320 } 12321 12322 12323 /// Implementations for the [ReflectableProperties] interface/ 12324 void getPropertiesList(scope void delegate(string name) sink) const {} 12325 /// ditto 12326 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 12327 /// ditto 12328 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 12329 return SetPropertyResult.notPermitted; 12330 } 12331 12332 12333 /+ 12334 /++ 12335 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 12336 12337 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. 12338 +/ 12339 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 12340 if(value.length == 0) { 12341 finalSink(memberName, `""`); 12342 return; 12343 } 12344 12345 char[1024] bufferBacking; 12346 char[] buffer = bufferBacking; 12347 int bufferPosition; 12348 12349 void sink(char ch) { 12350 if(bufferPosition >= buffer.length) 12351 buffer.length = buffer.length + 1024; 12352 buffer[bufferPosition++] = ch; 12353 } 12354 12355 sink('"'); 12356 12357 foreach(ch; value) { 12358 switch(ch) { 12359 case '\\': 12360 sink('\\'); sink('\\'); 12361 break; 12362 case '"': 12363 sink('\\'); sink('"'); 12364 break; 12365 case '\n': 12366 sink('\\'); sink('n'); 12367 break; 12368 case '\r': 12369 sink('\\'); sink('r'); 12370 break; 12371 case '\t': 12372 sink('\\'); sink('t'); 12373 break; 12374 default: 12375 sink(ch); 12376 } 12377 } 12378 12379 sink('"'); 12380 12381 finalSink(memberName, buffer[0 .. bufferPosition]); 12382 } 12383 +/ 12384 12385 /+ 12386 enum EventInitiator { 12387 system, 12388 minigui, 12389 user 12390 } 12391 12392 immutable EventInitiator; initiatedBy; 12393 +/ 12394 12395 /++ 12396 Events should generally follow the propagation model, but there's some exceptions 12397 to that rule. If so, they should override this to return false. In that case, only 12398 bubbling event handlers on the target itself and capturing event handlers on the containing 12399 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 12400 capture -> target -> bubble process.) 12401 12402 History: 12403 Added May 12, 2021 12404 +/ 12405 bool propagates() const pure nothrow @nogc @safe { 12406 return true; 12407 } 12408 12409 /++ 12410 hints as to whether preventDefault will actually do anything. not entirely reliable. 12411 12412 History: 12413 Added May 14, 2021 12414 +/ 12415 bool cancelable() const pure nothrow @nogc @safe { 12416 return true; 12417 } 12418 12419 /++ 12420 You can mix this into child class to register some boilerplate. It includes the `EventString` 12421 member, a constructor, and implementations of the dynamic get data interfaces. 12422 12423 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 12424 12425 12426 You can override the default EventString by simply providing your own in the form of 12427 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 12428 which provides some namespace protection against conflicts in other libraries while still being fairly 12429 easy to use. 12430 12431 If you provide your own constructor, it will override the default constructor provided here. A constructor 12432 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 12433 first argument to your constructor. 12434 12435 History: 12436 Added May 13, 2021. 12437 +/ 12438 protected static mixin template Register() { 12439 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 12440 this(Widget target) { super(EventString, target); } 12441 12442 mixin ReflectableProperties.RegisterGetters; 12443 } 12444 12445 /++ 12446 This is the widget that emitted the event. 12447 12448 12449 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 12450 12451 History: 12452 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 12453 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 12454 so I don't intend to remove these aliases. 12455 +/ 12456 Widget source; 12457 /// ditto 12458 alias source target; 12459 /// ditto 12460 alias source srcElement; 12461 12462 Widget relatedTarget; /// Note: likely to be deprecated at some point. 12463 12464 /// Prevents the default event handler (if there is one) from being called 12465 void preventDefault() { 12466 lastDefaultPrevented = true; 12467 defaultPrevented = true; 12468 } 12469 12470 /// Stops the event propagation immediately. 12471 void stopPropagation() { 12472 propagationStopped = true; 12473 } 12474 12475 private bool defaultPrevented; 12476 private bool propagationStopped; 12477 private string eventName; 12478 12479 private bool isBubbling; 12480 12481 /// 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. 12482 protected void adjustScrolling() { } 12483 /// ditto 12484 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 12485 12486 /++ 12487 this sends it only to the target. If you want propagation, use dispatch() instead. 12488 12489 This should be made private!!! 12490 12491 +/ 12492 void sendDirectly() { 12493 if(srcElement is null) 12494 return; 12495 12496 // 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. 12497 12498 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 12499 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 12500 12501 adjustScrolling(); 12502 12503 if(auto e = target.parentWindow) { 12504 if(auto handlers = "*" in e.capturingEventHandlers) 12505 foreach(handler; *handlers) 12506 if(handler) handler(e, this); 12507 if(auto handlers = eventName in e.capturingEventHandlers) 12508 foreach(handler; *handlers) 12509 if(handler) handler(e, this); 12510 } 12511 12512 auto e = srcElement; 12513 12514 if(auto handlers = eventName in e.bubblingEventHandlers) 12515 foreach(handler; *handlers) 12516 if(handler) handler(e, this); 12517 12518 if(auto handlers = "*" in e.bubblingEventHandlers) 12519 foreach(handler; *handlers) 12520 if(handler) handler(e, this); 12521 12522 // there's never a default for a catch-all event 12523 if(!defaultPrevented) 12524 if(eventName in e.defaultEventHandlers) 12525 e.defaultEventHandlers[eventName](e, this); 12526 } 12527 12528 /// this dispatches the element using the capture -> target -> bubble process 12529 void dispatch() { 12530 if(srcElement is null) 12531 return; 12532 12533 if(!propagates) { 12534 sendDirectly; 12535 return; 12536 } 12537 12538 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 12539 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 12540 12541 adjustScrolling(); 12542 // first capture, then bubble 12543 12544 Widget[] chain; 12545 Widget curr = srcElement; 12546 while(curr) { 12547 auto l = curr; 12548 chain ~= l; 12549 curr = curr.parent; 12550 } 12551 12552 isBubbling = false; 12553 12554 foreach_reverse(e; chain) { 12555 if(auto handlers = "*" in e.capturingEventHandlers) 12556 foreach(handler; *handlers) if(handler !is null) handler(e, this); 12557 12558 if(propagationStopped) 12559 break; 12560 12561 if(auto handlers = eventName in e.capturingEventHandlers) 12562 foreach(handler; *handlers) if(handler !is null) handler(e, this); 12563 12564 // the default on capture should really be to always do nothing 12565 12566 //if(!defaultPrevented) 12567 // if(eventName in e.defaultEventHandlers) 12568 // e.defaultEventHandlers[eventName](e.element, this); 12569 12570 if(propagationStopped) 12571 break; 12572 } 12573 12574 int adjustX; 12575 int adjustY; 12576 12577 isBubbling = true; 12578 if(!propagationStopped) 12579 foreach(e; chain) { 12580 if(auto handlers = eventName in e.bubblingEventHandlers) 12581 foreach(handler; *handlers) if(handler !is null) handler(e, this); 12582 12583 if(propagationStopped) 12584 break; 12585 12586 if(auto handlers = "*" in e.bubblingEventHandlers) 12587 foreach(handler; *handlers) if(handler !is null) handler(e, this); 12588 12589 if(propagationStopped) 12590 break; 12591 12592 if(e.encapsulatedChildren()) { 12593 adjustClientCoordinates(adjustX, adjustY); 12594 target = e; 12595 } else { 12596 adjustX += e.x; 12597 adjustY += e.y; 12598 } 12599 } 12600 12601 if(!defaultPrevented) 12602 foreach(e; chain) { 12603 if(eventName in e.defaultEventHandlers) 12604 e.defaultEventHandlers[eventName](e, this); 12605 } 12606 } 12607 12608 12609 /* old compatibility things */ 12610 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 12611 final @property { 12612 Key key() { return (cast(KeyEventBase) this).key; } 12613 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 12614 12615 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 12616 bool altKey() { return (cast(KeyEventBase) this).altKey; } 12617 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 12618 } 12619 12620 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 12621 final @property { 12622 int clientX() { return (cast(MouseEventBase) this).clientX; } 12623 int clientY() { return (cast(MouseEventBase) this).clientY; } 12624 12625 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 12626 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 12627 12628 int button() { return (cast(MouseEventBase) this).button; } 12629 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 12630 } 12631 12632 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 12633 final @property { 12634 int state() { 12635 if(auto meb = cast(MouseEventBase) this) 12636 return meb.state; 12637 if(auto keb = cast(KeyEventBase) this) 12638 return keb.state; 12639 assert(0); 12640 } 12641 } 12642 12643 deprecated("Use a CharEvent instead of Event in your handler going forward") 12644 final @property { 12645 dchar character() { 12646 if(auto ce = cast(CharEvent) this) 12647 return ce.character; 12648 return dchar.init; 12649 } 12650 } 12651 12652 // for change events 12653 @property { 12654 /// 12655 int intValue() { return 0; } 12656 /// 12657 string stringValue() { return null; } 12658 } 12659 } 12660 12661 /++ 12662 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 12663 12664 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 12665 dynamic and custom events, but the static list helps ensure you get them right. 12666 12667 If this is declared, you can use [Widget.emit] to send the event. 12668 12669 All events work the same way though, following the capture->widget->bubble model described under [Event]. 12670 12671 History: 12672 Added May 4, 2021 12673 +/ 12674 mixin template Emits(EventType) { 12675 import arsd.minigui : EventString; 12676 static if(is(EventType : Event) && !is(EventType == Event)) 12677 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 12678 else 12679 static assert(0, "You can only emit subclasses of Event"); 12680 } 12681 12682 /// ditto 12683 mixin template Emits(string eventString) { 12684 mixin("private Event[0] emits_" ~ eventString ~";"); 12685 } 12686 12687 /* 12688 class SignalEvent(string name) : Event { 12689 12690 } 12691 */ 12692 12693 /++ 12694 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". 12695 12696 12697 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. 12698 12699 History: 12700 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 12701 +/ 12702 class CommandEvent : Event { 12703 enum EventString = "command"; 12704 this(Widget source, string CommandString = EventString) { 12705 super(CommandString, source); 12706 } 12707 } 12708 12709 /++ 12710 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 12711 +/ 12712 class CommandEventWithArgs(Args...) : CommandEvent { 12713 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 12714 Args args; 12715 } 12716 12717 /++ 12718 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. 12719 12720 See [CommandEvent] for more information. 12721 12722 Returns: 12723 The [EventListener] you can use to remove the handler. 12724 +/ 12725 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 12726 return w.addEventListener(CommandString, (Event ev) { 12727 if(ev.target is w) 12728 return; // it does not consume its own commands! 12729 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 12730 handler(cev.args); 12731 ev.stopPropagation(); 12732 } 12733 }); 12734 } 12735 12736 /++ 12737 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. 12738 +/ 12739 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 12740 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 12741 event.dispatch(); 12742 } 12743 12744 class ResizeEvent : Event { 12745 enum EventString = "resize"; 12746 12747 this(Widget target) { super(EventString, target); } 12748 12749 override bool propagates() const { return false; } 12750 } 12751 12752 /++ 12753 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 12754 12755 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. 12756 12757 History: 12758 Added June 21, 2021 (dub v10.1) 12759 +/ 12760 class ClosingEvent : Event { 12761 enum EventString = "closing"; 12762 12763 this(Widget target) { super(EventString, target); } 12764 12765 override bool propagates() const { return false; } 12766 override bool cancelable() const { return true; } 12767 } 12768 12769 /// ditto 12770 class ClosedEvent : Event { 12771 enum EventString = "closed"; 12772 12773 this(Widget target) { super(EventString, target); } 12774 12775 override bool propagates() const { return false; } 12776 override bool cancelable() const { return false; } 12777 } 12778 12779 /// 12780 class BlurEvent : Event { 12781 enum EventString = "blur"; 12782 12783 // FIXME: related target? 12784 this(Widget target) { super(EventString, target); } 12785 12786 override bool propagates() const { return false; } 12787 } 12788 12789 /// 12790 class FocusEvent : Event { 12791 enum EventString = "focus"; 12792 12793 // FIXME: related target? 12794 this(Widget target) { super(EventString, target); } 12795 12796 override bool propagates() const { return false; } 12797 } 12798 12799 /++ 12800 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 12801 12802 History: 12803 Added July 3, 2021 12804 +/ 12805 class FocusInEvent : Event { 12806 enum EventString = "focusin"; 12807 12808 // FIXME: related target? 12809 this(Widget target) { super(EventString, target); } 12810 12811 override bool cancelable() const { return false; } 12812 } 12813 12814 /// ditto 12815 class FocusOutEvent : Event { 12816 enum EventString = "focusout"; 12817 12818 // FIXME: related target? 12819 this(Widget target) { super(EventString, target); } 12820 12821 override bool cancelable() const { return false; } 12822 } 12823 12824 /// 12825 class ScrollEvent : Event { 12826 enum EventString = "scroll"; 12827 this(Widget target) { super(EventString, target); } 12828 12829 override bool cancelable() const { return false; } 12830 } 12831 12832 /++ 12833 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 12834 12835 History: 12836 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 12837 +/ 12838 class CharEvent : Event { 12839 enum EventString = "char"; 12840 this(Widget target, dchar ch) { 12841 character = ch; 12842 super(EventString, target); 12843 } 12844 12845 immutable dchar character; 12846 } 12847 12848 /++ 12849 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 12850 +/ 12851 abstract class ChangeEventBase : Event { 12852 enum EventString = "change"; 12853 this(Widget target) { 12854 super(EventString, target); 12855 } 12856 12857 /+ 12858 // idk where or how exactly i want to do this. 12859 // i might come back to it later. 12860 12861 // If a widget itself broadcasts one of theses itself, it stops propagation going down 12862 // this way the source doesn't get too confused (think of a nested scroll widget) 12863 // 12864 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 12865 // then you consume that command and change you scroll x position to whatever. then you do 12866 // some kind of change event that is broadcast back to the children and any horizontal scroll 12867 // listeners are now able to update, without having an explicit connection between them. 12868 void broadcastToChildren(string fieldName) { 12869 12870 } 12871 +/ 12872 } 12873 12874 /++ 12875 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. 12876 12877 12878 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). 12879 12880 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);` 12881 12882 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 12883 12884 History: 12885 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. 12886 +/ 12887 class ChangeEvent(T) : ChangeEventBase { 12888 this(Widget target, T delegate() getNewValue) { 12889 assert(getNewValue !is null); 12890 this.getNewValue = getNewValue; 12891 super(target); 12892 } 12893 12894 private T delegate() getNewValue; 12895 12896 /++ 12897 Gets the new value that just changed. 12898 +/ 12899 @property T value() { 12900 return getNewValue(); 12901 } 12902 12903 /// compatibility method for old generic Events 12904 static if(is(immutable T == immutable int)) 12905 override int intValue() { return value; } 12906 /// ditto 12907 static if(is(immutable T == immutable string)) 12908 override string stringValue() { return value; } 12909 } 12910 12911 /++ 12912 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 12913 12914 12915 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 12916 12917 History: 12918 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 12919 +/ 12920 abstract class KeyEventBase : Event { 12921 this(string name, Widget target) { 12922 super(name, target); 12923 } 12924 12925 // for key events 12926 Key key; /// 12927 12928 KeyEvent originalKeyEvent; 12929 12930 /++ 12931 Indicates the current state of the given keyboard modifier keys. 12932 12933 History: 12934 Added to events on April 15, 2020. 12935 +/ 12936 bool ctrlKey; 12937 12938 /// ditto 12939 bool altKey; 12940 12941 /// ditto 12942 bool shiftKey; 12943 12944 /++ 12945 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 12946 12947 See [arsd.simpledisplay.ModifierState] for other possible flags. 12948 +/ 12949 int state; 12950 12951 mixin Register; 12952 } 12953 12954 /++ 12955 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]. 12956 12957 12958 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 12959 12960 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. 12961 12962 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. 12963 12964 See_Also: [KeyUpEvent], [CharEvent] 12965 12966 History: 12967 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 12968 +/ 12969 class KeyDownEvent : KeyEventBase { 12970 enum EventString = "keydown"; 12971 this(Widget target) { super(EventString, target); } 12972 } 12973 12974 /++ 12975 Indicates that the user has released a key on the keyboard. 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 See_Also: [KeyDownEvent], [CharEvent] 12981 12982 History: 12983 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 12984 +/ 12985 class KeyUpEvent : KeyEventBase { 12986 enum EventString = "keyup"; 12987 this(Widget target) { super(EventString, target); } 12988 } 12989 12990 /++ 12991 Contains shared properties for various mouse events; 12992 12993 12994 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 12995 12996 History: 12997 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 12998 +/ 12999 abstract class MouseEventBase : Event { 13000 this(string name, Widget target) { 13001 super(name, target); 13002 } 13003 13004 // for mouse events 13005 int clientX; /// The mouse event location relative to the target widget 13006 int clientY; /// ditto 13007 13008 int viewportX; /// The mouse event location relative to the window origin 13009 int viewportY; /// ditto 13010 13011 int button; /// See: [MouseEvent.button] 13012 int buttonLinear; /// See: [MouseEvent.buttonLinear] 13013 13014 /++ 13015 Indicates the current state of the given keyboard modifier keys. 13016 13017 History: 13018 Added to mouse events on September 28, 2010. 13019 +/ 13020 bool ctrlKey; 13021 13022 /// ditto 13023 bool altKey; 13024 13025 /// ditto 13026 bool shiftKey; 13027 13028 13029 13030 int state; /// 13031 13032 /++ 13033 for consistent names with key event. 13034 13035 History: 13036 Added September 28, 2021 (dub v10.3) 13037 +/ 13038 alias modifierState = state; 13039 13040 /++ 13041 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 13042 13043 History: 13044 Added May 15, 2021 13045 +/ 13046 bool isMouseWheel() { 13047 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 13048 } 13049 13050 // private 13051 override void adjustClientCoordinates(int deltaX, int deltaY) { 13052 clientX += deltaX; 13053 clientY += deltaY; 13054 } 13055 13056 override void adjustScrolling() { 13057 version(custom_widgets) { // TEMP 13058 viewportX = clientX; 13059 viewportY = clientY; 13060 if(auto se = cast(ScrollableWidget) srcElement) { 13061 clientX += se.scrollOrigin.x; 13062 clientY += se.scrollOrigin.y; 13063 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 13064 //clientX += se.scrollX_; 13065 //clientY += se.scrollY_; 13066 } 13067 } 13068 } 13069 13070 mixin Register; 13071 } 13072 13073 /++ 13074 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 13075 13076 13077 $(WARNING 13078 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 13079 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 13080 behavior. 13081 ) 13082 13083 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 13084 13085 [MouseUpEvent] is sent when the user releases a mouse button. 13086 13087 [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.) 13088 13089 [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. 13090 13091 [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. 13092 13093 [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. 13094 13095 [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. 13096 13097 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 13098 13099 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 13100 13101 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 13102 13103 Rationale: 13104 13105 If you only want to do drag, mousedown/up works just fine being consistently sent. 13106 13107 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). 13108 13109 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. 13110 13111 History: 13112 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. 13113 +/ 13114 class MouseUpEvent : MouseEventBase { 13115 enum EventString = "mouseup"; /// 13116 this(Widget target) { super(EventString, target); } 13117 } 13118 /// ditto 13119 class MouseDownEvent : MouseEventBase { 13120 enum EventString = "mousedown"; /// 13121 this(Widget target) { super(EventString, target); } 13122 } 13123 /// ditto 13124 class MouseMoveEvent : MouseEventBase { 13125 enum EventString = "mousemove"; /// 13126 this(Widget target) { super(EventString, target); } 13127 } 13128 /// ditto 13129 class ClickEvent : MouseEventBase { 13130 enum EventString = "click"; /// 13131 this(Widget target) { super(EventString, target); } 13132 } 13133 /// ditto 13134 class DoubleClickEvent : MouseEventBase { 13135 enum EventString = "dblclick"; /// 13136 this(Widget target) { super(EventString, target); } 13137 } 13138 /// ditto 13139 class MouseOverEvent : Event { 13140 enum EventString = "mouseover"; /// 13141 this(Widget target) { super(EventString, target); } 13142 } 13143 /// ditto 13144 class MouseOutEvent : Event { 13145 enum EventString = "mouseout"; /// 13146 this(Widget target) { super(EventString, target); } 13147 } 13148 /// ditto 13149 class MouseEnterEvent : Event { 13150 enum EventString = "mouseenter"; /// 13151 this(Widget target) { super(EventString, target); } 13152 13153 override bool propagates() const { return false; } 13154 } 13155 /// ditto 13156 class MouseLeaveEvent : Event { 13157 enum EventString = "mouseleave"; /// 13158 this(Widget target) { super(EventString, target); } 13159 13160 override bool propagates() const { return false; } 13161 } 13162 13163 private bool isAParentOf(Widget a, Widget b) { 13164 if(a is null || b is null) 13165 return false; 13166 13167 while(b !is null) { 13168 if(a is b) 13169 return true; 13170 b = b.parent; 13171 } 13172 13173 return false; 13174 } 13175 13176 private struct WidgetAtPointResponse { 13177 Widget widget; 13178 13179 // x, y relative to the widget in the response. 13180 int x; 13181 int y; 13182 } 13183 13184 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 13185 assert(starting !is null); 13186 13187 starting.addScrollPosition(x, y); 13188 13189 auto child = starting.getChildAtPosition(x, y); 13190 while(child) { 13191 if(child.hidden) 13192 continue; 13193 starting = child; 13194 x -= child.x; 13195 y -= child.y; 13196 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 13197 child = r.widget; 13198 if(child is starting) 13199 break; 13200 } 13201 return WidgetAtPointResponse(starting, x, y); 13202 } 13203 13204 version(win32_widgets) { 13205 private: 13206 import core.sys.windows.commctrl; 13207 13208 pragma(lib, "comctl32"); 13209 shared static this() { 13210 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 13211 INITCOMMONCONTROLSEX ic; 13212 ic.dwSize = cast(DWORD) ic.sizeof; 13213 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 13214 if(!InitCommonControlsEx(&ic)) { 13215 //import std.stdio; writeln("ICC failed"); 13216 } 13217 } 13218 13219 13220 // everything from here is just win32 headers copy pasta 13221 private: 13222 extern(Windows): 13223 13224 alias HANDLE HMENU; 13225 HMENU CreateMenu(); 13226 bool SetMenu(HWND, HMENU); 13227 HMENU CreatePopupMenu(); 13228 enum MF_POPUP = 0x10; 13229 enum MF_STRING = 0; 13230 13231 13232 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 13233 struct INITCOMMONCONTROLSEX { 13234 DWORD dwSize; 13235 DWORD dwICC; 13236 } 13237 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 13238 enum { 13239 IDB_STD_SMALL_COLOR, 13240 IDB_STD_LARGE_COLOR, 13241 IDB_VIEW_SMALL_COLOR = 4, 13242 IDB_VIEW_LARGE_COLOR = 5 13243 } 13244 enum { 13245 STD_CUT, 13246 STD_COPY, 13247 STD_PASTE, 13248 STD_UNDO, 13249 STD_REDOW, 13250 STD_DELETE, 13251 STD_FILENEW, 13252 STD_FILEOPEN, 13253 STD_FILESAVE, 13254 STD_PRINTPRE, 13255 STD_PROPERTIES, 13256 STD_HELP, 13257 STD_FIND, 13258 STD_REPLACE, 13259 STD_PRINT // = 14 13260 } 13261 13262 alias HANDLE HIMAGELIST; 13263 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 13264 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 13265 BOOL ImageList_Destroy(HIMAGELIST); 13266 13267 uint MAKELONG(ushort a, ushort b) { 13268 return cast(uint) ((b << 16) | a); 13269 } 13270 13271 13272 struct TBBUTTON { 13273 int iBitmap; 13274 int idCommand; 13275 BYTE fsState; 13276 BYTE fsStyle; 13277 version(Win64) 13278 BYTE[6] bReserved; 13279 else 13280 BYTE[2] bReserved; 13281 DWORD dwData; 13282 INT_PTR iString; 13283 } 13284 13285 enum { 13286 TB_ADDBUTTONSA = WM_USER + 20, 13287 TB_INSERTBUTTONA = WM_USER + 21, 13288 TB_GETIDEALSIZE = WM_USER + 99, 13289 } 13290 13291 struct SIZE { 13292 LONG cx; 13293 LONG cy; 13294 } 13295 13296 13297 enum { 13298 TBSTATE_CHECKED = 1, 13299 TBSTATE_PRESSED = 2, 13300 TBSTATE_ENABLED = 4, 13301 TBSTATE_HIDDEN = 8, 13302 TBSTATE_INDETERMINATE = 16, 13303 TBSTATE_WRAP = 32 13304 } 13305 13306 13307 13308 enum { 13309 ILC_COLOR = 0, 13310 ILC_COLOR4 = 4, 13311 ILC_COLOR8 = 8, 13312 ILC_COLOR16 = 16, 13313 ILC_COLOR24 = 24, 13314 ILC_COLOR32 = 32, 13315 ILC_COLORDDB = 254, 13316 ILC_MASK = 1, 13317 ILC_PALETTE = 2048 13318 } 13319 13320 13321 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 13322 13323 13324 enum { 13325 TB_ENABLEBUTTON = WM_USER + 1, 13326 TB_CHECKBUTTON, 13327 TB_PRESSBUTTON, 13328 TB_HIDEBUTTON, 13329 TB_INDETERMINATE, // = WM_USER + 5, 13330 TB_ISBUTTONENABLED = WM_USER + 9, 13331 TB_ISBUTTONCHECKED, 13332 TB_ISBUTTONPRESSED, 13333 TB_ISBUTTONHIDDEN, 13334 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 13335 TB_SETSTATE = WM_USER + 17, 13336 TB_GETSTATE = WM_USER + 18, 13337 TB_ADDBITMAP = WM_USER + 19, 13338 TB_DELETEBUTTON = WM_USER + 22, 13339 TB_GETBUTTON, 13340 TB_BUTTONCOUNT, 13341 TB_COMMANDTOINDEX, 13342 TB_SAVERESTOREA, 13343 TB_CUSTOMIZE, 13344 TB_ADDSTRINGA, 13345 TB_GETITEMRECT, 13346 TB_BUTTONSTRUCTSIZE, 13347 TB_SETBUTTONSIZE, 13348 TB_SETBITMAPSIZE, 13349 TB_AUTOSIZE, // = WM_USER + 33, 13350 TB_GETTOOLTIPS = WM_USER + 35, 13351 TB_SETTOOLTIPS = WM_USER + 36, 13352 TB_SETPARENT = WM_USER + 37, 13353 TB_SETROWS = WM_USER + 39, 13354 TB_GETROWS, 13355 TB_GETBITMAPFLAGS, 13356 TB_SETCMDID, 13357 TB_CHANGEBITMAP, 13358 TB_GETBITMAP, 13359 TB_GETBUTTONTEXTA, 13360 TB_REPLACEBITMAP, // = WM_USER + 46, 13361 TB_GETBUTTONSIZE = WM_USER + 58, 13362 TB_SETBUTTONWIDTH = WM_USER + 59, 13363 TB_GETBUTTONTEXTW = WM_USER + 75, 13364 TB_SAVERESTOREW = WM_USER + 76, 13365 TB_ADDSTRINGW = WM_USER + 77, 13366 } 13367 13368 extern(Windows) 13369 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 13370 13371 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 13372 13373 13374 enum { 13375 TB_SETINDENT = WM_USER + 47, 13376 TB_SETIMAGELIST, 13377 TB_GETIMAGELIST, 13378 TB_LOADIMAGES, 13379 TB_GETRECT, 13380 TB_SETHOTIMAGELIST, 13381 TB_GETHOTIMAGELIST, 13382 TB_SETDISABLEDIMAGELIST, 13383 TB_GETDISABLEDIMAGELIST, 13384 TB_SETSTYLE, 13385 TB_GETSTYLE, 13386 //TB_GETBUTTONSIZE, 13387 //TB_SETBUTTONWIDTH, 13388 TB_SETMAXTEXTROWS, 13389 TB_GETTEXTROWS // = WM_USER + 61 13390 } 13391 13392 enum { 13393 CCM_FIRST = 0x2000, 13394 CCM_LAST = CCM_FIRST + 0x200, 13395 CCM_SETBKCOLOR = 8193, 13396 CCM_SETCOLORSCHEME = 8194, 13397 CCM_GETCOLORSCHEME = 8195, 13398 CCM_GETDROPTARGET = 8196, 13399 CCM_SETUNICODEFORMAT = 8197, 13400 CCM_GETUNICODEFORMAT = 8198, 13401 CCM_SETVERSION = 0x2007, 13402 CCM_GETVERSION = 0x2008, 13403 CCM_SETNOTIFYWINDOW = 0x2009 13404 } 13405 13406 13407 enum { 13408 PBM_SETRANGE = WM_USER + 1, 13409 PBM_SETPOS, 13410 PBM_DELTAPOS, 13411 PBM_SETSTEP, 13412 PBM_STEPIT, // = WM_USER + 5 13413 PBM_SETRANGE32 = 1030, 13414 PBM_GETRANGE, 13415 PBM_GETPOS, 13416 PBM_SETBARCOLOR, // = 1033 13417 PBM_SETBKCOLOR = CCM_SETBKCOLOR 13418 } 13419 13420 enum { 13421 PBS_SMOOTH = 1, 13422 PBS_VERTICAL = 4 13423 } 13424 13425 enum { 13426 ICC_LISTVIEW_CLASSES = 1, 13427 ICC_TREEVIEW_CLASSES = 2, 13428 ICC_BAR_CLASSES = 4, 13429 ICC_TAB_CLASSES = 8, 13430 ICC_UPDOWN_CLASS = 16, 13431 ICC_PROGRESS_CLASS = 32, 13432 ICC_HOTKEY_CLASS = 64, 13433 ICC_ANIMATE_CLASS = 128, 13434 ICC_WIN95_CLASSES = 255, 13435 ICC_DATE_CLASSES = 256, 13436 ICC_USEREX_CLASSES = 512, 13437 ICC_COOL_CLASSES = 1024, 13438 ICC_STANDARD_CLASSES = 0x00004000, 13439 } 13440 13441 enum WM_USER = 1024; 13442 } 13443 13444 version(win32_widgets) 13445 pragma(lib, "comdlg32"); 13446 13447 13448 /// 13449 enum GenericIcons : ushort { 13450 None, /// 13451 // these happen to match the win32 std icons numerically if you just subtract one from the value 13452 Cut, /// 13453 Copy, /// 13454 Paste, /// 13455 Undo, /// 13456 Redo, /// 13457 Delete, /// 13458 New, /// 13459 Open, /// 13460 Save, /// 13461 PrintPreview, /// 13462 Properties, /// 13463 Help, /// 13464 Find, /// 13465 Replace, /// 13466 Print, /// 13467 } 13468 13469 enum FileDialogType { 13470 Automatic, 13471 Open, 13472 Save 13473 } 13474 string previousFileReferenced; 13475 13476 /++ 13477 Used in automatic menu functions to indicate that the user should be able to browse for a file. 13478 13479 Params: 13480 storage = an alias to a `static string` variable that stores the last file referenced. It will 13481 use this to pre-fill the dialog with a suggestion. 13482 13483 Please note that it MUST be `static` or you will get compile errors. 13484 13485 filters = the filters param to [getFileName] 13486 13487 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 13488 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 13489 a save dialog box. Otherwise, it will show an open dialog box. 13490 +/ 13491 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 13492 string name; 13493 alias name this; 13494 } 13495 13496 /++ 13497 History: 13498 The dialog itself on Linux was modified on December 2, 2021 to include 13499 a directory picker in addition to the command line completion view. 13500 Future_directions: 13501 I want to add some kind of custom preview and maybe thumbnail thing in the future, 13502 at least on Linux, maybe on Windows too. 13503 +/ 13504 void getOpenFileName( 13505 void delegate(string) onOK, 13506 string prefilledName = null, 13507 string[] filters = null, 13508 void delegate() onCancel = null, 13509 ) 13510 { 13511 return getFileName(true, onOK, prefilledName, filters, onCancel); 13512 } 13513 13514 /++ 13515 History: 13516 onCancel was added November 6, 2021. 13517 +/ 13518 void getSaveFileName( 13519 void delegate(string) onOK, 13520 string prefilledName = null, 13521 string[] filters = null, 13522 void delegate() onCancel = null, 13523 ) 13524 { 13525 return getFileName(false, onOK, prefilledName, filters, onCancel); 13526 } 13527 13528 void getFileName( 13529 bool openOrSave, 13530 void delegate(string) onOK, 13531 string prefilledName = null, 13532 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 13533 void delegate() onCancel = null, 13534 ) 13535 { 13536 13537 version(win32_widgets) { 13538 import core.sys.windows.commdlg; 13539 /* 13540 Ofn.lStructSize = sizeof(OPENFILENAME); 13541 Ofn.hwndOwner = hWnd; 13542 Ofn.lpstrFilter = szFilter; 13543 Ofn.lpstrFile= szFile; 13544 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 13545 Ofn.lpstrFileTitle = szFileTitle; 13546 Ofn.nMaxFileTitle = sizeof(szFileTitle); 13547 Ofn.lpstrInitialDir = (LPSTR)NULL; 13548 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 13549 Ofn.lpstrTitle = szTitle; 13550 */ 13551 13552 13553 wchar[1024] file = 0; 13554 wchar[1024] filterBuffer = 0; 13555 makeWindowsString(prefilledName, file[]); 13556 OPENFILENAME ofn; 13557 ofn.lStructSize = ofn.sizeof; 13558 if(filters.length) { 13559 string filter; 13560 foreach(i, f; filters) { 13561 filter ~= f; 13562 filter ~= "\0"; 13563 } 13564 filter ~= "\0"; 13565 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 13566 } 13567 ofn.lpstrFile = file.ptr; 13568 ofn.nMaxFile = file.length; 13569 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 13570 { 13571 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 13572 if(okString.length && okString[$-1] == '\0') 13573 okString = okString[0..$-1]; 13574 onOK(okString); 13575 } else { 13576 if(onCancel) 13577 onCancel(); 13578 } 13579 } else version(custom_widgets) { 13580 if(filters.length == 0) 13581 filters = ["All Files\0*.*"]; 13582 auto picker = new FilePicker(prefilledName, filters); 13583 picker.onOK = onOK; 13584 picker.onCancel = onCancel; 13585 picker.show(); 13586 } 13587 } 13588 13589 version(custom_widgets) 13590 private 13591 class FilePicker : Dialog { 13592 void delegate(string) onOK; 13593 void delegate() onCancel; 13594 LineEdit lineEdit; 13595 13596 enum GetFilesResult { 13597 success, 13598 fileNotFound 13599 } 13600 static GetFilesResult getFiles(string cwd, scope void delegate(string name, bool isDirectory) dg) { 13601 version(Windows) { 13602 WIN32_FIND_DATA data; 13603 WCharzBuffer search = WCharzBuffer(cwd ~ "/*"); 13604 auto handle = FindFirstFileW(search.ptr, &data); 13605 scope(exit) if(handle !is INVALID_HANDLE_VALUE) FindClose(handle); 13606 if(handle is INVALID_HANDLE_VALUE) { 13607 if(GetLastError() == ERROR_FILE_NOT_FOUND) 13608 return GetFilesResult.fileNotFound; 13609 throw new WindowsApiException("FindFirstFileW"); 13610 } 13611 13612 try_more: 13613 13614 string name = makeUtf8StringFromWindowsString(data.cFileName[0 .. findIndexOfZero(data.cFileName[])]); 13615 13616 dg(name, (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? true : false); 13617 13618 auto ret = FindNextFileW(handle, &data); 13619 if(ret == 0) { 13620 if(GetLastError() == ERROR_NO_MORE_FILES) 13621 return GetFilesResult.success; 13622 throw new WindowsApiException("FindNextFileW"); 13623 } 13624 13625 goto try_more; 13626 13627 } else version(Posix) { 13628 import core.sys.posix.dirent; 13629 auto dir = opendir((cwd ~ "\0").ptr); 13630 scope(exit) 13631 if(dir) closedir(dir); 13632 if(dir is null) 13633 throw new ErrnoApiException("opendir [" ~ cwd ~ "]"); 13634 13635 auto dirent = readdir(dir); 13636 if(dirent is null) 13637 return GetFilesResult.fileNotFound; 13638 13639 try_more: 13640 13641 string name = dirent.d_name[0 .. findIndexOfZero(dirent.d_name[])].idup; 13642 13643 dg(name, dirent.d_type == DT_DIR); 13644 13645 dirent = readdir(dir); 13646 if(dirent is null) 13647 return GetFilesResult.success; 13648 13649 goto try_more; 13650 } else static assert(0); 13651 } 13652 13653 // returns common prefix 13654 string loadFiles(string cwd, string[] filters...) { 13655 string[] files; 13656 string[] dirs; 13657 13658 string commonPrefix; 13659 13660 getFiles(cwd, (string name, bool isDirectory) { 13661 if(name == ".") 13662 return; // skip this as unnecessary 13663 if(isDirectory) 13664 dirs ~= name; 13665 else { 13666 foreach(filter; filters) 13667 if( 13668 filter.length <= 1 || 13669 filter == "*.*" || 13670 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 13671 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 13672 ) 13673 { 13674 files ~= name; 13675 13676 if(filter.length > 0 && filter[$-1] == '*') { 13677 if(commonPrefix is null) { 13678 commonPrefix = name; 13679 } else { 13680 foreach(idx, char i; name) { 13681 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 13682 commonPrefix = commonPrefix[0 .. idx]; 13683 break; 13684 } 13685 } 13686 } 13687 } 13688 13689 break; 13690 } 13691 } 13692 }); 13693 13694 extern(C) static int comparator(scope const void* a, scope const void* b) { 13695 auto sa = *cast(string*) a; 13696 auto sb = *cast(string*) b; 13697 13698 for(int i = 0; i < sa.length; i++) { 13699 if(i == sb.length) 13700 return 1; 13701 return sa[i] - sb[i]; 13702 } 13703 13704 return 0; 13705 } 13706 13707 nonPhobosSort(files, &comparator); 13708 nonPhobosSort(dirs, &comparator); 13709 13710 listWidget.clear(); 13711 dirWidget.clear(); 13712 foreach(name; dirs) 13713 dirWidget.addOption(name); 13714 foreach(name; files) 13715 listWidget.addOption(name); 13716 13717 return commonPrefix; 13718 } 13719 13720 ListWidget listWidget; 13721 ListWidget dirWidget; 13722 13723 string currentDirectory; 13724 string[] processedFilters; 13725 13726 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"] 13727 this(string prefilledName, string[] filters, Window owner = null) { 13728 super(300, 200, "Choose File..."); // owner); 13729 13730 foreach(filter; filters) { 13731 while(filter.length && filter[0] != 0) { 13732 filter = filter[1 .. $]; 13733 } 13734 if(filter.length) 13735 filter = filter[1 .. $]; // trim off the 0 13736 13737 while(filter.length) { 13738 int idx = 0; 13739 while(idx < filter.length && filter[idx] != ';') { 13740 idx++; 13741 } 13742 13743 processedFilters ~= filter[0 .. idx]; 13744 if(idx < filter.length) 13745 idx++; // skip the ; 13746 filter = filter[idx .. $]; 13747 } 13748 } 13749 13750 currentDirectory = "."; 13751 13752 { 13753 auto hl = new HorizontalLayout(this); 13754 dirWidget = new ListWidget(hl); 13755 listWidget = new ListWidget(hl); 13756 13757 // double click events normally trigger something else but 13758 // here user might be clicking kinda fast and we'd rather just 13759 // keep it 13760 dirWidget.addEventListener((scope DoubleClickEvent dev) { 13761 auto ce = new ChangeEvent!void(dirWidget, () {}); 13762 ce.dispatch(); 13763 }); 13764 13765 dirWidget.addEventListener((scope ChangeEvent!void sce) { 13766 string v; 13767 foreach(o; dirWidget.options) 13768 if(o.selected) { 13769 v = o.label; 13770 break; 13771 } 13772 if(v.length) { 13773 currentDirectory ~= "/" ~ v; 13774 loadFiles(currentDirectory, processedFilters); 13775 } 13776 }); 13777 13778 // double click here, on the other hand, selects the file 13779 // and moves on 13780 listWidget.addEventListener((scope DoubleClickEvent dev) { 13781 OK(); 13782 }); 13783 } 13784 13785 lineEdit = new LineEdit(this); 13786 lineEdit.focus(); 13787 lineEdit.addEventListener(delegate(CharEvent event) { 13788 if(event.character == '\t' || event.character == '\n') 13789 event.preventDefault(); 13790 }); 13791 13792 listWidget.addEventListener(EventType.change, () { 13793 foreach(o; listWidget.options) 13794 if(o.selected) 13795 lineEdit.content = o.label; 13796 }); 13797 13798 loadFiles(currentDirectory, processedFilters); 13799 13800 lineEdit.addEventListener((KeyDownEvent event) { 13801 if(event.key == Key.Tab) { 13802 13803 auto current = lineEdit.content; 13804 if(current.length >= 2 && current[0 ..2] == "./") 13805 current = current[2 .. $]; 13806 13807 auto commonPrefix = loadFiles(".", current ~ "*"); 13808 13809 if(commonPrefix.length) 13810 lineEdit.content = commonPrefix; 13811 13812 // FIXME: if that is a directory, add the slash? or even go inside? 13813 13814 event.preventDefault(); 13815 } 13816 }); 13817 13818 lineEdit.content = prefilledName; 13819 13820 auto hl = new HorizontalLayout(60, this); 13821 auto cancelButton = new Button("Cancel", hl); 13822 auto okButton = new Button("OK", hl); 13823 13824 cancelButton.addEventListener(EventType.triggered, &Cancel); 13825 okButton.addEventListener(EventType.triggered, &OK); 13826 13827 this.addEventListener((KeyDownEvent event) { 13828 if(event.key == Key.Enter || event.key == Key.PadEnter) { 13829 event.preventDefault(); 13830 OK(); 13831 } 13832 if(event.key == Key.Escape) 13833 Cancel(); 13834 }); 13835 13836 } 13837 13838 override void OK() { 13839 if(lineEdit.content.length) { 13840 string accepted; 13841 auto c = lineEdit.content; 13842 if(c.length && c[0] == '/') 13843 accepted = c; 13844 else 13845 accepted = currentDirectory ~ "/" ~ lineEdit.content; 13846 13847 if(isDir(accepted)) { 13848 // FIXME: would be kinda nice to support ~ and collapse these paths too 13849 // FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later. 13850 currentDirectory = accepted; 13851 loadFiles(currentDirectory, processedFilters); 13852 lineEdit.content = ""; 13853 return; 13854 } 13855 13856 if(onOK) 13857 onOK(accepted); 13858 } 13859 close(); 13860 } 13861 13862 override void Cancel() { 13863 if(onCancel) 13864 onCancel(); 13865 close(); 13866 } 13867 } 13868 13869 private bool isDir(string name) { 13870 version(Windows) { 13871 auto ws = WCharzBuffer(name); 13872 auto ret = GetFileAttributesW(ws.ptr); 13873 if(ret == INVALID_FILE_ATTRIBUTES) 13874 return false; 13875 return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0; 13876 } else version(Posix) { 13877 import core.sys.posix.sys.stat; 13878 stat_t buf; 13879 auto ret = stat((name ~ '\0').ptr, &buf); 13880 if(ret == -1) 13881 return false; // I could probably check more specific errors tbh 13882 return (buf.st_mode & S_IFMT) == S_IFDIR; 13883 } else return false; 13884 } 13885 13886 /* 13887 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 13888 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 13889 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 13890 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 13891 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 13892 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 13893 http://www.sbin.org/doc/Xlib/chapt_03.html 13894 13895 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 13896 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 13897 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 13898 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 13899 */ 13900 13901 13902 // These are all for setMenuAndToolbarFromAnnotatedCode 13903 /// This item in the menu will be preceded by a separator line 13904 /// Group: generating_from_code 13905 struct separator {} 13906 deprecated("It was misspelled, use separator instead") alias seperator = separator; 13907 /// Program-wide keyboard shortcut to trigger the action 13908 /// Group: generating_from_code 13909 struct accelerator { string keyString; } 13910 /// tells which menu the action will be on 13911 /// Group: generating_from_code 13912 struct menu { string name; } 13913 /// Describes which toolbar section the action appears on 13914 /// Group: generating_from_code 13915 struct toolbar { string groupName; } 13916 /// 13917 /// Group: generating_from_code 13918 struct icon { ushort id; } 13919 /// 13920 /// Group: generating_from_code 13921 struct label { string label; } 13922 /// 13923 /// Group: generating_from_code 13924 struct hotkey { dchar ch; } 13925 /// 13926 /// Group: generating_from_code 13927 struct tip { string tip; } 13928 13929 13930 /++ 13931 Observes and allows inspection of an object via automatic gui 13932 +/ 13933 /// Group: generating_from_code 13934 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 13935 return new ObjectInspectionWindowImpl!(T)(t); 13936 } 13937 13938 class ObjectInspectionWindow : Window { 13939 this(int a, int b, string c) { 13940 super(a, b, c); 13941 } 13942 13943 abstract void readUpdatesFromObject(); 13944 } 13945 13946 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 13947 T t; 13948 this(T t) { 13949 this.t = t; 13950 13951 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 13952 13953 foreach(memberName; __traits(derivedMembers, T)) {{ 13954 alias member = I!(__traits(getMember, t, memberName))[0]; 13955 alias type = typeof(member); 13956 static if(is(type == int)) { 13957 auto le = new LabeledLineEdit(memberName ~ ": ", this); 13958 //le.addEventListener("char", (Event ev) { 13959 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 13960 //ev.preventDefault(); 13961 //}); 13962 le.addEventListener(EventType.change, (Event ev) { 13963 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 13964 }); 13965 13966 updateMemberDelegates[memberName] = () { 13967 le.content = toInternal!string(__traits(getMember, t, memberName)); 13968 }; 13969 } 13970 }} 13971 } 13972 13973 void delegate()[string] updateMemberDelegates; 13974 13975 override void readUpdatesFromObject() { 13976 foreach(k, v; updateMemberDelegates) 13977 v(); 13978 } 13979 } 13980 13981 /++ 13982 Creates a dialog based on a data structure. 13983 13984 --- 13985 dialog((YourStructure value) { 13986 // the user filled in the struct and clicked OK, 13987 // you can check the members now 13988 }); 13989 --- 13990 13991 Params: 13992 initialData = the initial value to show in the dialog. It will not modify this unless 13993 it is a class then it might, no promises. 13994 13995 History: 13996 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 13997 +/ 13998 /// Group: generating_from_code 13999 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 14000 dialog(T.init, onOK, onCancel, title); 14001 } 14002 /// ditto 14003 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 14004 auto dg = new AutomaticDialog!T(initialData, onOK, onCancel, title); 14005 dg.show(); 14006 } 14007 14008 private static template I(T...) { alias I = T; } 14009 14010 14011 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 14012 if(name == "id") 14013 return allLowerCase ? name : "ID"; 14014 14015 char[160] buffer; 14016 int bufferIndex = 0; 14017 bool shouldCap = true; 14018 bool shouldSpace; 14019 bool lastWasCap; 14020 foreach(idx, char ch; name) { 14021 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 14022 14023 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 14024 if(lastWasCap) { 14025 // two caps in a row, don't change. Prolly acronym. 14026 } else { 14027 if(idx) 14028 shouldSpace = true; // new word, add space 14029 } 14030 14031 lastWasCap = true; 14032 } else { 14033 lastWasCap = false; 14034 } 14035 14036 if(shouldSpace) { 14037 buffer[bufferIndex++] = space; 14038 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 14039 shouldSpace = false; 14040 } 14041 if(shouldCap) { 14042 if(ch >= 'a' && ch <= 'z') 14043 ch -= 32; 14044 shouldCap = false; 14045 } 14046 if(allLowerCase && ch >= 'A' && ch <= 'Z') 14047 ch += 32; 14048 buffer[bufferIndex++] = ch; 14049 } 14050 return buffer[0 .. bufferIndex].idup; 14051 } 14052 14053 /++ 14054 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. 14055 +/ 14056 class AutomaticDialog(T) : Dialog { 14057 T t; 14058 14059 void delegate(T) onOK; 14060 void delegate() onCancel; 14061 14062 override int paddingTop() { return defaultLineHeight; } 14063 override int paddingBottom() { return defaultLineHeight; } 14064 override int paddingRight() { return defaultLineHeight; } 14065 override int paddingLeft() { return defaultLineHeight; } 14066 14067 this(T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 14068 assert(onOK !is null); 14069 14070 t = initialData; 14071 14072 static if(is(T == class)) { 14073 if(t is null) 14074 t = new T(); 14075 } 14076 this.onOK = onOK; 14077 this.onCancel = onCancel; 14078 super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + 4 + 2) + Window.lineHeight + 56, title); 14079 14080 static if(is(T == class)) 14081 this.addDataControllerWidget(t); 14082 else 14083 this.addDataControllerWidget(&t); 14084 14085 auto hl = new HorizontalLayout(this); 14086 auto stretch = new HorizontalSpacer(hl); // to right align 14087 auto ok = new CommandButton("OK", hl); 14088 auto cancel = new CommandButton("Cancel", hl); 14089 ok.addEventListener(EventType.triggered, &OK); 14090 cancel.addEventListener(EventType.triggered, &Cancel); 14091 14092 this.addEventListener((KeyDownEvent ev) { 14093 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 14094 ok.focus(); 14095 OK(); 14096 ev.preventDefault(); 14097 } 14098 if(ev.key == Key.Escape) { 14099 Cancel(); 14100 ev.preventDefault(); 14101 } 14102 }); 14103 14104 //this.children[0].focus(); 14105 } 14106 14107 override void OK() { 14108 onOK(t); 14109 close(); 14110 } 14111 14112 override void Cancel() { 14113 if(onCancel) 14114 onCancel(); 14115 close(); 14116 } 14117 } 14118 14119 private template baseClassCount(Class) { 14120 private int helper() { 14121 int count = 0; 14122 static if(is(Class bases == super)) { 14123 foreach(base; bases) 14124 static if(is(base == class)) 14125 count += 1 + baseClassCount!base; 14126 } 14127 return count; 14128 } 14129 14130 enum int baseClassCount = helper(); 14131 } 14132 14133 private long stringToLong(string s) { 14134 long ret; 14135 if(s.length == 0) 14136 return ret; 14137 bool negative = s[0] == '-'; 14138 if(negative) 14139 s = s[1 .. $]; 14140 foreach(ch; s) { 14141 if(ch >= '0' && ch <= '9') { 14142 ret *= 10; 14143 ret += ch - '0'; 14144 } 14145 } 14146 if(negative) 14147 ret = -ret; 14148 return ret; 14149 } 14150 14151 14152 interface ReflectableProperties { 14153 /++ 14154 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 14155 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 14156 json in the current implementation. 14157 14158 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 14159 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 14160 as of the June 2, 2021 release. 14161 14162 History: 14163 Added June 2, 2021. 14164 14165 See_Also: [getPropertyAsString], [setPropertyFromString] 14166 +/ 14167 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 14168 /++ 14169 Requests a property to be delivered to you as a string, through your `sink` delegate. 14170 14171 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 14172 be interpreted as json, otherwise, it is just a plain string. 14173 14174 The sink should always be called exactly once for each call (it is basically a return value, but it might 14175 use a local buffer it maintains instead of allocating a return value). 14176 14177 History: 14178 Added June 2, 2021. 14179 14180 See_Also: [getPropertiesList], [setPropertyFromString] 14181 +/ 14182 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 14183 /++ 14184 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. 14185 14186 History: 14187 Added June 2, 2021. 14188 14189 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 14190 +/ 14191 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 14192 14193 /// [setPropertyFromString] possible return values 14194 enum SetPropertyResult { 14195 success = 0, /// the property has been successfully set to the request value 14196 notPermitted = -1, /// the property exists but it cannot be changed at this time 14197 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 14198 noSuchProperty = -3, /// there is no property by that name 14199 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 14200 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) 14201 } 14202 14203 /++ 14204 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 14205 14206 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 14207 14208 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 14209 rarely need to use these building blocks directly. 14210 +/ 14211 mixin template RegisterSetters() { 14212 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 14213 switch(name) { 14214 foreach(memberName; __traits(derivedMembers, typeof(this))) { 14215 case memberName: 14216 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 14217 if(value != "true" && value != "false") 14218 return SetPropertyResult.wrongFormat; 14219 __traits(getMember, this, memberName) = value == "true" ? true : false; 14220 return SetPropertyResult.success; 14221 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 14222 import core.stdc.stdlib; 14223 char[128] zero = 0; 14224 if(buffer.length + 1 >= zero.length) 14225 return SetPropertyResult.wrongFormat; 14226 zero[0 .. buffer.length] = buffer[]; 14227 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 14228 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 14229 import core.stdc.stdlib; 14230 char[128] zero = 0; 14231 if(buffer.length + 1 >= zero.length) 14232 return SetPropertyResult.wrongFormat; 14233 zero[0 .. buffer.length] = buffer[]; 14234 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 14235 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 14236 __traits(getMember, this, memberName) = value.idup; 14237 } else { 14238 return SetPropertyResult.notImplemented; 14239 } 14240 14241 } 14242 default: 14243 return super.setPropertyFromString(name, value, valueIsJson); 14244 } 14245 } 14246 } 14247 14248 /++ 14249 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 14250 14251 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 14252 14253 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 14254 rarely need to use these building blocks directly. 14255 +/ 14256 mixin template RegisterGetters() { 14257 override void getPropertiesList(scope void delegate(string name) sink) const { 14258 super.getPropertiesList(sink); 14259 14260 foreach(memberName; __traits(derivedMembers, typeof(this))) { 14261 sink(memberName); 14262 } 14263 } 14264 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 14265 switch(name) { 14266 foreach(memberName; __traits(derivedMembers, typeof(this))) { 14267 case memberName: 14268 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 14269 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 14270 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 14271 import core.stdc.stdio; 14272 char[32] buffer; 14273 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 14274 sink(name, buffer[0 .. len], true); 14275 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 14276 import core.stdc.stdio; 14277 char[32] buffer; 14278 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 14279 sink(name, buffer[0 .. len], true); 14280 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 14281 sink(name, __traits(getMember, this, memberName), false); 14282 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 14283 } else { 14284 sink(name, null, true); 14285 } 14286 14287 return; 14288 } 14289 default: 14290 return super.getPropertyAsString(name, sink); 14291 } 14292 } 14293 } 14294 } 14295 14296 private struct Stack(T) { 14297 this(int maxSize) { 14298 internalLength = 0; 14299 arr = initialBuffer[]; 14300 } 14301 14302 ///. 14303 void push(T t) { 14304 if(internalLength >= arr.length) { 14305 auto oldarr = arr; 14306 if(arr.length < 4096) 14307 arr = new T[arr.length * 2]; 14308 else 14309 arr = new T[arr.length + 4096]; 14310 arr[0 .. oldarr.length] = oldarr[]; 14311 } 14312 14313 arr[internalLength] = t; 14314 internalLength++; 14315 } 14316 14317 ///. 14318 T pop() { 14319 assert(internalLength); 14320 internalLength--; 14321 return arr[internalLength]; 14322 } 14323 14324 ///. 14325 T peek() { 14326 assert(internalLength); 14327 return arr[internalLength - 1]; 14328 } 14329 14330 ///. 14331 @property bool empty() { 14332 return internalLength ? false : true; 14333 } 14334 14335 ///. 14336 private T[] arr; 14337 private size_t internalLength; 14338 private T[64] initialBuffer; 14339 // 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), 14340 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 14341 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 14342 } 14343 14344 /// 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. 14345 private struct WidgetStream { 14346 14347 ///. 14348 @property Widget front() { 14349 return current.widget; 14350 } 14351 14352 /// Use Widget.tree instead. 14353 this(Widget start) { 14354 current.widget = start; 14355 current.childPosition = -1; 14356 isEmpty = false; 14357 stack = typeof(stack)(0); 14358 } 14359 14360 /* 14361 Handle it 14362 handle its children 14363 14364 */ 14365 14366 ///. 14367 void popFront() { 14368 more: 14369 if(isEmpty) return; 14370 14371 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 14372 14373 current.childPosition++; 14374 if(current.childPosition >= current.widget.children.length) { 14375 if(stack.empty()) 14376 isEmpty = true; 14377 else { 14378 current = stack.pop(); 14379 goto more; 14380 } 14381 } else { 14382 stack.push(current); 14383 current.widget = current.widget.children[current.childPosition]; 14384 current.childPosition = -1; 14385 } 14386 } 14387 14388 ///. 14389 @property bool empty() { 14390 return isEmpty; 14391 } 14392 14393 private: 14394 14395 struct Current { 14396 Widget widget; 14397 int childPosition; 14398 } 14399 14400 Current current; 14401 14402 Stack!(Current) stack; 14403 14404 bool isEmpty; 14405 } 14406 14407 14408 /+ 14409 14410 I could fix up the hierarchy kinda like this 14411 14412 class Widget { 14413 Widget[] children() { return null; } 14414 } 14415 interface WidgetContainer { 14416 Widget asWidget(); 14417 void addChild(Widget w); 14418 14419 // alias asWidget this; // but meh 14420 } 14421 14422 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 14423 14424 class Layout : Widget, WidgetContainer {} 14425 14426 class Window : WidgetContainer {} 14427 14428 14429 All constructors that previously took Widgets should now take WidgetContainers instead 14430 14431 14432 14433 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". 14434 +/ 14435 14436 /+ 14437 LAYOUTS 2.0 14438 14439 can just be assigned as a function. assigning a new one will cause it to be immediately called. 14440 14441 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 14442 14443 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 14444 14445 and even Paint can just use computedStyle... 14446 14447 background color 14448 font 14449 border color and style 14450 14451 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 14452 please note that many widgets and in some modes will completely ignore properties as they will. 14453 they are just hints you set, not promises. 14454 14455 14456 14457 14458 14459 So generally the existing virtual functions are just the default for the class. But individual objects 14460 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 14461 +/ 14462 14463 /++ 14464 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. 14465 14466 History: 14467 Added May 24, 2021. 14468 +/ 14469 struct WidgetBackground { 14470 /++ 14471 A background with the given solid color. 14472 +/ 14473 this(Color color) { 14474 this.color = color; 14475 } 14476 14477 this(WidgetBackground bg) { 14478 this = bg; 14479 } 14480 14481 /++ 14482 Creates a widget from the string. 14483 14484 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 14485 +/ 14486 static WidgetBackground fromString(string s) { 14487 return WidgetBackground(Color.fromString(s)); 14488 } 14489 14490 private Color color; 14491 } 14492 14493 /++ 14494 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!) 14495 14496 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. 14497 14498 You should not inherit from this directly, but instead use [VisualTheme]. 14499 14500 History: 14501 Added May 8, 2021 14502 +/ 14503 abstract class BaseVisualTheme { 14504 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 14505 abstract void doPaint(Widget widget, WidgetPainter painter); 14506 14507 /+ 14508 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 14509 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 14510 +/ 14511 14512 /++ 14513 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 14514 where the interpretation of the string varies for each property and may include things like measurement units. 14515 +/ 14516 abstract string getPropertyString(Widget widget, string propertyName); 14517 14518 /++ 14519 Default background color of the window. Widgets also use this to simulate transparency. 14520 14521 Probably some shade of grey. 14522 +/ 14523 abstract Color windowBackgroundColor(); 14524 abstract Color widgetBackgroundColor(); 14525 abstract Color foregroundColor(); 14526 abstract Color lightAccentColor(); 14527 abstract Color darkAccentColor(); 14528 14529 /++ 14530 Color used to indicate active selections in lists and text boxes, etc. 14531 +/ 14532 abstract Color selectionColor(); 14533 14534 abstract OperatingSystemFont defaultFont(); 14535 14536 private OperatingSystemFont defaultFontCache_; 14537 private bool defaultFontCachePopulated; 14538 private OperatingSystemFont defaultFontCached() { 14539 if(!defaultFontCachePopulated) { 14540 // FIXME: set this to false if X disconnect or if visual theme changes 14541 defaultFontCache_ = defaultFont(); 14542 defaultFontCachePopulated = true; 14543 } 14544 return defaultFontCache_; 14545 } 14546 } 14547 14548 /+ 14549 A widget should have: 14550 classList 14551 dataset 14552 attributes 14553 computedStyles 14554 state (persistent) 14555 dynamic state (focused, hover, etc) 14556 +/ 14557 14558 // visualTheme.computedStyle(this).paddingLeft 14559 14560 14561 /++ 14562 This is your entry point to create your own visual theme for custom widgets. 14563 +/ 14564 abstract class VisualTheme(CRTP) : BaseVisualTheme { 14565 override string getPropertyString(Widget widget, string propertyName) { 14566 return null; 14567 } 14568 14569 /+ 14570 mixin StyleOverride!Widget 14571 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 14572 w.useStyleProperties(dg); 14573 } 14574 +/ 14575 14576 final override void doPaint(Widget widget, WidgetPainter painter) { 14577 auto derived = cast(CRTP) cast(void*) this; 14578 14579 scope void delegate(Widget, WidgetPainter) bestMatch; 14580 int bestMatchScore; 14581 14582 static if(__traits(hasMember, CRTP, "paint")) 14583 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 14584 static if(is(typeof(overload) Params == __parameters)) { 14585 static assert(Params.length == 2); 14586 static assert(is(Params[0] : Widget)); 14587 static assert(is(Params[1] == WidgetPainter)); 14588 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); 14589 14590 alias type = Params[0]; 14591 if(cast(type) widget) { 14592 auto score = baseClassCount!type; 14593 14594 if(score > bestMatchScore) { 14595 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 14596 bestMatchScore = score; 14597 } 14598 } 14599 } else static assert(0, "paint should be a method."); 14600 } 14601 14602 if(bestMatch) 14603 bestMatch(widget, painter); 14604 else 14605 widget.paint(painter); 14606 } 14607 14608 // 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 14609 override Color windowBackgroundColor() { return Color(212, 212, 212); } 14610 override Color widgetBackgroundColor() { return Color.white; } 14611 override Color foregroundColor() { return Color.black; } 14612 override Color darkAccentColor() { return Color(172, 172, 172); } 14613 override Color lightAccentColor() { return Color(223, 223, 223); } 14614 override Color selectionColor() { return Color(0, 0, 128); } 14615 override OperatingSystemFont defaultFont() { return null; } // will just use the default out of simpledisplay's xfontstr 14616 14617 private static struct Cached { 14618 // i prolly want to do this 14619 } 14620 } 14621 14622 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 14623 /+ 14624 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 14625 Color windowBackgroundColor() { return Color(242, 242, 242); } 14626 Color darkAccentColor() { return windowBackgroundColor; } 14627 Color lightAccentColor() { return windowBackgroundColor; } 14628 +/ 14629 } 14630 14631 /++ 14632 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 14633 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 14634 14635 History: 14636 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 14637 +/ 14638 class StateChanged(alias field) : Event { 14639 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 14640 override bool cancelable() const { return false; } 14641 this(Widget target, typeof(field) newValue) { 14642 this.newValue = newValue; 14643 super(EventString, target); 14644 } 14645 14646 typeof(field) newValue; 14647 } 14648 14649 /++ 14650 Convenience function to add a `triggered` event listener. 14651 14652 Its implementation is simply `w.addEventListener("triggered", dg);` 14653 14654 History: 14655 Added November 27, 2021 (dub v10.4) 14656 +/ 14657 void addWhenTriggered(Widget w, void delegate() dg) { 14658 w.addEventListener("triggered", dg); 14659 } 14660 14661 /++ 14662 Observable varables can be added to widgets and when they are changed, it fires 14663 off a [StateChanged] event so you can react to it. 14664 14665 It is implemented as a getter and setter property, along with another helper you 14666 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 14667 event through the usual means. Just give the name of the variable. See [StateChanged] for an 14668 example. 14669 14670 History: 14671 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 14672 +/ 14673 mixin template Observable(T, string name) { 14674 private T backing; 14675 14676 mixin(q{ 14677 void } ~ name ~ q{_changed (void delegate(T) dg) { 14678 this.addEventListener((StateChanged!this_thing ev) { 14679 dg(ev.newValue); 14680 }); 14681 } 14682 14683 @property T } ~ name ~ q{ () { 14684 return backing; 14685 } 14686 14687 @property void } ~ name ~ q{ (T t) { 14688 backing = t; 14689 auto event = new StateChanged!this_thing(this, t); 14690 event.dispatch(); 14691 } 14692 }); 14693 14694 mixin("private alias this_thing = " ~ name ~ ";"); 14695 } 14696 14697 14698 private bool startsWith(string test, string thing) { 14699 if(test.length < thing.length) 14700 return false; 14701 return test[0 .. thing.length] == thing; 14702 } 14703 14704 private bool endsWith(string test, string thing) { 14705 if(test.length < thing.length) 14706 return false; 14707 return test[$ - thing.length .. $] == thing; 14708 } 14709 14710 // still do layout delegation 14711 // and... split off Window from Widget. 14712 14713 version(minigui_screenshots) 14714 struct Screenshot { 14715 string name; 14716 } 14717 14718 version(minigui_screenshots) 14719 static if(__VERSION__ > 2092) 14720 mixin(q{ 14721 shared static this() { 14722 import core.runtime; 14723 14724 static UnitTestResult screenshotMagic() { 14725 string name; 14726 14727 import arsd.png; 14728 14729 auto results = new Window(); 14730 auto button = new Button("do it", results); 14731 14732 Window.newWindowCreated = delegate(Window w) { 14733 Timer timer; 14734 timer = new Timer(250, { 14735 auto img = w.win.takeScreenshot(); 14736 timer.destroy(); 14737 14738 version(Windows) 14739 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 14740 else 14741 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 14742 14743 w.close(); 14744 }); 14745 }; 14746 14747 button.addWhenTriggered( { 14748 14749 foreach(test; __traits(getUnitTests, mixin(__MODULE__))) { 14750 name = null; 14751 static foreach(attr; __traits(getAttributes, test)) { 14752 static if(is(typeof(attr) == Screenshot)) 14753 name = attr.name; 14754 } 14755 if(name.length) { 14756 test(); 14757 } 14758 } 14759 14760 }); 14761 14762 results.loop(); 14763 14764 return UnitTestResult(0, 0, false, false); 14765 } 14766 14767 14768 Runtime.extendedModuleUnitTester = &screenshotMagic; 14769 } 14770 }); 14771 version(minigui_screenshots) { 14772 version(unittest) 14773 void main() {} 14774 else static assert(0, "dont forget the -unittest flag to dmd"); 14775 } 14776 14777 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 14778 // FIXME: make multiple accelerators disambiguate based ona rgs 14779 // FIXME: MainWindow ctor should have same arg order as Window 14780 // FIXME: mainwindow ctor w/ client area size instead of total size. 14781 // 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. 14782 // FIXME: tri-state checkbox 14783 // FIXME: subordinate controls grouping...