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 /* 8 9 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 10 11 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 12 */ 13 14 // FIXME: text label must be copyable to the clipboard, at least as a full chunk. 15 16 // FIXME: opt-in file picker widget with image support 17 18 // FIXME: number widget 19 20 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm 21 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles 22 23 // osx style menu search. 24 25 // would be cool for a scroll bar to have marking capabilities 26 // kinda like vim's marks just on clicks etc and visual representation 27 // generically. may be cool to add an up arrow to the bottom too 28 // 29 // leave a shadow of where you last were for going back easily 30 31 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 32 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 33 // the window. 34 35 // so what about context menus? 36 37 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 38 39 // FIXME: make the scroll thing go to bottom when the content changes. 40 41 // 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 42 43 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 44 45 46 // FIXME: add a command search thingy built in and implement tip. 47 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 48 49 // On Windows: 50 // FIXME: various labels look broken in high contrast mode 51 // FIXME: changing themes while the program is upen doesn't trigger a redraw 52 53 // add note about manifest to documentation. also icons. 54 55 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 56 // FIXME: clear the corner of scrollbars if they pop up 57 58 // minigui needs to have a stdout redirection for gui mode on windows writeln 59 60 // I kinda wanna do state reacting. sort of. idk tho 61 62 // need a viewer widget that works like a web page - arrows scroll down consistently 63 64 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 65 66 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 67 // and help info about menu items. 68 // and search in menus? 69 70 // FIXME: a scroll area event signaling when a thing comes into view might be good 71 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 72 73 // FIXME: unify Windows style line endings 74 75 /* 76 TODO: 77 78 pie menu 79 80 class Form with submit behavior -- see AutomaticDialog 81 82 disabled widgets and menu items 83 84 event cleanup 85 tooltips. 86 api improvements 87 88 margins are kinda broken, they don't collapse like they should. at least. 89 90 a table form btw would be a horizontal layout of vertical layouts holding each column 91 that would give the same width things 92 */ 93 94 /* 95 96 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 97 */ 98 99 /++ 100 minigui is a smallish GUI widget library, aiming to be on par with at least 101 HTML4 forms and a few other expected gui components. It uses native controls 102 on Windows and does its own thing on Linux (Mac is not currently supported but 103 may be later, and should use native controls) to keep size down. The Linux 104 appearance is similar to Windows 95 and avoids using images to maintain network 105 efficiency on remote X connections, though you can customize that. 106 107 108 minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color], 109 on which it is built. simpledisplay provides the low-level interfaces and minigui 110 builds the concept of widgets inside the windows on top of it. 111 112 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 113 It isn't hugely concerned with appearance - on Windows, it just uses the native 114 controls and native theme, and on Linux, it keeps it simple and I may change that 115 at any time, though after May 2021, you can customize some things with css-inspired 116 [Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`, 117 you can use the custom implementation there too, but... you shouldn't.) 118 119 The event model is similar to what you use in the browser with Javascript and the 120 layout engine tries to automatically fit things in, similar to a css flexbox. 121 122 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 123 `-L/SUBSYSTEM:WINDOWS:5.0`, for example, because otherwise you'll get a 124 console and other visual bugs. 125 126 HTML_To_Classes: 127 $(SMALL_TABLE 128 HTML Code | Minigui Class 129 130 `<input type="text">` | [LineEdit] 131 `<textarea>` | [TextEdit] 132 `<select>` | [DropDownSelection] 133 `<input type="checkbox">` | [Checkbox] 134 `<input type="radio">` | [Radiobox] 135 `<button>` | [Button] 136 ) 137 138 139 Stretchiness: 140 The default is 4. You can use larger numbers for things that should 141 consume a lot of space, and lower numbers for ones that are better at 142 smaller sizes. 143 144 Overlapped_input: 145 COMING EVENTUALLY: 146 minigui will include a little bit of I/O functionality that just works 147 with the event loop. If you want to get fancy, I suggest spinning up 148 another thread and posting events back and forth. 149 150 $(H2 Add ons) 151 See the `minigui_addons` directory in the arsd repo for some add on widgets 152 you can import separately too. 153 154 $(H3 XML definitions) 155 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 156 157 $(H3 Scriptability) 158 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 159 in this documentation, it means you can call it from the script language. 160 161 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 162 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 163 164 --- 165 import arsd.minigui_xml; 166 import arsd.script; 167 168 var globals = var.emptyObject; 169 globals.makeWidgetFromString = &makeWidgetFromString; 170 171 // this now works 172 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 173 --- 174 175 More to come. 176 177 History: 178 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 179 180 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 181 tag this as version 2.0. 182 183 Among the changes: 184 $(LIST 185 * 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. 186 187 See [Event] for details. 188 189 * 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. 190 191 See [DoubleClickEvent] for details. 192 193 * 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. 194 195 See [Widget.Style] for details. 196 197 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 198 199 * 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. 200 201 * 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. 202 203 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 204 205 * 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. 206 207 * Various non-breaking additions. 208 ) 209 +/ 210 module arsd.minigui; 211 212 /++ 213 This hello world sample will have an oversized button, but that's ok, you see your first window! 214 +/ 215 version(Demo) 216 unittest { 217 import arsd.minigui; 218 219 void main() { 220 auto window = new MainWindow(); 221 222 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 223 auto button = new Button("Close", window); 224 button.addEventListener((scope ClickEvent ev) { 225 window.close(); 226 }); 227 228 window.loop(); 229 } 230 231 main(); // exclude from docs 232 } 233 234 public import arsd.simpledisplay; 235 /++ 236 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 237 238 History: 239 Was private until May 15, 2021. 240 +/ 241 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 242 243 version(Windows) { 244 import core.sys.windows.winnls; 245 import core.sys.windows.windef; 246 import core.sys.windows.basetyps; 247 import core.sys.windows.winbase; 248 import core.sys.windows.winuser; 249 import core.sys.windows.wingdi; 250 static import gdi = core.sys.windows.wingdi; 251 } 252 253 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 254 private bool lastDefaultPrevented; 255 256 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 257 alias scriptable = arsd_jsvar_compatible; 258 259 version(Windows) { 260 // use native widgets when available unless specifically asked otherwise 261 version(custom_widgets) { 262 enum bool UsingCustomWidgets = true; 263 enum bool UsingWin32Widgets = false; 264 } else { 265 version = win32_widgets; 266 enum bool UsingCustomWidgets = false; 267 enum bool UsingWin32Widgets = true; 268 } 269 // and native theming when needed 270 //version = win32_theming; 271 } else { 272 enum bool UsingCustomWidgets = true; 273 enum bool UsingWin32Widgets = false; 274 version=custom_widgets; 275 } 276 277 278 279 /* 280 281 The main goals of minigui.d are to: 282 1) Provide basic widgets that just work in a lightweight lib. 283 I basically want things comparable to a plain HTML form, 284 plus the easy and obvious things you expect from Windows 285 apps like a menu. 286 2) Use native things when possible for best functionality with 287 least library weight. 288 3) Give building blocks to provide easy extension for your 289 custom widgets, or hooking into additional native widgets 290 I didn't wrap. 291 4) Provide interfaces for easy interaction between third 292 party minigui extensions. (event model, perhaps 293 signals/slots, drop-in ease of use bits.) 294 5) Zero non-system dependencies, including Phobos as much as 295 I reasonably can. It must only import arsd.color and 296 my simpledisplay.d. If you need more, it will have to be 297 an extension module. 298 6) An easy layout system that generally works. 299 300 A stretch goal is to make it easy to make gui forms with code, 301 some kind of resource file (xml?) and even a wysiwyg designer. 302 303 Another stretch goal is to make it easy to hook data into the gui, 304 including from reflection. So like auto-generate a form from a 305 function signature or struct definition, or show a list from an 306 array that automatically updates as the array is changed. Then, 307 your program focuses on the data more than the gui interaction. 308 309 310 311 STILL NEEDED: 312 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 313 * slider 314 * listbox 315 * spinner 316 * label? 317 * rich text 318 */ 319 320 321 /+ 322 enum LayoutMethods { 323 verticalFlex, 324 horizontalFlex, 325 inlineBlock, // left to right, no stretch, goes to next line as needed 326 static, // just set to x, y 327 verticalNoStretch, // browser style default 328 329 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 330 331 grid, // magic 332 } 333 +/ 334 335 /++ 336 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. 337 338 339 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. 340 341 --- 342 class MinimalWidget : Widget { 343 this(Widget parent) { 344 super(parent); 345 } 346 } 347 --- 348 349 $(SIDEBAR 350 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. 351 ) 352 353 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. 354 355 Among the things you'll most likely want to change in your custom widget: 356 357 $(LIST 358 * 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.) 359 360 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. 361 362 Do this $(I after) calling the `super` constructor. 363 364 * 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. 365 366 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 367 368 * 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. 369 370 * 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. 371 ) 372 373 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. 374 375 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 376 377 Your own custom-drawn and native system controls can exist side-by-side. 378 379 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. 380 +/ 381 class Widget : ReflectableProperties { 382 383 /+ 384 /++ 385 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 386 387 History: 388 Added September 15, 2021 389 implemented.... ??? 390 +/ 391 void prepareReflection(this This)() { 392 393 } 394 +/ 395 396 private bool _enabled = true; 397 398 /++ 399 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. 400 401 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. 402 403 History: 404 Added November 23, 2021 (dub v10.4) 405 406 Warning: the specific behavior of disabling with parents may change in the future. 407 Bugs: 408 Currently only implemented for widgets backed by native Windows controls. 409 410 See_Also: [disabledReason], [disabledBy] 411 +/ 412 @property bool enabled() { 413 return disabledBy() is null; 414 } 415 416 /// ditto 417 @property void enabled(bool yes) { 418 _enabled = yes; 419 version(win32_widgets) { 420 if(hwnd) 421 EnableWindow(hwnd, yes); 422 } 423 setDynamicState(DynamicState.disabled, yes); 424 } 425 426 private string disabledReason_; 427 428 /++ 429 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. 430 431 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 432 433 History: 434 Added November 23, 2021 (dub v10.4) 435 See_Also: [enabled], [disabledBy] 436 +/ 437 @property string disabledReason() { 438 auto w = disabledBy(); 439 return (w is null) ? null : w.disabledReason_; 440 } 441 442 /// ditto 443 @property void disabledReason(string reason) { 444 disabledReason_ = reason; 445 } 446 447 /++ 448 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. 449 450 History: 451 Added November 25, 2021 (dub v10.4) 452 See_Also: [enabled], [disabledReason] 453 +/ 454 Widget disabledBy() { 455 Widget p = this; 456 while(p) { 457 if(!p._enabled) 458 return p; 459 p = p.parent; 460 } 461 return null; 462 } 463 464 /// Implementations of [ReflectableProperties] interface. See the interface for details. 465 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 466 if(valueIsJson) 467 return SetPropertyResult.wrongFormat; 468 switch(name) { 469 case "name": 470 this.name = value.idup; 471 return SetPropertyResult.success; 472 case "statusTip": 473 this.statusTip = value.idup; 474 return SetPropertyResult.success; 475 default: 476 return SetPropertyResult.noSuchProperty; 477 } 478 } 479 /// ditto 480 void getPropertiesList(scope void delegate(string name) sink) const { 481 sink("name"); 482 sink("statusTip"); 483 } 484 /// ditto 485 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 486 switch(name) { 487 case "name": 488 sink(name, this.name, false); 489 return; 490 case "statusTip": 491 sink(name, this.statusTip, false); 492 return; 493 default: 494 sink(name, null, true); 495 } 496 } 497 498 /++ 499 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 500 501 History: 502 Added November 25, 2021 503 +/ 504 int scaleWithDpi(int value, int assumedDpi = 96) { 505 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 506 // 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. 507 // this also covers the case when actualDpi returns 0. 508 if(divide < 96) 509 divide = 96; 510 return value * divide / assumedDpi; 511 } 512 513 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 514 // I'll think up something better eventually 515 protected final int defaultLineHeight() { 516 return scaleWithDpi(Window.lineHeight); 517 } 518 519 /++ 520 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. 521 522 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. 523 524 History: 525 Added May 22, 2021 526 +/ 527 protected bool encapsulatedChildren() { 528 return false; 529 } 530 531 // Default layout properties { 532 533 int minWidth() { return 0; } 534 int minHeight() { 535 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 536 int sum = 0; 537 foreach(child; children) { 538 sum += child.minHeight(); 539 sum += child.marginTop(); 540 sum += child.marginBottom(); 541 } 542 543 return sum; 544 } 545 int maxWidth() { return int.max; } 546 int maxHeight() { return int.max; } 547 int widthStretchiness() { return 4; } 548 int heightStretchiness() { return 4; } 549 550 /++ 551 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 552 553 History: 554 Added June 15, 2021 (dub v10.1) 555 +/ 556 int widthShrinkiness() { return 0; } 557 /// ditto 558 int heightShrinkiness() { return 0; } 559 560 /++ 561 The initial size of the widget for layout calculations. Default is 0. 562 563 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 564 565 History: 566 Added June 15, 2021 (dub v10.1) 567 +/ 568 int flexBasisWidth() { return 0; } 569 /// ditto 570 int flexBasisHeight() { return 0; } 571 572 int marginLeft() { return 0; } 573 int marginRight() { return 0; } 574 int marginTop() { return 0; } 575 int marginBottom() { return 0; } 576 int paddingLeft() { return 0; } 577 int paddingRight() { return 0; } 578 int paddingTop() { return 0; } 579 int paddingBottom() { return 0; } 580 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 581 582 private bool recomputeChildLayoutRequired = true; 583 private static class RecomputeEvent {} 584 private __gshared rce = new RecomputeEvent(); 585 protected final void queueRecomputeChildLayout() { 586 recomputeChildLayoutRequired = true; 587 588 if(this.parentWindow) { 589 auto sw = this.parentWindow.win; 590 assert(sw !is null); 591 if(!sw.eventQueued!RecomputeEvent) { 592 sw.postEvent(rce); 593 // import std.stdio; writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 594 } 595 } 596 597 } 598 599 protected final void recomputeChildLayoutEntry() { 600 if(recomputeChildLayoutRequired) { 601 recomputeChildLayout(); 602 recomputeChildLayoutRequired = false; 603 redraw(); 604 } else { 605 // I still need to check the tree just in case one of them was queued up 606 // and the event came up here instead of there. 607 foreach(child; children) 608 child.recomputeChildLayoutEntry(); 609 } 610 } 611 612 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 613 void recomputeChildLayout() { 614 .recomputeChildLayout!"height"(this); 615 } 616 617 // } 618 619 620 /++ 621 Returns the style's tag name string this object uses. 622 623 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. 624 625 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 626 627 History: 628 Added May 10, 2021 629 +/ 630 string styleTagName() const { 631 string n = typeid(this).name; 632 foreach_reverse(idx, ch; n) 633 if(ch == '.') { 634 n = n[idx + 1 .. $]; 635 break; 636 } 637 return n; 638 } 639 640 /// API for the [styleClassList] 641 static struct ClassList { 642 private Widget widget; 643 644 /// 645 void add(string s) { 646 widget.styleClassList_ ~= s; 647 } 648 649 /// 650 void remove(string s) { 651 foreach(idx, s1; widget.styleClassList_) 652 if(s1 == s) { 653 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 654 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 655 widget.styleClassList_.assumeSafeAppend(); 656 return; 657 } 658 } 659 660 /// Returns true if it was added, false if it was removed. 661 bool toggle(string s) { 662 if(contains(s)) { 663 remove(s); 664 return false; 665 } else { 666 add(s); 667 return true; 668 } 669 } 670 671 /// 672 bool contains(string s) const { 673 foreach(s1; widget.styleClassList_) 674 if(s1 == s) 675 return true; 676 return false; 677 678 } 679 } 680 681 private string[] styleClassList_; 682 683 /++ 684 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. 685 686 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 687 688 History: 689 Added May 10, 2021 690 +/ 691 inout(ClassList) styleClassList() inout { 692 return cast(inout(ClassList)) ClassList(cast() this); 693 } 694 695 /++ 696 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. 697 698 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. 699 700 The upper 32 bits are available for your own extensions. 701 702 History: 703 Added May 10, 2021 704 +/ 705 enum DynamicState : ulong { 706 focus = (1 << 0), /// the widget currently has the keyboard focus 707 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 708 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!) 709 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!) 710 checked = (1 << 4), /// the widget is toggleable and currently toggled on 711 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 712 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 713 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 714 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. 715 716 USER_BEGIN = (1UL << 32), 717 } 718 719 // I want to add the primary and cancel styles to buttons at least at some point somehow. 720 721 /// ditto 722 @property ulong dynamicState() { return dynamicState_; } 723 /// ditto 724 @property ulong dynamicState(ulong newValue) { 725 if(dynamicState != newValue) { 726 auto old = dynamicState_; 727 dynamicState_ = newValue; 728 729 useStyleProperties((scope Widget.Style s) { 730 if(s.variesWithState(old ^ newValue)) 731 redraw(); 732 }); 733 } 734 return dynamicState_; 735 } 736 737 /// ditto 738 void setDynamicState(ulong flags, bool state) { 739 auto ds = dynamicState_; 740 if(state) 741 ds |= flags; 742 else 743 ds &= ~flags; 744 745 dynamicState = ds; 746 } 747 748 private ulong dynamicState_; 749 750 deprecated("Use dynamic styles instead now") { 751 Color backgroundColor() { return backgroundColor_; } 752 void backgroundColor(Color c){ this.backgroundColor_ = c; } 753 754 MouseCursor cursor() { return GenericCursor.Default; } 755 } private Color backgroundColor_ = Color.transparent; 756 757 758 /++ 759 Style properties are defined as an accessory class so they can be referenced and overridden independently. 760 761 It is here so there can be a specificity switch. 762 763 See [OverrideStyle] for a helper function to use your own. 764 765 History: 766 Added May 11, 2021 767 +/ 768 static class Style/* : StyleProperties*/ { 769 public Widget widget; // public because the mixin template needs access to it 770 771 /++ 772 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 773 774 History: 775 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. 776 +/ 777 bool variesWithState(ulong dynamicStateFlags) { 778 version(win32_widgets) { 779 if(widget.hwnd) 780 return false; 781 } 782 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 783 } 784 785 /// 786 Color foregroundColor() { 787 return WidgetPainter.visualTheme.foregroundColor; 788 } 789 790 /// 791 WidgetBackground background() { 792 // the default is a "transparent" background, which means 793 // it goes as far up as it can to get the color 794 if (widget.backgroundColor_ != Color.transparent) 795 return WidgetBackground(widget.backgroundColor_); 796 if (widget.parent) 797 return widget.parent.getComputedStyle.background; 798 return WidgetBackground(widget.backgroundColor_); 799 } 800 801 private static OperatingSystemFont fontCached_; 802 private OperatingSystemFont fontCached() { 803 if(fontCached_ is null) 804 fontCached_ = font(); 805 return fontCached_; 806 } 807 808 /++ 809 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. 810 +/ 811 OperatingSystemFont font() { 812 return null; 813 } 814 815 /++ 816 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. 817 818 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 819 820 History: 821 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 822 +/ 823 MouseCursor cursor() { 824 return GenericCursor.Default; 825 } 826 827 FrameStyle borderStyle() { 828 return FrameStyle.none; 829 } 830 831 /++ 832 +/ 833 Color borderColor() { 834 return Color.transparent; 835 } 836 837 FrameStyle outlineStyle() { 838 if(widget.dynamicState & DynamicState.focus) 839 return FrameStyle.dotted; 840 else 841 return FrameStyle.none; 842 } 843 844 Color outlineColor() { 845 return foregroundColor; 846 } 847 } 848 849 /++ 850 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 851 The basic usage is simple: 852 853 --- 854 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 855 // override style hints as-needed here 856 } 857 OverrideStyle!Style; // add the method 858 --- 859 860 $(TIP 861 While the class is not forced to be `static`, for best results, it should be. A non-static class 862 can not be inherited by other objects whereas the static one can. A property on the base class, 863 called [Widget.Style.widget|widget], is available for you to access its properties. 864 ) 865 866 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 867 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 868 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 869 870 871 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 872 You may also just override `variesWithState` when you use this flag. 873 874 --- 875 mixin OverrideStyle!( 876 DynamicState.focus, YourFocusedStyle, 877 DynamicState.hover, YourHoverStyle, 878 YourDefaultStyle 879 ) 880 --- 881 882 It checks if `dynamicState` matches the state and if so, returns the object given. 883 884 If there is no state mask given, the next one matches everything. The first match given is used. 885 886 However, since in most cases you'll want check state inside your individual methods, you probably won't 887 find much use for this whole-class swap out. 888 889 History: 890 Added May 16, 2021 891 +/ 892 static protected mixin template OverrideStyle(S...) { 893 override void useStyleProperties(scope void delegate(scope Widget.Style props) dg) { 894 ulong mask = 0; 895 foreach(idx, thing; S) { 896 static if(is(typeof(thing) : ulong)) { 897 mask = thing; 898 } else { 899 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 900 //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."); 901 scope Widget.Style s = new thing(); 902 s.widget = this; 903 dg(s); 904 return; 905 } 906 } 907 } 908 } 909 } 910 /++ 911 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 912 +/ 913 void useStyleProperties(scope void delegate(scope Style props) dg) { 914 scope Style s = new Style(); 915 s.widget = this; 916 dg(s); 917 } 918 919 920 protected void sendResizeEvent() { 921 this.emit!ResizeEvent(); 922 } 923 924 Menu contextMenu(int x, int y) { return null; } 925 926 final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) { 927 if(parentWindow is null || parentWindow.win is null) return false; 928 929 auto menu = this.contextMenu(x, y); 930 if(menu is null) 931 return false; 932 933 version(win32_widgets) { 934 // FIXME: if it is -1, -1, do it at the current selection location instead 935 // tho the corner of the window, whcih it does now, isn't the literal worst. 936 937 if(screenX < 0 && screenY < 0) { 938 auto p = this.globalCoordinates(); 939 if(screenX == -2) 940 p.x += x; 941 if(screenY == -2) 942 p.y += y; 943 944 screenX = p.x; 945 screenY = p.y; 946 } 947 948 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 949 throw new Exception("TrackContextMenuEx"); 950 } else version(custom_widgets) { 951 menu.popup(this, x, y); 952 } 953 954 return true; 955 } 956 957 /++ 958 Removes this widget from its parent. 959 960 History: 961 `removeWidget` was made `final` on May 11, 2021. 962 +/ 963 @scriptable 964 final void removeWidget() { 965 auto p = this.parent; 966 if(p) { 967 int item; 968 for(item = 0; item < p._children.length; item++) 969 if(p._children[item] is this) 970 break; 971 auto idx = item; 972 for(; item < p._children.length - 1; item++) 973 p._children[item] = p._children[item + 1]; 974 p._children = p._children[0 .. $-1]; 975 976 this.parent.widgetRemoved(idx, this); 977 //this.parent = null; 978 } 979 version(win32_widgets) { 980 removeAllChildren(); 981 if(hwnd) { 982 DestroyWindow(hwnd); 983 hwnd = null; 984 } 985 } 986 } 987 988 /++ 989 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. 990 991 History: 992 Added September 19, 2021 993 +/ 994 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 995 996 /++ 997 Removes all child widgets from `this`. You should not use the removed widgets again. 998 999 Note that on Windows, it also destroys the native handles for the removed children recursively. 1000 1001 History: 1002 Added July 1, 2021 (dub v10.2) 1003 +/ 1004 void removeAllChildren() { 1005 version(win32_widgets) 1006 foreach(child; _children) { 1007 child.removeAllChildren(); 1008 if(child.hwnd) { 1009 DestroyWindow(child.hwnd); 1010 child.hwnd = null; 1011 } 1012 } 1013 auto orig = this._children; 1014 this._children = null; 1015 foreach(idx, w; orig) 1016 this.widgetRemoved(idx, w); 1017 } 1018 1019 /++ 1020 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1021 +/ 1022 @scriptable 1023 Widget getChildByName(string name) { 1024 return getByName(name); 1025 } 1026 /++ 1027 Finds the nearest descendant with the requested type and [name]. May return `this`. 1028 +/ 1029 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1030 if(this.name == name) 1031 if(auto c = cast(WidgetClass) this) 1032 return c; 1033 foreach(child; children) { 1034 auto w = child.getByName(name); 1035 if(auto c = cast(WidgetClass) w) 1036 return c; 1037 } 1038 return null; 1039 } 1040 1041 /++ 1042 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. 1043 Names should be unique in a window. 1044 1045 See_Also: [getByName], [getChildByName] 1046 +/ 1047 @scriptable string name; 1048 1049 private EventHandler[][string] bubblingEventHandlers; 1050 private EventHandler[][string] capturingEventHandlers; 1051 1052 /++ 1053 Default event handlers. These are called on the appropriate 1054 event unless [Event.preventDefault] is called on the event at 1055 some point through the bubbling process. 1056 1057 1058 If you are implementing your own widget and want to add custom 1059 events, you should follow the same pattern here: create a virtual 1060 function named `defaultEventHandler_eventname` with the implementation, 1061 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1062 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1063 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1064 This ensures virtual dispatch based on the correct subclass. 1065 1066 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1067 overridden version. 1068 1069 You only need to do that on parent classes adding NEW event types. If you 1070 just want to change the default behavior of an existing event type in a subclass, 1071 you override the function (and optionally call `super.method_name`) like normal. 1072 1073 +/ 1074 protected EventHandler[string] defaultEventHandlers; 1075 1076 /// ditto 1077 void setupDefaultEventHandlers() { 1078 defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); }; 1079 defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); }; 1080 defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); }; 1081 defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); }; 1082 defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); }; 1083 defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); }; 1084 defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); }; 1085 defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); }; 1086 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); }; 1087 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); }; 1088 defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); }; 1089 defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); }; 1090 defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); }; 1091 defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); }; 1092 defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); }; 1093 defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); }; 1094 defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); }; 1095 defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); }; 1096 } 1097 1098 /// ditto 1099 void defaultEventHandler_click(ClickEvent event) {} 1100 /// ditto 1101 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1102 /// ditto 1103 void defaultEventHandler_keydown(KeyDownEvent event) {} 1104 /// ditto 1105 void defaultEventHandler_keyup(KeyUpEvent event) {} 1106 /// ditto 1107 void defaultEventHandler_mousedown(MouseDownEvent event) { 1108 if(event.button == MouseButton.left) { 1109 if(this.tabStop) 1110 this.focus(); 1111 } 1112 } 1113 /// ditto 1114 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1115 /// ditto 1116 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1117 /// ditto 1118 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1119 /// ditto 1120 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1121 /// ditto 1122 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1123 /// ditto 1124 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1125 /// ditto 1126 void defaultEventHandler_char(CharEvent event) {} 1127 /// ditto 1128 void defaultEventHandler_triggered(Event event) {} 1129 /// ditto 1130 void defaultEventHandler_change(Event event) {} 1131 /// ditto 1132 void defaultEventHandler_focus(Event event) {} 1133 /// ditto 1134 void defaultEventHandler_blur(Event event) {} 1135 /// ditto 1136 void defaultEventHandler_focusin(Event event) {} 1137 /// ditto 1138 void defaultEventHandler_focusout(Event event) {} 1139 1140 /++ 1141 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1142 1143 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1144 1145 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1146 of participating in handler delegation. 1147 1148 $(TIP 1149 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. 1150 ) 1151 +/ 1152 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1153 return addEventListener(event, (Widget, scope Event e) { 1154 if(e.srcElement is this) 1155 handler(); 1156 }, useCapture); 1157 } 1158 1159 /// ditto 1160 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1161 return addEventListener(event, (Widget, Event e) { 1162 if(e.srcElement is this) 1163 handler(e); 1164 }, useCapture); 1165 } 1166 1167 /// ditto 1168 @scriptable 1169 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1170 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1171 } 1172 1173 /// ditto 1174 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1175 static if(is(Handler Fn == delegate)) { 1176 static if(is(Fn Params == __parameters)) { 1177 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1178 auto ty = cast(Params[0]) e; 1179 if(ty !is null) 1180 handler(ty); 1181 }, useCapture); 1182 } else static assert(0); 1183 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1184 } 1185 1186 /// ditto 1187 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1188 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1189 } 1190 1191 /// ditto 1192 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1193 if(event.length > 2 && event[0..2] == "on") 1194 event = event[2 .. $]; 1195 1196 if(useCapture) 1197 capturingEventHandlers[event] ~= handler; 1198 else 1199 bubblingEventHandlers[event] ~= handler; 1200 1201 return EventListener(this, event, handler, useCapture); 1202 } 1203 1204 /// ditto 1205 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1206 if(event.length > 2 && event[0..2] == "on") 1207 event = event[2 .. $]; 1208 1209 if(useCapture) { 1210 if(event in capturingEventHandlers) 1211 foreach(ref evt; capturingEventHandlers[event]) 1212 if(evt is handler) evt = null; 1213 } else { 1214 if(event in bubblingEventHandlers) 1215 foreach(ref evt; bubblingEventHandlers[event]) 1216 if(evt is handler) evt = null; 1217 } 1218 } 1219 1220 /// ditto 1221 void removeEventListener(EventListener listener) { 1222 removeEventListener(listener.event, listener.handler, listener.useCapture); 1223 } 1224 1225 static if(UsingSimpledisplayX11) { 1226 void discardXConnectionState() { 1227 foreach(child; children) 1228 child.discardXConnectionState(); 1229 } 1230 1231 void recreateXConnectionState() { 1232 foreach(child; children) 1233 child.recreateXConnectionState(); 1234 redraw(); 1235 } 1236 } 1237 1238 /++ 1239 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1240 1241 History: 1242 `globalCoordinates` was made `final` on May 11, 2021. 1243 +/ 1244 Point globalCoordinates() { 1245 int x = this.x; 1246 int y = this.y; 1247 auto p = this.parent; 1248 while(p) { 1249 x += p.x; 1250 y += p.y; 1251 p = p.parent; 1252 } 1253 1254 static if(UsingSimpledisplayX11) { 1255 auto dpy = XDisplayConnection.get; 1256 arsd.simpledisplay.Window dummyw; 1257 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1258 } else { 1259 POINT pt; 1260 pt.x = x; 1261 pt.y = y; 1262 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1263 x = pt.x; 1264 y = pt.y; 1265 } 1266 1267 return Point(x, y); 1268 } 1269 1270 version(win32_widgets) 1271 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1272 1273 version(win32_widgets) 1274 /// Called when a WM_COMMAND is sent to the associated hwnd. 1275 void handleWmCommand(ushort cmd, ushort id) {} 1276 1277 version(win32_widgets) 1278 /++ 1279 Called when a WM_NOTIFY is sent to the associated hwnd. 1280 1281 History: 1282 +/ 1283 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1284 1285 version(win32_widgets) 1286 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); } 1287 1288 /++ 1289 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1290 1291 Updates to this variable will only be made visible on the next mouse enter event. 1292 +/ 1293 @scriptable string statusTip; 1294 // string toolTip; 1295 // string helpText; 1296 1297 /++ 1298 If true, this widget can be focused via keyboard control with the tab key. 1299 1300 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1301 +/ 1302 bool tabStop = true; 1303 /++ 1304 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.) 1305 +/ 1306 int tabOrder; 1307 1308 version(win32_widgets) { 1309 static Widget[HWND] nativeMapping; 1310 /// The native handle, if there is one. 1311 HWND hwnd; 1312 WNDPROC originalWindowProcedure; 1313 1314 SimpleWindow simpleWindowWrappingHwnd; 1315 1316 // please note it IGNORES your return value and does NOT forward it to Windows! 1317 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1318 return 0; 1319 } 1320 } 1321 private bool implicitlyCreated; 1322 1323 /// 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. 1324 int x; 1325 /// ditto 1326 int y; 1327 private int _width; 1328 private int _height; 1329 private Widget[] _children; 1330 private Widget _parent; 1331 private Window _parentWindow; 1332 1333 /++ 1334 Returns the window to which this widget is attached. 1335 1336 History: 1337 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. 1338 +/ 1339 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1340 private @property void parentWindow(Window parent) { 1341 _parentWindow = parent; 1342 foreach(child; children) 1343 child.parentWindow = parent; // please note that this is recursive 1344 } 1345 1346 /++ 1347 Returns the list of the widget's children. 1348 1349 History: 1350 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1351 1352 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1353 +/ 1354 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1355 1356 /++ 1357 Returns the widget's parent. 1358 1359 History: 1360 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1361 1362 The parent should only be managed by the [addChild] and [removeWidget] method. 1363 +/ 1364 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1365 1366 /// The widget's current size. 1367 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1368 /// ditto 1369 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1370 1371 /// Only the layout manager should be calling these. 1372 final protected @property int width(int a) @safe { return _width = a; } 1373 /// ditto 1374 final protected @property int height(int a) @safe { return _height = a; } 1375 1376 /++ 1377 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. 1378 1379 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 1380 +/ 1381 protected void registerMovement() { 1382 version(win32_widgets) { 1383 if(hwnd) { 1384 auto pos = getChildPositionRelativeToParentHwnd(this); 1385 MoveWindow(hwnd, pos[0], pos[1], width, height, true); 1386 } 1387 } 1388 sendResizeEvent(); 1389 } 1390 1391 /// Creates the widget and adds it to the parent. 1392 this(Widget parent) { 1393 if(parent !is null) 1394 parent.addChild(this); 1395 setupDefaultEventHandlers(); 1396 } 1397 1398 /// 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. 1399 @scriptable 1400 bool isFocused() { 1401 return parentWindow && parentWindow.focusedWidget is this; 1402 } 1403 1404 private bool showing_ = true; 1405 /// 1406 bool showing() { return showing_; } 1407 /// 1408 bool hidden() { return !showing_; } 1409 /++ 1410 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. 1411 +/ 1412 void showing(bool s, bool recalculate = true) { 1413 auto so = showing_; 1414 showing_ = s; 1415 if(s != so) { 1416 version(win32_widgets) 1417 if(hwnd) 1418 ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE); 1419 1420 if(parent && recalculate) { 1421 parent.queueRecomputeChildLayout(); 1422 parent.redraw(); 1423 } 1424 1425 foreach(child; children) 1426 child.showing(s, false); 1427 1428 } 1429 queueRecomputeChildLayout(); 1430 redraw(); 1431 } 1432 /// Convenience method for `showing = true` 1433 @scriptable 1434 void show() { 1435 showing = true; 1436 } 1437 /// Convenience method for `showing = false` 1438 @scriptable 1439 void hide() { 1440 showing = false; 1441 } 1442 1443 /// 1444 @scriptable 1445 void focus() { 1446 assert(parentWindow !is null); 1447 if(isFocused()) 1448 return; 1449 1450 if(parentWindow.focusedWidget) { 1451 // FIXME: more details here? like from and to 1452 auto from = parentWindow.focusedWidget; 1453 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 1454 parentWindow.focusedWidget = null; 1455 from.emit!BlurEvent(); 1456 this.emit!FocusOutEvent(); 1457 } 1458 1459 1460 version(win32_widgets) { 1461 if(this.hwnd !is null) 1462 SetFocus(this.hwnd); 1463 } 1464 //else static if(UsingSimpledisplayX11) 1465 //this.parentWindow.win.focus(); 1466 1467 parentWindow.focusedWidget = this; 1468 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 1469 this.emit!FocusEvent(); 1470 this.emit!FocusInEvent(); 1471 } 1472 1473 /+ 1474 /++ 1475 Unfocuses the widget. This may reset 1476 +/ 1477 @scriptable 1478 void blur() { 1479 1480 } 1481 +/ 1482 1483 1484 /++ 1485 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 1486 1487 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 1488 +/ 1489 void attachedToWindow(Window w) {} 1490 /++ 1491 Callback when the widget is added to another widget. 1492 1493 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 1494 +/ 1495 void addedTo(Widget w) {} 1496 1497 /++ 1498 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. 1499 1500 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 1501 +/ 1502 protected void addChild(Widget w, int position = int.max) { 1503 w._parent = this; 1504 if(position == int.max || position == children.length) { 1505 _children ~= w; 1506 } else { 1507 assert(position < _children.length); 1508 _children.length = _children.length + 1; 1509 for(int i = cast(int) _children.length - 1; i > position; i--) 1510 _children[i] = _children[i - 1]; 1511 _children[position] = w; 1512 } 1513 1514 this.parentWindow = this._parentWindow; 1515 1516 w.addedTo(this); 1517 1518 if(this.hidden) 1519 w.showing = false; 1520 1521 if(parentWindow !is null) { 1522 w.attachedToWindow(parentWindow); 1523 parentWindow.queueRecomputeChildLayout(); 1524 parentWindow.redraw(); 1525 } 1526 } 1527 1528 /++ 1529 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. 1530 +/ 1531 Widget getChildAtPosition(int x, int y) { 1532 // it goes backward so the last one to show gets picked first 1533 // might use z-index later 1534 foreach_reverse(child; children) { 1535 if(child.hidden) 1536 continue; 1537 if(child.x <= x && child.y <= y 1538 && ((x - child.x) < child.width) 1539 && ((y - child.y) < child.height)) 1540 { 1541 return child; 1542 } 1543 } 1544 1545 return null; 1546 } 1547 1548 /++ 1549 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. 1550 1551 History: 1552 Added July 2, 2021 (v10.2) 1553 +/ 1554 protected void addScrollPosition(ref int x, ref int y) {}; 1555 1556 /++ 1557 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. 1558 1559 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. 1560 1561 [paint] is not called for system widgets as the OS library draws them instead. 1562 1563 1564 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. 1565 1566 You should also look at [WidgetPainter.visualTheme] to be theme aware. 1567 1568 History: 1569 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. 1570 +/ 1571 void paint(WidgetPainter painter) { 1572 version(win32_widgets) 1573 if(hwnd) 1574 return; 1575 painter.drawThemed(&paintContent); // note this refers to the following overload 1576 } 1577 1578 /++ 1579 Responsible for drawing the content as the theme engine is responsible for other elements. 1580 1581 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 1582 1583 Params: 1584 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. 1585 1586 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. 1587 1588 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. 1589 1590 Returns: 1591 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. 1592 1593 History: 1594 Added May 15, 2021 1595 +/ 1596 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 1597 return bounds; 1598 } 1599 1600 deprecated("Change ScreenPainter to WidgetPainter") 1601 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 1602 1603 /// I don't actually like the name of this 1604 /// this draws a background on it 1605 void erase(WidgetPainter painter) { 1606 version(win32_widgets) 1607 if(hwnd) return; // Windows will do it. I think. 1608 1609 auto c = getComputedStyle().background.color; 1610 painter.fillColor = c; 1611 painter.outlineColor = c; 1612 1613 version(win32_widgets) { 1614 HANDLE b, p; 1615 if(c.a == 0) { 1616 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 1617 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 1618 } 1619 } 1620 painter.drawRectangle(Point(0, 0), width, height); 1621 version(win32_widgets) { 1622 if(c.a == 0) { 1623 SelectObject(painter.impl.hdc, p); 1624 SelectObject(painter.impl.hdc, b); 1625 } 1626 } 1627 } 1628 1629 /// 1630 WidgetPainter draw() { 1631 int x = this.x, y = this.y; 1632 auto parent = this.parent; 1633 while(parent) { 1634 x += parent.x; 1635 y += parent.y; 1636 parent = parent.parent; 1637 } 1638 1639 auto painter = parentWindow.win.draw(); 1640 painter.originX = x; 1641 painter.originY = y; 1642 painter.setClipRectangle(Point(0, 0), width, height); 1643 return WidgetPainter(painter, this); 1644 } 1645 1646 /// 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. 1647 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force = false) { 1648 if(hidden) 1649 return; 1650 1651 int paintX = x; 1652 int paintY = y; 1653 if(this.useNativeDrawing()) { 1654 paintX = 0; 1655 paintY = 0; 1656 lox = 0; 1657 loy = 0; 1658 containment = Rectangle(0, 0, int.max, int.max); 1659 } 1660 1661 painter.originX = lox + paintX; 1662 painter.originY = loy + paintY; 1663 1664 bool actuallyPainted = false; 1665 1666 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 1667 if(clip == Rectangle.init) { 1668 //import std.stdio; writeln(this, " clipped out"); 1669 return; 1670 } 1671 1672 if(redrawRequested || force) { 1673 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 1674 1675 painter.drawingUpon = this; 1676 1677 erase(painter); 1678 if(painter.visualTheme) 1679 painter.visualTheme.doPaint(this, painter); 1680 else 1681 paint(painter); 1682 1683 redrawRequested = false; 1684 actuallyPainted = true; 1685 } 1686 1687 foreach(child; children) { 1688 version(win32_widgets) 1689 if(child.useNativeDrawing()) continue; 1690 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted); 1691 } 1692 1693 version(win32_widgets) 1694 foreach(child; children) { 1695 if(child.useNativeDrawing) { 1696 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw, child); 1697 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted); 1698 } 1699 } 1700 } 1701 1702 protected bool useNativeDrawing() nothrow { 1703 version(win32_widgets) 1704 return hwnd !is null; 1705 else 1706 return false; 1707 } 1708 1709 private static class RedrawEvent {} 1710 private __gshared re = new RedrawEvent(); 1711 1712 private bool redrawRequested; 1713 /// 1714 final void redraw(string file = __FILE__, size_t line = __LINE__) { 1715 redrawRequested = true; 1716 1717 if(this.parentWindow) { 1718 auto sw = this.parentWindow.win; 1719 assert(sw !is null); 1720 if(!sw.eventQueued!RedrawEvent) { 1721 sw.postEvent(re); 1722 // import std.stdio; writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1723 } 1724 } 1725 } 1726 1727 private SimpleWindow drawableWindow; 1728 1729 /++ 1730 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. 1731 1732 Returns: 1733 `true` if you should do your default behavior. 1734 1735 History: 1736 Added May 5, 2021 1737 1738 Bugs: 1739 It does not do the static checks on gdc right now. 1740 +/ 1741 final protected bool emit(EventType, this This, Args...)(Args args) { 1742 version(GNU) {} else 1743 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1744 auto e = new EventType(this, args); 1745 e.dispatch(); 1746 return !e.defaultPrevented; 1747 } 1748 /// ditto 1749 final protected bool emit(string eventString, this This)() { 1750 auto e = new Event(eventString, this); 1751 e.dispatch(); 1752 return !e.defaultPrevented; 1753 } 1754 1755 /++ 1756 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. 1757 1758 History: 1759 Added May 5, 2021 1760 +/ 1761 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 1762 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1763 return addEventListener(handler); 1764 } 1765 1766 /++ 1767 Gets the computed style properties from the visual theme. 1768 1769 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].) 1770 1771 History: 1772 Added May 8, 2021 1773 +/ 1774 final StyleInformation getComputedStyle() { 1775 return StyleInformation(this); 1776 } 1777 1778 // FIXME: I kinda want to hide events from implementation widgets 1779 // so it just catches them all and stops propagation... 1780 // i guess i can do it with a event listener on star. 1781 1782 mixin Emits!KeyDownEvent; /// 1783 mixin Emits!KeyUpEvent; /// 1784 mixin Emits!CharEvent; /// 1785 1786 mixin Emits!MouseDownEvent; /// 1787 mixin Emits!MouseUpEvent; /// 1788 mixin Emits!ClickEvent; /// 1789 mixin Emits!DoubleClickEvent; /// 1790 mixin Emits!MouseMoveEvent; /// 1791 mixin Emits!MouseOverEvent; /// 1792 mixin Emits!MouseOutEvent; /// 1793 mixin Emits!MouseEnterEvent; /// 1794 mixin Emits!MouseLeaveEvent; /// 1795 1796 mixin Emits!ResizeEvent; /// 1797 1798 mixin Emits!BlurEvent; /// 1799 mixin Emits!FocusEvent; /// 1800 1801 mixin Emits!FocusInEvent; /// 1802 mixin Emits!FocusOutEvent; /// 1803 } 1804 1805 /+ 1806 /++ 1807 Interface to indicate that the widget has a simple value property. 1808 1809 History: 1810 Added August 26, 2021 1811 +/ 1812 interface HasValue!T { 1813 /// Getter 1814 @property T value(); 1815 /// Setter 1816 @property void value(T); 1817 } 1818 1819 /++ 1820 Interface to indicate that the widget has a range of possible values for its simple value property. 1821 This would be present on something like a slider or possibly a number picker. 1822 1823 History: 1824 Added September 11, 2021 1825 +/ 1826 interface HasRangeOfValues!T : HasValue!T { 1827 /// The minimum and maximum values in the range, inclusive. 1828 @property T minValue(); 1829 @property void minValue(T); /// ditto 1830 @property T maxValue(); /// ditto 1831 @property void maxValue(T); /// ditto 1832 1833 /// The smallest step the user interface allows. User may still type in values without this limitation. 1834 @property void step(T); 1835 @property T step(); /// ditto 1836 } 1837 1838 /++ 1839 Interface to indicate that the widget has a list of possible values the user can choose from. 1840 This would be present on something like a drop-down selector. 1841 1842 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 1843 combobox. 1844 1845 History: 1846 Added September 11, 2021 1847 +/ 1848 interface HasListOfValues!T : HasValue!T { 1849 @property T[] values; 1850 @property void values(T[]); 1851 1852 @property int selectedIndex(); // note it may return -1! 1853 @property void selectedIndex(int); 1854 } 1855 +/ 1856 1857 /++ 1858 History: 1859 Added September 2021 (dub v10.4) 1860 +/ 1861 class GridLayout : Layout { 1862 1863 // 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. 1864 1865 /++ 1866 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 1867 +/ 1868 enum Gravity { 1869 Center = 0, 1870 NorthWest = North | West, 1871 North = 0b10_00, 1872 NorthEast = North | East, 1873 West = 0b00_10, 1874 East = 0b00_01, 1875 SouthWest = South | West, 1876 South = 0b01_00, 1877 SouthEast = South | East, 1878 } 1879 1880 /++ 1881 The width and height are in some proportional units and can often just be 12. 1882 +/ 1883 this(int width, int height, Widget parent) { 1884 this.gridWidth = width; 1885 this.gridHeight = height; 1886 super(parent); 1887 } 1888 1889 /++ 1890 Sets the position of the given child. 1891 1892 The units of these arguments are in the proportional grid units you set in the constructor. 1893 +/ 1894 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 1895 // ensure it is in bounds 1896 // then ensure no overlaps 1897 1898 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 1899 1900 foreach(ref position; positions) { 1901 if(position.widget is child) { 1902 position = p; 1903 goto set; 1904 } 1905 } 1906 1907 positions ~= p; 1908 1909 set: 1910 1911 // FIXME: should this batch? 1912 queueRecomputeChildLayout(); 1913 1914 return child; 1915 } 1916 1917 override void addChild(Widget w, int position = int.max) { 1918 super.addChild(w, position); 1919 //positions ~= ChildPosition(w); 1920 if(position != int.max) { 1921 // FIXME: align it so they actually match. 1922 } 1923 } 1924 1925 override void widgetRemoved(size_t idx, Widget w) { 1926 // FIXME: keep the positions array aligned 1927 // positions[idx].widget = null; 1928 } 1929 1930 override void recomputeChildLayout() { 1931 registerMovement(); 1932 int onGrid = cast(int) positions.length; 1933 c: foreach(child; children) { 1934 // just snap it to the grid 1935 if(onGrid) 1936 foreach(position; positions) 1937 if(position.widget is child) { 1938 child.x = this.width * position.x / this.gridWidth; 1939 child.y = this.height * position.y / this.gridHeight; 1940 child.width = this.width * position.width / this.gridWidth; 1941 child.height = this.height * position.height / this.gridHeight; 1942 1943 auto diff = child.width - child.maxWidth(); 1944 // FIXME: gravity? 1945 if(diff > 0) { 1946 child.width = child.width - diff; 1947 1948 if(position.gravity & Gravity.West) { 1949 // nothing needed, already aligned 1950 } else if(position.gravity & Gravity.East) { 1951 child.x += diff; 1952 } else { 1953 child.x += diff / 2; 1954 } 1955 } 1956 1957 diff = child.height - child.maxHeight(); 1958 // FIXME: gravity? 1959 if(diff > 0) { 1960 child.height = child.height - diff; 1961 1962 if(position.gravity & Gravity.North) { 1963 // nothing needed, already aligned 1964 } else if(position.gravity & Gravity.South) { 1965 child.y += diff; 1966 } else { 1967 child.y += diff / 2; 1968 } 1969 } 1970 1971 1972 child.recomputeChildLayout(); 1973 onGrid--; 1974 continue c; 1975 } 1976 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 1977 } 1978 } 1979 1980 private struct ChildPosition { 1981 Widget widget; 1982 int x; 1983 int y; 1984 int width; 1985 int height; 1986 Gravity gravity; 1987 } 1988 private ChildPosition[] positions; 1989 1990 int gridWidth = 12; 1991 int gridHeight = 12; 1992 } 1993 1994 /// 1995 abstract class ComboboxBase : Widget { 1996 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 1997 // or to always show the list, we want CBS_SIMPLE == 1 1998 version(win32_widgets) 1999 this(uint style, Widget parent) { 2000 super(parent); 2001 createWin32Window(this, "ComboBox"w, null, style); 2002 } 2003 else version(custom_widgets) 2004 this(Widget parent) { 2005 super(parent); 2006 2007 addEventListener((KeyDownEvent event) { 2008 if(event.key == Key.Up) { 2009 if(selection_ > -1) { // -1 means select blank 2010 selection_--; 2011 fireChangeEvent(); 2012 } 2013 event.preventDefault(); 2014 } 2015 if(event.key == Key.Down) { 2016 if(selection_ + 1 < options.length) { 2017 selection_++; 2018 fireChangeEvent(); 2019 } 2020 event.preventDefault(); 2021 } 2022 2023 }); 2024 2025 } 2026 else static assert(false); 2027 2028 private string[] options; 2029 private int selection_ = -1; 2030 2031 void addOption(string s) { 2032 options ~= s; 2033 version(win32_widgets) 2034 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2035 } 2036 2037 int getSelection() { 2038 return selection_; 2039 } 2040 2041 /++ 2042 History: 2043 Added November 17, 2021 2044 +/ 2045 string getSelectionString() { 2046 return selection_ == -1 ? null : options[selection_]; 2047 } 2048 2049 void setSelection(int idx) { 2050 selection_ = idx; 2051 version(win32_widgets) 2052 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2053 2054 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2055 t.dispatch(); 2056 } 2057 2058 static class SelectionChangedEvent : Event { 2059 this(Widget target, int iv, string sv) { 2060 super("change", target); 2061 this.iv = iv; 2062 this.sv = sv; 2063 } 2064 immutable int iv; 2065 immutable string sv; 2066 2067 override @property string stringValue() { return sv; } 2068 override @property int intValue() { return iv; } 2069 } 2070 2071 version(win32_widgets) 2072 override void handleWmCommand(ushort cmd, ushort id) { 2073 if(cmd == CBN_SELCHANGE) { 2074 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2075 fireChangeEvent(); 2076 } 2077 } 2078 2079 private void fireChangeEvent() { 2080 if(selection_ >= options.length) 2081 selection_ = -1; 2082 2083 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2084 t.dispatch(); 2085 } 2086 2087 version(win32_widgets) { 2088 override int minHeight() { return defaultLineHeight + 6; } 2089 override int maxHeight() { return defaultLineHeight + 6; } 2090 } else { 2091 override int minHeight() { return defaultLineHeight + 4; } 2092 override int maxHeight() { return defaultLineHeight + 4; } 2093 } 2094 2095 version(custom_widgets) { 2096 2097 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2098 2099 SimpleWindow dropDown; 2100 void popup() { 2101 auto w = width; 2102 // FIXME: suggestedDropdownHeight see below 2103 auto h = cast(int) this.options.length * defaultLineHeight + 8; 2104 2105 auto coord = this.globalCoordinates(); 2106 auto dropDown = new SimpleWindow( 2107 w, h, 2108 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null); 2109 2110 dropDown.move(coord.x, coord.y + this.height); 2111 2112 { 2113 auto cs = getComputedStyle(); 2114 auto painter = dropDown.draw(); 2115 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2116 auto p = Point(4, 4); 2117 painter.outlineColor = cs.foregroundColor; 2118 foreach(option; options) { 2119 painter.drawText(p, option); 2120 p.y += defaultLineHeight; 2121 } 2122 } 2123 2124 dropDown.setEventHandlers( 2125 (MouseEvent event) { 2126 if(event.type == MouseEventType.buttonReleased) { 2127 dropDown.close(); 2128 auto element = (event.y - 4) / defaultLineHeight; 2129 if(element >= 0 && element <= options.length) { 2130 selection_ = element; 2131 2132 fireChangeEvent(); 2133 } 2134 } 2135 } 2136 ); 2137 2138 dropDown.visibilityChanged = (bool visible) { 2139 if(visible) { 2140 this.redraw(); 2141 dropDown.grabInput(); 2142 } else { 2143 dropDown.releaseInputGrab(); 2144 } 2145 }; 2146 2147 dropDown.show(); 2148 } 2149 2150 } 2151 } 2152 2153 /++ 2154 A drop-down list where the user must select one of the 2155 given options. Like `<select>` in HTML. 2156 +/ 2157 class DropDownSelection : ComboboxBase { 2158 this(Widget parent) { 2159 version(win32_widgets) 2160 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 2161 else version(custom_widgets) { 2162 super(parent); 2163 2164 addEventListener("focus", () { this.redraw; }); 2165 addEventListener("blur", () { this.redraw; }); 2166 addEventListener(EventType.change, () { this.redraw; }); 2167 addEventListener("mousedown", () { this.focus(); this.popup(); }); 2168 addEventListener((KeyDownEvent event) { 2169 if(event.key == Key.Space) 2170 popup(); 2171 }); 2172 } else static assert(false); 2173 } 2174 2175 mixin Padding!q{2}; 2176 static class Style : Widget.Style { 2177 override FrameStyle borderStyle() { return FrameStyle.risen; } 2178 } 2179 mixin OverrideStyle!Style; 2180 2181 version(custom_widgets) 2182 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2183 auto cs = getComputedStyle(); 2184 2185 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 2186 2187 painter.outlineColor = cs.foregroundColor; 2188 painter.fillColor = cs.foregroundColor; 2189 Point[4] triangle; 2190 enum padding = 6; 2191 enum paddingV = 7; 2192 enum triangleWidth = 10; 2193 triangle[0] = Point(width - padding - triangleWidth, paddingV); 2194 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 2195 triangle[2] = Point(width - padding - 0, paddingV); 2196 triangle[3] = triangle[0]; 2197 painter.drawPolygon(triangle[]); 2198 2199 return bounds; 2200 } 2201 2202 version(win32_widgets) 2203 override void registerMovement() { 2204 version(win32_widgets) { 2205 if(hwnd) { 2206 auto pos = getChildPositionRelativeToParentHwnd(this); 2207 // the height given to this from Windows' perspective is supposed 2208 // to include the drop down's height. so I add to it to give some 2209 // room for that. 2210 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 2211 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 2212 } 2213 } 2214 sendResizeEvent(); 2215 } 2216 } 2217 2218 /++ 2219 A text box with a drop down arrow listing selections. 2220 The user can choose from the list, or type their own. 2221 +/ 2222 class FreeEntrySelection : ComboboxBase { 2223 this(Widget parent) { 2224 version(win32_widgets) 2225 super(2 /* CBS_DROPDOWN */, parent); 2226 else version(custom_widgets) { 2227 super(parent); 2228 auto hl = new HorizontalLayout(this); 2229 lineEdit = new LineEdit(hl); 2230 2231 tabStop = false; 2232 2233 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2234 2235 auto btn = new class ArrowButton { 2236 this() { 2237 super(ArrowDirection.down, hl); 2238 } 2239 override int maxHeight() { 2240 return int.max; 2241 } 2242 }; 2243 //btn.addDirectEventListener("focus", &lineEdit.focus); 2244 btn.addEventListener("triggered", &this.popup); 2245 addEventListener(EventType.change, (Event event) { 2246 lineEdit.content = event.stringValue; 2247 lineEdit.focus(); 2248 redraw(); 2249 }); 2250 } 2251 else static assert(false); 2252 } 2253 2254 version(custom_widgets) { 2255 LineEdit lineEdit; 2256 } 2257 } 2258 2259 /++ 2260 A combination of free entry with a list below it. 2261 +/ 2262 class ComboBox : ComboboxBase { 2263 this(Widget parent) { 2264 version(win32_widgets) 2265 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 2266 else version(custom_widgets) { 2267 super(parent); 2268 lineEdit = new LineEdit(this); 2269 listWidget = new ListWidget(this); 2270 listWidget.multiSelect = false; 2271 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 2272 string c = null; 2273 foreach(option; listWidget.options) 2274 if(option.selected) { 2275 c = option.label; 2276 break; 2277 } 2278 lineEdit.content = c; 2279 }); 2280 2281 listWidget.tabStop = false; 2282 this.tabStop = false; 2283 listWidget.addEventListener("focus", &lineEdit.focus); 2284 this.addEventListener("focus", &lineEdit.focus); 2285 2286 addDirectEventListener(EventType.change, { 2287 listWidget.setSelection(selection_); 2288 if(selection_ != -1) 2289 lineEdit.content = options[selection_]; 2290 lineEdit.focus(); 2291 redraw(); 2292 }); 2293 2294 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2295 2296 listWidget.addDirectEventListener(EventType.change, { 2297 int set = -1; 2298 foreach(idx, opt; listWidget.options) 2299 if(opt.selected) { 2300 set = cast(int) idx; 2301 break; 2302 } 2303 if(set != selection_) 2304 this.setSelection(set); 2305 }); 2306 } else static assert(false); 2307 } 2308 2309 override int minHeight() { return defaultLineHeight * 3; } 2310 override int maxHeight() { return int.max; } 2311 override int heightStretchiness() { return 5; } 2312 2313 version(custom_widgets) { 2314 LineEdit lineEdit; 2315 ListWidget listWidget; 2316 2317 override void addOption(string s) { 2318 listWidget.options ~= ListWidget.Option(s); 2319 ComboboxBase.addOption(s); 2320 } 2321 } 2322 } 2323 2324 /+ 2325 class Spinner : Widget { 2326 version(win32_widgets) 2327 this(Widget parent) { 2328 super(parent); 2329 parentWindow = parent.parentWindow; 2330 auto hlayout = new HorizontalLayout(this); 2331 lineEdit = new LineEdit(hlayout); 2332 upDownControl = new UpDownControl(hlayout); 2333 } 2334 2335 LineEdit lineEdit; 2336 UpDownControl upDownControl; 2337 } 2338 2339 class UpDownControl : Widget { 2340 version(win32_widgets) 2341 this(Widget parent) { 2342 super(parent); 2343 parentWindow = parent.parentWindow; 2344 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 2345 } 2346 2347 override int minHeight() { return defaultLineHeight; } 2348 override int maxHeight() { return defaultLineHeight * 3/2; } 2349 2350 override int minWidth() { return defaultLineHeight * 3/2; } 2351 override int maxWidth() { return defaultLineHeight * 3/2; } 2352 } 2353 +/ 2354 2355 /+ 2356 class DataView : Widget { 2357 // this is the omnibus data viewer 2358 // the internal data layout is something like: 2359 // string[string][] but also each node can have parents 2360 } 2361 +/ 2362 2363 2364 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 2365 2366 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 2367 2368 // FIXME: menus should prolly capture the mouse. ugh i kno. 2369 /* 2370 TextEdit needs: 2371 2372 * caret manipulation 2373 * selection control 2374 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 2375 2376 For example: 2377 2378 connect(paste, &textEdit.insertTextAtCaret); 2379 2380 would be nice. 2381 2382 2383 2384 I kinda want an omnibus dataview that combines list, tree, 2385 and table - it can be switched dynamically between them. 2386 2387 Flattening policy: only show top level, show recursive, show grouped 2388 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 2389 2390 Single select, multi select, organization, drag+drop 2391 */ 2392 2393 //static if(UsingSimpledisplayX11) 2394 version(win32_widgets) {} 2395 else version(custom_widgets) { 2396 enum scrollClickRepeatInterval = 50; 2397 2398 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 2399 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 2400 enum activeTabColor = lightAccentColor; 2401 enum hoveringColor = Color(228, 228, 228); 2402 enum buttonColor = windowBackgroundColor; 2403 enum depressedButtonColor = darkAccentColor; 2404 enum activeListXorColor = Color(255, 255, 127); 2405 enum progressBarColor = Color(0, 0, 128); 2406 enum activeMenuItemColor = Color(0, 0, 128); 2407 2408 }} 2409 else static assert(false); 2410 deprecated("Get these properties off the `visualTheme` instead.") { 2411 // these are used by horizontal rule so not just custom_widgets. for now at least. 2412 enum darkAccentColor = Color(172, 172, 172); 2413 enum lightAccentColor = Color(223, 223, 223); // used to be 223 2414 } 2415 2416 private const(wchar)* toWstringzInternal(in char[] s) { 2417 wchar[] str; 2418 str.reserve(s.length + 1); 2419 foreach(dchar ch; s) 2420 str ~= ch; 2421 str ~= '\0'; 2422 return str.ptr; 2423 } 2424 2425 static if(SimpledisplayTimerAvailable) 2426 void setClickRepeat(Widget w, int interval, int delay = 250) { 2427 Timer timer; 2428 int delayRemaining = delay / interval; 2429 if(delayRemaining <= 1) 2430 delayRemaining = 2; 2431 2432 immutable originalDelayRemaining = delayRemaining; 2433 2434 w.addDirectEventListener("mousedown", (Event ev) { 2435 if(ev.srcElement !is w) 2436 return; 2437 if(timer !is null) { 2438 timer.destroy(); 2439 timer = null; 2440 } 2441 delayRemaining = originalDelayRemaining; 2442 timer = new Timer(interval, () { 2443 if(delayRemaining > 0) 2444 delayRemaining--; 2445 else { 2446 auto ev = new ClickEvent(w); 2447 ev.sendDirectly(); 2448 } 2449 }); 2450 }); 2451 2452 w.addDirectEventListener("mouseup", (Event ev) { 2453 if(ev.srcElement !is w) 2454 return; 2455 if(timer !is null) { 2456 timer.destroy(); 2457 timer = null; 2458 } 2459 }); 2460 2461 w.addDirectEventListener("mouseleave", (Event ev) { 2462 if(ev.srcElement !is w) 2463 return; 2464 if(timer !is null) { 2465 timer.destroy(); 2466 timer = null; 2467 } 2468 }); 2469 2470 } 2471 else 2472 void setClickRepeat(Widget w, int interval, int delay = 250) {} 2473 2474 enum FrameStyle { 2475 none, /// 2476 risen, /// a 3d pop-out effect (think Windows 95 button) 2477 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 2478 solid, /// 2479 dotted, /// 2480 fantasy, /// a style based on a popular fantasy video game 2481 } 2482 2483 version(custom_widgets) 2484 deprecated 2485 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 2486 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2487 } 2488 2489 version(custom_widgets) 2490 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 2491 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 2492 } 2493 2494 version(custom_widgets) 2495 deprecated 2496 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 2497 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2498 } 2499 2500 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 2501 int borderWidth; 2502 final switch(style) { 2503 case FrameStyle.sunk, FrameStyle.risen: 2504 // outer layer 2505 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 2506 borderWidth = 2; 2507 break; 2508 case FrameStyle.none: 2509 painter.outlineColor = background; 2510 borderWidth = 0; 2511 break; 2512 case FrameStyle.solid: 2513 painter.pen = Pen(border, 1); 2514 borderWidth = 1; 2515 break; 2516 case FrameStyle.dotted: 2517 painter.pen = Pen(border, 1, Pen.Style.Dotted); 2518 borderWidth = 1; 2519 break; 2520 case FrameStyle.fantasy: 2521 painter.pen = Pen(border, 3); 2522 borderWidth = 3; 2523 break; 2524 } 2525 2526 painter.fillColor = background; 2527 painter.drawRectangle(Point(x + 0, y + 0), width, height); 2528 2529 2530 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 2531 // 3d effect 2532 auto vt = WidgetPainter.visualTheme; 2533 2534 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 2535 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 2536 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 2537 2538 // inner layer 2539 //right, bottom 2540 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 2541 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 2542 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 2543 // left, top 2544 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 2545 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 2546 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 2547 } else if(style == FrameStyle.fantasy) { 2548 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 2549 painter.fillColor = Color.transparent; 2550 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 2551 } 2552 2553 return borderWidth; 2554 } 2555 2556 /++ 2557 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. 2558 2559 See_Also: 2560 [MenuItem] 2561 [ToolButton] 2562 [Menu.addItem] 2563 +/ 2564 class Action { 2565 version(win32_widgets) { 2566 private int id; 2567 private static int lastId = 9000; 2568 private static Action[int] mapping; 2569 } 2570 2571 KeyEvent accelerator; 2572 2573 // FIXME: disable message 2574 // and toggle thing? 2575 // ??? and trigger arguments too ??? 2576 2577 /++ 2578 Params: 2579 label = the textual label 2580 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 2581 triggered = initial handler, more can be added via the [triggered] member. 2582 +/ 2583 this(string label, ushort icon = 0, void delegate() triggered = null) { 2584 this.label = label; 2585 this.iconId = icon; 2586 if(triggered !is null) 2587 this.triggered ~= triggered; 2588 version(win32_widgets) { 2589 id = ++lastId; 2590 mapping[id] = this; 2591 } 2592 } 2593 2594 private string label; 2595 private ushort iconId; 2596 // icon 2597 2598 // when it is triggered, the triggered event is fired on the window 2599 /// The list of handlers when it is triggered. 2600 void delegate()[] triggered; 2601 } 2602 2603 /* 2604 plan: 2605 keyboard accelerators 2606 2607 * menus (and popups and tooltips) 2608 * status bar 2609 * toolbars and buttons 2610 2611 sortable table view 2612 2613 maybe notification area icons 2614 basic clipboard 2615 2616 * radio box 2617 splitter 2618 toggle buttons (optionally mutually exclusive, like in Paint) 2619 label, rich text display, multi line plain text (selectable) 2620 * fieldset 2621 * nestable grid layout 2622 single line text input 2623 * multi line text input 2624 slider 2625 spinner 2626 list box 2627 drop down 2628 combo box 2629 auto complete box 2630 * progress bar 2631 2632 terminal window/widget (on unix it might even be a pty but really idk) 2633 2634 ok button 2635 cancel button 2636 2637 keyboard hotkeys 2638 2639 scroll widget 2640 2641 event redirections and network transparency 2642 script integration 2643 */ 2644 2645 2646 /* 2647 MENUS 2648 2649 auto bar = new MenuBar(window); 2650 window.menuBar = bar; 2651 2652 auto fileMenu = bar.addItem(new Menu("&File")); 2653 fileMenu.addItem(new MenuItem("&Exit")); 2654 2655 2656 EVENTS 2657 2658 For controls, you should usually use "triggered" rather than "click", etc., because 2659 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 2660 This is the case on menus and pushbuttons. 2661 2662 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 2663 */ 2664 2665 2666 /* 2667 enum LinePreference { 2668 AlwaysOnOwnLine, // always on its own line 2669 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 2670 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 2671 } 2672 */ 2673 2674 /++ 2675 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. 2676 2677 --- 2678 class MyWidget : Widget { 2679 this(Widget parent) { super(parent); } 2680 2681 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 2682 mixin Padding!q{4}; 2683 2684 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 2685 mixin Margin!q{8}; 2686 2687 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 2688 // while Top/Bottom/Right remain 8 from the mixin above. 2689 override int marginLeft() { return 2; } 2690 } 2691 --- 2692 2693 2694 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]). 2695 2696 Padding is the area inside a widget where its background is drawn, but the content avoids. 2697 2698 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!). 2699 2700 * 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. 2701 +/ 2702 mixin template Padding(string code) { 2703 override int paddingLeft() { return mixin(code);} 2704 override int paddingRight() { return mixin(code);} 2705 override int paddingTop() { return mixin(code);} 2706 override int paddingBottom() { return mixin(code);} 2707 } 2708 2709 /// ditto 2710 mixin template Margin(string code) { 2711 override int marginLeft() { return mixin(code);} 2712 override int marginRight() { return mixin(code);} 2713 override int marginTop() { return mixin(code);} 2714 override int marginBottom() { return mixin(code);} 2715 } 2716 2717 private 2718 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 2719 enum calcingV = relevantMeasure == "height"; 2720 2721 parent.registerMovement(); 2722 2723 if(parent.children.length == 0) 2724 return; 2725 2726 auto parentStyle = parent.getComputedStyle(); 2727 2728 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 2729 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 2730 2731 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 2732 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 2733 2734 // my own width and height should already be set by the caller of this function... 2735 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 2736 mixin("parentStyle.padding"~firstThingy~"()") - 2737 mixin("parentStyle.padding"~secondThingy~"()"); 2738 2739 int stretchinessSum; 2740 int stretchyChildSum; 2741 int lastMargin = 0; 2742 2743 int shrinkinessSum; 2744 int shrinkyChildSum; 2745 2746 // set initial size 2747 foreach(child; parent.children) { 2748 2749 auto childStyle = child.getComputedStyle(); 2750 2751 if(cast(StaticPosition) child) 2752 continue; 2753 if(child.hidden) 2754 continue; 2755 2756 const iw = child.flexBasisWidth(); 2757 const ih = child.flexBasisHeight(); 2758 2759 static if(calcingV) { 2760 child.width = parent.width - 2761 mixin("childStyle.margin"~otherFirstThingy~"()") - 2762 mixin("childStyle.margin"~otherSecondThingy~"()") - 2763 mixin("parentStyle.padding"~otherFirstThingy~"()") - 2764 mixin("parentStyle.padding"~otherSecondThingy~"()"); 2765 2766 if(child.width < 0) 2767 child.width = 0; 2768 if(child.width > childStyle.maxWidth()) 2769 child.width = childStyle.maxWidth(); 2770 2771 if(iw > 0) { 2772 auto totalPossible = child.width; 2773 if(child.width > iw && child.widthStretchiness() == 0) 2774 child.width = iw; 2775 } 2776 2777 child.height = mymax(childStyle.minHeight(), ih); 2778 } else { 2779 // set to take all the space 2780 child.height = parent.height - 2781 mixin("childStyle.margin"~firstThingy~"()") - 2782 mixin("childStyle.margin"~secondThingy~"()") - 2783 mixin("parentStyle.padding"~firstThingy~"()") - 2784 mixin("parentStyle.padding"~secondThingy~"()"); 2785 2786 // then clamp it 2787 if(child.height < 0) 2788 child.height = 0; 2789 if(child.height > childStyle.maxHeight()) 2790 child.height = childStyle.maxHeight(); 2791 2792 // and if possible, respect the ideal target 2793 if(ih > 0) { 2794 auto totalPossible = child.height; 2795 if(child.height > ih && child.heightStretchiness() == 0) 2796 child.height = ih; 2797 } 2798 2799 // if we have an ideal, try to respect it, otehrwise, just use the minimum 2800 child.width = mymax(childStyle.minWidth(), iw); 2801 } 2802 2803 spaceRemaining -= mixin("child." ~ relevantMeasure); 2804 2805 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 2806 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 2807 lastMargin = margin; 2808 spaceRemaining -= thisMargin + margin; 2809 2810 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 2811 stretchinessSum += s; 2812 if(s > 0) 2813 stretchyChildSum++; 2814 2815 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 2816 shrinkinessSum += s2; 2817 if(s2 > 0) 2818 shrinkyChildSum++; 2819 } 2820 2821 if(spaceRemaining < 0 && shrinkyChildSum) { 2822 // shrink to get into the space if it is possible 2823 auto toRemove = -spaceRemaining; 2824 auto removalPerItem = toRemove * shrinkinessSum / shrinkyChildSum; 2825 auto remainder = toRemove * shrinkinessSum % shrinkyChildSum; 2826 2827 // FIXME: wtf why am i shrinking things with no shrinkiness? 2828 2829 foreach(child; parent.children) { 2830 auto childStyle = child.getComputedStyle(); 2831 if(cast(StaticPosition) child) 2832 continue; 2833 if(child.hidden) 2834 continue; 2835 static if(calcingV) { 2836 auto maximum = childStyle.maxHeight(); 2837 } else { 2838 auto maximum = childStyle.maxWidth(); 2839 } 2840 2841 mixin("child._" ~ relevantMeasure) -= removalPerItem + remainder; // this is removing more than needed to trigger the next thing. ugh. 2842 2843 spaceRemaining += removalPerItem + remainder; 2844 } 2845 } 2846 2847 // stretch to fill space 2848 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 2849 auto spacePerChild = spaceRemaining / stretchinessSum; 2850 bool spreadEvenly; 2851 bool giveToBiggest; 2852 if(spacePerChild <= 0) { 2853 spacePerChild = spaceRemaining / stretchyChildSum; 2854 spreadEvenly = true; 2855 } 2856 if(spacePerChild <= 0) { 2857 giveToBiggest = true; 2858 } 2859 int previousSpaceRemaining = spaceRemaining; 2860 stretchinessSum = 0; 2861 Widget mostStretchy; 2862 int mostStretchyS; 2863 foreach(child; parent.children) { 2864 auto childStyle = child.getComputedStyle(); 2865 if(cast(StaticPosition) child) 2866 continue; 2867 if(child.hidden) 2868 continue; 2869 static if(calcingV) { 2870 auto maximum = childStyle.maxHeight(); 2871 } else { 2872 auto maximum = childStyle.maxWidth(); 2873 } 2874 2875 if(mixin("child." ~ relevantMeasure) >= maximum) { 2876 auto adj = mixin("child." ~ relevantMeasure) - maximum; 2877 mixin("child._" ~ relevantMeasure) -= adj; 2878 spaceRemaining += adj; 2879 continue; 2880 } 2881 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 2882 if(s <= 0) 2883 continue; 2884 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 2885 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 2886 spaceRemaining -= spaceAdjustment; 2887 if(mixin("child." ~ relevantMeasure) > maximum) { 2888 auto diff = mixin("child." ~ relevantMeasure) - maximum; 2889 mixin("child._" ~ relevantMeasure) -= diff; 2890 spaceRemaining += diff; 2891 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 2892 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 2893 if(mostStretchy is null || s >= mostStretchyS) { 2894 mostStretchy = child; 2895 mostStretchyS = s; 2896 } 2897 } 2898 } 2899 2900 if(giveToBiggest && mostStretchy !is null) { 2901 auto child = mostStretchy; 2902 auto childStyle = child.getComputedStyle(); 2903 int spaceAdjustment = spaceRemaining; 2904 2905 static if(calcingV) 2906 auto maximum = childStyle.maxHeight(); 2907 else 2908 auto maximum = childStyle.maxWidth(); 2909 2910 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 2911 spaceRemaining -= spaceAdjustment; 2912 if(mixin("child._" ~ relevantMeasure) > maximum) { 2913 auto diff = mixin("child." ~ relevantMeasure) - maximum; 2914 mixin("child._" ~ relevantMeasure) -= diff; 2915 spaceRemaining += diff; 2916 } 2917 } 2918 2919 if(spaceRemaining == previousSpaceRemaining) 2920 break; // apparently nothing more we can do 2921 2922 } 2923 2924 // position 2925 lastMargin = 0; 2926 int currentPos = mixin("parent.padding"~firstThingy~"()"); 2927 foreach(child; parent.children) { 2928 auto childStyle = child.getComputedStyle(); 2929 if(cast(StaticPosition) child) { 2930 child.recomputeChildLayout(); 2931 continue; 2932 } 2933 if(child.hidden) 2934 continue; 2935 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 2936 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 2937 currentPos += thisMargin; 2938 static if(calcingV) { 2939 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 2940 child.y = currentPos; 2941 } else { 2942 child.x = currentPos; 2943 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 2944 2945 } 2946 currentPos += mixin("child." ~ relevantMeasure); 2947 currentPos += margin; 2948 lastMargin = margin; 2949 2950 child.recomputeChildLayout(); 2951 } 2952 } 2953 2954 int mymax(int a, int b) { return a > b ? a : b; } 2955 int mymax(int a, int b, int c) { 2956 auto d = mymax(a, b); 2957 return c > d ? c : d; 2958 } 2959 2960 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 2961 // and here, it must be integrable with the layout, the event system, and not be painted over. 2962 version(win32_widgets) { 2963 2964 // this function just does stuff that a parent window needs for redirection 2965 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 2966 this_.hookedWndProc(msg, wParam, lParam); 2967 2968 switch(msg) { 2969 2970 case WM_VSCROLL, WM_HSCROLL: 2971 auto pos = HIWORD(wParam); 2972 auto m = LOWORD(wParam); 2973 2974 auto scrollbarHwnd = cast(HWND) lParam; 2975 2976 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 2977 2978 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 2979 2980 switch(m) { 2981 /+ 2982 // I don't think those messages are ever actually sent normally by the widget itself, 2983 // they are more used for the keyboard interface. methinks. 2984 case SB_BOTTOM: 2985 //import std.stdio; writeln("end"); 2986 auto event = new Event("scrolltoend", *widgetp); 2987 event.dispatch(); 2988 //if(!event.defaultPrevented) 2989 break; 2990 case SB_TOP: 2991 //import std.stdio; writeln("top"); 2992 auto event = new Event("scrolltobeginning", *widgetp); 2993 event.dispatch(); 2994 break; 2995 case SB_ENDSCROLL: 2996 // idk 2997 break; 2998 +/ 2999 case SB_LINEDOWN: 3000 (*widgetp).emitCommand!"scrolltonextline"(); 3001 return 0; 3002 case SB_LINEUP: 3003 (*widgetp).emitCommand!"scrolltopreviousline"(); 3004 return 0; 3005 case SB_PAGEDOWN: 3006 (*widgetp).emitCommand!"scrolltonextpage"(); 3007 return 0; 3008 case SB_PAGEUP: 3009 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3010 return 0; 3011 case SB_THUMBPOSITION: 3012 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3013 ev.dispatch(); 3014 return 0; 3015 case SB_THUMBTRACK: 3016 // eh kinda lying but i like the real time update display 3017 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3018 ev.dispatch(); 3019 // the event loop doesn't seem to carry on with a requested redraw.. 3020 // so we request it to get our dirty bit set... 3021 // then we need to immediately actually redraw it too for instant feedback to user 3022 if(this_.parentWindow) 3023 this_.parentWindow.actualRedraw(); 3024 return 0; 3025 default: 3026 } 3027 } 3028 break; 3029 3030 case WM_CONTEXTMENU: 3031 auto hwndFrom = cast(HWND) wParam; 3032 3033 auto xPos = cast(short) LOWORD(lParam); 3034 auto yPos = cast(short) HIWORD(lParam); 3035 3036 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3037 POINT p; 3038 p.x = xPos; 3039 p.y = yPos; 3040 ScreenToClient(hwnd, &p); 3041 auto clientX = cast(ushort) p.x; 3042 auto clientY = cast(ushort) p.y; 3043 3044 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 3045 3046 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 3047 return 0; 3048 } 3049 } 3050 break; 3051 3052 case WM_DRAWITEM: 3053 auto dis = cast(DRAWITEMSTRUCT*) lParam; 3054 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 3055 return (*widgetp).handleWmDrawItem(dis); 3056 } 3057 break; 3058 3059 case WM_NOTIFY: 3060 auto hdr = cast(NMHDR*) lParam; 3061 auto hwndFrom = hdr.hwndFrom; 3062 auto code = hdr.code; 3063 3064 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3065 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 3066 } 3067 break; 3068 case WM_COMMAND: 3069 auto handle = cast(HWND) lParam; 3070 auto cmd = HIWORD(wParam); 3071 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 3072 3073 default: 3074 // pass it on 3075 } 3076 return 0; 3077 } 3078 3079 3080 3081 extern(Windows) 3082 private 3083 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 3084 // but can i merge them?! 3085 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3086 //import std.stdio; try { writeln(iMessage); } catch(Exception e) {}; 3087 3088 if(auto te = hWnd in Widget.nativeMapping) { 3089 try { 3090 3091 te.hookedWndProc(iMessage, wParam, lParam); 3092 3093 int mustReturn; 3094 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 3095 if(mustReturn) 3096 return ret; 3097 3098 if(iMessage == WM_SETFOCUS) { 3099 auto lol = *te; 3100 while(lol !is null && lol.implicitlyCreated) 3101 lol = lol.parent; 3102 lol.focus(); 3103 //(*te).parentWindow.focusedWidget = lol; 3104 } 3105 3106 3107 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 3108 SetBkMode(cast(HDC) wParam, TRANSPARENT); 3109 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 3110 //GetStockObject(NULL_BRUSH); 3111 } 3112 3113 auto pos = getChildPositionRelativeToParentOrigin(*te); 3114 lastDefaultPrevented = false; 3115 // try {import std.stdio; writeln(typeid(*te)); } catch(Exception e) {} 3116 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 3117 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 3118 else { 3119 // it was something we recognized, should only call the window procedure if the default was not prevented 3120 } 3121 } catch(Exception e) { 3122 assert(0, e.toString()); 3123 } 3124 return 0; 3125 } 3126 assert(0, "shouldn't be receiving messages for this window...."); 3127 //import std.conv; 3128 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 3129 } 3130 3131 extern(Windows) 3132 private 3133 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3134 if(iMessage == WM_ERASEBKGND) { 3135 auto dc = GetDC(hWnd); 3136 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 3137 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 3138 RECT r; 3139 GetWindowRect(hWnd, &r); 3140 // since the pen is null, to fill the whole space, we need the +1 on both. 3141 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 3142 SelectObject(dc, p); 3143 SelectObject(dc, b); 3144 ReleaseDC(hWnd, dc); 3145 return 1; 3146 } 3147 return HookedWndProc(hWnd, iMessage, wParam, lParam); 3148 } 3149 3150 /++ 3151 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 3152 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 3153 of minigui's expectations. 3154 3155 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 3156 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 3157 3158 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 3159 3160 To check if you can use this, use `static if(UsingWin32Widgets)`. 3161 +/ 3162 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 3163 assert(p.parentWindow !is null); 3164 assert(p.parentWindow.win.impl.hwnd !is null); 3165 3166 auto bsgroupbox = style == BS_GROUPBOX; 3167 3168 HWND phwnd; 3169 3170 auto wtf = p.parent; 3171 while(wtf) { 3172 if(wtf.hwnd !is null) { 3173 phwnd = wtf.hwnd; 3174 break; 3175 } 3176 wtf = wtf.parent; 3177 } 3178 3179 if(phwnd is null) 3180 phwnd = p.parentWindow.win.impl.hwnd; 3181 3182 assert(phwnd !is null); 3183 3184 WCharzBuffer wt = WCharzBuffer(windowText); 3185 3186 style |= WS_VISIBLE | WS_CHILD; 3187 //if(className != WC_TABCONTROL) 3188 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 3189 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 3190 CW_USEDEFAULT, CW_USEDEFAULT, 100, 100, 3191 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 3192 3193 assert(p.hwnd !is null); 3194 3195 3196 static HFONT font; 3197 if(font is null) { 3198 NONCLIENTMETRICS params; 3199 params.cbSize = params.sizeof; 3200 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 3201 font = CreateFontIndirect(¶ms.lfMessageFont); 3202 } 3203 } 3204 3205 if(font) 3206 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 3207 3208 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 3209 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 3210 Widget.nativeMapping[p.hwnd] = p; 3211 3212 if(bsgroupbox) 3213 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 3214 else 3215 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3216 3217 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 3218 3219 p.registerMovement(); 3220 } 3221 } 3222 3223 version(win32_widgets) 3224 private 3225 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 3226 if(hwnd is null || hwnd in Widget.nativeMapping) 3227 return true; 3228 auto parent = cast(Widget) cast(void*) lparam; 3229 Widget p = new Widget(null); 3230 p._parent = parent; 3231 p.parentWindow = parent.parentWindow; 3232 p.hwnd = hwnd; 3233 p.implicitlyCreated = true; 3234 Widget.nativeMapping[p.hwnd] = p; 3235 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3236 return true; 3237 } 3238 3239 /++ 3240 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 3241 +/ 3242 struct WidgetPainter { 3243 this(ScreenPainter screenPainter, Widget drawingUpon) { 3244 this.drawingUpon = drawingUpon; 3245 this.screenPainter = screenPainter; 3246 if(auto font = visualTheme.defaultFontCached) 3247 this.screenPainter.setFont(font); 3248 } 3249 3250 /// 3251 ScreenPainter screenPainter; 3252 /// Forward to the screen painter for other methods 3253 alias screenPainter this; 3254 3255 private Widget drawingUpon; 3256 3257 /++ 3258 This is the list of rectangles that actually need to be redrawn. 3259 3260 Not actually implemented yet. 3261 +/ 3262 Rectangle[] invalidatedRectangles; 3263 3264 private static BaseVisualTheme _visualTheme; 3265 3266 /++ 3267 Functions to access the visual theme and helpers to easily use it. 3268 3269 These are aware of the current widget's computed style out of the theme. 3270 +/ 3271 static @property BaseVisualTheme visualTheme() { 3272 if(_visualTheme is null) 3273 _visualTheme = new DefaultVisualTheme(); 3274 return _visualTheme; 3275 } 3276 3277 /// ditto 3278 static @property void visualTheme(BaseVisualTheme theme) { 3279 _visualTheme = theme; 3280 } 3281 3282 /// ditto 3283 Color themeForeground() { 3284 return drawingUpon.getComputedStyle().foregroundColor(); 3285 } 3286 3287 /// ditto 3288 Color themeBackground() { 3289 return drawingUpon.getComputedStyle().background.color; 3290 } 3291 3292 int isDarkTheme() { 3293 return 0; // unspecified, yes, no as enum. FIXME 3294 } 3295 3296 /++ 3297 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. 3298 3299 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 3300 3301 If you change teh clip rectangle, you should change it back before you return. 3302 3303 3304 The sequence it uses is: 3305 background 3306 content (delegated to you) 3307 border 3308 focused outline 3309 selected overlay 3310 3311 Example code: 3312 3313 --- 3314 void paint(WidgetPainter painter) { 3315 painter.drawThemed((bounds) { 3316 return bounds; // if the selection overlay should be contained, you can return it here. 3317 }); 3318 } 3319 --- 3320 +/ 3321 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 3322 drawThemed((WidgetPainter painter, const Rectangle bounds) { 3323 return drawBody(bounds); 3324 }); 3325 } 3326 // this overload is actually mroe for setting the delegate to a virtual function 3327 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 3328 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 3329 3330 auto cs = drawingUpon.getComputedStyle(); 3331 3332 auto bg = cs.background.color; 3333 3334 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 3335 3336 rect.left += borderWidth; 3337 rect.right -= borderWidth; 3338 rect.top += borderWidth; 3339 rect.bottom -= borderWidth; 3340 3341 auto insideBorderRect = rect; 3342 3343 rect.left += cs.paddingLeft; 3344 rect.right -= cs.paddingRight; 3345 rect.top += cs.paddingTop; 3346 rect.bottom += cs.paddingBottom; 3347 3348 this.outlineColor = this.themeForeground; 3349 this.fillColor = bg; 3350 3351 auto widgetFont = cs.fontCached; 3352 if(widgetFont !is null) 3353 this.setFont(widgetFont); 3354 3355 rect = drawBody(this, rect); 3356 3357 if(widgetFont !is null) { 3358 if(auto vtFont = visualTheme.defaultFontCached) 3359 this.setFont(vtFont); 3360 else 3361 this.setFont(null); 3362 } 3363 3364 if(auto os = cs.outlineStyle()) { 3365 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 3366 this.fillColor = Color.transparent; 3367 this.drawRectangle(insideBorderRect); 3368 } 3369 } 3370 3371 /++ 3372 First, draw the background. 3373 Then draw your content. 3374 Next, draw the border. 3375 And the focused indicator. 3376 And the is-selected box. 3377 3378 If it is focused i can draw the outline too... 3379 3380 If selected i can even do the xor action but that's at the end. 3381 +/ 3382 void drawThemeBackground() { 3383 3384 } 3385 3386 void drawThemeBorder() { 3387 3388 } 3389 3390 // all this stuff is a dangerous experiment.... 3391 static class ScriptableVersion { 3392 ScreenPainterImplementation* p; 3393 int originX, originY; 3394 3395 @scriptable: 3396 void drawRectangle(int x, int y, int width, int height) { 3397 p.drawRectangle(x + originX, y + originY, width, height); 3398 } 3399 void drawLine(int x1, int y1, int x2, int y2) { 3400 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 3401 } 3402 void drawText(int x, int y, string text) { 3403 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 3404 } 3405 void setOutlineColor(int r, int g, int b) { 3406 p.pen = Pen(Color(r,g,b), 1); 3407 } 3408 void setFillColor(int r, int g, int b) { 3409 p.fillColor = Color(r,g,b); 3410 } 3411 } 3412 3413 ScriptableVersion toArsdJsvar() { 3414 auto sv = new ScriptableVersion; 3415 sv.p = this.screenPainter.impl; 3416 sv.originX = this.screenPainter.originX; 3417 sv.originY = this.screenPainter.originY; 3418 return sv; 3419 } 3420 3421 static WidgetPainter fromJsVar(T)(T t) { 3422 return WidgetPainter.init; 3423 } 3424 // done.......... 3425 } 3426 3427 3428 struct Style { 3429 static struct helper(string m, T) { 3430 enum method = m; 3431 T v; 3432 3433 mixin template MethodOverride(typeof(this) v) { 3434 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 3435 } 3436 } 3437 3438 static auto opDispatch(string method, T)(T value) { 3439 return helper!(method, T)(value); 3440 } 3441 } 3442 3443 /++ 3444 Implementation detail of the [ControlledBy] UDA. 3445 3446 History: 3447 Added Oct 28, 2020 3448 +/ 3449 struct ControlledBy_(T, Args...) { 3450 Args args; 3451 3452 static if(Args.length) 3453 this(Args args) { 3454 this.args = args; 3455 } 3456 3457 private T construct(Widget parent) { 3458 return new T(args, parent); 3459 } 3460 } 3461 3462 /++ 3463 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 3464 3465 History: 3466 Added Oct 28, 2020 3467 +/ 3468 auto ControlledBy(T, Args...)(Args args) { 3469 return ControlledBy_!(T, Args)(args); 3470 } 3471 3472 struct ContainerMeta { 3473 string name; 3474 ContainerMeta[] children; 3475 Widget function(Widget parent) factory; 3476 3477 Widget instantiate(Widget parent) { 3478 auto n = factory(parent); 3479 n.name = name; 3480 foreach(child; children) 3481 child.instantiate(n); 3482 return n; 3483 } 3484 } 3485 3486 /++ 3487 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 3488 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 3489 3490 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 3491 structures. It works fine on structs declared inside functions though. 3492 3493 See: https://issues.dlang.org/show_bug.cgi?id=21984 3494 +/ 3495 template Container(CArgs...) { 3496 static if(CArgs.length && is(CArgs[0] : Widget)) { 3497 private alias Super = CArgs[0]; 3498 private alias CArgs2 = CArgs[1 .. $]; 3499 } else { 3500 private alias Super = Layout; 3501 private alias CArgs2 = CArgs; 3502 } 3503 3504 class Container : Super { 3505 this(Widget parent) { super(parent); } 3506 3507 // just to partially support old gdc versions 3508 version(GNU) { 3509 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 3510 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 3511 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 3512 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 3513 } else mixin(q{ 3514 static foreach(Arg; CArgs2) { 3515 mixin Arg.MethodOverride!(Arg); 3516 } 3517 }); 3518 3519 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 3520 return ContainerMeta( 3521 name, 3522 children.dup, 3523 function (Widget parent) { return new typeof(this)(parent); } 3524 ); 3525 } 3526 3527 static ContainerMeta opCall(ContainerMeta[] children...) { 3528 return opCall(null, children); 3529 } 3530 } 3531 } 3532 3533 /++ 3534 The data controller widget is created by reflecting over the given 3535 data type. You can use [ControlledBy] as a UDA on a struct or 3536 just let it create things automatically. 3537 3538 Unlike [dialog], this uses real-time updating of the data and 3539 you add it to another window yourself. 3540 3541 --- 3542 struct Test { 3543 int x; 3544 int y; 3545 } 3546 3547 auto window = new Window(); 3548 auto dcw = new DataControllerWidget!Test(new Test, window); 3549 --- 3550 3551 The way it works is any public members are given a widget based 3552 on their data type, and public methods trigger an action button 3553 if no relevant parameters or a dialog action if it does have 3554 parameters, similar to the [menu] facility. 3555 3556 If you change data programmatically, without going through the 3557 DataControllerWidget methods, you will have to tell it something 3558 has changed and it needs to redraw. This is done with the `invalidate` 3559 method. 3560 3561 History: 3562 Added Oct 28, 2020 3563 +/ 3564 /// Group: generating_from_code 3565 class DataControllerWidget(T) : WidgetContainer { 3566 static if(is(T == class) || is(T : const E[], E)) 3567 private alias Tref = T; 3568 else 3569 private alias Tref = T*; 3570 3571 Tref datum; 3572 3573 /++ 3574 See_also: [addDataControllerWidget] 3575 +/ 3576 this(Tref datum, Widget parent) { 3577 this.datum = datum; 3578 3579 Widget cp = this; 3580 3581 super(parent); 3582 3583 foreach(attr; __traits(getAttributes, T)) 3584 static if(is(typeof(attr) == ContainerMeta)) { 3585 cp = attr.instantiate(this); 3586 } 3587 3588 auto def = this.getByName("default"); 3589 if(def !is null) 3590 cp = def; 3591 3592 Widget helper(string name) { 3593 auto maybe = this.getByName(name); 3594 if(maybe is null) 3595 return cp; 3596 return maybe; 3597 3598 } 3599 3600 foreach(member; __traits(allMembers, T)) 3601 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 3602 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 3603 void delegate() update; 3604 3605 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 3606 3607 if(update) 3608 updaters ~= update; 3609 3610 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 3611 w.addEventListener("triggered", delegate() { 3612 makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))(); 3613 notifyDataUpdated(); 3614 }); 3615 } else static if(is(typeof(w.isChecked) == bool)) { 3616 w.addEventListener(EventType.change, (Event ev) { 3617 __traits(getMember, this.datum, member) = w.isChecked; 3618 }); 3619 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 3620 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 3621 } else static if(is(typeof(w.value) == int)) { 3622 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 3623 } else static if(is(typeof(w) == DropDownSelection)) { 3624 // special case for this to kinda support enums and such. coudl be better though 3625 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 3626 } else { 3627 static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 3628 } 3629 } 3630 } 3631 3632 /++ 3633 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 3634 3635 History: 3636 Added May 28, 2021 3637 +/ 3638 void notifyDataUpdated() { 3639 foreach(updater; updaters) 3640 updater(); 3641 3642 this.emit!(ChangeEvent!void)(delegate{}); 3643 } 3644 3645 private Widget[string] memberWidgets; 3646 private void delegate()[] updaters; 3647 3648 mixin Emits!(ChangeEvent!void); 3649 } 3650 3651 private int saturatedSum(int[] values...) { 3652 int sum; 3653 foreach(value; values) { 3654 if(value == int.max) 3655 return int.max; 3656 sum += value; 3657 } 3658 return sum; 3659 } 3660 3661 void genericSetValue(T, W)(T* where, W what) { 3662 import std.conv; 3663 *where = to!T(what); 3664 //*where = cast(T) stringToLong(what); 3665 } 3666 3667 /++ 3668 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 3669 3670 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 3671 3672 Note that this creates the widget but does not attach any event handlers to it. 3673 +/ 3674 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 3675 3676 string displayName = __traits(identifier, tt).beautify; 3677 3678 static if(controlledByCount!tt == 1) { 3679 foreach(i, attr; __traits(getAttributes, tt)) { 3680 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 3681 auto w = attr.construct(parent); 3682 static if(__traits(compiles, w.setPosition(*valptr))) 3683 update = () { w.setPosition(*valptr); }; 3684 else static if(__traits(compiles, w.setValue(*valptr))) 3685 update = () { w.setValue(*valptr); }; 3686 3687 if(update) 3688 update(); 3689 return w; 3690 } 3691 } 3692 } else static if(controlledByCount!tt == 0) { 3693 static if(is(typeof(tt) == enum)) { 3694 // FIXME: update 3695 auto dds = new DropDownSelection(parent); 3696 foreach(idx, option; __traits(allMembers, typeof(tt))) { 3697 dds.addOption(option); 3698 if(__traits(getMember, typeof(tt), option) == *valptr) 3699 dds.setSelection(cast(int) idx); 3700 } 3701 return dds; 3702 } else static if(is(typeof(tt) == bool)) { 3703 auto box = new Checkbox(displayName, parent); 3704 update = () { box.isChecked = *valptr; }; 3705 update(); 3706 return box; 3707 } else static if(is(typeof(tt) : const long)) { 3708 auto le = new LabeledLineEdit(displayName, parent); 3709 update = () { le.content = toInternal!string(*valptr); }; 3710 update(); 3711 return le; 3712 } else static if(is(typeof(tt) : const string)) { 3713 auto le = new LabeledLineEdit(displayName, parent); 3714 update = () { le.content = *valptr; }; 3715 update(); 3716 return le; 3717 } else static if(is(typeof(tt) == function)) { 3718 auto w = new Button(displayName, parent); 3719 return w; 3720 } 3721 } else static assert(0, "multiple controllers not yet supported"); 3722 } 3723 3724 private template controlledByCount(alias tt) { 3725 static int helper() { 3726 int count; 3727 foreach(i, attr; __traits(getAttributes, tt)) 3728 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 3729 count++; 3730 return count; 3731 } 3732 3733 enum controlledByCount = helper; 3734 } 3735 3736 /++ 3737 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 3738 3739 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 3740 3741 History: 3742 The `redrawOnChange` parameter was added on May 28, 2021. 3743 +/ 3744 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class)) { 3745 auto dcw = new DataControllerWidget!T(t, parent); 3746 initializeDataControllerWidget(dcw, redrawOnChange); 3747 return dcw; 3748 } 3749 3750 /// ditto 3751 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 3752 auto dcw = new DataControllerWidget!T(t, parent); 3753 initializeDataControllerWidget(dcw, redrawOnChange); 3754 return dcw; 3755 } 3756 3757 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 3758 if(redrawOnChange !is null) 3759 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 3760 } 3761 3762 /++ 3763 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. 3764 3765 History: 3766 Finalized on June 3, 2021 for the dub v10.0 release 3767 +/ 3768 struct StyleInformation { 3769 private Widget w; 3770 private BaseVisualTheme visualTheme; 3771 3772 private this(Widget w) { 3773 this.w = w; 3774 this.visualTheme = WidgetPainter.visualTheme; 3775 } 3776 3777 /// Forwards to [Widget.Style] 3778 // through the [VisualTheme] 3779 public @property opDispatch(string name)() { 3780 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 3781 w.useStyleProperties((scope Widget.Style props) { 3782 //visualTheme.useStyleProperties(w, (props) { 3783 prop = __traits(getMember, props, name); 3784 }); 3785 return prop; 3786 } 3787 3788 @property { 3789 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 3790 /** */ int paddingLeft() { return w.paddingLeft(); } 3791 /** */ int paddingRight() { return w.paddingRight(); } 3792 /** */ int paddingTop() { return w.paddingTop(); } 3793 /** */ int paddingBottom() { return w.paddingBottom(); } 3794 3795 /** */ int marginLeft() { return w.marginLeft(); } 3796 /** */ int marginRight() { return w.marginRight(); } 3797 /** */ int marginTop() { return w.marginTop(); } 3798 /** */ int marginBottom() { return w.marginBottom(); } 3799 3800 /** */ int maxHeight() { return w.maxHeight(); } 3801 /** */ int minHeight() { return w.minHeight(); } 3802 3803 /** */ int maxWidth() { return w.maxWidth(); } 3804 /** */ int minWidth() { return w.minWidth(); } 3805 3806 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 3807 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 3808 3809 /** */ int heightStretchiness() { return w.heightStretchiness(); } 3810 /** */ int widthStretchiness() { return w.widthStretchiness(); } 3811 3812 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 3813 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 3814 3815 // Global helpers some of these are unstable. 3816 static: 3817 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 3818 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 3819 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 3820 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 3821 3822 /** */ Color activeTabColor() { return lightAccentColor; } 3823 /** */ Color buttonColor() { return windowBackgroundColor; } 3824 /** */ Color depressedButtonColor() { return darkAccentColor; } 3825 /** */ Color hoveringColor() { return Color(228, 228, 228); } 3826 /** */ Color activeListXorColor() { 3827 auto c = WidgetPainter.visualTheme.selectionColor(); 3828 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 3829 } 3830 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 3831 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 3832 } 3833 3834 3835 3836 /+ 3837 3838 private static auto extractStyleProperty(string name)(Widget w) { 3839 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 3840 w.useStyleProperties((props) { 3841 prop = __traits(getMember, props, name); 3842 }); 3843 return prop; 3844 } 3845 3846 // FIXME: clear this upon a X server disconnect 3847 private static OperatingSystemFont[string] fontCache; 3848 3849 T getProperty(T)(string name, lazy T default_) { 3850 if(visualTheme !is null) { 3851 auto str = visualTheme.getPropertyString(w, name); 3852 if(str is null) 3853 return default_; 3854 static if(is(T == Color)) 3855 return Color.fromString(str); 3856 else static if(is(T == Measurement)) 3857 return Measurement(cast(int) toInternal!int(str)); 3858 else static if(is(T == WidgetBackground)) 3859 return WidgetBackground.fromString(str); 3860 else static if(is(T == OperatingSystemFont)) { 3861 if(auto f = str in fontCache) 3862 return *f; 3863 else 3864 return fontCache[str] = new OperatingSystemFont(str); 3865 } else static if(is(T == FrameStyle)) { 3866 switch(str) { 3867 default: 3868 return FrameStyle.none; 3869 foreach(style; __traits(allMembers, FrameStyle)) 3870 case style: 3871 return __traits(getMember, FrameStyle, style); 3872 } 3873 } else static assert(0); 3874 } else 3875 return default_; 3876 } 3877 3878 static struct Measurement { 3879 int value; 3880 alias value this; 3881 } 3882 3883 @property: 3884 3885 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 3886 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 3887 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 3888 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 3889 3890 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 3891 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 3892 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 3893 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 3894 3895 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 3896 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 3897 3898 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 3899 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 3900 3901 3902 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 3903 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 3904 3905 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 3906 3907 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 3908 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 3909 3910 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 3911 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 3912 3913 3914 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 3915 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 3916 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 3917 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 3918 3919 Color activeTabColor() { return lightAccentColor; } 3920 Color buttonColor() { return windowBackgroundColor; } 3921 Color depressedButtonColor() { return darkAccentColor; } 3922 Color hoveringColor() { return Color(228, 228, 228); } 3923 Color activeListXorColor() { 3924 auto c = WidgetPainter.visualTheme.selectionColor(); 3925 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 3926 } 3927 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 3928 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 3929 +/ 3930 } 3931 3932 3933 3934 // pragma(msg, __traits(classInstanceSize, Widget)); 3935 3936 /*private*/ template EventString(E) { 3937 static if(is(typeof(E.EventString))) 3938 enum EventString = E.EventString; 3939 else 3940 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 3941 } 3942 3943 /*private*/ template EventStringIdentifier(E) { 3944 string helper() { 3945 auto es = EventString!E; 3946 char[] id = new char[](es.length * 2); 3947 size_t idx; 3948 foreach(char ch; es) { 3949 id[idx++] = cast(char)('a' + (ch >> 4)); 3950 id[idx++] = cast(char)('a' + (ch & 0x0f)); 3951 } 3952 return cast(string) id; 3953 } 3954 3955 enum EventStringIdentifier = helper(); 3956 } 3957 3958 3959 template classStaticallyEmits(This, EventType) { 3960 static if(is(This Base == super)) 3961 static if(is(Base : Widget)) 3962 enum baseEmits = classStaticallyEmits!(Base, EventType); 3963 else 3964 enum baseEmits = false; 3965 else 3966 enum baseEmits = false; 3967 3968 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 3969 3970 enum classStaticallyEmits = thisEmits || baseEmits; 3971 } 3972 3973 /++ 3974 A helper to make widgets out of other native windows. 3975 3976 History: 3977 Factored out of OpenGlWidget on November 5, 2021 3978 +/ 3979 class NestedChildWindowWidget : Widget { 3980 SimpleWindow win; 3981 3982 /// 3983 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 3984 this(SimpleWindow win, Widget parent) { 3985 this.parentWindow = parent.parentWindow; 3986 this.win = win; 3987 3988 super(parent); 3989 windowsetup(win); 3990 } 3991 3992 static protected SimpleWindow getParentWindow(Widget parent) { 3993 assert(parent !is null); 3994 SimpleWindow pwin = parent.parentWindow.win; 3995 3996 version(win32_widgets) { 3997 HWND phwnd; 3998 auto wtf = parent; 3999 while(wtf) { 4000 if(wtf.hwnd) { 4001 phwnd = wtf.hwnd; 4002 break; 4003 } 4004 wtf = wtf.parent; 4005 } 4006 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 4007 if(phwnd) 4008 pwin = new SimpleWindow(phwnd); 4009 } 4010 4011 return pwin; 4012 } 4013 4014 protected void windowsetup(SimpleWindow w) { 4015 /* 4016 win.onFocusChange = (bool getting) { 4017 if(getting) 4018 this.focus(); 4019 }; 4020 */ 4021 4022 version(win32_widgets) { 4023 Widget.nativeMapping[win.hwnd] = this; 4024 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4025 } else { 4026 win.setEventHandlers( 4027 (MouseEvent e) { 4028 Widget p = this; 4029 while(p ! is parentWindow) { 4030 e.x += p.x; 4031 e.y += p.y; 4032 p = p.parent; 4033 } 4034 parentWindow.dispatchMouseEvent(e); 4035 }, 4036 (KeyEvent e) { 4037 //import std.stdio; 4038 //writefln("%x %s", cast(uint) e.key, e.key); 4039 parentWindow.dispatchKeyEvent(e); 4040 }, 4041 (dchar e) { 4042 parentWindow.dispatchCharEvent(e); 4043 }, 4044 ); 4045 } 4046 4047 } 4048 4049 override void showing(bool s, bool recalc) { 4050 auto cur = hidden; 4051 win.hidden = !s; 4052 if(cur != s && s) 4053 redraw(); 4054 } 4055 4056 /// OpenGL widgets cannot have child widgets. Do not call this. 4057 /* @disable */ final override void addChild(Widget, int) { 4058 throw new Error("cannot add children to OpenGL widgets"); 4059 } 4060 4061 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 4062 /// Keep in mind that events like mouse coordinates are still relative to your size. 4063 override void registerMovement() { 4064 //import std.stdio; writefln("%d %d %d %d", x,y,width,height); 4065 version(win32_widgets) 4066 auto pos = getChildPositionRelativeToParentHwnd(this); 4067 else 4068 auto pos = getChildPositionRelativeToParentOrigin(this); 4069 win.moveResize(pos[0], pos[1], width, height); 4070 4071 registerMovementAdditionalWork(); 4072 sendResizeEvent(); 4073 } 4074 4075 abstract void registerMovementAdditionalWork(); 4076 } 4077 4078 /++ 4079 Nests an opengl capable window inside this window as a widget. 4080 4081 You may also just want to create an additional [SimpleWindow] with 4082 [OpenGlOptions.yes] yourself. 4083 4084 An OpenGL widget cannot have child widgets. It will throw if you try. 4085 +/ 4086 static if(OpenGlEnabled) 4087 class OpenGlWidget : NestedChildWindowWidget { 4088 4089 override void registerMovementAdditionalWork() { 4090 win.setAsCurrentOpenGlContext(); 4091 } 4092 4093 /// 4094 this(Widget parent) { 4095 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4096 super(win, parent); 4097 } 4098 4099 override void paint(WidgetPainter painter) { 4100 glViewport(0, 0, this.width, this.height); 4101 win.redrawOpenGlSceneNow(); 4102 } 4103 4104 void redrawOpenGlScene(void delegate() dg) { 4105 win.redrawOpenGlScene = dg; 4106 } 4107 } 4108 4109 version(custom_widgets) 4110 private alias ListWidgetBase = ScrollableWidget; 4111 else 4112 private alias ListWidgetBase = Widget; 4113 4114 /++ 4115 A list widget contains a list of strings that the user can examine and select. 4116 4117 4118 In the future, items in the list may be possible to be more than just strings. 4119 4120 See_Also: 4121 [TableView] 4122 +/ 4123 class ListWidget : ListWidgetBase { 4124 /// 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. 4125 mixin Emits!(ChangeEvent!void); 4126 4127 static struct Option { 4128 string label; 4129 bool selected; 4130 } 4131 4132 /++ 4133 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 4134 +/ 4135 void setSelection(int y) { 4136 if(!multiSelect) 4137 foreach(ref opt; options) 4138 opt.selected = false; 4139 if(y >= 0 && y < options.length) 4140 options[y].selected = !options[y].selected; 4141 4142 this.emit!(ChangeEvent!void)(delegate {}); 4143 4144 version(custom_widgets) 4145 redraw(); 4146 } 4147 4148 version(custom_widgets) 4149 override void defaultEventHandler_click(ClickEvent event) { 4150 this.focus(); 4151 auto y = (event.clientY - 4) / defaultLineHeight; 4152 if(y >= 0 && y < options.length) { 4153 setSelection(y); 4154 } 4155 super.defaultEventHandler_click(event); 4156 } 4157 4158 this(Widget parent) { 4159 tabStop = false; 4160 super(parent); 4161 version(win32_widgets) 4162 createWin32Window(this, WC_LISTBOX, "", 4163 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 4164 } 4165 4166 version(win32_widgets) 4167 override void handleWmCommand(ushort code, ushort id) { 4168 switch(code) { 4169 case LBN_SELCHANGE: 4170 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 4171 setSelection(cast(int) sel); 4172 break; 4173 default: 4174 } 4175 } 4176 4177 4178 version(custom_widgets) 4179 override void paintFrameAndBackground(WidgetPainter painter) { 4180 draw3dFrame(this, painter, FrameStyle.sunk, Color.white); 4181 } 4182 4183 version(custom_widgets) 4184 override void paint(WidgetPainter painter) { 4185 auto cs = getComputedStyle(); 4186 auto pos = Point(4, 4); 4187 foreach(idx, option; options) { 4188 painter.fillColor = Color.white; 4189 painter.outlineColor = Color.white; 4190 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4191 painter.outlineColor = cs.foregroundColor; 4192 painter.drawText(pos, option.label); 4193 if(option.selected) { 4194 painter.rasterOp = RasterOp.xor; 4195 painter.outlineColor = Color.white; 4196 painter.fillColor = cs.activeListXorColor; 4197 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4198 painter.rasterOp = RasterOp.normal; 4199 } 4200 pos.y += defaultLineHeight; 4201 } 4202 } 4203 4204 static class Style : Widget.Style { 4205 override WidgetBackground background() { 4206 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 4207 } 4208 } 4209 mixin OverrideStyle!Style; 4210 //mixin Padding!q{2}; 4211 4212 void addOption(string text) { 4213 options ~= Option(text); 4214 version(win32_widgets) { 4215 WCharzBuffer buffer = WCharzBuffer(text); 4216 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 4217 } 4218 version(custom_widgets) { 4219 setContentSize(width, cast(int) (options.length * defaultLineHeight)); 4220 redraw(); 4221 } 4222 } 4223 4224 void clear() { 4225 options = null; 4226 version(win32_widgets) { 4227 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 4228 {} 4229 4230 } else version(custom_widgets) { 4231 redraw(); 4232 } 4233 } 4234 4235 Option[] options; 4236 version(win32_widgets) 4237 enum multiSelect = false; /// not implemented yet 4238 else 4239 bool multiSelect; 4240 4241 override int heightStretchiness() { return 6; } 4242 } 4243 4244 4245 4246 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 4247 enum ScrollBarShowPolicy { 4248 automatic, /// automatically show the scroll bar if it is necessary 4249 never, /// never show the scroll bar (scrolling must be done programmatically) 4250 always /// always show the scroll bar, even if it is disabled 4251 } 4252 4253 /++ 4254 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 4255 4256 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 4257 +/ 4258 // FIXME ScrollBarShowPolicy 4259 // FIXME: use the ScrollMessageWidget in here now that it exists 4260 class ScrollableWidget : Widget { 4261 // FIXME: make line size configurable 4262 // FIXME: add keyboard controls 4263 version(win32_widgets) { 4264 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 4265 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 4266 auto pos = HIWORD(wParam); 4267 auto m = LOWORD(wParam); 4268 4269 // FIXME: I can reintroduce the 4270 // scroll bars now by using this 4271 // in the top-level window handler 4272 // to forward comamnds 4273 auto scrollbarHwnd = lParam; 4274 switch(m) { 4275 case SB_BOTTOM: 4276 if(msg == WM_HSCROLL) 4277 horizontalScrollTo(contentWidth_); 4278 else 4279 verticalScrollTo(contentHeight_); 4280 break; 4281 case SB_TOP: 4282 if(msg == WM_HSCROLL) 4283 horizontalScrollTo(0); 4284 else 4285 verticalScrollTo(0); 4286 break; 4287 case SB_ENDSCROLL: 4288 // idk 4289 break; 4290 case SB_LINEDOWN: 4291 if(msg == WM_HSCROLL) 4292 horizontalScroll(16); 4293 else 4294 verticalScroll(16); 4295 break; 4296 case SB_LINEUP: 4297 if(msg == WM_HSCROLL) 4298 horizontalScroll(-16); 4299 else 4300 verticalScroll(-16); 4301 break; 4302 case SB_PAGEDOWN: 4303 if(msg == WM_HSCROLL) 4304 horizontalScroll(100); 4305 else 4306 verticalScroll(100); 4307 break; 4308 case SB_PAGEUP: 4309 if(msg == WM_HSCROLL) 4310 horizontalScroll(-100); 4311 else 4312 verticalScroll(-100); 4313 break; 4314 case SB_THUMBPOSITION: 4315 case SB_THUMBTRACK: 4316 if(msg == WM_HSCROLL) 4317 horizontalScrollTo(pos); 4318 else 4319 verticalScrollTo(pos); 4320 4321 if(m == SB_THUMBTRACK) { 4322 // the event loop doesn't seem to carry on with a requested redraw.. 4323 // so we request it to get our dirty bit set... 4324 redraw(); 4325 4326 // then we need to immediately actually redraw it too for instant feedback to user 4327 if(parentWindow) 4328 parentWindow.actualRedraw(); 4329 } 4330 break; 4331 default: 4332 } 4333 } 4334 return super.hookedWndProc(msg, wParam, lParam); 4335 } 4336 } 4337 /// 4338 this(Widget parent) { 4339 this.parentWindow = parent.parentWindow; 4340 4341 version(win32_widgets) { 4342 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 4343 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 4344 super(parent); 4345 } else version(custom_widgets) { 4346 outerContainer = new InternalScrollableContainerWidget(this, parent); 4347 super(outerContainer); 4348 } else static assert(0); 4349 } 4350 4351 version(custom_widgets) 4352 InternalScrollableContainerWidget outerContainer; 4353 4354 override void defaultEventHandler_click(ClickEvent event) { 4355 if(event.button == MouseButton.wheelUp) 4356 verticalScroll(-16); 4357 if(event.button == MouseButton.wheelDown) 4358 verticalScroll(16); 4359 super.defaultEventHandler_click(event); 4360 } 4361 4362 override void defaultEventHandler_keydown(KeyDownEvent event) { 4363 switch(event.key) { 4364 case Key.Left: 4365 horizontalScroll(-16); 4366 break; 4367 case Key.Right: 4368 horizontalScroll(16); 4369 break; 4370 case Key.Up: 4371 verticalScroll(-16); 4372 break; 4373 case Key.Down: 4374 verticalScroll(16); 4375 break; 4376 case Key.Home: 4377 verticalScrollTo(0); 4378 break; 4379 case Key.End: 4380 verticalScrollTo(contentHeight); 4381 break; 4382 case Key.PageUp: 4383 verticalScroll(-160); 4384 break; 4385 case Key.PageDown: 4386 verticalScroll(160); 4387 break; 4388 default: 4389 } 4390 super.defaultEventHandler_keydown(event); 4391 } 4392 4393 4394 version(win32_widgets) 4395 override void recomputeChildLayout() { 4396 super.recomputeChildLayout(); 4397 SCROLLINFO info; 4398 info.cbSize = info.sizeof; 4399 info.nPage = viewportHeight; 4400 info.fMask = SIF_PAGE | SIF_RANGE; 4401 info.nMin = 0; 4402 info.nMax = contentHeight_; 4403 SetScrollInfo(hwnd, SB_VERT, &info, true); 4404 4405 info.cbSize = info.sizeof; 4406 info.nPage = viewportWidth; 4407 info.fMask = SIF_PAGE | SIF_RANGE; 4408 info.nMin = 0; 4409 info.nMax = contentWidth_; 4410 SetScrollInfo(hwnd, SB_HORZ, &info, true); 4411 } 4412 4413 /* 4414 Scrolling 4415 ------------ 4416 4417 You are assigned a width and a height by the layout engine, which 4418 is your viewport box. However, you may draw more than that by setting 4419 a contentWidth and contentHeight. 4420 4421 If these can be contained by the viewport, no scrollbar is displayed. 4422 If they cannot fit though, it will automatically show scroll as necessary. 4423 4424 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 4425 is zero, no vertical scrolling is performed. 4426 4427 If scrolling is necessary, the lib will automatically work with the bars. 4428 When you redraw, the origin and clipping info in the painter is set so if 4429 you just draw everything, it will work, but you can be more efficient by checking 4430 the viewportWidth, viewportHeight, and scrollOrigin members. 4431 */ 4432 4433 /// 4434 final @property int viewportWidth() { 4435 return width - (showingVerticalScroll ? 16 : 0); 4436 } 4437 /// 4438 final @property int viewportHeight() { 4439 return height - (showingHorizontalScroll ? 16 : 0); 4440 } 4441 4442 // FIXME property 4443 Point scrollOrigin_; 4444 4445 /// 4446 final const(Point) scrollOrigin() { 4447 return scrollOrigin_; 4448 } 4449 4450 // the user sets these two 4451 private int contentWidth_ = 0; 4452 private int contentHeight_ = 0; 4453 4454 /// 4455 int contentWidth() { return contentWidth_; } 4456 /// 4457 int contentHeight() { return contentHeight_; } 4458 4459 /// 4460 void setContentSize(int width, int height) { 4461 contentWidth_ = width; 4462 contentHeight_ = height; 4463 4464 version(custom_widgets) { 4465 if(showingVerticalScroll || showingHorizontalScroll) { 4466 outerContainer.recomputeChildLayout(); 4467 } 4468 4469 if(showingVerticalScroll()) 4470 outerContainer.verticalScrollBar.redraw(); 4471 if(showingHorizontalScroll()) 4472 outerContainer.horizontalScrollBar.redraw(); 4473 } else version(win32_widgets) { 4474 recomputeChildLayout(); 4475 } else static assert(0); 4476 } 4477 4478 /// 4479 void verticalScroll(int delta) { 4480 verticalScrollTo(scrollOrigin.y + delta); 4481 } 4482 /// 4483 void verticalScrollTo(int pos) { 4484 scrollOrigin_.y = pos; 4485 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 4486 scrollOrigin_.y = contentHeight - viewportHeight; 4487 4488 if(scrollOrigin_.y < 0) 4489 scrollOrigin_.y = 0; 4490 4491 version(win32_widgets) { 4492 SCROLLINFO info; 4493 info.cbSize = info.sizeof; 4494 info.fMask = SIF_POS; 4495 info.nPos = scrollOrigin_.y; 4496 SetScrollInfo(hwnd, SB_VERT, &info, true); 4497 } else version(custom_widgets) { 4498 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 4499 } else static assert(0); 4500 4501 redraw(); 4502 } 4503 4504 /// 4505 void horizontalScroll(int delta) { 4506 horizontalScrollTo(scrollOrigin.x + delta); 4507 } 4508 /// 4509 void horizontalScrollTo(int pos) { 4510 scrollOrigin_.x = pos; 4511 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 4512 scrollOrigin_.x = contentWidth - viewportWidth; 4513 4514 if(scrollOrigin_.x < 0) 4515 scrollOrigin_.x = 0; 4516 4517 version(win32_widgets) { 4518 SCROLLINFO info; 4519 info.cbSize = info.sizeof; 4520 info.fMask = SIF_POS; 4521 info.nPos = scrollOrigin_.x; 4522 SetScrollInfo(hwnd, SB_HORZ, &info, true); 4523 } else version(custom_widgets) { 4524 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 4525 } else static assert(0); 4526 4527 redraw(); 4528 } 4529 /// 4530 void scrollTo(Point p) { 4531 verticalScrollTo(p.y); 4532 horizontalScrollTo(p.x); 4533 } 4534 4535 /// 4536 void ensureVisibleInScroll(Point p) { 4537 auto rect = viewportRectangle(); 4538 if(rect.contains(p)) 4539 return; 4540 if(p.x < rect.left) 4541 horizontalScroll(p.x - rect.left); 4542 else if(p.x > rect.right) 4543 horizontalScroll(p.x - rect.right); 4544 4545 if(p.y < rect.top) 4546 verticalScroll(p.y - rect.top); 4547 else if(p.y > rect.bottom) 4548 verticalScroll(p.y - rect.bottom); 4549 } 4550 4551 /// 4552 void ensureVisibleInScroll(Rectangle rect) { 4553 ensureVisibleInScroll(rect.upperLeft); 4554 ensureVisibleInScroll(rect.lowerRight); 4555 } 4556 4557 /// 4558 Rectangle viewportRectangle() { 4559 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 4560 } 4561 4562 /// 4563 bool showingHorizontalScroll() { 4564 return contentWidth > width; 4565 } 4566 /// 4567 bool showingVerticalScroll() { 4568 return contentHeight > height; 4569 } 4570 4571 /// This is called before the ordinary paint delegate, 4572 /// giving you a chance to draw the window frame, etc, 4573 /// before the scroll clip takes effect 4574 void paintFrameAndBackground(WidgetPainter painter) { 4575 version(win32_widgets) { 4576 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 4577 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 4578 // since the pen is null, to fill the whole space, we need the +1 on both. 4579 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 4580 SelectObject(painter.impl.hdc, p); 4581 SelectObject(painter.impl.hdc, b); 4582 } 4583 4584 } 4585 4586 // make space for the scroll bar, and that's it. 4587 final override int paddingRight() { return 16; } 4588 final override int paddingBottom() { return 16; } 4589 4590 /* 4591 END SCROLLING 4592 */ 4593 4594 override WidgetPainter draw() { 4595 int x = this.x, y = this.y; 4596 auto parent = this.parent; 4597 while(parent) { 4598 x += parent.x; 4599 y += parent.y; 4600 parent = parent.parent; 4601 } 4602 4603 //version(win32_widgets) { 4604 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw() : parentWindow.win.draw(); 4605 //} else { 4606 auto painter = parentWindow.win.draw(); 4607 //} 4608 painter.originX = x; 4609 painter.originY = y; 4610 4611 painter.originX = painter.originX - scrollOrigin.x; 4612 painter.originY = painter.originY - scrollOrigin.y; 4613 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 4614 4615 return WidgetPainter(painter, this); 4616 } 4617 4618 mixin ScrollableChildren; 4619 } 4620 4621 // you need to have a Point scrollOrigin in the class somewhere 4622 // and a paintFrameAndBackground 4623 private mixin template ScrollableChildren() { 4624 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force = false) { 4625 if(hidden) 4626 return; 4627 4628 //version(win32_widgets) 4629 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw() : parentWindow.win.draw(); 4630 4631 painter.originX = lox + x; 4632 painter.originY = loy + y; 4633 4634 bool actuallyPainted = false; 4635 4636 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 4637 if(clip == Rectangle.init) 4638 return; 4639 4640 if(force || redrawRequested) { 4641 //painter.setClipRectangle(scrollOrigin, width, height); 4642 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 4643 paintFrameAndBackground(painter); 4644 } 4645 4646 painter.originX = painter.originX - scrollOrigin.x; 4647 painter.originY = painter.originY - scrollOrigin.y; 4648 if(force || redrawRequested) { 4649 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 4650 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 4651 4652 //erase(painter); // we paintFrameAndBackground above so no need 4653 if(painter.visualTheme) 4654 painter.visualTheme.doPaint(this, painter); 4655 else 4656 paint(painter); 4657 4658 actuallyPainted = true; 4659 redrawRequested = false; 4660 } 4661 foreach(child; children) { 4662 if(cast(FixedPosition) child) 4663 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted); 4664 else 4665 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted); 4666 } 4667 } 4668 } 4669 4670 private class InternalScrollableContainerInsideWidget : ContainerWidget { 4671 ScrollableContainerWidget scw; 4672 4673 this(ScrollableContainerWidget parent) { 4674 scw = parent; 4675 super(parent); 4676 } 4677 4678 version(custom_widgets) 4679 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force = false) { 4680 if(hidden) 4681 return; 4682 4683 bool actuallyPainted = false; 4684 4685 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 4686 4687 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 4688 if(clip == Rectangle.init) 4689 return; 4690 4691 painter.originX = lox + x - scrollOrigin.x; 4692 painter.originY = loy + y - scrollOrigin.y; 4693 if(force || redrawRequested) { 4694 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 4695 4696 erase(painter); 4697 if(painter.visualTheme) 4698 painter.visualTheme.doPaint(this, painter); 4699 else 4700 paint(painter); 4701 4702 actuallyPainted = true; 4703 redrawRequested = false; 4704 } 4705 foreach(child; children) { 4706 if(cast(FixedPosition) child) 4707 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted); 4708 else 4709 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted); 4710 } 4711 } 4712 4713 version(custom_widgets) 4714 override protected void addScrollPosition(ref int x, ref int y) { 4715 x += scw.scrollX_; 4716 y += scw.scrollY_; 4717 } 4718 } 4719 4720 /++ 4721 A widget meant to contain other widgets that may need to scroll. 4722 4723 History: 4724 Added July 1, 2021 (dub v10.2) 4725 +/ 4726 class ScrollableContainerWidget : ContainerWidget { 4727 /// 4728 this(Widget parent) { 4729 super(parent); 4730 4731 container = new InternalScrollableContainerInsideWidget(this); 4732 hsb = new HorizontalScrollbar(this); 4733 vsb = new VerticalScrollbar(this); 4734 4735 tabStop = false; 4736 container.tabStop = false; 4737 magic = true; 4738 4739 4740 vsb.addEventListener("scrolltonextline", () { 4741 scrollBy(0, 16); 4742 }); 4743 vsb.addEventListener("scrolltopreviousline", () { 4744 scrollBy(0, -16); 4745 }); 4746 vsb.addEventListener("scrolltonextpage", () { 4747 scrollBy(0, container.height); 4748 }); 4749 vsb.addEventListener("scrolltopreviouspage", () { 4750 scrollBy(0, -container.height); 4751 }); 4752 vsb.addEventListener((scope ScrollToPositionEvent spe) { 4753 scrollTo(scrollX_, spe.value); 4754 }); 4755 4756 this.addEventListener(delegate (scope ClickEvent e) { 4757 if(e.button == MouseButton.wheelUp) { 4758 if(!e.defaultPrevented) 4759 scrollBy(0, -16); 4760 e.stopPropagation(); 4761 } else if(e.button == MouseButton.wheelDown) { 4762 if(!e.defaultPrevented) 4763 scrollBy(0, 16); 4764 e.stopPropagation(); 4765 } 4766 }); 4767 } 4768 4769 /+ 4770 override void defaultEventHandler_click(ClickEvent e) { 4771 } 4772 +/ 4773 4774 override void removeAllChildren() { 4775 container.removeAllChildren(); 4776 } 4777 4778 void scrollTo(int x, int y) { 4779 scrollBy(x - scrollX_, y - scrollY_); 4780 } 4781 4782 void scrollBy(int x, int y) { 4783 auto ox = scrollX_; 4784 auto oy = scrollY_; 4785 4786 auto nx = ox + x; 4787 auto ny = oy + y; 4788 4789 if(nx < 0) 4790 nx = 0; 4791 if(ny < 0) 4792 ny = 0; 4793 4794 auto maxX = hsb.max - container.width; 4795 if(maxX < 0) maxX = 0; 4796 auto maxY = vsb.max - container.height; 4797 if(maxY < 0) maxY = 0; 4798 4799 if(nx > maxX) 4800 nx = maxX; 4801 if(ny > maxY) 4802 ny = maxY; 4803 4804 auto dx = nx - ox; 4805 auto dy = ny - oy; 4806 4807 if(dx || dy) { 4808 version(win32_widgets) 4809 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 4810 else 4811 redraw(); 4812 4813 hsb.setPosition = nx; 4814 vsb.setPosition = ny; 4815 4816 scrollX_ = nx; 4817 scrollY_ = ny; 4818 } 4819 } 4820 4821 private int scrollX_; 4822 private int scrollY_; 4823 4824 void setTotalArea(int width, int height) { 4825 hsb.setMax(width); 4826 vsb.setMax(height); 4827 } 4828 4829 /// 4830 void setViewableArea(int width, int height) { 4831 hsb.setViewableArea(width); 4832 vsb.setViewableArea(height); 4833 } 4834 4835 private bool magic; 4836 override void addChild(Widget w, int position = int.max) { 4837 if(magic) 4838 container.addChild(w, position); 4839 else 4840 super.addChild(w, position); 4841 } 4842 4843 override void recomputeChildLayout() { 4844 if(hsb is null || vsb is null || container is null) return; 4845 4846 /+ 4847 import std.stdio; writeln(x, " ", y , " ", width, " ", height); 4848 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 4849 +/ 4850 4851 registerMovement(); 4852 4853 hsb.height = 16; // FIXME? are tese 16s sane? 4854 hsb.x = 0; 4855 hsb.y = this.height - hsb.height; 4856 hsb.width = this.width - 16; 4857 hsb.recomputeChildLayout(); 4858 4859 vsb.width = 16; // FIXME? 4860 vsb.x = this.width - vsb.width; 4861 vsb.y = 0; 4862 vsb.height = this.height - 16; 4863 vsb.recomputeChildLayout(); 4864 4865 container.x = 0; 4866 container.y = 0; 4867 container.width = this.width - vsb.width; 4868 container.height = this.height - hsb.height; 4869 container.recomputeChildLayout(); 4870 4871 scrollX_ = 0; 4872 scrollY_ = 0; 4873 4874 hsb.setPosition(0); 4875 vsb.setPosition(0); 4876 4877 setTotalArea(this.ContainerWidget.minWidth(), this.ContainerWidget.minHeight()); 4878 setViewableArea(width, height); 4879 } 4880 4881 override int minHeight() { return 64; } 4882 4883 HorizontalScrollbar hsb; 4884 VerticalScrollbar vsb; 4885 ContainerWidget container; 4886 } 4887 4888 4889 version(custom_widgets) 4890 private class InternalScrollableContainerWidget : Widget { 4891 4892 ScrollableWidget sw; 4893 4894 VerticalScrollbar verticalScrollBar; 4895 HorizontalScrollbar horizontalScrollBar; 4896 4897 this(ScrollableWidget sw, Widget parent) { 4898 this.sw = sw; 4899 4900 this.tabStop = false; 4901 4902 horizontalScrollBar = new HorizontalScrollbar(this); 4903 verticalScrollBar = new VerticalScrollbar(this); 4904 4905 horizontalScrollBar.showing_ = false; 4906 verticalScrollBar.showing_ = false; 4907 4908 horizontalScrollBar.addEventListener("scrolltonextline", { 4909 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 4910 sw.horizontalScrollTo(horizontalScrollBar.position); 4911 }); 4912 horizontalScrollBar.addEventListener("scrolltopreviousline", { 4913 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 4914 sw.horizontalScrollTo(horizontalScrollBar.position); 4915 }); 4916 verticalScrollBar.addEventListener("scrolltonextline", { 4917 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 4918 sw.verticalScrollTo(verticalScrollBar.position); 4919 }); 4920 verticalScrollBar.addEventListener("scrolltopreviousline", { 4921 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 4922 sw.verticalScrollTo(verticalScrollBar.position); 4923 }); 4924 horizontalScrollBar.addEventListener("scrolltonextpage", { 4925 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 4926 sw.horizontalScrollTo(horizontalScrollBar.position); 4927 }); 4928 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 4929 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 4930 sw.horizontalScrollTo(horizontalScrollBar.position); 4931 }); 4932 verticalScrollBar.addEventListener("scrolltonextpage", { 4933 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 4934 sw.verticalScrollTo(verticalScrollBar.position); 4935 }); 4936 verticalScrollBar.addEventListener("scrolltopreviouspage", { 4937 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 4938 sw.verticalScrollTo(verticalScrollBar.position); 4939 }); 4940 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 4941 horizontalScrollBar.setPosition(event.intValue); 4942 sw.horizontalScrollTo(horizontalScrollBar.position); 4943 }); 4944 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 4945 verticalScrollBar.setPosition(event.intValue); 4946 sw.verticalScrollTo(verticalScrollBar.position); 4947 }); 4948 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 4949 horizontalScrollBar.setPosition(event.intValue); 4950 sw.horizontalScrollTo(horizontalScrollBar.position); 4951 }); 4952 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 4953 verticalScrollBar.setPosition(event.intValue); 4954 }); 4955 4956 super(parent); 4957 } 4958 4959 // this is supposed to be basically invisible... 4960 override int minWidth() { return sw.minWidth; } 4961 override int minHeight() { return sw.minHeight; } 4962 override int maxWidth() { return sw.maxWidth; } 4963 override int maxHeight() { return sw.maxHeight; } 4964 override int widthStretchiness() { return sw.widthStretchiness; } 4965 override int heightStretchiness() { return sw.heightStretchiness; } 4966 override int marginLeft() { return sw.marginLeft; } 4967 override int marginRight() { return sw.marginRight; } 4968 override int marginTop() { return sw.marginTop; } 4969 override int marginBottom() { return sw.marginBottom; } 4970 override int paddingLeft() { return sw.paddingLeft; } 4971 override int paddingRight() { return sw.paddingRight; } 4972 override int paddingTop() { return sw.paddingTop; } 4973 override int paddingBottom() { return sw.paddingBottom; } 4974 override void focus() { sw.focus(); } 4975 4976 4977 override void recomputeChildLayout() { 4978 // The stupid thing needs to calculate if a scroll bar is needed... 4979 recomputeChildLayoutHelper(); 4980 // then running it again will position things correctly if the bar is NOT needed 4981 recomputeChildLayoutHelper(); 4982 4983 // this sucks but meh it barely works 4984 } 4985 4986 private void recomputeChildLayoutHelper() { 4987 if(sw is null) return; 4988 4989 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 4990 if(horizontalScrollBar && verticalScrollBar) { 4991 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 4992 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 4993 horizontalScrollBar.x = 0; 4994 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 4995 4996 verticalScrollBar.width = verticalScrollBar.minWidth(); 4997 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 4998 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 4999 verticalScrollBar.y = 0 + 2; 5000 5001 sw.x = 0; 5002 sw.y = 0; 5003 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 5004 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 5005 5006 if(sw.contentWidth_ <= this.width) 5007 sw.scrollOrigin_.x = 0; 5008 if(sw.contentHeight_ <= this.height) 5009 sw.scrollOrigin_.y = 0; 5010 5011 horizontalScrollBar.recomputeChildLayout(); 5012 verticalScrollBar.recomputeChildLayout(); 5013 sw.recomputeChildLayout(); 5014 } 5015 5016 if(sw.contentWidth_ <= this.width) 5017 sw.scrollOrigin_.x = 0; 5018 if(sw.contentHeight_ <= this.height) 5019 sw.scrollOrigin_.y = 0; 5020 5021 if(sw.showingHorizontalScroll()) 5022 horizontalScrollBar.showing(true, false); 5023 else 5024 horizontalScrollBar.showing(false, false); 5025 if(sw.showingVerticalScroll()) 5026 verticalScrollBar.showing(true, false); 5027 else 5028 verticalScrollBar.showing(false, false); 5029 5030 verticalScrollBar.setViewableArea(sw.viewportHeight()); 5031 verticalScrollBar.setMax(sw.contentHeight); 5032 verticalScrollBar.setPosition(sw.scrollOrigin.y); 5033 5034 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 5035 horizontalScrollBar.setMax(sw.contentWidth); 5036 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 5037 } 5038 } 5039 5040 /* 5041 class ScrollableClientWidget : Widget { 5042 this(Widget parent) { 5043 super(parent); 5044 } 5045 override void paint(WidgetPainter p) { 5046 parent.paint(p); 5047 } 5048 } 5049 */ 5050 5051 /++ 5052 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. 5053 +/ 5054 abstract class Slider : Widget { 5055 this(int min, int max, int step, Widget parent) { 5056 min_ = min; 5057 max_ = max; 5058 step_ = step; 5059 page_ = step; 5060 super(parent); 5061 } 5062 5063 private int min_; 5064 private int max_; 5065 private int step_; 5066 private int position_; 5067 private int page_; 5068 5069 // selection start and selection end 5070 // tics 5071 // tooltip? 5072 // some way to see and just type the value 5073 // win32 buddy controls are labels 5074 5075 /// 5076 void setMin(int a) { 5077 min_ = a; 5078 version(custom_widgets) 5079 redraw(); 5080 version(win32_widgets) 5081 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 5082 } 5083 /// 5084 int min() { 5085 return min_; 5086 } 5087 /// 5088 void setMax(int a) { 5089 max_ = a; 5090 version(custom_widgets) 5091 redraw(); 5092 version(win32_widgets) 5093 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 5094 } 5095 /// 5096 int max() { 5097 return max_; 5098 } 5099 /// 5100 void setPosition(int a) { 5101 if(a > max) 5102 a = max; 5103 if(a < min) 5104 a = min; 5105 position_ = a; 5106 version(custom_widgets) 5107 setPositionCustom(a); 5108 5109 version(win32_widgets) 5110 setPositionWindows(a); 5111 } 5112 version(win32_widgets) { 5113 protected abstract void setPositionWindows(int a); 5114 } 5115 5116 protected abstract int win32direction(); 5117 5118 /++ 5119 Alias for [position] for better compatibility with generic code. 5120 5121 History: 5122 Added October 5, 2021 5123 +/ 5124 @property int value() { 5125 return position; 5126 } 5127 5128 /// 5129 int position() { 5130 return position_; 5131 } 5132 /// 5133 void setStep(int a) { 5134 step_ = a; 5135 version(win32_widgets) 5136 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 5137 } 5138 /// 5139 int step() { 5140 return step_; 5141 } 5142 /// 5143 void setPageSize(int a) { 5144 page_ = a; 5145 version(win32_widgets) 5146 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 5147 } 5148 /// 5149 int pageSize() { 5150 return page_; 5151 } 5152 5153 private void notify() { 5154 auto event = new ChangeEvent!int(this, &this.position); 5155 event.dispatch(); 5156 } 5157 5158 version(win32_widgets) 5159 void win32Setup(int style) { 5160 createWin32Window(this, TRACKBAR_CLASS, "", 5161 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 5162 5163 // the trackbar sends the same messages as scroll, which 5164 // our other layer sends as these... just gonna translate 5165 // here 5166 this.addDirectEventListener("scrolltoposition", (Event event) { 5167 event.stopPropagation(); 5168 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 5169 notify(); 5170 }); 5171 this.addDirectEventListener("scrolltonextline", (Event event) { 5172 event.stopPropagation(); 5173 this.setPosition(this.position + this.step_ * this.win32direction); 5174 notify(); 5175 }); 5176 this.addDirectEventListener("scrolltopreviousline", (Event event) { 5177 event.stopPropagation(); 5178 this.setPosition(this.position - this.step_ * this.win32direction); 5179 notify(); 5180 }); 5181 this.addDirectEventListener("scrolltonextpage", (Event event) { 5182 event.stopPropagation(); 5183 this.setPosition(this.position + this.page_ * this.win32direction); 5184 notify(); 5185 }); 5186 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 5187 event.stopPropagation(); 5188 this.setPosition(this.position - this.page_ * this.win32direction); 5189 notify(); 5190 }); 5191 5192 setMin(min_); 5193 setMax(max_); 5194 setStep(step_); 5195 setPageSize(page_); 5196 } 5197 5198 version(custom_widgets) { 5199 protected MouseTrackingWidget thumb; 5200 5201 protected abstract void setPositionCustom(int a); 5202 5203 override void defaultEventHandler_keydown(KeyDownEvent event) { 5204 switch(event.key) { 5205 case Key.Up: 5206 case Key.Right: 5207 setPosition(position() - step() * win32direction); 5208 changed(); 5209 break; 5210 case Key.Down: 5211 case Key.Left: 5212 setPosition(position() + step() * win32direction); 5213 changed(); 5214 break; 5215 case Key.Home: 5216 setPosition(win32direction > 0 ? min() : max()); 5217 changed(); 5218 break; 5219 case Key.End: 5220 setPosition(win32direction > 0 ? max() : min()); 5221 changed(); 5222 break; 5223 case Key.PageUp: 5224 setPosition(position() - pageSize() * win32direction); 5225 changed(); 5226 break; 5227 case Key.PageDown: 5228 setPosition(position() + pageSize() * win32direction); 5229 changed(); 5230 break; 5231 default: 5232 } 5233 super.defaultEventHandler_keydown(event); 5234 } 5235 5236 protected void changed() { 5237 auto ev = new ChangeEvent!int(this, &position); 5238 ev.dispatch(); 5239 } 5240 } 5241 } 5242 5243 /++ 5244 5245 +/ 5246 class VerticalSlider : Slider { 5247 this(int min, int max, int step, Widget parent) { 5248 version(custom_widgets) 5249 initialize(); 5250 5251 super(min, max, step, parent); 5252 5253 version(win32_widgets) 5254 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 5255 } 5256 5257 protected override int win32direction() { 5258 return -1; 5259 } 5260 5261 version(win32_widgets) 5262 protected override void setPositionWindows(int a) { 5263 // the windows thing makes the top 0 and i don't like that. 5264 SendMessage(hwnd, TBM_SETPOS, true, max - a); 5265 } 5266 5267 version(custom_widgets) 5268 private void initialize() { 5269 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 5270 5271 thumb.tabStop = false; 5272 5273 thumb.thumbWidth = width; 5274 thumb.thumbHeight = 16; 5275 5276 thumb.addEventListener(EventType.change, () { 5277 auto sx = thumb.positionY * max() / (thumb.height - 16); 5278 sx = max - sx; 5279 //informProgramThatUserChangedPosition(sx); 5280 5281 position_ = sx; 5282 5283 changed(); 5284 }); 5285 } 5286 5287 version(custom_widgets) 5288 override void recomputeChildLayout() { 5289 thumb.thumbWidth = this.width; 5290 super.recomputeChildLayout(); 5291 setPositionCustom(position_); 5292 } 5293 5294 version(custom_widgets) 5295 protected override void setPositionCustom(int a) { 5296 if(max()) 5297 thumb.positionY = (max - a) * (thumb.height - 16) / max(); 5298 redraw(); 5299 } 5300 } 5301 5302 /++ 5303 5304 +/ 5305 class HorizontalSlider : Slider { 5306 this(int min, int max, int step, Widget parent) { 5307 version(custom_widgets) 5308 initialize(); 5309 5310 super(min, max, step, parent); 5311 5312 version(win32_widgets) 5313 win32Setup(TBS_HORZ); 5314 } 5315 5316 version(win32_widgets) 5317 protected override void setPositionWindows(int a) { 5318 SendMessage(hwnd, TBM_SETPOS, true, a); 5319 } 5320 5321 protected override int win32direction() { 5322 return 1; 5323 } 5324 5325 version(custom_widgets) 5326 private void initialize() { 5327 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 5328 5329 thumb.tabStop = false; 5330 5331 thumb.thumbWidth = 16; 5332 thumb.thumbHeight = height; 5333 5334 thumb.addEventListener(EventType.change, () { 5335 auto sx = thumb.positionX * max() / (thumb.width - 16); 5336 //informProgramThatUserChangedPosition(sx); 5337 5338 position_ = sx; 5339 5340 changed(); 5341 }); 5342 } 5343 5344 version(custom_widgets) 5345 override void recomputeChildLayout() { 5346 thumb.thumbHeight = this.height; 5347 super.recomputeChildLayout(); 5348 setPositionCustom(position_); 5349 } 5350 5351 version(custom_widgets) 5352 protected override void setPositionCustom(int a) { 5353 if(max()) 5354 thumb.positionX = a * (thumb.width - 16) / max(); 5355 redraw(); 5356 } 5357 } 5358 5359 5360 /// 5361 abstract class ScrollbarBase : Widget { 5362 /// 5363 this(Widget parent) { 5364 super(parent); 5365 tabStop = false; 5366 } 5367 5368 private int viewableArea_; 5369 private int max_; 5370 private int step_ = 16; 5371 private int position_; 5372 5373 /// 5374 bool atEnd() { 5375 return position_ + viewableArea_ >= max_; 5376 } 5377 5378 /// 5379 bool atStart() { 5380 return position_ == 0; 5381 } 5382 5383 /// 5384 void setViewableArea(int a) { 5385 viewableArea_ = a; 5386 version(custom_widgets) 5387 redraw(); 5388 } 5389 /// 5390 void setMax(int a) { 5391 max_ = a; 5392 version(custom_widgets) 5393 redraw(); 5394 } 5395 /// 5396 int max() { 5397 return max_; 5398 } 5399 /// 5400 void setPosition(int a) { 5401 if(a == int.max) 5402 a = max; 5403 position_ = max ? a : 0; 5404 if(position_ + viewableArea_ > max) 5405 position_ = max - viewableArea_; 5406 if(position_ < 0) 5407 position_ = 0; 5408 version(custom_widgets) 5409 redraw(); 5410 } 5411 /// 5412 int position() { 5413 return position_; 5414 } 5415 /// 5416 void setStep(int a) { 5417 step_ = a; 5418 } 5419 /// 5420 int step() { 5421 return step_; 5422 } 5423 5424 // FIXME: remove this.... maybe 5425 /+ 5426 protected void informProgramThatUserChangedPosition(int n) { 5427 position_ = n; 5428 auto evt = new Event(EventType.change, this); 5429 evt.intValue = n; 5430 evt.dispatch(); 5431 } 5432 +/ 5433 5434 version(custom_widgets) { 5435 abstract protected int getBarDim(); 5436 int thumbSize() { 5437 if(viewableArea_ >= max_) 5438 return getBarDim(); 5439 5440 int res; 5441 if(max_) { 5442 res = getBarDim() * viewableArea_ / max_; 5443 } 5444 if(res < 6) 5445 res = 6; 5446 5447 return res; 5448 } 5449 5450 int thumbPosition() { 5451 /* 5452 viewableArea_ is the viewport height/width 5453 position_ is where we are 5454 */ 5455 if(max_) { 5456 if(position_ + viewableArea_ >= max_) 5457 return getBarDim - thumbSize; 5458 return getBarDim * position_ / max_; 5459 } 5460 return 0; 5461 } 5462 } 5463 } 5464 5465 //public import mgt; 5466 5467 /++ 5468 A mouse tracking widget is one that follows the mouse when dragged inside it. 5469 5470 Concrete subclasses may include a scrollbar thumb and a volume control. 5471 +/ 5472 //version(custom_widgets) 5473 class MouseTrackingWidget : Widget { 5474 5475 /// 5476 int positionX() { return positionX_; } 5477 /// 5478 int positionY() { return positionY_; } 5479 5480 /// 5481 void positionX(int p) { positionX_ = p; } 5482 /// 5483 void positionY(int p) { positionY_ = p; } 5484 5485 private int positionX_; 5486 private int positionY_; 5487 5488 /// 5489 enum Orientation { 5490 horizontal, /// 5491 vertical, /// 5492 twoDimensional, /// 5493 } 5494 5495 private int thumbWidth_; 5496 private int thumbHeight_; 5497 5498 /// 5499 int thumbWidth() { return thumbWidth_; } 5500 /// 5501 int thumbHeight() { return thumbHeight_; } 5502 /// 5503 int thumbWidth(int a) { return thumbWidth_ = a; } 5504 /// 5505 int thumbHeight(int a) { return thumbHeight_ = a; } 5506 5507 private bool dragging; 5508 private bool hovering; 5509 private int startMouseX, startMouseY; 5510 5511 /// 5512 this(Orientation orientation, Widget parent) { 5513 super(parent); 5514 5515 //assert(parentWindow !is null); 5516 5517 addEventListener((MouseDownEvent event) { 5518 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 5519 dragging = true; 5520 startMouseX = event.clientX - positionX; 5521 startMouseY = event.clientY - positionY; 5522 parentWindow.captureMouse(this); 5523 } else { 5524 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 5525 positionX = event.clientX - thumbWidth / 2; 5526 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 5527 positionY = event.clientY - thumbHeight / 2; 5528 5529 if(positionX + thumbWidth > this.width) 5530 positionX = this.width - thumbWidth; 5531 if(positionY + thumbHeight > this.height) 5532 positionY = this.height - thumbHeight; 5533 5534 if(positionX < 0) 5535 positionX = 0; 5536 if(positionY < 0) 5537 positionY = 0; 5538 5539 5540 // this.emit!(ChangeEvent!void)(); 5541 auto evt = new Event(EventType.change, this); 5542 evt.sendDirectly(); 5543 5544 redraw(); 5545 5546 } 5547 }); 5548 5549 addEventListener(EventType.mouseup, (Event event) { 5550 dragging = false; 5551 parentWindow.releaseMouseCapture(); 5552 }); 5553 5554 addEventListener(EventType.mouseout, (Event event) { 5555 if(!hovering) 5556 return; 5557 hovering = false; 5558 redraw(); 5559 }); 5560 5561 int lpx, lpy; 5562 5563 addEventListener((MouseMoveEvent event) { 5564 auto oh = hovering; 5565 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 5566 hovering = true; 5567 } else { 5568 hovering = false; 5569 } 5570 if(!dragging) { 5571 if(hovering != oh) 5572 redraw(); 5573 return; 5574 } 5575 5576 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 5577 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 5578 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 5579 positionY = event.clientY - startMouseY; 5580 5581 if(positionX + thumbWidth > this.width) 5582 positionX = this.width - thumbWidth; 5583 if(positionY + thumbHeight > this.height) 5584 positionY = this.height - thumbHeight; 5585 5586 if(positionX < 0) 5587 positionX = 0; 5588 if(positionY < 0) 5589 positionY = 0; 5590 5591 if(positionX != lpx || positionY != lpy) { 5592 auto evt = new Event(EventType.change, this); 5593 evt.sendDirectly(); 5594 5595 lpx = positionX; 5596 lpy = positionY; 5597 } 5598 5599 redraw(); 5600 }); 5601 } 5602 5603 version(custom_widgets) 5604 override void paint(WidgetPainter painter) { 5605 auto cs = getComputedStyle(); 5606 auto c = darken(cs.windowBackgroundColor, 0.2); 5607 painter.outlineColor = c; 5608 painter.fillColor = c; 5609 painter.drawRectangle(Point(0, 0), this.width, this.height); 5610 5611 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 5612 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 5613 } 5614 } 5615 5616 //version(custom_widgets) 5617 //private 5618 class HorizontalScrollbar : ScrollbarBase { 5619 5620 version(custom_widgets) { 5621 private MouseTrackingWidget thumb; 5622 5623 override int getBarDim() { 5624 return thumb.width; 5625 } 5626 } 5627 5628 override void setViewableArea(int a) { 5629 super.setViewableArea(a); 5630 5631 version(win32_widgets) { 5632 SCROLLINFO info; 5633 info.cbSize = info.sizeof; 5634 info.nPage = a + 1; 5635 info.fMask = SIF_PAGE; 5636 SetScrollInfo(hwnd, SB_CTL, &info, true); 5637 } else version(custom_widgets) { 5638 thumb.positionX = thumbPosition; 5639 thumb.thumbWidth = thumbSize; 5640 thumb.redraw(); 5641 } else static assert(0); 5642 5643 } 5644 5645 override void setMax(int a) { 5646 super.setMax(a); 5647 version(win32_widgets) { 5648 SCROLLINFO info; 5649 info.cbSize = info.sizeof; 5650 info.nMin = 0; 5651 info.nMax = max; 5652 info.fMask = SIF_RANGE; 5653 SetScrollInfo(hwnd, SB_CTL, &info, true); 5654 } else version(custom_widgets) { 5655 thumb.positionX = thumbPosition; 5656 thumb.thumbWidth = thumbSize; 5657 thumb.redraw(); 5658 } 5659 } 5660 5661 override void setPosition(int a) { 5662 super.setPosition(a); 5663 version(win32_widgets) { 5664 SCROLLINFO info; 5665 info.cbSize = info.sizeof; 5666 info.fMask = SIF_POS; 5667 info.nPos = position; 5668 SetScrollInfo(hwnd, SB_CTL, &info, true); 5669 } else version(custom_widgets) { 5670 thumb.positionX = thumbPosition(); 5671 thumb.thumbWidth = thumbSize; 5672 thumb.redraw(); 5673 } else static assert(0); 5674 } 5675 5676 this(Widget parent) { 5677 super(parent); 5678 5679 version(win32_widgets) { 5680 createWin32Window(this, "Scrollbar"w, "", 5681 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 5682 } else version(custom_widgets) { 5683 auto vl = new HorizontalLayout(this); 5684 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 5685 leftButton.setClickRepeat(scrollClickRepeatInterval); 5686 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 5687 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 5688 rightButton.setClickRepeat(scrollClickRepeatInterval); 5689 5690 leftButton.tabStop = false; 5691 rightButton.tabStop = false; 5692 thumb.tabStop = false; 5693 5694 leftButton.addEventListener(EventType.triggered, () { 5695 this.emitCommand!"scrolltopreviousline"(); 5696 //informProgramThatUserChangedPosition(position - step()); 5697 }); 5698 rightButton.addEventListener(EventType.triggered, () { 5699 this.emitCommand!"scrolltonextline"(); 5700 //informProgramThatUserChangedPosition(position + step()); 5701 }); 5702 5703 thumb.thumbWidth = this.minWidth; 5704 thumb.thumbHeight = 16; 5705 5706 thumb.addEventListener(EventType.change, () { 5707 auto sx = thumb.positionX * max() / thumb.width; 5708 //informProgramThatUserChangedPosition(sx); 5709 5710 auto ev = new ScrollToPositionEvent(this, sx); 5711 ev.dispatch(); 5712 }); 5713 } 5714 } 5715 5716 override int minHeight() { return 16; } 5717 override int maxHeight() { return 16; } 5718 override int minWidth() { return 48; } 5719 } 5720 5721 class ScrollToPositionEvent : Event { 5722 enum EventString = "scrolltoposition"; 5723 5724 this(Widget target, int value) { 5725 this.value = value; 5726 super(EventString, target); 5727 } 5728 5729 immutable int value; 5730 5731 override @property int intValue() { 5732 return value; 5733 } 5734 } 5735 5736 //version(custom_widgets) 5737 //private 5738 class VerticalScrollbar : ScrollbarBase { 5739 5740 version(custom_widgets) { 5741 override int getBarDim() { 5742 return thumb.height; 5743 } 5744 5745 private MouseTrackingWidget thumb; 5746 } 5747 5748 override void setViewableArea(int a) { 5749 super.setViewableArea(a); 5750 5751 version(win32_widgets) { 5752 SCROLLINFO info; 5753 info.cbSize = info.sizeof; 5754 info.nPage = a + 1; 5755 info.fMask = SIF_PAGE; 5756 SetScrollInfo(hwnd, SB_CTL, &info, true); 5757 } else version(custom_widgets) { 5758 thumb.positionY = thumbPosition; 5759 thumb.thumbHeight = thumbSize; 5760 thumb.redraw(); 5761 } else static assert(0); 5762 5763 } 5764 5765 override void setMax(int a) { 5766 super.setMax(a); 5767 version(win32_widgets) { 5768 SCROLLINFO info; 5769 info.cbSize = info.sizeof; 5770 info.nMin = 0; 5771 info.nMax = max; 5772 info.fMask = SIF_RANGE; 5773 SetScrollInfo(hwnd, SB_CTL, &info, true); 5774 } else version(custom_widgets) { 5775 thumb.positionY = thumbPosition; 5776 thumb.thumbHeight = thumbSize; 5777 thumb.redraw(); 5778 } 5779 } 5780 5781 override void setPosition(int a) { 5782 super.setPosition(a); 5783 version(win32_widgets) { 5784 SCROLLINFO info; 5785 info.cbSize = info.sizeof; 5786 info.fMask = SIF_POS; 5787 info.nPos = position; 5788 SetScrollInfo(hwnd, SB_CTL, &info, true); 5789 } else version(custom_widgets) { 5790 thumb.positionY = thumbPosition; 5791 thumb.thumbHeight = thumbSize; 5792 thumb.redraw(); 5793 } else static assert(0); 5794 } 5795 5796 this(Widget parent) { 5797 super(parent); 5798 5799 version(win32_widgets) { 5800 createWin32Window(this, "Scrollbar"w, "", 5801 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 5802 } else version(custom_widgets) { 5803 auto vl = new VerticalLayout(this); 5804 auto upButton = new ArrowButton(ArrowDirection.up, vl); 5805 upButton.setClickRepeat(scrollClickRepeatInterval); 5806 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 5807 auto downButton = new ArrowButton(ArrowDirection.down, vl); 5808 downButton.setClickRepeat(scrollClickRepeatInterval); 5809 5810 upButton.addEventListener(EventType.triggered, () { 5811 this.emitCommand!"scrolltopreviousline"(); 5812 //informProgramThatUserChangedPosition(position - step()); 5813 }); 5814 downButton.addEventListener(EventType.triggered, () { 5815 this.emitCommand!"scrolltonextline"(); 5816 //informProgramThatUserChangedPosition(position + step()); 5817 }); 5818 5819 thumb.thumbWidth = this.minWidth; 5820 thumb.thumbHeight = 16; 5821 5822 thumb.addEventListener(EventType.change, () { 5823 auto sy = thumb.positionY * max() / thumb.height; 5824 5825 auto ev = new ScrollToPositionEvent(this, sy); 5826 ev.dispatch(); 5827 5828 //informProgramThatUserChangedPosition(sy); 5829 }); 5830 5831 upButton.tabStop = false; 5832 downButton.tabStop = false; 5833 thumb.tabStop = false; 5834 } 5835 } 5836 5837 override int minWidth() { return 16; } 5838 override int maxWidth() { return 16; } 5839 override int minHeight() { return 48; } 5840 } 5841 5842 5843 /++ 5844 EXPERIMENTAL 5845 5846 A widget specialized for being a container for other widgets. 5847 5848 History: 5849 Added May 29, 2021. Not stabilized at this time. 5850 +/ 5851 class WidgetContainer : Widget { 5852 this(Widget parent) { 5853 tabStop = false; 5854 super(parent); 5855 } 5856 5857 override int maxHeight() { 5858 if(this.children.length == 1) { 5859 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 5860 } else { 5861 return int.max; 5862 } 5863 } 5864 5865 override int maxWidth() { 5866 if(this.children.length == 1) { 5867 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 5868 } else { 5869 return int.max; 5870 } 5871 } 5872 5873 /+ 5874 5875 override int minHeight() { 5876 int largest = 0; 5877 int margins = 0; 5878 int lastMargin = 0; 5879 foreach(child; children) { 5880 auto mh = child.minHeight(); 5881 if(mh > largest) 5882 largest = mh; 5883 margins += mymax(lastMargin, child.marginTop()); 5884 lastMargin = child.marginBottom(); 5885 } 5886 return largest + margins; 5887 } 5888 5889 override int maxHeight() { 5890 int largest = 0; 5891 int margins = 0; 5892 int lastMargin = 0; 5893 foreach(child; children) { 5894 auto mh = child.maxHeight(); 5895 if(mh == int.max) 5896 return int.max; 5897 if(mh > largest) 5898 largest = mh; 5899 margins += mymax(lastMargin, child.marginTop()); 5900 lastMargin = child.marginBottom(); 5901 } 5902 return largest + margins; 5903 } 5904 5905 override int minWidth() { 5906 int min; 5907 foreach(child; children) { 5908 auto cm = child.minWidth; 5909 if(cm > min) 5910 min = cm; 5911 } 5912 return min + paddingLeft + paddingRight; 5913 } 5914 5915 override int minHeight() { 5916 int min; 5917 foreach(child; children) { 5918 auto cm = child.minHeight; 5919 if(cm > min) 5920 min = cm; 5921 } 5922 return min + paddingTop + paddingBottom; 5923 } 5924 5925 override int maxHeight() { 5926 int largest = 0; 5927 int margins = 0; 5928 int lastMargin = 0; 5929 foreach(child; children) { 5930 auto mh = child.maxHeight(); 5931 if(mh == int.max) 5932 return int.max; 5933 if(mh > largest) 5934 largest = mh; 5935 margins += mymax(lastMargin, child.marginTop()); 5936 lastMargin = child.marginBottom(); 5937 } 5938 return largest + margins; 5939 } 5940 5941 override int heightStretchiness() { 5942 int max; 5943 foreach(child; children) { 5944 auto c = child.heightStretchiness; 5945 if(c > max) 5946 max = c; 5947 } 5948 return max; 5949 } 5950 5951 override int marginTop() { 5952 if(this.children.length) 5953 return this.children[0].marginTop; 5954 return 0; 5955 } 5956 +/ 5957 } 5958 5959 /// 5960 abstract class Layout : Widget { 5961 this(Widget parent) { 5962 tabStop = false; 5963 super(parent); 5964 } 5965 } 5966 5967 /++ 5968 Makes all children minimum width and height, placing them down 5969 left to right, top to bottom. 5970 5971 Useful if you want to make a list of buttons that automatically 5972 wrap to a new line when necessary. 5973 +/ 5974 class InlineBlockLayout : Layout { 5975 /// 5976 this(Widget parent) { super(parent); } 5977 5978 override void recomputeChildLayout() { 5979 registerMovement(); 5980 5981 int x = this.paddingLeft, y = this.paddingTop; 5982 5983 int lineHeight; 5984 int previousMargin = 0; 5985 int previousMarginBottom = 0; 5986 5987 foreach(child; children) { 5988 if(child.hidden) 5989 continue; 5990 if(cast(FixedPosition) child) { 5991 child.recomputeChildLayout(); 5992 continue; 5993 } 5994 child.width = child.flexBasisWidth(); 5995 if(child.width == 0) 5996 child.width = child.minWidth(); 5997 if(child.width == 0) 5998 child.width = 32; 5999 6000 child.height = child.flexBasisHeight(); 6001 if(child.height == 0) 6002 child.height = child.minHeight(); 6003 if(child.height == 0) 6004 child.height = 32; 6005 6006 if(x + child.width + paddingRight > this.width) { 6007 x = this.paddingLeft; 6008 y += lineHeight; 6009 lineHeight = 0; 6010 previousMargin = 0; 6011 previousMarginBottom = 0; 6012 } 6013 6014 auto margin = child.marginLeft; 6015 if(previousMargin > margin) 6016 margin = previousMargin; 6017 6018 x += margin; 6019 6020 child.x = x; 6021 child.y = y; 6022 6023 int marginTopApplied; 6024 if(child.marginTop > previousMarginBottom) { 6025 child.y += child.marginTop; 6026 marginTopApplied = child.marginTop; 6027 } 6028 6029 x += child.width; 6030 previousMargin = child.marginRight; 6031 6032 if(child.marginBottom > previousMarginBottom) 6033 previousMarginBottom = child.marginBottom; 6034 6035 auto h = child.height + previousMarginBottom + marginTopApplied; 6036 if(h > lineHeight) 6037 lineHeight = h; 6038 6039 child.recomputeChildLayout(); 6040 } 6041 6042 } 6043 6044 override int minWidth() { 6045 int min; 6046 foreach(child; children) { 6047 auto cm = child.minWidth; 6048 if(cm > min) 6049 min = cm; 6050 } 6051 return min + paddingLeft + paddingRight; 6052 } 6053 6054 override int minHeight() { 6055 int min; 6056 foreach(child; children) { 6057 auto cm = child.minHeight; 6058 if(cm > min) 6059 min = cm; 6060 } 6061 return min + paddingTop + paddingBottom; 6062 } 6063 } 6064 6065 /++ 6066 A tab widget is a set of clickable tab buttons followed by a content area. 6067 6068 6069 Tabs can change existing content or can be new pages. 6070 6071 When the user picks a different tab, a `change` message is generated. 6072 +/ 6073 class TabWidget : Widget { 6074 this(Widget parent) { 6075 super(parent); 6076 6077 tabStop = false; 6078 6079 version(win32_widgets) { 6080 createWin32Window(this, WC_TABCONTROL, "", 0); 6081 } else version(custom_widgets) { 6082 addEventListener((ClickEvent event) { 6083 if(event.target !is this) return; 6084 if(event.clientY < tabBarHeight) { 6085 auto t = (event.clientX / tabWidth); 6086 if(t >= 0 && t < children.length) 6087 setCurrentTab(t); 6088 } 6089 }); 6090 } else static assert(0); 6091 } 6092 6093 override int marginTop() { return 4; } 6094 override int paddingBottom() { return 4; } 6095 6096 override int minHeight() { 6097 int max = 0; 6098 foreach(child; children) 6099 max = mymax(child.minHeight, max); 6100 6101 6102 version(win32_widgets) { 6103 RECT rect; 6104 rect.right = this.width; 6105 rect.bottom = max; 6106 TabCtrl_AdjustRect(hwnd, true, &rect); 6107 6108 max = rect.bottom; 6109 } else { 6110 max += defaultLineHeight + 4; 6111 } 6112 6113 6114 return max; 6115 } 6116 6117 version(win32_widgets) 6118 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 6119 switch(code) { 6120 case TCN_SELCHANGE: 6121 auto sel = TabCtrl_GetCurSel(hwnd); 6122 showOnly(sel); 6123 break; 6124 default: 6125 } 6126 return 0; 6127 } 6128 6129 override void addChild(Widget child, int pos = int.max) { 6130 if(auto twp = cast(TabWidgetPage) child) { 6131 super.addChild(child, pos); 6132 if(pos == int.max) 6133 pos = cast(int) this.children.length - 1; 6134 6135 version(win32_widgets) { 6136 TCITEM item; 6137 item.mask = TCIF_TEXT; 6138 WCharzBuffer buf = WCharzBuffer(twp.title); 6139 item.pszText = buf.ptr; 6140 SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 6141 } else version(custom_widgets) { 6142 } 6143 6144 if(pos != getCurrentTab) { 6145 child.showing = false; 6146 } 6147 } else { 6148 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 6149 } 6150 } 6151 6152 override void recomputeChildLayout() { 6153 version(win32_widgets) { 6154 this.registerMovement(); 6155 6156 RECT rect; 6157 GetWindowRect(hwnd, &rect); 6158 6159 auto left = rect.left; 6160 auto top = rect.top; 6161 6162 TabCtrl_AdjustRect(hwnd, false, &rect); 6163 foreach(child; children) { 6164 if(!child.showing) continue; 6165 child.x = rect.left - left; 6166 child.y = rect.top - top; 6167 child.width = rect.right - rect.left; 6168 child.height = rect.bottom - rect.top; 6169 child.recomputeChildLayout(); 6170 } 6171 } else version(custom_widgets) { 6172 this.registerMovement(); 6173 foreach(child; children) { 6174 if(!child.showing) continue; 6175 child.x = 2; 6176 child.y = tabBarHeight + 2; // for the border 6177 child.width = width - 4; // for the border 6178 child.height = height - tabBarHeight - 2 - 2; // for the border 6179 child.recomputeChildLayout(); 6180 } 6181 } else static assert(0); 6182 } 6183 6184 version(custom_widgets) { 6185 private int currentTab_; 6186 private int tabBarHeight() { return defaultLineHeight; } 6187 int tabWidth = 80; 6188 } 6189 6190 version(win32_widgets) 6191 override void paint(WidgetPainter painter) {} 6192 6193 version(custom_widgets) 6194 override void paint(WidgetPainter painter) { 6195 auto cs = getComputedStyle(); 6196 6197 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 6198 6199 int posX = 0; 6200 foreach(idx, child; children) { 6201 if(auto twp = cast(TabWidgetPage) child) { 6202 auto isCurrent = idx == getCurrentTab(); 6203 6204 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 6205 6206 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 6207 painter.outlineColor = cs.foregroundColor; 6208 painter.drawText(Point(posX + 4, 2), twp.title); 6209 6210 if(isCurrent) { 6211 painter.outlineColor = cs.windowBackgroundColor; 6212 painter.fillColor = Color.transparent; 6213 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 6214 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 6215 6216 painter.outlineColor = Color.white; 6217 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 6218 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 6219 painter.outlineColor = cs.activeTabColor; 6220 painter.drawPixel(Point(posX, tabBarHeight - 1)); 6221 } 6222 6223 posX += tabWidth - 2; 6224 } 6225 } 6226 } 6227 6228 /// 6229 @scriptable 6230 void setCurrentTab(int item) { 6231 version(win32_widgets) 6232 TabCtrl_SetCurSel(hwnd, item); 6233 else version(custom_widgets) 6234 currentTab_ = item; 6235 else static assert(0); 6236 6237 showOnly(item); 6238 } 6239 6240 /// 6241 @scriptable 6242 int getCurrentTab() { 6243 version(win32_widgets) 6244 return TabCtrl_GetCurSel(hwnd); 6245 else version(custom_widgets) 6246 return currentTab_; // FIXME 6247 else static assert(0); 6248 } 6249 6250 /// 6251 @scriptable 6252 void removeTab(int item) { 6253 if(item && item == getCurrentTab()) 6254 setCurrentTab(item - 1); 6255 6256 version(win32_widgets) { 6257 TabCtrl_DeleteItem(hwnd, item); 6258 } 6259 6260 for(int a = item; a < children.length - 1; a++) 6261 this._children[a] = this._children[a + 1]; 6262 this._children = this._children[0 .. $-1]; 6263 } 6264 6265 /// 6266 @scriptable 6267 TabWidgetPage addPage(string title) { 6268 return new TabWidgetPage(title, this); 6269 } 6270 6271 private void showOnly(int item) { 6272 foreach(idx, child; children) { 6273 child.showing(false, false); // batch the recalculates for the end 6274 } 6275 6276 foreach(idx, child; children) { 6277 if(idx == item) { 6278 child.showing(true, false); 6279 if(parentWindow) { 6280 auto f = parentWindow.getFirstFocusable(child); 6281 if(f) 6282 f.focus(); 6283 } 6284 recomputeChildLayout(); 6285 } 6286 } 6287 6288 version(win32_widgets) { 6289 InvalidateRect(hwnd, null, true); 6290 } else version(custom_widgets) { 6291 this.redraw(); 6292 } 6293 } 6294 } 6295 6296 /++ 6297 A page widget is basically a tab widget with hidden tabs. 6298 6299 You add [TabWidgetPage]s to it. 6300 +/ 6301 class PageWidget : Widget { 6302 this(Widget parent) { 6303 super(parent); 6304 } 6305 6306 override int minHeight() { 6307 int max = 0; 6308 foreach(child; children) 6309 max = mymax(child.minHeight, max); 6310 6311 return max; 6312 } 6313 6314 6315 override void addChild(Widget child, int pos = int.max) { 6316 if(auto twp = cast(TabWidgetPage) child) { 6317 super.addChild(child, pos); 6318 if(pos == int.max) 6319 pos = cast(int) this.children.length - 1; 6320 6321 if(pos != getCurrentTab) { 6322 child.showing = false; 6323 } 6324 } else { 6325 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 6326 } 6327 } 6328 6329 override void recomputeChildLayout() { 6330 this.registerMovement(); 6331 foreach(child; children) { 6332 child.x = 0; 6333 child.y = 0; 6334 child.width = width; 6335 child.height = height; 6336 child.recomputeChildLayout(); 6337 } 6338 } 6339 6340 private int currentTab_; 6341 6342 /// 6343 @scriptable 6344 void setCurrentTab(int item) { 6345 currentTab_ = item; 6346 6347 showOnly(item); 6348 } 6349 6350 /// 6351 @scriptable 6352 int getCurrentTab() { 6353 return currentTab_; 6354 } 6355 6356 /// 6357 @scriptable 6358 void removeTab(int item) { 6359 if(item && item == getCurrentTab()) 6360 setCurrentTab(item - 1); 6361 6362 for(int a = item; a < children.length - 1; a++) 6363 this._children[a] = this._children[a + 1]; 6364 this._children = this._children[0 .. $-1]; 6365 } 6366 6367 /// 6368 @scriptable 6369 TabWidgetPage addPage(string title) { 6370 return new TabWidgetPage(title, this); 6371 } 6372 6373 private void showOnly(int item) { 6374 foreach(idx, child; children) 6375 if(idx == item) { 6376 child.show(); 6377 child.recomputeChildLayout(); 6378 } else { 6379 child.hide(); 6380 } 6381 } 6382 6383 } 6384 6385 /++ 6386 6387 +/ 6388 class TabWidgetPage : Widget { 6389 string title; 6390 this(string title, Widget parent) { 6391 this.title = title; 6392 this.tabStop = false; 6393 super(parent); 6394 6395 ///* 6396 version(win32_widgets) { 6397 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 6398 } 6399 //*/ 6400 } 6401 6402 override int minHeight() { 6403 int sum = 0; 6404 foreach(child; children) 6405 sum += child.minHeight(); 6406 return sum; 6407 } 6408 } 6409 6410 version(none) 6411 /++ 6412 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 6413 6414 I think I need to modify the layout algorithms to support this. 6415 +/ 6416 class CollapsableSidebar : Widget { 6417 6418 } 6419 6420 /// Stacks the widgets vertically, taking all the available width for each child. 6421 class VerticalLayout : Layout { 6422 // most of this is intentionally blank - widget's default is vertical layout right now 6423 /// 6424 this(Widget parent) { super(parent); } 6425 } 6426 6427 /// Stacks the widgets horizontally, taking all the available height for each child. 6428 class HorizontalLayout : Layout { 6429 /// 6430 this(Widget parent) { super(parent); } 6431 override void recomputeChildLayout() { 6432 .recomputeChildLayout!"width"(this); 6433 } 6434 6435 override int minHeight() { 6436 int largest = 0; 6437 int margins = 0; 6438 int lastMargin = 0; 6439 foreach(child; children) { 6440 auto mh = child.minHeight(); 6441 if(mh > largest) 6442 largest = mh; 6443 margins += mymax(lastMargin, child.marginTop()); 6444 lastMargin = child.marginBottom(); 6445 } 6446 return largest + margins; 6447 } 6448 6449 override int maxHeight() { 6450 int largest = 0; 6451 int margins = 0; 6452 int lastMargin = 0; 6453 foreach(child; children) { 6454 auto mh = child.maxHeight(); 6455 if(mh == int.max) 6456 return int.max; 6457 if(mh > largest) 6458 largest = mh; 6459 margins += mymax(lastMargin, child.marginTop()); 6460 lastMargin = child.marginBottom(); 6461 } 6462 return largest + margins; 6463 } 6464 6465 override int heightStretchiness() { 6466 int max; 6467 foreach(child; children) { 6468 auto c = child.heightStretchiness; 6469 if(c > max) 6470 max = c; 6471 } 6472 return max; 6473 } 6474 6475 } 6476 6477 private wstring Win32Class(wstring name)() { 6478 static bool classRegistered; 6479 if(!classRegistered) { 6480 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 6481 WNDCLASSEX wc; 6482 wc.cbSize = wc.sizeof; 6483 wc.hInstance = hInstance; 6484 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 6485 wc.lpfnWndProc = &DefWindowProc; 6486 wc.lpszClassName = name.ptr; 6487 if(!RegisterClassExW(&wc)) 6488 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 6489 classRegistered = true; 6490 } 6491 6492 return name; 6493 } 6494 6495 /+ 6496 version(win32_widgets) 6497 extern(Windows) 6498 private 6499 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 6500 switch(iMessage) { 6501 case WM_PAINT: 6502 if(auto te = hWnd in Widget.nativeMapping) { 6503 try { 6504 //te.redraw(); 6505 import std.stdio; writeln(te, " drawing"); 6506 } catch(Exception) {} 6507 } 6508 return DefWindowProc(hWnd, iMessage, wParam, lParam); 6509 default: 6510 return DefWindowProc(hWnd, iMessage, wParam, lParam); 6511 } 6512 } 6513 +/ 6514 6515 6516 /++ 6517 A widget specifically designed to hold other widgets. 6518 6519 History: 6520 Added July 1, 2021 6521 +/ 6522 class ContainerWidget : Widget { 6523 this(Widget parent) { 6524 super(parent); 6525 this.tabStop = false; 6526 6527 version(win32_widgets) { 6528 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 6529 } 6530 } 6531 } 6532 6533 /++ 6534 A widget that takes your widget, puts scroll bars around it, and sends 6535 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 6536 no effort to automatically scroll or clip its child widgets - it just sends 6537 the messages. 6538 6539 6540 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 6541 The scroll coordinates are all given in a unit you interpret as you wish. One 6542 of these units is moved on each press of the arrow buttons and represents the 6543 smallest amount the user can scroll. The intention is for this to be one line, 6544 one item in a list, one row in a table, etc. Whatever makes sense for your widget 6545 in each direction that the user might be interested in. 6546 6547 You can set a "page size" with the [step] property. (Yes, I regret the name...) 6548 This is the amount it jumps when the user pressed page up and page down, or clicks 6549 in the exposed part of the scroll bar. 6550 6551 You should add child content to the ScrollMessageWidget. However, it is important to 6552 note that the coordinates are always independent of the scroll position! It is YOUR 6553 responsibility to do any necessary transforms, clipping, etc., while drawing the 6554 content and interpreting mouse events if they are supposed to change with the scroll. 6555 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 6556 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 6557 you more control (which can be considerably more efficient and adapted to your actual data) 6558 at the expense of you also needing to be aware of its reality. 6559 6560 Please note that it does NOT react to mouse wheel events or various keyboard events as of 6561 version 10.3. Maybe this will change in the future.... 6562 +/ 6563 class ScrollMessageWidget : Widget { 6564 this(Widget parent) { 6565 super(parent); 6566 6567 container = new Widget(this); 6568 hsb = new HorizontalScrollbar(this); 6569 vsb = new VerticalScrollbar(this); 6570 6571 hsb.addEventListener("scrolltonextline", { 6572 hsb.setPosition(hsb.position + 1); 6573 notify(); 6574 }); 6575 hsb.addEventListener("scrolltopreviousline", { 6576 hsb.setPosition(hsb.position - 1); 6577 notify(); 6578 }); 6579 vsb.addEventListener("scrolltonextline", { 6580 vsb.setPosition(vsb.position + 1); 6581 notify(); 6582 }); 6583 vsb.addEventListener("scrolltopreviousline", { 6584 vsb.setPosition(vsb.position - 1); 6585 notify(); 6586 }); 6587 hsb.addEventListener("scrolltonextpage", { 6588 hsb.setPosition(hsb.position + hsb.step_); 6589 notify(); 6590 }); 6591 hsb.addEventListener("scrolltopreviouspage", { 6592 hsb.setPosition(hsb.position - hsb.step_); 6593 notify(); 6594 }); 6595 vsb.addEventListener("scrolltonextpage", { 6596 vsb.setPosition(vsb.position + vsb.step_); 6597 notify(); 6598 }); 6599 vsb.addEventListener("scrolltopreviouspage", { 6600 vsb.setPosition(vsb.position - vsb.step_); 6601 notify(); 6602 }); 6603 hsb.addEventListener("scrolltoposition", (Event event) { 6604 hsb.setPosition(event.intValue); 6605 notify(); 6606 }); 6607 vsb.addEventListener("scrolltoposition", (Event event) { 6608 vsb.setPosition(event.intValue); 6609 notify(); 6610 }); 6611 6612 6613 tabStop = false; 6614 container.tabStop = false; 6615 magic = true; 6616 } 6617 6618 /++ 6619 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 6620 6621 6622 The defaults for [addDefaultWheelListeners] are: 6623 6624 $(LIST 6625 * Mouse wheel scrolls vertically 6626 * Alt key + mouse wheel scrolls horiontally 6627 * Shift + mouse wheel scrolls faster. 6628 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 6629 ) 6630 6631 The defaults for [addDefaultKeyboardListeners] are: 6632 6633 $(LIST 6634 * Arrow keys scroll by the given amounts 6635 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 6636 * Page up and down scroll by the vertical viewable area 6637 * Home and end scroll to the start and end of the verticle viewable area. 6638 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 6639 ) 6640 6641 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 6642 6643 Params: 6644 horizontalArrowScrollAmount = 6645 verticalArrowScrollAmount = 6646 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 6647 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 6648 shiftMultiplier = multiplies the scroll amount by this when shift is held 6649 +/ 6650 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 6651 auto _this = this; 6652 6653 container.addEventListener((scope KeyDownEvent ke) { 6654 switch(ke.key) { 6655 case Key.Left: 6656 _this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 6657 break; 6658 case Key.Right: 6659 _this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 6660 break; 6661 case Key.Up: 6662 _this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 6663 break; 6664 case Key.Down: 6665 _this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 6666 break; 6667 case Key.PageUp: 6668 if(ke.altKey) 6669 _this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 6670 else 6671 _this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 6672 break; 6673 case Key.PageDown: 6674 if(ke.altKey) 6675 _this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 6676 else 6677 _this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 6678 break; 6679 case Key.Home: 6680 if(ke.altKey) 6681 _this.scrollLeft(short.max * 16); 6682 else 6683 _this.scrollUp(short.max * 16); 6684 break; 6685 case Key.End: 6686 if(ke.altKey) 6687 _this.scrollRight(short.max * 16); 6688 else 6689 _this.scrollDown(short.max * 16); 6690 break; 6691 6692 default: 6693 // ignore, not for us. 6694 } 6695 6696 }); 6697 } 6698 6699 /// ditto 6700 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 6701 auto _this = this; 6702 container.addEventListener((scope ClickEvent ce) { 6703 6704 if(ce.target && ce.target.tabStop) 6705 ce.target.focus(); 6706 6707 // ctrl is reserved for the application 6708 if(ce.ctrlKey) 6709 return; 6710 6711 if(horizontalWheelScrollAmount == 0 && ce.altKey) 6712 return; 6713 6714 if(shiftMultiplier == 0 && ce.shiftKey) 6715 return; 6716 6717 if(ce.button == MouseButton.wheelDown) { 6718 if(ce.altKey) 6719 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 6720 else 6721 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 6722 } else if(ce.button == MouseButton.wheelUp) { 6723 if(ce.altKey) 6724 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 6725 else 6726 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 6727 } 6728 }); 6729 } 6730 6731 /++ 6732 Scrolls the given amount. 6733 6734 History: 6735 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. 6736 +/ 6737 void scrollUp(int amount = 1) { 6738 vsb.setPosition(vsb.position - amount); 6739 notify(); 6740 } 6741 /// ditto 6742 void scrollDown(int amount = 1) { 6743 vsb.setPosition(vsb.position + amount); 6744 notify(); 6745 } 6746 /// ditto 6747 void scrollLeft(int amount = 1) { 6748 hsb.setPosition(hsb.position - amount); 6749 notify(); 6750 } 6751 /// ditto 6752 void scrollRight(int amount = 1) { 6753 hsb.setPosition(hsb.position + amount); 6754 notify(); 6755 } 6756 6757 /// 6758 VerticalScrollbar verticalScrollBar() { return vsb; } 6759 /// 6760 HorizontalScrollbar horizontalScrollBar() { return hsb; } 6761 6762 void notify() { 6763 this.emit!ScrollEvent(); 6764 } 6765 6766 mixin Emits!ScrollEvent; 6767 6768 /// 6769 Point position() { 6770 return Point(hsb.position, vsb.position); 6771 } 6772 6773 /// 6774 void setPosition(int x, int y) { 6775 hsb.setPosition(x); 6776 vsb.setPosition(y); 6777 } 6778 6779 /// 6780 void setPageSize(int unitsX, int unitsY) { 6781 hsb.setStep(unitsX); 6782 vsb.setStep(unitsY); 6783 } 6784 6785 /// 6786 void setTotalArea(int width, int height) { 6787 hsb.setMax(width); 6788 vsb.setMax(height); 6789 } 6790 6791 /// 6792 void setViewableArea(int width, int height) { 6793 hsb.setViewableArea(width); 6794 vsb.setViewableArea(height); 6795 6796 bool needsNotify = false; 6797 6798 // FIXME: if at any point the rhs is outside the scrollbar, we need 6799 // to reset to 0. but it should remember the old position in case the 6800 // window resizes again, so it can kinda return ot where it was. 6801 // 6802 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 6803 if(width > hsb.max) { 6804 // there's plenty of room to display it all so we need to reset to zero 6805 // FIXME: adjust so it matches the note above 6806 hsb.setPosition(0); 6807 needsNotify = true; 6808 } 6809 if(height > vsb.max) { 6810 // there's plenty of room to display it all so we need to reset to zero 6811 // FIXME: adjust so it matches the note above 6812 vsb.setPosition(0); 6813 needsNotify = true; 6814 } 6815 if(needsNotify) 6816 notify(); 6817 } 6818 6819 private bool magic; 6820 override void addChild(Widget w, int position = int.max) { 6821 if(magic) 6822 container.addChild(w, position); 6823 else 6824 super.addChild(w, position); 6825 } 6826 6827 override void recomputeChildLayout() { 6828 if(hsb is null || vsb is null || container is null) return; 6829 6830 registerMovement(); 6831 6832 hsb.height = 16; // FIXME? are tese 16s sane? 6833 hsb.x = 0; 6834 hsb.y = this.height - hsb.height; 6835 hsb.width = this.width - 16; 6836 hsb.recomputeChildLayout(); 6837 6838 vsb.width = 16; // FIXME? 6839 vsb.x = this.width - vsb.width; 6840 vsb.y = 0; 6841 vsb.height = this.height - 16; 6842 vsb.recomputeChildLayout(); 6843 6844 if(this.header is null) { 6845 container.x = 0; 6846 container.y = 0; 6847 container.width = this.width - vsb.width; 6848 container.height = this.height - hsb.height; 6849 container.recomputeChildLayout(); 6850 } else { 6851 header.x = 0; 6852 header.y = 0; 6853 header.width = this.width - vsb.width; 6854 header.height = 16; // size of the button 6855 header.recomputeChildLayout(); 6856 6857 container.x = 0; 6858 container.y = 16; 6859 container.width = this.width - vsb.width; 6860 container.height = this.height - hsb.height - 16; 6861 container.recomputeChildLayout(); 6862 } 6863 } 6864 6865 HorizontalScrollbar hsb; 6866 VerticalScrollbar vsb; 6867 Widget container; 6868 private Widget header; 6869 6870 /++ 6871 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 6872 6873 History: 6874 Added September 27, 2021 (dub v10.3) 6875 +/ 6876 Widget getHeader() { 6877 if(this.header is null) { 6878 magic = false; 6879 scope(exit) magic = true; 6880 this.header = new Widget(this); 6881 recomputeChildLayout(); 6882 } 6883 return this.header; 6884 } 6885 } 6886 6887 /++ 6888 Bypasses automatic layout for its children, using manual positioning and sizing only. 6889 While you need to manually position them, you must ensure they are inside the StaticLayout's 6890 bounding box to avoid undefined behavior. 6891 6892 You should almost never use this. 6893 +/ 6894 class StaticLayout : Layout { 6895 /// 6896 this(Widget parent) { super(parent); } 6897 override void recomputeChildLayout() { 6898 registerMovement(); 6899 foreach(child; children) 6900 child.recomputeChildLayout(); 6901 } 6902 } 6903 6904 /++ 6905 Bypasses automatic positioning when being laid out. It is your responsibility to make 6906 room for this widget in the parent layout. 6907 6908 Its children are laid out normally, unless there is exactly one, in which case it takes 6909 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 6910 can do that with `padding`). 6911 +/ 6912 class StaticPosition : Layout { 6913 /// 6914 this(Widget parent) { super(parent); } 6915 6916 override void recomputeChildLayout() { 6917 registerMovement(); 6918 if(this.children.length == 1) { 6919 auto child = children[0]; 6920 child.x = 0; 6921 child.y = 0; 6922 child.width = this.width; 6923 child.height = this.height; 6924 child.recomputeChildLayout(); 6925 } else 6926 foreach(child; children) 6927 child.recomputeChildLayout(); 6928 } 6929 6930 alias width = typeof(super).width; 6931 alias height = typeof(super).height; 6932 6933 @property int width(int w) @nogc pure @safe nothrow { 6934 return this._width = w; 6935 } 6936 6937 @property int height(int w) @nogc pure @safe nothrow { 6938 return this._height = w; 6939 } 6940 6941 } 6942 6943 /++ 6944 FixedPosition is like [StaticPosition], but its coordinates 6945 are always relative to the viewport, meaning they do not scroll with 6946 the parent content. 6947 +/ 6948 class FixedPosition : StaticPosition { 6949 /// 6950 this(Widget parent) { super(parent); } 6951 } 6952 6953 version(win32_widgets) 6954 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 6955 if(true) { 6956 // cmd == 0 = menu, cmd == 1 = accelerator 6957 if(auto item = idm in Action.mapping) { 6958 foreach(handler; (*item).triggered) 6959 handler(); 6960 /* 6961 auto event = new Event("triggered", *item); 6962 event.button = idm; 6963 event.dispatch(); 6964 */ 6965 return 0; 6966 } 6967 } 6968 if(handle) 6969 if(auto widgetp = handle in Widget.nativeMapping) { 6970 (*widgetp).handleWmCommand(cmd, idm); 6971 return 0; 6972 } 6973 return 1; 6974 } 6975 6976 6977 /// 6978 class Window : Widget { 6979 int mouseCaptureCount = 0; 6980 Widget mouseCapturedBy; 6981 void captureMouse(Widget byWhom) { 6982 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 6983 mouseCaptureCount++; 6984 mouseCapturedBy = byWhom; 6985 win.grabInput(); 6986 } 6987 void releaseMouseCapture() { 6988 mouseCaptureCount--; 6989 mouseCapturedBy = null; 6990 win.releaseInputGrab(); 6991 } 6992 6993 /// 6994 @scriptable 6995 @property bool focused() { 6996 return win.focused; 6997 } 6998 6999 static class Style : Widget.Style { 7000 override WidgetBackground background() { 7001 version(custom_widgets) 7002 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 7003 else version(win32_widgets) 7004 return WidgetBackground(Color.transparent); 7005 else static assert(0); 7006 } 7007 } 7008 mixin OverrideStyle!Style; 7009 7010 /++ 7011 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. 7012 +/ 7013 static int lineHeight() { 7014 OperatingSystemFont font; 7015 if(auto vt = WidgetPainter.visualTheme) { 7016 font = vt.defaultFontCached(); 7017 } 7018 7019 if(font is null) { 7020 static int defaultHeightCache; 7021 if(defaultHeightCache == 0) { 7022 font = new OperatingSystemFont; 7023 font.loadDefault; 7024 defaultHeightCache = font.height() * 5 / 4; 7025 } 7026 return defaultHeightCache; 7027 } 7028 7029 return font.height() * 5 / 4; 7030 } 7031 7032 Widget focusedWidget; 7033 7034 private SimpleWindow win_; 7035 7036 @property { 7037 /++ 7038 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 7039 7040 History: 7041 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 7042 +/ 7043 public SimpleWindow win() { 7044 return win_; 7045 } 7046 /// 7047 protected void win(SimpleWindow w) { 7048 win_ = w; 7049 } 7050 } 7051 7052 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 7053 this(Widget p) { 7054 tabStop = false; 7055 super(p); 7056 } 7057 7058 7059 7060 private void actualRedraw() { 7061 if(recomputeChildLayoutRequired) 7062 recomputeChildLayoutEntry(); 7063 if(!showing) return; 7064 7065 assert(parentWindow !is null); 7066 7067 auto w = drawableWindow; 7068 if(w is null) 7069 w = parentWindow.win; 7070 7071 if(w.closed()) 7072 return; 7073 7074 auto ugh = this.parent; 7075 int lox, loy; 7076 while(ugh) { 7077 lox += ugh.x; 7078 loy += ugh.y; 7079 ugh = ugh.parent; 7080 } 7081 auto painter = w.draw(); 7082 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max)); 7083 } 7084 7085 7086 private bool skipNextChar = false; 7087 7088 /++ 7089 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 7090 7091 This constructor is intended primarily for internal use and may be changed to `protected` later. 7092 +/ 7093 this(SimpleWindow win) { 7094 7095 static if(UsingSimpledisplayX11) { 7096 win.discardAdditionalConnectionState = &discardXConnectionState; 7097 win.recreateAdditionalConnectionState = &recreateXConnectionState; 7098 } 7099 7100 tabStop = false; 7101 super(null); 7102 this.win = win; 7103 7104 win.addEventListener((Widget.RedrawEvent) { 7105 if(win.eventQueued!RecomputeEvent) { 7106 // import std.stdio; writeln("skipping"); 7107 return; // let the recompute event do the actual redraw 7108 } 7109 this.actualRedraw(); 7110 }); 7111 7112 win.addEventListener((Widget.RecomputeEvent) { 7113 recomputeChildLayoutEntry(); 7114 if(win.eventQueued!RedrawEvent) 7115 return; // let the queued one do it 7116 else { 7117 // import std.stdio; writeln("drawing"); 7118 this.actualRedraw(); // if not queued, it needs to be done now anyway 7119 } 7120 }); 7121 7122 this.width = win.width; 7123 this.height = win.height; 7124 this.parentWindow = this; 7125 7126 win.closeQuery = () { 7127 if(this.emit!ClosingEvent()) 7128 win.close(); 7129 }; 7130 win.onClosing = () { 7131 this.emit!ClosedEvent(); 7132 }; 7133 7134 win.windowResized = (int w, int h) { 7135 this.width = w; 7136 this.height = h; 7137 recomputeChildLayout(); 7138 version(win32_widgets) 7139 InvalidateRect(hwnd, null, true); 7140 redraw(); 7141 }; 7142 7143 win.onFocusChange = (bool getting) { 7144 if(this.focusedWidget) { 7145 if(getting) { 7146 this.focusedWidget.emit!FocusEvent(); 7147 this.focusedWidget.emit!FocusInEvent(); 7148 } else { 7149 this.focusedWidget.emit!BlurEvent(); 7150 this.focusedWidget.emit!FocusOutEvent(); 7151 } 7152 } 7153 7154 if(getting) { 7155 this.emit!FocusEvent(); 7156 this.emit!FocusInEvent(); 7157 } else { 7158 this.emit!BlurEvent(); 7159 this.emit!FocusOutEvent(); 7160 } 7161 }; 7162 7163 win.onDpiChanged = { 7164 this.queueRecomputeChildLayout(); 7165 }; 7166 7167 win.setEventHandlers( 7168 (MouseEvent e) { 7169 dispatchMouseEvent(e); 7170 }, 7171 (KeyEvent e) { 7172 //import std.stdio; 7173 //writefln("%x %s", cast(uint) e.key, e.key); 7174 dispatchKeyEvent(e); 7175 }, 7176 (dchar e) { 7177 if(e == 13) e = 10; // hack? 7178 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 7179 dispatchCharEvent(e); 7180 }, 7181 ); 7182 7183 addEventListener("char", (Widget, Event ev) { 7184 if(skipNextChar) { 7185 ev.preventDefault(); 7186 skipNextChar = false; 7187 } 7188 }); 7189 7190 version(win32_widgets) 7191 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 7192 if(hwnd !is this.win.impl.hwnd) 7193 return 1; // we don't care... pass it on 7194 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 7195 if(mustReturn) 7196 return ret; 7197 return 1; // pass it on 7198 }; 7199 } 7200 7201 version(win32_widgets) 7202 override void paint(WidgetPainter painter) { 7203 /* 7204 RECT rect; 7205 rect.right = this.width; 7206 rect.bottom = this.height; 7207 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 7208 */ 7209 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 7210 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 7211 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 7212 // since the pen is null, to fill the whole space, we need the +1 on both. 7213 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 7214 SelectObject(painter.impl.hdc, p); 7215 SelectObject(painter.impl.hdc, b); 7216 } 7217 version(custom_widgets) 7218 override void paint(WidgetPainter painter) { 7219 auto cs = getComputedStyle(); 7220 painter.fillColor = cs.windowBackgroundColor; 7221 painter.outlineColor = cs.windowBackgroundColor; 7222 painter.drawRectangle(Point(0, 0), this.width, this.height); 7223 } 7224 7225 7226 override void defaultEventHandler_keydown(KeyDownEvent event) { 7227 Widget _this = event.target; 7228 7229 if(event.key == Key.Tab) { 7230 /* Window tab ordering is a recursive thingy with each group */ 7231 7232 // FIXME inefficient 7233 Widget[] helper(Widget p) { 7234 if(p.hidden) 7235 return null; 7236 Widget[] childOrdering; 7237 7238 auto children = p.children.dup; 7239 7240 while(true) { 7241 // UIs should be generally small, so gonna brute force it a little 7242 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 7243 7244 Widget smallestTab; 7245 foreach(ref c; children) { 7246 if(c is null) continue; 7247 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 7248 smallestTab = c; 7249 c = null; 7250 } 7251 } 7252 if(smallestTab !is null) { 7253 if(smallestTab.tabStop && !smallestTab.hidden) 7254 childOrdering ~= smallestTab; 7255 if(!smallestTab.hidden) 7256 childOrdering ~= helper(smallestTab); 7257 } else 7258 break; 7259 7260 } 7261 7262 return childOrdering; 7263 } 7264 7265 Widget[] tabOrdering = helper(this); 7266 7267 Widget recipient; 7268 7269 if(tabOrdering.length) { 7270 bool seenThis = false; 7271 Widget previous; 7272 foreach(idx, child; tabOrdering) { 7273 if(child is focusedWidget) { 7274 7275 if(event.shiftKey) { 7276 if(idx == 0) 7277 recipient = tabOrdering[$-1]; 7278 else 7279 recipient = tabOrdering[idx - 1]; 7280 break; 7281 } 7282 7283 seenThis = true; 7284 if(idx + 1 == tabOrdering.length) { 7285 // we're at the end, either move to the next group 7286 // or start back over 7287 recipient = tabOrdering[0]; 7288 } 7289 continue; 7290 } 7291 if(seenThis) { 7292 recipient = child; 7293 break; 7294 } 7295 previous = child; 7296 } 7297 } 7298 7299 if(recipient !is null) { 7300 // import std.stdio; writeln(typeid(recipient)); 7301 recipient.focus(); 7302 7303 skipNextChar = true; 7304 } 7305 } 7306 7307 debug if(event.key == Key.F12) { 7308 if(devTools) { 7309 devTools.close(); 7310 devTools = null; 7311 } else { 7312 devTools = new DevToolWindow(this); 7313 devTools.show(); 7314 } 7315 } 7316 } 7317 7318 debug DevToolWindow devTools; 7319 7320 7321 /++ 7322 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 7323 7324 History: 7325 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 7326 7327 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 7328 +/ 7329 this(int width = 500, int height = 500, string title = null) { 7330 if(title is null) { 7331 import core.runtime; 7332 if(Runtime.args.length) 7333 title = Runtime.args[0]; 7334 } 7335 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow); 7336 7337 this(win); 7338 } 7339 7340 /// ditto 7341 this(string title, int width = 500, int height = 500) { 7342 this(width, height, title); 7343 } 7344 7345 /// 7346 @scriptable 7347 void close() { 7348 win.close(); 7349 // I synchronize here upon window closing to ensure all child windows 7350 // get updated too before the event loop. This avoids some random X errors. 7351 static if(UsingSimpledisplayX11) { 7352 runInGuiThread( { 7353 XSync(XDisplayConnection.get, false); 7354 }); 7355 } 7356 } 7357 7358 bool dispatchKeyEvent(KeyEvent ev) { 7359 auto wid = focusedWidget; 7360 if(wid is null) 7361 wid = this; 7362 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 7363 event.originalKeyEvent = ev; 7364 event.key = ev.key; 7365 event.state = ev.modifierState; 7366 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 7367 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 7368 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 7369 event.dispatch(); 7370 7371 return true; 7372 } 7373 7374 bool dispatchCharEvent(dchar ch) { 7375 if(focusedWidget) { 7376 auto event = new CharEvent(focusedWidget, ch); 7377 event.dispatch(); 7378 } 7379 return true; 7380 } 7381 7382 Widget mouseLastOver; 7383 Widget mouseLastDownOn; 7384 bool lastWasDoubleClick; 7385 bool dispatchMouseEvent(MouseEvent ev) { 7386 auto eleR = widgetAtPoint(this, ev.x, ev.y); 7387 auto ele = eleR.widget; 7388 7389 auto captureEle = ele; 7390 7391 if(mouseCapturedBy !is null) { 7392 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 7393 captureEle = mouseCapturedBy; 7394 } 7395 7396 // a hack to get it relative to the widget. 7397 eleR.x = ev.x; 7398 eleR.y = ev.y; 7399 auto pain = captureEle; 7400 while(pain) { 7401 eleR.x -= pain.x; 7402 eleR.y -= pain.y; 7403 pain.addScrollPosition(eleR.x, eleR.y); 7404 pain = pain.parent; 7405 } 7406 7407 void populateMouseEventBase(MouseEventBase event) { 7408 event.button = ev.button; 7409 event.buttonLinear = ev.buttonLinear; 7410 event.state = ev.modifierState; 7411 event.clientX = eleR.x; 7412 event.clientY = eleR.y; 7413 7414 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 7415 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 7416 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 7417 } 7418 7419 if(ev.type == MouseEventType.buttonPressed) { 7420 { 7421 auto event = new MouseDownEvent(captureEle); 7422 populateMouseEventBase(event); 7423 event.dispatch(); 7424 } 7425 7426 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 7427 auto event = new DoubleClickEvent(captureEle); 7428 populateMouseEventBase(event); 7429 event.dispatch(); 7430 lastWasDoubleClick = ev.doubleClick; 7431 } else { 7432 lastWasDoubleClick = false; 7433 } 7434 7435 mouseLastDownOn = ele; 7436 } else if(ev.type == MouseEventType.buttonReleased) { 7437 { 7438 auto event = new MouseUpEvent(captureEle); 7439 populateMouseEventBase(event); 7440 event.dispatch(); 7441 } 7442 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 7443 auto event = new ClickEvent(captureEle); 7444 populateMouseEventBase(event); 7445 event.dispatch(); 7446 } 7447 } else if(ev.type == MouseEventType.motion) { 7448 // motion 7449 { 7450 auto event = new MouseMoveEvent(captureEle); 7451 populateMouseEventBase(event); // fills in button which is meaningless but meh 7452 event.dispatch(); 7453 } 7454 7455 if(mouseLastOver !is ele) { 7456 if(ele !is null) { 7457 if(!isAParentOf(ele, mouseLastOver)) { 7458 ele.setDynamicState(DynamicState.hover, true); 7459 auto event = new MouseEnterEvent(ele); 7460 event.relatedTarget = mouseLastOver; 7461 event.sendDirectly(); 7462 7463 ele.useStyleProperties((scope Widget.Style s) { 7464 ele.parentWindow.win.cursor = s.cursor; 7465 }); 7466 } 7467 } 7468 7469 if(mouseLastOver !is null) { 7470 if(!isAParentOf(mouseLastOver, ele)) { 7471 mouseLastOver.setDynamicState(DynamicState.hover, false); 7472 auto event = new MouseLeaveEvent(mouseLastOver); 7473 event.relatedTarget = ele; 7474 event.sendDirectly(); 7475 } 7476 } 7477 7478 if(ele !is null) { 7479 auto event = new MouseOverEvent(ele); 7480 event.relatedTarget = mouseLastOver; 7481 event.dispatch(); 7482 } 7483 7484 if(mouseLastOver !is null) { 7485 auto event = new MouseOutEvent(mouseLastOver); 7486 event.relatedTarget = ele; 7487 event.dispatch(); 7488 } 7489 7490 mouseLastOver = ele; 7491 } 7492 } 7493 7494 return true; 7495 } 7496 7497 /// Shows the window and runs the application event loop. 7498 @scriptable 7499 void loop() { 7500 show(); 7501 win.eventLoop(0); 7502 } 7503 7504 private bool firstShow = true; 7505 7506 @scriptable 7507 override void show() { 7508 bool rd = false; 7509 if(firstShow) { 7510 firstShow = false; 7511 recomputeChildLayout(); 7512 auto f = getFirstFocusable(this); // FIXME: autofocus? 7513 if(f) 7514 f.focus(); 7515 redraw(); 7516 } 7517 win.show(); 7518 super.show(); 7519 } 7520 @scriptable 7521 override void hide() { 7522 win.hide(); 7523 super.hide(); 7524 } 7525 7526 static Widget getFirstFocusable(Widget start) { 7527 if(start.tabStop && !start.hidden) 7528 return start; 7529 7530 if(!start.hidden) 7531 foreach(child; start.children) { 7532 auto f = getFirstFocusable(child); 7533 if(f !is null) 7534 return f; 7535 } 7536 return null; 7537 } 7538 7539 mixin Emits!ClosingEvent; 7540 mixin Emits!ClosedEvent; 7541 } 7542 7543 debug private class DevToolWindow : Window { 7544 Window p; 7545 7546 TextEdit parentList; 7547 TextEdit logWindow; 7548 TextLabel clickX, clickY; 7549 7550 this(Window p) { 7551 this.p = p; 7552 super(400, 300, "Developer Toolbox"); 7553 7554 logWindow = new TextEdit(this); 7555 parentList = new TextEdit(this); 7556 7557 auto hl = new HorizontalLayout(this); 7558 clickX = new TextLabel("", TextAlignment.Right, hl); 7559 clickY = new TextLabel("", TextAlignment.Right, hl); 7560 7561 parentListeners ~= p.addEventListener("*", (Event ev) { 7562 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 7563 }); 7564 7565 parentListeners ~= p.addEventListener((ClickEvent ev) { 7566 auto s = ev.srcElement; 7567 string list = s.toString(); 7568 s = s.parent; 7569 while(s) { 7570 list ~= "\n"; 7571 list ~= s.toString(); 7572 s = s.parent; 7573 } 7574 parentList.content = list; 7575 7576 clickX.label = toInternal!string(ev.clientX); 7577 clickY.label = toInternal!string(ev.clientY); 7578 }); 7579 } 7580 7581 EventListener[] parentListeners; 7582 7583 override void close() { 7584 assert(p !is null); 7585 foreach(p; parentListeners) 7586 p.disconnect(); 7587 parentListeners = null; 7588 p.devTools = null; 7589 p = null; 7590 super.close(); 7591 } 7592 7593 override void defaultEventHandler_keydown(KeyDownEvent ev) { 7594 if(ev.key == Key.F12) { 7595 this.close(); 7596 if(p) 7597 p.devTools = null; 7598 } else { 7599 super.defaultEventHandler_keydown(ev); 7600 } 7601 } 7602 7603 void log(T...)(T t) { 7604 string str; 7605 import std.conv; 7606 foreach(i; t) 7607 str ~= to!string(i); 7608 str ~= "\n"; 7609 logWindow.addText(str); 7610 7611 version(custom_widgets) 7612 logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 7613 } 7614 } 7615 7616 /++ 7617 A dialog is a transient window that intends to get information from 7618 the user before being dismissed. 7619 +/ 7620 abstract class Dialog : Window { 7621 /// 7622 this(int width, int height, string title = null) { 7623 super(width, height, title); 7624 } 7625 7626 /// 7627 abstract void OK(); 7628 7629 /// 7630 void Cancel() { 7631 this.close(); 7632 } 7633 } 7634 7635 /++ 7636 A custom widget similar to the HTML5 <details> tag. 7637 +/ 7638 version(none) 7639 class DetailsView : Widget { 7640 7641 } 7642 7643 // FIXME: maybe i should expose the other list views Windows offers too 7644 7645 /++ 7646 A TableView is a widget made to display a table of data strings. 7647 7648 7649 Future_Directions: 7650 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 7651 7652 I will add a selection changed event at some point, as well as item clicked events. 7653 History: 7654 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 7655 See_Also: 7656 [ListWidget] which displays a list of strings without additional columns. 7657 +/ 7658 class TableView : Widget { 7659 /++ 7660 7661 +/ 7662 this(Widget parent) { 7663 super(parent); 7664 7665 version(win32_widgets) { 7666 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 7667 } else version(custom_widgets) { 7668 auto smw = new ScrollMessageWidget(this); 7669 smw.addDefaultKeyboardListeners(); 7670 smw.addDefaultWheelListeners(1, 16); 7671 tvwi = new TableViewWidgetInner(this, smw); 7672 } 7673 } 7674 7675 // FIXME: auto-size columns on double click of header thing like in Windows 7676 // it need only make the currently displayed things fit well. 7677 7678 7679 private ColumnInfo[] columns; 7680 private int itemCount; 7681 7682 version(custom_widgets) private { 7683 TableViewWidgetInner tvwi; 7684 } 7685 7686 /// Passed to [setColumnInfo] 7687 static struct ColumnInfo { 7688 const(char)[] name; /// the name displayed in the header 7689 int width; /// the default width, in pixels 7690 TextAlignment alignment; /// alignment of the text in the cell 7691 7692 /++ 7693 After all the pixel widths have been assigned, any left over 7694 space is divided up among all columns and distributed to according 7695 to the widthPercent field. 7696 7697 For example, if you have two fields, both with width 50 and one with 7698 widthPercent of 25 and the other with widthPercent of 75, and the 7699 container is 200 pixels wide, first both get their width of 50. 7700 then the 100 remaining pixels are split up, so the one gets a total 7701 of 75 pixels and the other gets a total of 125. 7702 7703 This is automatically applied as the window is resized. 7704 7705 If there is not enough space - that is, when a horizontal scrollbar 7706 needs to appear - there are 0 pixels divided up, and thus everyone 7707 gets 0. This can cause a column to shrink out of proportion when 7708 passing the scroll threshold. 7709 7710 It is important to still set a fixed width (that is, to populate the 7711 `width` field) even if you use the percents because that will be the 7712 default minimum in the event of a scroll bar appearing. 7713 7714 The percents total in the column can never exceed 100 or be less than 0. 7715 Doing this will trigger an assert error. 7716 7717 History: 7718 Added November 10, 2021 (dub v10.4) 7719 +/ 7720 int widthPercent; 7721 7722 7723 private int calculatedWidth; 7724 } 7725 /++ 7726 Sets the number of columns along with information about the headers. 7727 7728 Please note: on Windows, the first column ignores your alignment preference 7729 and is always left aligned. 7730 +/ 7731 void setColumnInfo(ColumnInfo[] columns...) { 7732 7733 foreach(ref c; this.columns) 7734 c.name = c.name.idup; 7735 this.columns = columns.dup; 7736 7737 updateCalculatedWidth(false); 7738 7739 version(custom_widgets) { 7740 tvwi.header.updateHeaders(); 7741 tvwi.updateScrolls(); 7742 } else version(win32_widgets) 7743 foreach(i, ref column; columns) { 7744 LVCOLUMN lvColumn; 7745 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 7746 lvColumn.cx = column.calculatedWidth; 7747 7748 auto bfr = WCharzBuffer(column.name); 7749 lvColumn.pszText = bfr.ptr; 7750 7751 if(column.alignment & TextAlignment.Center) 7752 lvColumn.fmt = LVCFMT_CENTER; 7753 else if(column.alignment & TextAlignment.Right) 7754 lvColumn.fmt = LVCFMT_RIGHT; 7755 else 7756 lvColumn.fmt = LVCFMT_LEFT; 7757 7758 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 7759 throw new WindowsApiException("Insert Column Fail"); 7760 } 7761 } 7762 7763 private void updateCalculatedWidth(bool informWindows) { 7764 7765 int remaining = this.width; 7766 foreach(column; columns) 7767 remaining -= column.width; 7768 if(remaining < 0) 7769 remaining = 0; 7770 7771 int percentTotal; 7772 foreach(i, ref column; columns) { 7773 percentTotal += column.widthPercent; 7774 auto c = column.width + (remaining * column.widthPercent) / 100; 7775 column.calculatedWidth = c; 7776 version(win32_widgets) 7777 if(informWindows) 7778 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 7779 } 7780 7781 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 7782 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)."); 7783 7784 7785 } 7786 7787 override void registerMovement() { 7788 super.registerMovement(); 7789 7790 updateCalculatedWidth(true); 7791 } 7792 7793 /++ 7794 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. 7795 +/ 7796 void setItemCount(int count) { 7797 this.itemCount = count; 7798 version(custom_widgets) { 7799 tvwi.updateScrolls(); 7800 redraw(); 7801 } else version(win32_widgets) { 7802 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 7803 } 7804 } 7805 7806 /++ 7807 Clears all items; 7808 +/ 7809 void clear() { 7810 this.itemCount = 0; 7811 this.columns = null; 7812 version(custom_widgets) { 7813 tvwi.header.updateHeaders(); 7814 tvwi.updateScrolls(); 7815 redraw(); 7816 } else version(win32_widgets) { 7817 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 7818 } 7819 } 7820 7821 /+ 7822 version(win32_widgets) 7823 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 7824 auto itemId = dis.itemID; 7825 auto hdc = dis.hDC; 7826 auto rect = dis.rcItem; 7827 switch(dis.itemAction) { 7828 case ODA_DRAWENTIRE: 7829 7830 // FIXME: do other items 7831 // FIXME: do the focus rectangle i guess 7832 // FIXME: alignment 7833 // FIXME: column width 7834 // FIXME: padding left 7835 // FIXME: check dpi scaling 7836 // FIXME: don't owner draw unless it is necessary. 7837 7838 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 7839 RECT itemRect; 7840 itemRect.top = 1; // subitem idx, 1-based 7841 itemRect.left = LVIR_BOUNDS; 7842 7843 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 7844 itemRect.left += padding; 7845 7846 getData(itemId, 0, (in char[] data) { 7847 auto wdata = WCharzBuffer(data); 7848 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 7849 7850 }); 7851 goto case; 7852 case ODA_FOCUS: 7853 if(dis.itemState & ODS_FOCUS) 7854 DrawFocusRect(hdc, &rect); 7855 break; 7856 case ODA_SELECT: 7857 // itemState & ODS_SELECTED 7858 break; 7859 default: 7860 } 7861 return 1; 7862 } 7863 +/ 7864 7865 7866 version(win32_widgets) 7867 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 7868 switch(code) { 7869 case NM_CUSTOMDRAW: 7870 auto s = cast(NMLVCUSTOMDRAW*) hdr; 7871 switch(s.nmcd.dwDrawStage) { 7872 case CDDS_PREPAINT: 7873 if(getCellStyle is null) 7874 return 0; 7875 7876 mustReturn = true; 7877 return CDRF_NOTIFYITEMDRAW; 7878 case CDDS_ITEMPREPAINT: 7879 mustReturn = true; 7880 return CDRF_NOTIFYSUBITEMDRAW; 7881 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 7882 mustReturn = true; 7883 7884 if(getCellStyle is null) // this SHOULD never happen... 7885 return 0; 7886 7887 auto style = getCellStyle(s.nmcd.dwItemSpec, s.iSubItem); 7888 if(style == CellStyle.init) 7889 return 0; // allow default processing to continue 7890 7891 if(style.flags & CellStyle.Flags.textColorSet) 7892 s.clrText = style.textColor.asWindowsColorRef; 7893 else 7894 s.clrText = 0; // reset in case it was set from last iteration not a fan 7895 if(style.flags & CellStyle.Flags.backgroundColorSet) 7896 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 7897 else 7898 s.clrTextBk = Color.white.asWindowsColorRef; // need to reset it... not a fan of this 7899 7900 return CDRF_NEWFONT; 7901 default: 7902 return 0; 7903 7904 } 7905 case NM_RETURN: // no need since i subclass keydown 7906 break; 7907 case LVN_COLUMNCLICK: 7908 auto info = cast(LPNMLISTVIEW) hdr; 7909 this.emit!HeaderClickedEvent(info.iSubItem); 7910 break; 7911 case NM_CLICK: 7912 case NM_DBLCLK: 7913 case NM_RCLICK: 7914 case NM_RDBLCLK: 7915 // the item/subitem is set here and that can be a useful notification 7916 // even beyond the normal click notification 7917 break; 7918 case LVN_GETDISPINFO: 7919 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 7920 if(info.item.mask & LVIF_TEXT) { 7921 if(getData) { 7922 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 7923 auto bfr = WCharzBuffer(dataReceived); 7924 auto len = info.item.cchTextMax; 7925 if(bfr.length < len) 7926 len = cast(typeof(len)) bfr.length; 7927 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 7928 info.item.pszText[len] = 0; 7929 }); 7930 } else { 7931 info.item.pszText[0] = 0; 7932 } 7933 //info.item.iItem 7934 //if(info.item.iSubItem) 7935 } 7936 break; 7937 default: 7938 } 7939 return 0; 7940 } 7941 7942 override bool encapsulatedChildren() { 7943 return true; 7944 } 7945 7946 /++ 7947 Informs the control that content has changed. 7948 7949 History: 7950 Added November 10, 2021 (dub v10.4) 7951 +/ 7952 void update() { 7953 version(custom_widgets) 7954 redraw(); 7955 else { 7956 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 7957 UpdateWindow(hwnd); 7958 } 7959 7960 7961 } 7962 7963 /++ 7964 Called by the system to request the text content of an individual cell. You 7965 should pass the text into the provided `sink` delegate. This function will be 7966 called for each visible cell as-needed when drawing. 7967 +/ 7968 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 7969 7970 /++ 7971 Available per-cell style customization options. Use one of the constructors 7972 provided to set the values conveniently, or default construct it and set individual 7973 values yourself. Just remember to set the `flags` so your values are actually used. 7974 If the flag isn't set, the field is ignored and the system default is used instead. 7975 7976 This is returned by the [getCellStyle] delegate. 7977 7978 Examples: 7979 --- 7980 // assumes you have a variables called `my_data` which is an array of arrays of numbers 7981 auto table = new TableView(window); 7982 // snip: you would set up columns here 7983 7984 // this is how you provide data to the table view class 7985 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 7986 import std.conv; 7987 sink(to!string(my_data[row][column])); 7988 }; 7989 7990 // and this is how you customize the colors 7991 table.getCellStyle = delegate(int row, int column) { 7992 return (my_data[row][column] < 0) ? 7993 TableView.CellStyle(Color.red); // make negative numbers red 7994 : TableView.CellStyle.init; // leave the rest alone 7995 }; 7996 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 7997 --- 7998 7999 History: 8000 Added November 27, 2021 (dub v10.4) 8001 +/ 8002 struct CellStyle { 8003 /// 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. 8004 this(Color textColor) { 8005 this.textColor = textColor; 8006 this.flags |= Flags.textColorSet; 8007 } 8008 /// Sets a custom text and background color. 8009 this(Color textColor, Color backgroundColor) { 8010 this.textColor = textColor; 8011 this.backgroundColor = backgroundColor; 8012 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 8013 } 8014 8015 Color textColor; 8016 Color backgroundColor; 8017 int flags; /// bitmask of [Flags] 8018 /// available options to combine into [flags] 8019 enum Flags { 8020 textColorSet = 1 << 0, 8021 backgroundColorSet = 1 << 1, 8022 } 8023 } 8024 /++ 8025 Companion delegate to [getData] that allows you to custom style each 8026 cell of the table. 8027 8028 Returns: 8029 A [CellStyle] structure that describes the desired style for the 8030 given cell. `return CellStyle.init` if you want the default style. 8031 8032 History: 8033 Added November 27, 2021 (dub v10.4) 8034 +/ 8035 CellStyle delegate(int row, int column) getCellStyle; 8036 8037 8038 8039 // i want to be able to do things like draw little colored things to show red for negative numbers 8040 // or background color indicators or even in-cell charts 8041 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 8042 8043 /++ 8044 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 8045 +/ 8046 mixin Emits!HeaderClickedEvent; 8047 } 8048 8049 /++ 8050 This is emitted by the [TableView] when a user clicks on a column header. 8051 8052 Its member `columnIndex` has the zero-based index of the column that was clicked. 8053 8054 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 8055 8056 History: 8057 Added November 27, 2021 (dub v10.4) 8058 +/ 8059 class HeaderClickedEvent : Event { 8060 enum EventString = "HeaderClicked"; 8061 this(Widget target, int columnIndex) { 8062 this.columnIndex = columnIndex; 8063 super(EventString, target); 8064 } 8065 8066 /// The index of the column 8067 int columnIndex; 8068 8069 /// 8070 override @property int intValue() { 8071 return columnIndex; 8072 } 8073 } 8074 8075 version(custom_widgets) 8076 private class TableViewWidgetInner : Widget { 8077 8078 // wrap this thing in a ScrollMessageWidget 8079 8080 TableView tvw; 8081 ScrollMessageWidget smw; 8082 HeaderWidget header; 8083 8084 this(TableView tvw, ScrollMessageWidget smw) { 8085 this.tvw = tvw; 8086 this.smw = smw; 8087 super(smw); 8088 8089 this.tabStop = true; 8090 8091 header = new HeaderWidget(this, smw.getHeader()); 8092 8093 smw.addEventListener("scroll", () { 8094 this.redraw(); 8095 header.redraw(); 8096 }); 8097 8098 8099 // I need headers outside the scroll area but rendered on the same line as the up arrow 8100 // FIXME: add a fixed header to the SMW 8101 } 8102 8103 void updateScrolls() { 8104 int w; 8105 foreach(column; tvw.columns) 8106 w += column.width; 8107 smw.setTotalArea(w, tvw.itemCount); 8108 columnsWidth = w; 8109 } 8110 8111 private int columnsWidth; 8112 8113 int lh = 16; // FIXME lineHeight 8114 8115 override void registerMovement() { 8116 super.registerMovement(); 8117 // FIXME: actual column width. it might need to be done per-pixel instead of per-colum 8118 smw.setViewableArea(this.width, this.height / lh); 8119 } 8120 8121 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 8122 int x; 8123 int y; 8124 8125 int row = smw.position.y; 8126 8127 enum padding = 3; 8128 8129 foreach(lol; 0 .. this.height / lh) { 8130 if(row >= tvw.itemCount) 8131 break; 8132 x = 0; 8133 foreach(columnNumber, column; tvw.columns) { 8134 auto x2 = x + column.calculatedWidth; 8135 auto smwx = smw.position.x; 8136 8137 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 8138 auto startX = x; 8139 auto endX = x + column.calculatedWidth; 8140 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 8141 case TextAlignment.Left: startX += padding; break; 8142 case TextAlignment.Center: startX += padding; endX -= padding; break; 8143 case TextAlignment.Right: endX -= padding; break; 8144 default: /* broken */ break; 8145 } 8146 tvw.getData(row, cast(int) columnNumber, (info) { 8147 // auto clip = painter.setClipRectangle( 8148 8149 void dotext(WidgetPainter painter) { 8150 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 8151 } 8152 8153 if(tvw.getCellStyle !is null) { 8154 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 8155 8156 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 8157 auto tempPainter = painter; 8158 tempPainter.fillColor = style.backgroundColor; 8159 tempPainter.outlineColor = style.backgroundColor; 8160 8161 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 8162 Point(endX - smw.position.x, y + lh)); 8163 } 8164 auto tempPainter = painter; 8165 if(style.flags & TableView.CellStyle.Flags.textColorSet) 8166 tempPainter.outlineColor = style.textColor; 8167 8168 dotext(tempPainter); 8169 } else { 8170 dotext(painter); 8171 } 8172 }); 8173 } 8174 8175 x += column.calculatedWidth; 8176 } 8177 row++; 8178 y += lh; 8179 } 8180 return bounds; 8181 } 8182 8183 static class Style : Widget.Style { 8184 override WidgetBackground background() { 8185 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 8186 } 8187 } 8188 mixin OverrideStyle!Style; 8189 8190 private static class HeaderWidget : Widget { 8191 this(TableViewWidgetInner tvw, Widget parent) { 8192 super(parent); 8193 this.tvw = tvw; 8194 8195 this.remainder = new Button("", this); 8196 8197 this.addEventListener((scope ClickEvent ev) { 8198 int header = -1; 8199 foreach(idx, child; this.children[1 .. $]) { 8200 if(child is ev.target) { 8201 header = cast(int) idx; 8202 break; 8203 } 8204 } 8205 8206 if(header != -1) { 8207 auto hce = new HeaderClickedEvent(tvw.tvw, header); 8208 hce.dispatch(); 8209 } 8210 8211 }); 8212 } 8213 8214 void updateHeaders() { 8215 foreach(child; children[1 .. $]) 8216 child.removeWidget(); 8217 8218 foreach(column; tvw.tvw.columns) { 8219 // the cast is ok because I dup it above, just the type is never changed. 8220 // all this is private so it should never get messed up. 8221 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 8222 } 8223 } 8224 8225 Button remainder; 8226 TableViewWidgetInner tvw; 8227 8228 override void recomputeChildLayout() { 8229 registerMovement(); 8230 int pos; 8231 foreach(idx, child; children[1 .. $]) { 8232 if(idx >= tvw.tvw.columns.length) 8233 continue; 8234 child.x = pos; 8235 child.y = 0; 8236 child.width = tvw.tvw.columns[idx].calculatedWidth; 8237 child.height = 16;// this.height; 8238 pos += child.width; 8239 8240 child.recomputeChildLayout(); 8241 } 8242 8243 if(remainder is null) 8244 return; 8245 8246 remainder.x = pos; 8247 remainder.y = 0; 8248 if(pos < this.width) 8249 remainder.width = this.width - pos;// + 4; 8250 else 8251 remainder.width = 0; 8252 remainder.height = 16; 8253 8254 remainder.recomputeChildLayout(); 8255 } 8256 8257 // for the scrollable children mixin 8258 Point scrollOrigin() { 8259 return Point(tvw.smw.position.x, 0); 8260 } 8261 void paintFrameAndBackground(WidgetPainter painter) { } 8262 8263 mixin ScrollableChildren; 8264 } 8265 } 8266 8267 /+ 8268 8269 // given struct / array / number / string / etc, make it viewable and editable 8270 class DataViewerWidget : Widget { 8271 8272 } 8273 8274 // this is just the tab list with no associated page 8275 class TabMessageWidget : Widget { 8276 8277 } 8278 +/ 8279 8280 /++ 8281 A line edit box with an associated label. 8282 8283 History: 8284 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 8285 8286 ``` 8287 Old: ________ 8288 8289 New: 8290 ____________ 8291 ``` 8292 8293 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 8294 8295 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 8296 horizontal label but left aligned. You may also consider a [GridLayout]. 8297 +/ 8298 alias LabeledLineEdit = Labeled!LineEdit; 8299 8300 /++ 8301 History: 8302 Added May 19, 2021 8303 +/ 8304 class Labeled(T) : Widget { 8305 /// 8306 this(string label, Widget parent) { 8307 super(parent); 8308 initialize!VerticalLayout(label, TextAlignment.Left, parent); 8309 } 8310 8311 /++ 8312 History: 8313 The alignment parameter was added May 17, 2021 8314 +/ 8315 this(string label, TextAlignment alignment, Widget parent) { 8316 super(parent); 8317 initialize!HorizontalLayout(label, alignment, parent); 8318 } 8319 8320 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 8321 tabStop = false; 8322 horizontal = is(L == HorizontalLayout); 8323 auto hl = new L(this); 8324 this.label = new TextLabel(label, alignment, hl); 8325 this.lineEdit = new T(hl); 8326 8327 this.label.labelFor = this.lineEdit; 8328 } 8329 8330 private bool horizontal; 8331 8332 TextLabel label; /// 8333 T lineEdit; /// 8334 8335 override int flexBasisWidth() { return 250; } 8336 8337 override int minHeight() { return (horizontal ? 1 : 2) * defaultLineHeight + 4; } 8338 override int maxHeight() { return (horizontal ? 1 : 2) * defaultLineHeight + 4; } 8339 override int marginTop() { return 4; } 8340 override int marginBottom() { return 4; } 8341 8342 /// 8343 @property string content() { 8344 return lineEdit.content; 8345 } 8346 /// 8347 @property void content(string c) { 8348 return lineEdit.content(c); 8349 } 8350 8351 /// 8352 void selectAll() { 8353 lineEdit.selectAll(); 8354 } 8355 8356 override void focus() { 8357 lineEdit.focus(); 8358 } 8359 } 8360 8361 /++ 8362 A labeled password edit. 8363 8364 History: 8365 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 8366 8367 The default parameters for the constructors were also removed on May 19, 2021 8368 +/ 8369 alias LabeledPasswordEdit = Labeled!PasswordEdit; 8370 8371 private string toMenuLabel(string s) { 8372 string n; 8373 n.reserve(s.length); 8374 foreach(c; s) 8375 if(c == '_') 8376 n ~= ' '; 8377 else 8378 n ~= c; 8379 return n; 8380 } 8381 8382 private void delegate() makeAutomaticHandler(alias fn, T)(T t) { 8383 static if(is(T : void delegate())) { 8384 return t; 8385 } else { 8386 static if(is(typeof(fn) Params == __parameters)) 8387 struct S { 8388 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 8389 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 8390 } else mixin(q{ 8391 static foreach(idx, ignore; Params) { 8392 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 8393 } 8394 }); 8395 } 8396 return () { 8397 dialog((S s) { 8398 cast(void) t(s.tupleof); 8399 }, null, __traits(identifier, fn)); 8400 }; 8401 } 8402 } 8403 8404 private template hasAnyRelevantAnnotations(a...) { 8405 bool helper() { 8406 bool any; 8407 foreach(attr; a) { 8408 static if(is(typeof(attr) == .menu)) 8409 any = true; 8410 else static if(is(typeof(attr) == .toolbar)) 8411 any = true; 8412 else static if(is(attr == .separator)) 8413 any = true; 8414 else static if(is(typeof(attr) == .accelerator)) 8415 any = true; 8416 else static if(is(typeof(attr) == .hotkey)) 8417 any = true; 8418 else static if(is(typeof(attr) == .icon)) 8419 any = true; 8420 else static if(is(typeof(attr) == .label)) 8421 any = true; 8422 else static if(is(typeof(attr) == .tip)) 8423 any = true; 8424 } 8425 return any; 8426 } 8427 8428 enum bool hasAnyRelevantAnnotations = helper(); 8429 } 8430 8431 /++ 8432 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. 8433 +/ 8434 class MainWindow : Window { 8435 /// 8436 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 8437 super(initialWidth, initialHeight, title); 8438 8439 _clientArea = new ClientAreaWidget(); 8440 _clientArea.x = 0; 8441 _clientArea.y = 0; 8442 _clientArea.width = this.width; 8443 _clientArea.height = this.height; 8444 _clientArea.tabStop = false; 8445 8446 super.addChild(_clientArea); 8447 8448 statusBar = new StatusBar(this); 8449 } 8450 8451 /++ 8452 Adds a menu and toolbar from annotated functions. 8453 8454 --- 8455 struct Commands { 8456 @menu("File") { 8457 void New() {} 8458 void Open() {} 8459 void Save() {} 8460 @separator 8461 void Exit() @accelerator("Alt+F4") @hotkey('x') { 8462 window.close(); 8463 } 8464 } 8465 8466 @menu("Edit") { 8467 void Undo() { 8468 undo(); 8469 } 8470 @separator 8471 void Cut() {} 8472 void Copy() {} 8473 void Paste() {} 8474 } 8475 8476 @menu("Help") { 8477 void About() {} 8478 } 8479 } 8480 8481 Commands commands; 8482 8483 window.setMenuAndToolbarFromAnnotatedCode(commands); 8484 --- 8485 8486 Note that you can call this function multiple times and it will add the items in order to the given items. 8487 8488 +/ 8489 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 8490 setMenuAndToolbarFromAnnotatedCode_internal(t); 8491 } 8492 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 8493 setMenuAndToolbarFromAnnotatedCode_internal(t); 8494 } 8495 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 8496 Action[] toolbarActions; 8497 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 8498 Menu[string] mcs; 8499 8500 foreach(menu; menuBar.subMenus) { 8501 mcs[menu.label] = menu; 8502 } 8503 8504 foreach(memberName; __traits(derivedMembers, T)) { 8505 static if(memberName != "this") 8506 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 8507 .menu menu; 8508 .toolbar toolbar; 8509 bool separator; 8510 .accelerator accelerator; 8511 .hotkey hotkey; 8512 .icon icon; 8513 string label; 8514 string tip; 8515 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 8516 static if(is(typeof(attr) == .menu)) 8517 menu = attr; 8518 else static if(is(typeof(attr) == .toolbar)) 8519 toolbar = attr; 8520 else static if(is(attr == .separator)) 8521 separator = true; 8522 else static if(is(typeof(attr) == .accelerator)) 8523 accelerator = attr; 8524 else static if(is(typeof(attr) == .hotkey)) 8525 hotkey = attr; 8526 else static if(is(typeof(attr) == .icon)) 8527 icon = attr; 8528 else static if(is(typeof(attr) == .label)) 8529 label = attr.label; 8530 else static if(is(typeof(attr) == .tip)) 8531 tip = attr.tip; 8532 } 8533 8534 if(menu !is .menu.init || toolbar !is .toolbar.init) { 8535 ushort correctIcon = icon.id; // FIXME 8536 if(label.length == 0) 8537 label = memberName.toMenuLabel; 8538 8539 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName)); 8540 8541 auto action = new Action(label, correctIcon, handler); 8542 8543 if(accelerator.keyString.length) { 8544 auto ke = KeyEvent.parse(accelerator.keyString); 8545 action.accelerator = ke; 8546 accelerators[ke.toStr] = handler; 8547 } 8548 8549 if(toolbar !is .toolbar.init) 8550 toolbarActions ~= action; 8551 if(menu !is .menu.init) { 8552 Menu mc; 8553 if(menu.name in mcs) { 8554 mc = mcs[menu.name]; 8555 } else { 8556 mc = new Menu(menu.name, this); 8557 menuBar.addItem(mc); 8558 mcs[menu.name] = mc; 8559 } 8560 8561 if(separator) 8562 mc.addSeparator(); 8563 mc.addItem(new MenuItem(action)); 8564 } 8565 } 8566 } 8567 } 8568 8569 this.menuBar = menuBar; 8570 8571 if(toolbarActions.length) { 8572 auto tb = new ToolBar(toolbarActions, this); 8573 } 8574 } 8575 8576 void delegate()[string] accelerators; 8577 8578 override void defaultEventHandler_keydown(KeyDownEvent event) { 8579 auto str = event.originalKeyEvent.toStr; 8580 if(auto acl = str in accelerators) 8581 (*acl)(); 8582 super.defaultEventHandler_keydown(event); 8583 } 8584 8585 override void defaultEventHandler_mouseover(MouseOverEvent event) { 8586 super.defaultEventHandler_mouseover(event); 8587 if(this.statusBar !is null && event.target.statusTip.length) 8588 this.statusBar.parts[0].content = event.target.statusTip; 8589 else if(this.statusBar !is null && this.statusTip.length) 8590 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 8591 } 8592 8593 override void addChild(Widget c, int position = int.max) { 8594 if(auto tb = cast(ToolBar) c) 8595 version(win32_widgets) 8596 super.addChild(c, 0); 8597 else version(custom_widgets) 8598 super.addChild(c, menuBar ? 1 : 0); 8599 else static assert(0); 8600 else 8601 clientArea.addChild(c, position); 8602 } 8603 8604 ToolBar _toolBar; 8605 /// 8606 ToolBar toolBar() { return _toolBar; } 8607 /// 8608 ToolBar toolBar(ToolBar t) { 8609 _toolBar = t; 8610 foreach(child; this.children) 8611 if(child is t) 8612 return t; 8613 version(win32_widgets) 8614 super.addChild(t, 0); 8615 else version(custom_widgets) 8616 super.addChild(t, menuBar ? 1 : 0); 8617 else static assert(0); 8618 return t; 8619 } 8620 8621 MenuBar _menu; 8622 /// 8623 MenuBar menuBar() { return _menu; } 8624 /// 8625 MenuBar menuBar(MenuBar m) { 8626 if(m is _menu) { 8627 version(custom_widgets) 8628 recomputeChildLayout(); 8629 return m; 8630 } 8631 8632 if(_menu !is null) { 8633 // make sure it is sanely removed 8634 // FIXME 8635 } 8636 8637 _menu = m; 8638 8639 version(win32_widgets) { 8640 SetMenu(parentWindow.win.impl.hwnd, m.handle); 8641 } else version(custom_widgets) { 8642 super.addChild(m, 0); 8643 8644 // clientArea.y = menu.height; 8645 // clientArea.height = this.height - menu.height; 8646 8647 recomputeChildLayout(); 8648 } else static assert(false); 8649 8650 return _menu; 8651 } 8652 private Widget _clientArea; 8653 /// 8654 @property Widget clientArea() { return _clientArea; } 8655 protected @property void clientArea(Widget wid) { 8656 _clientArea = wid; 8657 } 8658 8659 private StatusBar _statusBar; 8660 /// 8661 @property StatusBar statusBar() { return _statusBar; } 8662 /// 8663 @property void statusBar(StatusBar bar) { 8664 _statusBar = bar; 8665 super.addChild(_statusBar); 8666 } 8667 8668 /// 8669 @property string title() { return parentWindow.win.title; } 8670 /// 8671 @property void title(string title) { parentWindow.win.title = title; } 8672 } 8673 8674 /+ 8675 This is really an implementation detail of [MainWindow] 8676 +/ 8677 private class ClientAreaWidget : Widget { 8678 this() { 8679 this.tabStop = false; 8680 super(null); 8681 //sa = new ScrollableWidget(this); 8682 } 8683 /* 8684 ScrollableWidget sa; 8685 override void addChild(Widget w, int position) { 8686 if(sa is null) 8687 super.addChild(w, position); 8688 else { 8689 sa.addChild(w, position); 8690 sa.setContentSize(this.minWidth + 1, this.minHeight); 8691 import std.stdio; writeln(sa.contentWidth, "x", sa.contentHeight); 8692 } 8693 } 8694 */ 8695 } 8696 8697 /** 8698 Toolbars are lists of buttons (typically icons) that appear under the menu. 8699 Each button ought to correspond to a menu item, represented by [Action] objects. 8700 */ 8701 class ToolBar : Widget { 8702 version(win32_widgets) { 8703 private const int idealHeight; 8704 override int minHeight() { return idealHeight; } 8705 override int maxHeight() { return idealHeight; } 8706 } else version(custom_widgets) { 8707 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 8708 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 8709 } else static assert(false); 8710 override int heightStretchiness() { return 0; } 8711 8712 version(win32_widgets) 8713 HIMAGELIST imageList; 8714 8715 this(Widget parent) { 8716 this(null, parent); 8717 } 8718 8719 /// 8720 this(Action[] actions, Widget parent) { 8721 super(parent); 8722 8723 tabStop = false; 8724 8725 version(win32_widgets) { 8726 // so i like how the flat thing looks on windows, but not on wine 8727 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 8728 // leave it commented 8729 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 8730 8731 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 8732 8733 imageList = ImageList_Create( 8734 // width, height 8735 16, 16, 8736 ILC_COLOR16 | ILC_MASK, 8737 16 /*numberOfButtons*/, 0); 8738 8739 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageList); 8740 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 8741 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 8742 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 8743 8744 TBBUTTON[] buttons; 8745 8746 // FIXME: I_IMAGENONE is if here is no icon 8747 foreach(action; actions) 8748 buttons ~= TBBUTTON( 8749 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 8750 action.id, 8751 TBSTATE_ENABLED, // state 8752 0, // style 8753 0, // reserved array, just zero it out 8754 0, // dwData 8755 cast(size_t) toWstringzInternal(action.label) // INT_PTR 8756 ); 8757 8758 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 8759 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 8760 8761 SIZE size; 8762 import core.sys.windows.commctrl; 8763 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 8764 idealHeight = size.cy + 4; // the plus 4 is a hack 8765 8766 /* 8767 RECT rect; 8768 GetWindowRect(hwnd, &rect); 8769 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 8770 */ 8771 8772 assert(idealHeight); 8773 } else version(custom_widgets) { 8774 foreach(action; actions) 8775 new ToolButton(action, this); 8776 } else static assert(false); 8777 } 8778 8779 override void recomputeChildLayout() { 8780 .recomputeChildLayout!"width"(this); 8781 } 8782 } 8783 8784 enum toolbarIconSize = 24; 8785 8786 /// 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. 8787 class ToolButton : Button { 8788 /// 8789 this(string label, Widget parent) { 8790 super(label, parent); 8791 tabStop = false; 8792 } 8793 /// 8794 this(Action action, Widget parent) { 8795 super(action.label, parent); 8796 tabStop = false; 8797 this.action = action; 8798 } 8799 8800 version(custom_widgets) 8801 override void defaultEventHandler_click(ClickEvent event) { 8802 foreach(handler; action.triggered) 8803 handler(); 8804 } 8805 8806 Action action; 8807 8808 override int maxWidth() { return toolbarIconSize; } 8809 override int minWidth() { return toolbarIconSize; } 8810 override int maxHeight() { return toolbarIconSize; } 8811 override int minHeight() { return toolbarIconSize; } 8812 8813 version(custom_widgets) 8814 override void paint(WidgetPainter painter) { 8815 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 8816 painter.outlineColor = Color.black; 8817 8818 // I want to get from 16 to 24. that's * 3 / 2 8819 static assert(toolbarIconSize >= 16); 8820 enum multiplier = toolbarIconSize / 8; 8821 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 8822 switch(action.iconId) { 8823 case GenericIcons.New: 8824 painter.fillColor = Color.white; 8825 painter.drawPolygon( 8826 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 8827 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 8828 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 8829 ); 8830 break; 8831 case GenericIcons.Save: 8832 painter.fillColor = Color.white; 8833 painter.outlineColor = Color.black; 8834 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 8835 8836 // the label 8837 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 8838 8839 // the slider 8840 painter.fillColor = Color.black; 8841 painter.outlineColor = Color.black; 8842 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 8843 8844 painter.fillColor = Color.white; 8845 painter.outlineColor = Color.white; 8846 // the disc window 8847 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 8848 break; 8849 case GenericIcons.Open: 8850 painter.fillColor = Color.white; 8851 painter.drawPolygon( 8852 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 8853 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 8854 painter.drawPolygon( 8855 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 8856 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 8857 Point(2, 6) * multiplier / divisor); 8858 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 8859 break; 8860 case GenericIcons.Copy: 8861 painter.fillColor = Color.white; 8862 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 8863 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 8864 break; 8865 case GenericIcons.Cut: 8866 painter.fillColor = Color.transparent; 8867 painter.outlineColor = getComputedStyle.foregroundColor(); 8868 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 8869 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 8870 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 8871 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 8872 break; 8873 case GenericIcons.Paste: 8874 painter.fillColor = Color.white; 8875 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 8876 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 8877 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 8878 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 8879 painter.fillColor = Color.black; 8880 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 8881 break; 8882 case GenericIcons.Help: 8883 painter.outlineColor = getComputedStyle.foregroundColor(); 8884 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 8885 break; 8886 case GenericIcons.Undo: 8887 painter.fillColor = Color.transparent; 8888 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 8889 painter.outlineColor = Color.black; 8890 painter.fillColor = Color.black; 8891 painter.drawPolygon( 8892 Point(4, 4) * multiplier / divisor, 8893 Point(8, 2) * multiplier / divisor, 8894 Point(8, 6) * multiplier / divisor, 8895 Point(4, 4) * multiplier / divisor, 8896 ); 8897 break; 8898 case GenericIcons.Redo: 8899 painter.fillColor = Color.transparent; 8900 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 8901 painter.outlineColor = Color.black; 8902 painter.fillColor = Color.black; 8903 painter.drawPolygon( 8904 Point(10, 4) * multiplier / divisor, 8905 Point(6, 2) * multiplier / divisor, 8906 Point(6, 6) * multiplier / divisor, 8907 Point(10, 4) * multiplier / divisor, 8908 ); 8909 break; 8910 default: 8911 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 8912 } 8913 return bounds; 8914 }); 8915 } 8916 8917 } 8918 8919 8920 /// 8921 class MenuBar : Widget { 8922 MenuItem[] items; 8923 Menu[] subMenus; 8924 8925 version(win32_widgets) { 8926 HMENU handle; 8927 /// 8928 this(Widget parent = null) { 8929 super(parent); 8930 8931 handle = CreateMenu(); 8932 tabStop = false; 8933 } 8934 } else version(custom_widgets) { 8935 /// 8936 this(Widget parent = null) { 8937 tabStop = false; // these are selected some other way 8938 super(parent); 8939 } 8940 8941 mixin Padding!q{2}; 8942 } else static assert(false); 8943 8944 version(custom_widgets) 8945 override void paint(WidgetPainter painter) { 8946 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 8947 } 8948 8949 /// 8950 MenuItem addItem(MenuItem item) { 8951 this.addChild(item); 8952 items ~= item; 8953 version(win32_widgets) { 8954 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 8955 } 8956 return item; 8957 } 8958 8959 8960 /// 8961 Menu addItem(Menu item) { 8962 8963 subMenus ~= item; 8964 8965 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 8966 8967 addChild(mbItem); 8968 items ~= mbItem; 8969 8970 version(win32_widgets) { 8971 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 8972 } else version(custom_widgets) { 8973 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 8974 item.popup(mbItem); 8975 }; 8976 } else static assert(false); 8977 8978 return item; 8979 } 8980 8981 override void recomputeChildLayout() { 8982 .recomputeChildLayout!"width"(this); 8983 } 8984 8985 override int maxHeight() { return defaultLineHeight + 4; } 8986 override int minHeight() { return defaultLineHeight + 4; } 8987 } 8988 8989 8990 /** 8991 Status bars appear at the bottom of a MainWindow. 8992 They are made out of Parts, with a width and content. 8993 8994 They can have multiple parts or be in simple mode. FIXME: implement 8995 8996 8997 sb.parts[0].content = "Status bar text!"; 8998 */ 8999 class StatusBar : Widget { 9000 private Part[] partsArray; 9001 /// 9002 struct Parts { 9003 @disable this(); 9004 this(StatusBar owner) { this.owner = owner; } 9005 //@disable this(this); 9006 /// 9007 @property int length() { return cast(int) owner.partsArray.length; } 9008 private StatusBar owner; 9009 private this(StatusBar owner, Part[] parts) { 9010 this.owner.partsArray = parts; 9011 this.owner = owner; 9012 } 9013 /// 9014 Part opIndex(int p) { 9015 if(owner.partsArray.length == 0) 9016 this ~= new StatusBar.Part(300); 9017 return owner.partsArray[p]; 9018 } 9019 9020 /// 9021 Part opOpAssign(string op : "~" )(Part p) { 9022 assert(owner.partsArray.length < 255); 9023 p.owner = this.owner; 9024 p.idx = cast(int) owner.partsArray.length; 9025 owner.partsArray ~= p; 9026 version(win32_widgets) { 9027 int[256] pos; 9028 int cpos = 0; 9029 foreach(idx, part; owner.partsArray) { 9030 if(part.width) 9031 cpos += part.width; 9032 else 9033 cpos += 100; 9034 9035 if(idx + 1 == owner.partsArray.length) 9036 pos[idx] = -1; 9037 else 9038 pos[idx] = cpos; 9039 } 9040 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 9041 } else version(custom_widgets) { 9042 owner.redraw(); 9043 } else static assert(false); 9044 9045 return p; 9046 } 9047 } 9048 9049 private Parts _parts; 9050 /// 9051 final @property Parts parts() { 9052 return _parts; 9053 } 9054 9055 /// 9056 static class Part { 9057 int width; 9058 StatusBar owner; 9059 9060 /// 9061 this(int w = 100) { width = w; } 9062 9063 private int idx; 9064 private string _content; 9065 /// 9066 @property string content() { return _content; } 9067 /// 9068 @property void content(string s) { 9069 version(win32_widgets) { 9070 _content = s; 9071 WCharzBuffer bfr = WCharzBuffer(s); 9072 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 9073 } else version(custom_widgets) { 9074 if(_content != s) { 9075 _content = s; 9076 owner.redraw(); 9077 } 9078 } else static assert(false); 9079 } 9080 } 9081 string simpleModeContent; 9082 bool inSimpleMode; 9083 9084 9085 /// 9086 this(Widget parent) { 9087 super(null); // FIXME 9088 _parts = Parts(this); 9089 tabStop = false; 9090 version(win32_widgets) { 9091 parentWindow = parent.parentWindow; 9092 createWin32Window(this, "msctls_statusbar32"w, "", 0); 9093 9094 RECT rect; 9095 GetWindowRect(hwnd, &rect); 9096 idealHeight = rect.bottom - rect.top; 9097 assert(idealHeight); 9098 } else version(custom_widgets) { 9099 } else static assert(false); 9100 } 9101 9102 version(custom_widgets) 9103 override void paint(WidgetPainter painter) { 9104 auto cs = getComputedStyle(); 9105 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 9106 int cpos = 0; 9107 int remainingLength = this.width; 9108 foreach(idx, part; this.partsArray) { 9109 auto partWidth = part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 9110 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 9111 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 9112 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 9113 9114 painter.outlineColor = cs.foregroundColor(); 9115 painter.fillColor = cs.foregroundColor(); 9116 9117 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 9118 cpos += partWidth; 9119 remainingLength -= partWidth; 9120 } 9121 } 9122 9123 9124 version(win32_widgets) { 9125 private const int idealHeight; 9126 override int maxHeight() { return idealHeight; } 9127 override int minHeight() { return idealHeight; } 9128 } else version(custom_widgets) { 9129 override int maxHeight() { return defaultLineHeight + 4; } 9130 override int minHeight() { return defaultLineHeight + 4; } 9131 } else static assert(false); 9132 } 9133 9134 /// Displays an in-progress indicator without known values 9135 version(none) 9136 class IndefiniteProgressBar : Widget { 9137 version(win32_widgets) 9138 this(Widget parent) { 9139 super(parent); 9140 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 9141 tabStop = false; 9142 } 9143 override int minHeight() { return 10; } 9144 } 9145 9146 /// A progress bar with a known endpoint and completion amount 9147 class ProgressBar : Widget { 9148 this(Widget parent) { 9149 version(win32_widgets) { 9150 super(parent); 9151 createWin32Window(this, "msctls_progress32"w, "", 0); 9152 tabStop = false; 9153 } else version(custom_widgets) { 9154 super(parent); 9155 max = 100; 9156 step = 10; 9157 tabStop = false; 9158 } else static assert(0); 9159 } 9160 9161 version(custom_widgets) 9162 override void paint(WidgetPainter painter) { 9163 auto cs = getComputedStyle(); 9164 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 9165 painter.fillColor = cs.progressBarColor; 9166 painter.drawRectangle(Point(0, 0), width * current / max, height); 9167 } 9168 9169 9170 version(custom_widgets) { 9171 int current; 9172 int max; 9173 int step; 9174 } 9175 9176 /// 9177 void advanceOneStep() { 9178 version(win32_widgets) 9179 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 9180 else version(custom_widgets) 9181 addToPosition(step); 9182 else static assert(false); 9183 } 9184 9185 /// 9186 void setStepIncrement(int increment) { 9187 version(win32_widgets) 9188 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 9189 else version(custom_widgets) 9190 step = increment; 9191 else static assert(false); 9192 } 9193 9194 /// 9195 void addToPosition(int amount) { 9196 version(win32_widgets) 9197 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 9198 else version(custom_widgets) 9199 setPosition(current + amount); 9200 else static assert(false); 9201 } 9202 9203 /// 9204 void setPosition(int pos) { 9205 version(win32_widgets) 9206 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 9207 else version(custom_widgets) { 9208 current = pos; 9209 if(current > max) 9210 current = max; 9211 redraw(); 9212 } 9213 else static assert(false); 9214 } 9215 9216 /// 9217 void setRange(ushort min, ushort max) { 9218 version(win32_widgets) 9219 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 9220 else version(custom_widgets) { 9221 this.max = max; 9222 } 9223 else static assert(false); 9224 } 9225 9226 override int minHeight() { return 10; } 9227 } 9228 9229 version(custom_widgets) 9230 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 9231 thisLabel.reserve(label.length); 9232 bool justSawAmpersand; 9233 foreach(ch; label) { 9234 if(justSawAmpersand) { 9235 justSawAmpersand = false; 9236 if(ch == '&') { 9237 goto plain; 9238 } 9239 thisAccelerator = ch; 9240 } else { 9241 if(ch == '&') { 9242 justSawAmpersand = true; 9243 continue; 9244 } 9245 plain: 9246 thisLabel ~= ch; 9247 } 9248 } 9249 } 9250 9251 /++ 9252 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. 9253 9254 9255 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 9256 9257 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 9258 9259 History: 9260 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. 9261 +/ 9262 class Fieldset : Widget { 9263 // FIXME: on Windows,it doesn't draw the background on the label 9264 // on X, it doesn't fix the clipping rectangle for it 9265 version(win32_widgets) 9266 override int paddingTop() { return defaultLineHeight; } 9267 else version(custom_widgets) 9268 override int paddingTop() { return defaultLineHeight + 2; } 9269 else static assert(false); 9270 override int paddingBottom() { return 6; } 9271 override int paddingLeft() { return 6; } 9272 override int paddingRight() { return 6; } 9273 9274 override int marginLeft() { return 6; } 9275 override int marginRight() { return 6; } 9276 override int marginTop() { return 2; } 9277 override int marginBottom() { return 2; } 9278 9279 string legend; 9280 9281 version(custom_widgets) private dchar accelerator; 9282 9283 this(string legend, Widget parent) { 9284 version(win32_widgets) { 9285 super(parent); 9286 this.legend = legend; 9287 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 9288 tabStop = false; 9289 } else version(custom_widgets) { 9290 super(parent); 9291 tabStop = false; 9292 9293 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 9294 } else static assert(0); 9295 } 9296 9297 version(custom_widgets) 9298 override void paint(WidgetPainter painter) { 9299 painter.fillColor = Color.transparent; 9300 auto cs = getComputedStyle(); 9301 painter.pen = Pen(cs.foregroundColor, 1); 9302 painter.drawRectangle(Point(0, defaultLineHeight / 2), width, height - Window.lineHeight / 2); 9303 9304 auto tx = painter.textSize(legend); 9305 painter.outlineColor = Color.transparent; 9306 9307 static if(UsingSimpledisplayX11) { 9308 painter.fillColor = getComputedStyle().windowBackgroundColor; 9309 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 9310 } else version(Windows) { 9311 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 9312 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 9313 SelectObject(painter.impl.hdc, b); 9314 } else static assert(0); 9315 painter.outlineColor = cs.foregroundColor; 9316 painter.drawText(Point(8, 0), legend); 9317 } 9318 9319 9320 override int maxHeight() { 9321 auto m = paddingTop() + paddingBottom(); 9322 foreach(child; children) { 9323 auto mh = child.maxHeight(); 9324 if(mh == int.max) 9325 return int.max; 9326 m += mh; 9327 m += child.marginBottom(); 9328 m += child.marginTop(); 9329 } 9330 m += 6; 9331 if(m < minHeight) 9332 return minHeight; 9333 return m; 9334 } 9335 9336 override int minHeight() { 9337 auto m = paddingTop() + paddingBottom(); 9338 foreach(child; children) { 9339 m += child.minHeight(); 9340 m += child.marginBottom(); 9341 m += child.marginTop(); 9342 } 9343 return m + 6; 9344 } 9345 } 9346 9347 /// Draws a line 9348 class HorizontalRule : Widget { 9349 mixin Margin!q{ 2 }; 9350 override int minHeight() { return 2; } 9351 override int maxHeight() { return 2; } 9352 9353 /// 9354 this(Widget parent) { 9355 super(parent); 9356 } 9357 9358 override void paint(WidgetPainter painter) { 9359 auto cs = getComputedStyle(); 9360 painter.outlineColor = cs.darkAccentColor; 9361 painter.drawLine(Point(0, 0), Point(width, 0)); 9362 painter.outlineColor = cs.lightAccentColor; 9363 painter.drawLine(Point(0, 1), Point(width, 1)); 9364 } 9365 } 9366 9367 /// ditto 9368 class VerticalRule : Widget { 9369 mixin Margin!q{ 2 }; 9370 override int minWidth() { return 2; } 9371 override int maxWidth() { return 2; } 9372 9373 /// 9374 this(Widget parent) { 9375 super(parent); 9376 } 9377 9378 override void paint(WidgetPainter painter) { 9379 auto cs = getComputedStyle(); 9380 painter.outlineColor = cs.darkAccentColor; 9381 painter.drawLine(Point(0, 0), Point(0, height)); 9382 painter.outlineColor = cs.lightAccentColor; 9383 painter.drawLine(Point(1, 0), Point(1, height)); 9384 } 9385 } 9386 9387 9388 /// 9389 class Menu : Window { 9390 void remove() { 9391 foreach(i, child; parentWindow.children) 9392 if(child is this) { 9393 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 9394 break; 9395 } 9396 parentWindow.redraw(); 9397 9398 parentWindow.releaseMouseCapture(); 9399 } 9400 9401 /// 9402 void addSeparator() { 9403 version(win32_widgets) 9404 AppendMenu(handle, MF_SEPARATOR, 0, null); 9405 else version(custom_widgets) 9406 auto hr = new HorizontalRule(this); 9407 else static assert(0); 9408 } 9409 9410 override int paddingTop() { return 4; } 9411 override int paddingBottom() { return 4; } 9412 override int paddingLeft() { return 2; } 9413 override int paddingRight() { return 2; } 9414 9415 version(win32_widgets) {} 9416 else version(custom_widgets) { 9417 SimpleWindow dropDown; 9418 Widget menuParent; 9419 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 9420 this.menuParent = parent; 9421 9422 int w = 150; 9423 int h = paddingTop + paddingBottom; 9424 if(this.children.length) { 9425 // hacking it to get the ideal height out of recomputeChildLayout 9426 this.width = w; 9427 this.height = h; 9428 this.recomputeChildLayout(); 9429 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 9430 h += paddingBottom; 9431 9432 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 9433 } 9434 9435 if(offsetY == int.min) 9436 offsetY = parent.defaultLineHeight; 9437 9438 auto coord = parent.globalCoordinates(); 9439 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 9440 this.x = 0; 9441 this.y = 0; 9442 this.width = dropDown.width; 9443 this.height = dropDown.height; 9444 this.drawableWindow = dropDown; 9445 this.recomputeChildLayout(); 9446 9447 static if(UsingSimpledisplayX11) 9448 XSync(XDisplayConnection.get, 0); 9449 9450 dropDown.visibilityChanged = (bool visible) { 9451 if(visible) { 9452 this.redraw(); 9453 dropDown.grabInput(); 9454 } else { 9455 dropDown.releaseInputGrab(); 9456 } 9457 }; 9458 9459 dropDown.show(); 9460 9461 clickListener = this.addEventListener((scope ClickEvent ev) { 9462 unpopup(); 9463 // need to unlock asap just in case other user handlers block... 9464 static if(UsingSimpledisplayX11) 9465 flushGui(); 9466 }, true /* again for asap action */); 9467 } 9468 9469 EventListener clickListener; 9470 } 9471 else static assert(false); 9472 9473 version(custom_widgets) 9474 void unpopup() { 9475 mouseLastOver = mouseLastDownOn = null; 9476 dropDown.hide(); 9477 if(!menuParent.parentWindow.win.closed) { 9478 if(auto maw = cast(MouseActivatedWidget) menuParent) { 9479 maw.setDynamicState(DynamicState.depressed, false); 9480 maw.redraw(); 9481 } 9482 menuParent.parentWindow.win.focus(); 9483 } 9484 clickListener.disconnect(); 9485 } 9486 9487 MenuItem[] items; 9488 9489 /// 9490 MenuItem addItem(MenuItem item) { 9491 addChild(item); 9492 items ~= item; 9493 version(win32_widgets) { 9494 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 9495 } 9496 return item; 9497 } 9498 9499 string label; 9500 9501 version(win32_widgets) { 9502 HMENU handle; 9503 /// 9504 this(string label, Widget parent) { 9505 // not actually passing the parent since it effs up the drawing 9506 super(cast(Widget) null);// parent); 9507 this.label = label; 9508 handle = CreatePopupMenu(); 9509 } 9510 } else version(custom_widgets) { 9511 /// 9512 this(string label, Widget parent) { 9513 9514 if(dropDown) { 9515 dropDown.close(); 9516 } 9517 dropDown = new SimpleWindow( 9518 150, 4, 9519 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 9520 9521 this.label = label; 9522 9523 super(dropDown); 9524 } 9525 } else static assert(false); 9526 9527 override int maxHeight() { return defaultLineHeight; } 9528 override int minHeight() { return defaultLineHeight; } 9529 9530 version(custom_widgets) 9531 override void paint(WidgetPainter painter) { 9532 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 9533 } 9534 } 9535 9536 /++ 9537 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 9538 +/ 9539 class MenuItem : MouseActivatedWidget { 9540 Menu submenu; 9541 9542 Action action; 9543 string label; 9544 9545 override int paddingLeft() { return 4; } 9546 9547 override int maxHeight() { return defaultLineHeight + 4; } 9548 override int minHeight() { return defaultLineHeight + 4; } 9549 override int minWidth() { return defaultLineHeight * cast(int) label.length + 8; } 9550 override int maxWidth() { 9551 if(cast(MenuBar) parent) { 9552 return defaultLineHeight / 2 * cast(int) label.length + 8; 9553 } 9554 return int.max; 9555 } 9556 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 9557 this(string lbl, Widget parent = null) { 9558 super(parent); 9559 //label = lbl; // FIXME 9560 foreach(char ch; lbl) // FIXME 9561 if(ch != '&') // FIXME 9562 label ~= ch; // FIXME 9563 tabStop = false; // these are selected some other way 9564 } 9565 9566 /// 9567 this(Action action, Widget parent = null) { 9568 assert(action !is null); 9569 this(action.label, parent); 9570 this.action = action; 9571 tabStop = false; // these are selected some other way 9572 } 9573 9574 version(custom_widgets) 9575 override void paint(WidgetPainter painter) { 9576 auto cs = getComputedStyle(); 9577 if(dynamicState & DynamicState.depressed) 9578 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 9579 if(dynamicState & DynamicState.hover) 9580 painter.outlineColor = cs.activeMenuItemColor; 9581 else 9582 painter.outlineColor = cs.foregroundColor; 9583 painter.fillColor = Color.transparent; 9584 painter.drawText(Point(cast(MenuBar) this.parent ? 4 : 20, 2), label, Point(width, height), TextAlignment.Left); 9585 if(action && action.accelerator !is KeyEvent.init) { 9586 painter.drawText(Point(cast(MenuBar) this.parent ? 4 : 20, 2), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right); 9587 9588 } 9589 } 9590 9591 static class Style : Widget.Style { 9592 override bool variesWithState(ulong dynamicStateFlags) { 9593 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 9594 } 9595 } 9596 mixin OverrideStyle!Style; 9597 9598 override void defaultEventHandler_triggered(Event event) { 9599 if(action) 9600 foreach(handler; action.triggered) 9601 handler(); 9602 9603 if(auto pmenu = cast(Menu) this.parent) 9604 pmenu.remove(); 9605 9606 super.defaultEventHandler_triggered(event); 9607 } 9608 } 9609 9610 version(win32_widgets) 9611 /// A "mouse activiated widget" is really just an abstract variant of button. 9612 class MouseActivatedWidget : Widget { 9613 @property bool isChecked() { 9614 assert(hwnd); 9615 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 9616 9617 } 9618 @property void isChecked(bool state) { 9619 assert(hwnd); 9620 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 9621 9622 } 9623 9624 override void handleWmCommand(ushort cmd, ushort id) { 9625 if(cmd == 0) { 9626 auto event = new Event(EventType.triggered, this); 9627 event.dispatch(); 9628 } 9629 } 9630 9631 this(Widget parent) { 9632 super(parent); 9633 } 9634 } 9635 else version(custom_widgets) 9636 /// ditto 9637 class MouseActivatedWidget : Widget { 9638 @property bool isChecked() { return isChecked_; } 9639 @property bool isChecked(bool b) { return isChecked_ = b; } 9640 9641 private bool isChecked_; 9642 9643 this(Widget parent) { 9644 super(parent); 9645 9646 addEventListener((MouseDownEvent ev) { 9647 if(ev.button == MouseButton.left) { 9648 setDynamicState(DynamicState.depressed, true); 9649 setDynamicState(DynamicState.hover, true); 9650 redraw(); 9651 } 9652 }); 9653 9654 addEventListener((MouseUpEvent ev) { 9655 if(ev.button == MouseButton.left) { 9656 setDynamicState(DynamicState.depressed, false); 9657 setDynamicState(DynamicState.hover, false); 9658 redraw(); 9659 } 9660 }); 9661 9662 addEventListener((MouseMoveEvent mme) { 9663 if(!(mme.state & ModifierState.leftButtonDown)) { 9664 if(dynamicState_ & DynamicState.depressed) { 9665 setDynamicState(DynamicState.depressed, false); 9666 redraw(); 9667 } 9668 } 9669 }); 9670 } 9671 9672 override void defaultEventHandler_focus(Event ev) { 9673 super.defaultEventHandler_focus(ev); 9674 this.redraw(); 9675 } 9676 override void defaultEventHandler_blur(Event ev) { 9677 super.defaultEventHandler_blur(ev); 9678 setDynamicState(DynamicState.depressed, false); 9679 this.redraw(); 9680 } 9681 override void defaultEventHandler_keydown(KeyDownEvent ev) { 9682 super.defaultEventHandler_keydown(ev); 9683 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 9684 setDynamicState(DynamicState.depressed, true); 9685 setDynamicState(DynamicState.hover, true); 9686 this.redraw(); 9687 } 9688 } 9689 override void defaultEventHandler_keyup(KeyUpEvent ev) { 9690 super.defaultEventHandler_keyup(ev); 9691 if(!(dynamicState & DynamicState.depressed)) 9692 return; 9693 setDynamicState(DynamicState.depressed, false); 9694 setDynamicState(DynamicState.hover, false); 9695 this.redraw(); 9696 9697 auto event = new Event(EventType.triggered, this); 9698 event.sendDirectly(); 9699 } 9700 override void defaultEventHandler_click(ClickEvent ev) { 9701 super.defaultEventHandler_click(ev); 9702 if(ev.button == MouseButton.left) { 9703 auto event = new Event(EventType.triggered, this); 9704 event.sendDirectly(); 9705 } 9706 } 9707 9708 } 9709 else static assert(false); 9710 9711 /* 9712 /++ 9713 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 9714 9715 Basically the same as a checkbox. 9716 +/ 9717 class OnOffSwitch : MouseActivatedWidget { 9718 9719 } 9720 */ 9721 9722 /++ 9723 History: 9724 Added June 15, 2021 (dub v10.1) 9725 +/ 9726 struct ImageLabel { 9727 /++ 9728 History: 9729 The `alignment` parameter was added on September 27, 2021 9730 +/ 9731 this(string label, TextAlignment alignment = TextAlignment.Center) { 9732 this.label = label; 9733 this.displayFlags = DisplayFlags.displayText; 9734 this.alignment = alignment; 9735 } 9736 9737 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 9738 this.label = label; 9739 this.image = image; 9740 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 9741 this.alignment = alignment; 9742 } 9743 9744 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 9745 this.image = image; 9746 this.displayFlags = DisplayFlags.displayImage; 9747 this.alignment = alignment; 9748 } 9749 9750 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 9751 this.label = label; 9752 this.image = image; 9753 this.alignment = alignment; 9754 this.displayFlags = displayFlags; 9755 } 9756 9757 string label; 9758 MemoryImage image; 9759 9760 enum DisplayFlags { 9761 displayText = 1 << 0, 9762 displayImage = 1 << 1, 9763 } 9764 9765 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 9766 9767 TextAlignment alignment; 9768 } 9769 9770 /++ 9771 A basic checked or not checked box with an attached label. 9772 9773 9774 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 9775 9776 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 9777 9778 History: 9779 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. 9780 +/ 9781 class Checkbox : MouseActivatedWidget { 9782 version(win32_widgets) { 9783 override int maxHeight() { return scaleWithDpi(16); } 9784 override int minHeight() { return scaleWithDpi(16); } 9785 } else version(custom_widgets) { 9786 override int maxHeight() { return defaultLineHeight; } 9787 override int minHeight() { return defaultLineHeight; } 9788 } else static assert(0); 9789 9790 override int marginLeft() { return 4; } 9791 9792 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 9793 9794 /++ 9795 Just an alias because I keep typing checked out of web habit. 9796 9797 History: 9798 Added May 31, 2021 9799 +/ 9800 alias checked = isChecked; 9801 9802 private string label; 9803 private dchar accelerator; 9804 9805 /++ 9806 +/ 9807 this(string label, Widget parent) { 9808 this(ImageLabel(label), Appearance.checkbox, parent); 9809 } 9810 9811 /// ditto 9812 this(string label, Appearance appearance, Widget parent) { 9813 this(ImageLabel(label), appearance, parent); 9814 } 9815 9816 /++ 9817 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 9818 9819 History: 9820 Added June 29, 2021 (dub v10.2) 9821 +/ 9822 enum Appearance { 9823 checkbox, /// a normal checkbox 9824 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 9825 //sliderswitch, 9826 } 9827 private Appearance appearance; 9828 9829 /// ditto 9830 private this(ImageLabel label, Appearance appearance, Widget parent) { 9831 super(parent); 9832 version(win32_widgets) { 9833 this.label = label.label; 9834 9835 uint extraStyle; 9836 final switch(appearance) { 9837 case Appearance.checkbox: 9838 break; 9839 case Appearance.pushbutton: 9840 extraStyle |= BS_PUSHLIKE; 9841 break; 9842 } 9843 9844 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 9845 } else version(custom_widgets) { 9846 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 9847 } else static assert(0); 9848 } 9849 9850 version(custom_widgets) 9851 override void paint(WidgetPainter painter) { 9852 auto cs = getComputedStyle(); 9853 if(isFocused()) { 9854 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 9855 painter.fillColor = cs.windowBackgroundColor; 9856 painter.drawRectangle(Point(0, 0), width, height); 9857 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 9858 } else { 9859 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 9860 painter.fillColor = cs.windowBackgroundColor; 9861 painter.drawRectangle(Point(0, 0), width, height); 9862 } 9863 9864 9865 enum buttonSize = 16; 9866 9867 painter.outlineColor = Color.black; 9868 painter.fillColor = Color.white; 9869 painter.drawRectangle(Point(2, 2), buttonSize - 2, buttonSize - 2); 9870 9871 if(isChecked) { 9872 painter.pen = Pen(Color.black, 2); 9873 // I'm using height so the checkbox is square 9874 enum padding = 5; 9875 painter.drawLine(Point(padding, padding), Point(buttonSize - (padding-2), buttonSize - (padding-2))); 9876 painter.drawLine(Point(buttonSize-(padding-2), padding), Point(padding, buttonSize - (padding-2))); 9877 9878 painter.pen = Pen(Color.black, 1); 9879 } 9880 9881 if(label !is null) { 9882 painter.outlineColor = cs.foregroundColor(); 9883 painter.fillColor = cs.foregroundColor(); 9884 9885 // FIXME: should prolly just align the baseline or something 9886 painter.drawText(Point(buttonSize + 4, 2), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 9887 } 9888 } 9889 9890 override void defaultEventHandler_triggered(Event ev) { 9891 isChecked = !isChecked; 9892 9893 this.emit!(ChangeEvent!bool)(&isChecked); 9894 9895 redraw(); 9896 } 9897 9898 /// Emits a change event with the checked state 9899 mixin Emits!(ChangeEvent!bool); 9900 } 9901 9902 /// Adds empty space to a layout. 9903 class VerticalSpacer : Widget { 9904 /// 9905 this(Widget parent) { 9906 super(parent); 9907 } 9908 } 9909 9910 /// ditto 9911 class HorizontalSpacer : Widget { 9912 /// 9913 this(Widget parent) { 9914 super(parent); 9915 this.tabStop = false; 9916 } 9917 } 9918 9919 9920 /++ 9921 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 9922 9923 9924 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 9925 9926 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 9927 9928 History: 9929 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. 9930 +/ 9931 class Radiobox : MouseActivatedWidget { 9932 9933 version(win32_widgets) { 9934 override int maxHeight() { return scaleWithDpi(16); } 9935 override int minHeight() { return scaleWithDpi(16); } 9936 } else version(custom_widgets) { 9937 override int maxHeight() { return defaultLineHeight; } 9938 override int minHeight() { return defaultLineHeight; } 9939 } else static assert(0); 9940 9941 override int marginLeft() { return 4; } 9942 9943 private string label; 9944 private dchar accelerator; 9945 9946 version(win32_widgets) 9947 this(string label, Widget parent) { 9948 super(parent); 9949 this.label = label; 9950 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 9951 } 9952 else version(custom_widgets) 9953 this(string label, Widget parent) { 9954 super(parent); 9955 label.extractWindowsStyleLabel(this.label, this.accelerator); 9956 height = 16; 9957 width = height + 4 + cast(int) label.length * 16; 9958 } 9959 else static assert(false); 9960 9961 version(custom_widgets) 9962 override void paint(WidgetPainter painter) { 9963 auto cs = getComputedStyle(); 9964 if(isFocused) { 9965 painter.fillColor = cs.windowBackgroundColor; 9966 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 9967 } else { 9968 painter.fillColor = cs.windowBackgroundColor; 9969 painter.outlineColor = cs.windowBackgroundColor; 9970 } 9971 painter.drawRectangle(Point(0, 0), width, height); 9972 9973 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 9974 9975 enum buttonSize = 16; 9976 9977 painter.outlineColor = Color.black; 9978 painter.fillColor = Color.white; 9979 painter.drawEllipse(Point(2, 2), Point(buttonSize - 2, buttonSize - 2)); 9980 if(isChecked) { 9981 painter.outlineColor = Color.black; 9982 painter.fillColor = Color.black; 9983 // I'm using height so the checkbox is square 9984 painter.drawEllipse(Point(5, 5), Point(buttonSize - 5, buttonSize - 5)); 9985 } 9986 9987 painter.outlineColor = cs.foregroundColor(); 9988 painter.fillColor = cs.foregroundColor(); 9989 9990 painter.drawText(Point(buttonSize + 4, 0), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 9991 } 9992 9993 9994 override void defaultEventHandler_triggered(Event ev) { 9995 isChecked = true; 9996 9997 if(this.parent) { 9998 foreach(child; this.parent.children) { 9999 if(child is this) continue; 10000 if(auto rb = cast(Radiobox) child) { 10001 rb.isChecked = false; 10002 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 10003 rb.redraw(); 10004 } 10005 } 10006 } 10007 10008 this.emit!(ChangeEvent!bool)(&this.isChecked); 10009 10010 redraw(); 10011 } 10012 10013 /// 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. 10014 mixin Emits!(ChangeEvent!bool); 10015 } 10016 10017 10018 /++ 10019 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 10020 10021 10022 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 10023 10024 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10025 10026 History: 10027 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. 10028 +/ 10029 class Button : MouseActivatedWidget { 10030 override int heightStretchiness() { return 3; } 10031 override int widthStretchiness() { return 3; } 10032 10033 /++ 10034 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. 10035 10036 History: 10037 Added July 2, 2021 10038 +/ 10039 public bool triggersOnMultiClick; 10040 10041 private string label_; 10042 private TextAlignment alignment; 10043 private dchar accelerator; 10044 10045 /// 10046 string label() { return label_; } 10047 /// 10048 void label(string l) { 10049 label_ = l; 10050 version(win32_widgets) { 10051 WCharzBuffer bfr = WCharzBuffer(l); 10052 SetWindowTextW(hwnd, bfr.ptr); 10053 } else version(custom_widgets) { 10054 redraw(); 10055 } 10056 } 10057 10058 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 10059 super.defaultEventHandler_dblclick(ev); 10060 if(triggersOnMultiClick) { 10061 if(ev.button == MouseButton.left) { 10062 auto event = new Event(EventType.triggered, this); 10063 event.sendDirectly(); 10064 } 10065 } 10066 } 10067 10068 private Sprite sprite; 10069 private int displayFlags; 10070 10071 /++ 10072 Creates a push button with the given label, which may be an image or some text. 10073 10074 Bugs: 10075 If the image is bigger than the button, it may not be displayed in the right position on Linux. 10076 10077 History: 10078 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 10079 +/ 10080 this(ImageLabel label, Widget parent) { 10081 version(win32_widgets) { 10082 // FIXME: use ideal button size instead 10083 width = 50; 10084 height = 30; 10085 super(parent); 10086 10087 // BS_BITMAP is set when we want image only, so checking for exactly that combination 10088 enum imgFlags = ImageLabel.DisplayFlags.displayImage; 10089 auto extraStyle = ((label.displayFlags & imgFlags) == imgFlags) ? BS_BITMAP : 0; 10090 10091 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle); 10092 10093 if(label.image) { 10094 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 10095 10096 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 10097 } 10098 10099 this.label = label.label; 10100 } else version(custom_widgets) { 10101 width = 50; 10102 height = 30; 10103 super(parent); 10104 10105 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 10106 10107 if(label.image) { 10108 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 10109 this.displayFlags = label.displayFlags; 10110 } 10111 10112 this.alignment = label.alignment; 10113 } 10114 } 10115 10116 /// 10117 this(string label, Widget parent) { 10118 this(ImageLabel(label), parent); 10119 } 10120 10121 override int minHeight() { return defaultLineHeight + 4; } 10122 10123 static class Style : Widget.Style { 10124 override WidgetBackground background() { 10125 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 10126 10127 auto pressed = DynamicState.depressed | DynamicState.hover; 10128 if((widget.dynamicState & pressed) == pressed) { 10129 return WidgetBackground(cs.depressedButtonColor()); 10130 } else if(widget.dynamicState & DynamicState.hover) { 10131 return WidgetBackground(cs.hoveringColor()); 10132 } else { 10133 return WidgetBackground(cs.buttonColor()); 10134 } 10135 } 10136 10137 override FrameStyle borderStyle() { 10138 auto pressed = DynamicState.depressed | DynamicState.hover; 10139 if((widget.dynamicState & pressed) == pressed) { 10140 return FrameStyle.sunk; 10141 } else { 10142 return FrameStyle.risen; 10143 } 10144 10145 } 10146 10147 override bool variesWithState(ulong dynamicStateFlags) { 10148 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 10149 } 10150 } 10151 mixin OverrideStyle!Style; 10152 10153 version(custom_widgets) 10154 override void paint(WidgetPainter painter) { 10155 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 10156 if(sprite) { 10157 sprite.drawAt( 10158 painter, 10159 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 10160 Point(0, 0), 10161 bounds.size 10162 ); 10163 } else { 10164 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 10165 } 10166 return bounds; 10167 }); 10168 } 10169 10170 override int flexBasisWidth() { 10171 version(win32_widgets) { 10172 SIZE size; 10173 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 10174 if(size.cx == 0) 10175 goto fallback; 10176 return size.cx + scaleWithDpi(16); 10177 } 10178 fallback: 10179 return scaleWithDpi(cast(int) label.length * 8 + 16); 10180 } 10181 10182 override int flexBasisHeight() { 10183 version(win32_widgets) { 10184 SIZE size; 10185 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 10186 if(size.cy == 0) 10187 goto fallback; 10188 return size.cy + scaleWithDpi(6); 10189 } 10190 fallback: 10191 return defaultLineHeight + 4; 10192 } 10193 } 10194 10195 /++ 10196 A button with a consistent size, suitable for user commands like OK and cANCEL. 10197 +/ 10198 class CommandButton : Button { 10199 this(string label, Widget parent) { 10200 super(label, parent); 10201 } 10202 10203 override int maxHeight() { 10204 return defaultLineHeight + 4; 10205 } 10206 10207 override int maxWidth() { 10208 return defaultLineHeight * 4; 10209 } 10210 10211 override int marginLeft() { return 12; } 10212 override int marginRight() { return 12; } 10213 override int marginTop() { return 12; } 10214 override int marginBottom() { return 12; } 10215 } 10216 10217 /// 10218 enum ArrowDirection { 10219 left, /// 10220 right, /// 10221 up, /// 10222 down /// 10223 } 10224 10225 /// 10226 version(custom_widgets) 10227 class ArrowButton : Button { 10228 /// 10229 this(ArrowDirection direction, Widget parent) { 10230 super("", parent); 10231 this.direction = direction; 10232 triggersOnMultiClick = true; 10233 } 10234 10235 private ArrowDirection direction; 10236 10237 override int minHeight() { return 16; } 10238 override int maxHeight() { return 16; } 10239 override int minWidth() { return 16; } 10240 override int maxWidth() { return 16; } 10241 10242 override void paint(WidgetPainter painter) { 10243 super.paint(painter); 10244 10245 auto cs = getComputedStyle(); 10246 10247 painter.outlineColor = cs.foregroundColor; 10248 painter.fillColor = cs.foregroundColor; 10249 10250 auto offset = Point((this.width - 16) / 2, (this.height - 16) / 2); 10251 10252 final switch(direction) { 10253 case ArrowDirection.up: 10254 painter.drawPolygon( 10255 Point(2, 10) + offset, 10256 Point(7, 5) + offset, 10257 Point(12, 10) + offset, 10258 Point(2, 10) + offset 10259 ); 10260 break; 10261 case ArrowDirection.down: 10262 painter.drawPolygon( 10263 Point(2, 6) + offset, 10264 Point(7, 11) + offset, 10265 Point(12, 6) + offset, 10266 Point(2, 6) + offset 10267 ); 10268 break; 10269 case ArrowDirection.left: 10270 painter.drawPolygon( 10271 Point(10, 2) + offset, 10272 Point(5, 7) + offset, 10273 Point(10, 12) + offset, 10274 Point(10, 2) + offset 10275 ); 10276 break; 10277 case ArrowDirection.right: 10278 painter.drawPolygon( 10279 Point(6, 2) + offset, 10280 Point(11, 7) + offset, 10281 Point(6, 12) + offset, 10282 Point(6, 2) + offset 10283 ); 10284 break; 10285 } 10286 } 10287 } 10288 10289 private 10290 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 10291 int x, y; 10292 Widget par = c; 10293 while(par) { 10294 x += par.x; 10295 y += par.y; 10296 par = par.parent; 10297 } 10298 return [x, y]; 10299 } 10300 10301 version(win32_widgets) 10302 private 10303 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 10304 // MapWindowPoints? 10305 int x, y; 10306 Widget par = c; 10307 while(par) { 10308 x += par.x; 10309 y += par.y; 10310 par = par.parent; 10311 if(par !is null && par.useNativeDrawing()) 10312 break; 10313 } 10314 return [x, y]; 10315 } 10316 10317 /// 10318 class ImageBox : Widget { 10319 private MemoryImage image_; 10320 10321 override int widthStretchiness() { return 1; } 10322 override int heightStretchiness() { return 1; } 10323 override int widthShrinkiness() { return 1; } 10324 override int heightShrinkiness() { return 1; } 10325 10326 override int flexBasisHeight() { 10327 return image_.height; 10328 } 10329 10330 override int flexBasisWidth() { 10331 return image_.width; 10332 } 10333 10334 /// 10335 public void setImage(MemoryImage image){ 10336 this.image_ = image; 10337 if(this.parentWindow && this.parentWindow.win) 10338 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_)); 10339 redraw(); 10340 } 10341 10342 /// How to fit the image in the box if they aren't an exact match in size? 10343 enum HowToFit { 10344 center, /// centers the image, cropping around all the edges as needed 10345 crop, /// always draws the image in the upper left, cropping the lower right if needed 10346 // stretch, /// not implemented 10347 } 10348 10349 private Sprite sprite; 10350 private HowToFit howToFit_; 10351 10352 private Color backgroundColor_; 10353 10354 /// 10355 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 10356 this.image_ = image; 10357 this.tabStop = false; 10358 this.howToFit_ = howToFit; 10359 this.backgroundColor_ = backgroundColor; 10360 super(parent); 10361 updateSprite(); 10362 } 10363 10364 /// ditto 10365 this(MemoryImage image, HowToFit howToFit, Widget parent) { 10366 this(image, howToFit, Color.transparent, parent); 10367 } 10368 10369 private void updateSprite() { 10370 if(sprite is null && this.parentWindow && this.parentWindow.win) { 10371 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_)); 10372 } 10373 } 10374 10375 override void paint(WidgetPainter painter) { 10376 updateSprite(); 10377 if(backgroundColor_.a) { 10378 painter.fillColor = backgroundColor_; 10379 painter.drawRectangle(Point(0, 0), width, height); 10380 } 10381 if(howToFit_ == HowToFit.crop) 10382 sprite.drawAt(painter, Point(0, 0)); 10383 else if(howToFit_ == HowToFit.center) { 10384 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 10385 } 10386 } 10387 } 10388 10389 /// 10390 class TextLabel : Widget { 10391 override int maxHeight() { return defaultLineHeight; } 10392 override int minHeight() { return defaultLineHeight; } 10393 override int minWidth() { return 32; } 10394 10395 override int flexBasisHeight() { return minHeight(); } 10396 override int flexBasisWidth() { return cast(int) label_.length * 7; } 10397 10398 string label_; 10399 10400 /++ 10401 Indicates which other control this label is here for. Similar to HTML `for` attribute. 10402 10403 In practice this means a click on the label will focus the `labelFor`. In future versions 10404 it will also set screen reader hints but that is not yet implemented. 10405 10406 History: 10407 Added October 3, 2021 (dub v10.4) 10408 +/ 10409 Widget labelFor; 10410 10411 /// 10412 @scriptable 10413 string label() { return label_; } 10414 10415 /// 10416 @scriptable 10417 void label(string l) { 10418 label_ = l; 10419 version(win32_widgets) { 10420 WCharzBuffer bfr = WCharzBuffer(l); 10421 SetWindowTextW(hwnd, bfr.ptr); 10422 } else version(custom_widgets) 10423 redraw(); 10424 } 10425 10426 /// 10427 this(string label, TextAlignment alignment, Widget parent) { 10428 this.label_ = label; 10429 this.alignment = alignment; 10430 this.tabStop = false; 10431 super(parent); 10432 10433 version(win32_widgets) 10434 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 10435 } 10436 10437 override void defaultEventHandler_click(scope ClickEvent ce) { 10438 if(this.labelFor !is null) 10439 this.labelFor.focus(); 10440 } 10441 10442 /++ 10443 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 10444 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 10445 +/ 10446 this(string label, Widget parent) { 10447 this(label, TextAlignment.Right, parent); 10448 } 10449 10450 10451 TextAlignment alignment; 10452 10453 version(custom_widgets) 10454 override void paint(WidgetPainter painter) { 10455 painter.outlineColor = getComputedStyle().foregroundColor; 10456 painter.drawText(Point(0, 0), this.label, Point(width, height), alignment); 10457 } 10458 10459 } 10460 10461 version(custom_widgets) 10462 private struct etc { 10463 mixin ExperimentalTextComponent; 10464 } 10465 10466 version(win32_widgets) 10467 alias EditableTextWidgetParent = Widget; /// 10468 else version(custom_widgets) 10469 alias EditableTextWidgetParent = ScrollableWidget; /// 10470 else static assert(0); 10471 10472 /// Contains the implementation of text editing 10473 abstract class EditableTextWidget : EditableTextWidgetParent { 10474 this(Widget parent) { 10475 super(parent); 10476 } 10477 10478 bool wordWrapEnabled_ = false; 10479 void wordWrapEnabled(bool enabled) { 10480 version(win32_widgets) { 10481 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 10482 } else version(custom_widgets) { 10483 wordWrapEnabled_ = enabled; // FIXME 10484 } else static assert(false); 10485 } 10486 10487 override int minWidth() { return 16; } 10488 override int minHeight() { return defaultLineHeight + 0; } // the +0 is to leave room for the padding 10489 override int widthStretchiness() { return 7; } 10490 10491 void selectAll() { 10492 version(win32_widgets) 10493 SendMessage(hwnd, EM_SETSEL, 0, -1); 10494 else version(custom_widgets) { 10495 textLayout.selectAll(); 10496 redraw(); 10497 } 10498 } 10499 10500 @property string content() { 10501 version(win32_widgets) { 10502 wchar[4096] bufferstack; 10503 wchar[] buffer; 10504 auto len = GetWindowTextLength(hwnd); 10505 if(len < bufferstack.length) 10506 buffer = bufferstack[0 .. len + 1]; 10507 else 10508 buffer = new wchar[](len + 1); 10509 10510 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 10511 if(l >= 0) 10512 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 10513 else 10514 return null; 10515 } else version(custom_widgets) { 10516 return textLayout.getPlainText(); 10517 } else static assert(false); 10518 } 10519 @property void content(string s) { 10520 version(win32_widgets) { 10521 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 10522 SetWindowTextW(hwnd, bfr.ptr); 10523 } else version(custom_widgets) { 10524 textLayout.clear(); 10525 textLayout.addText(s); 10526 10527 { 10528 // FIXME: it should be able to get this info easier 10529 auto painter = draw(); 10530 textLayout.redoLayout(painter); 10531 } 10532 auto cbb = textLayout.contentBoundingBox(); 10533 setContentSize(cbb.width, cbb.height); 10534 /* 10535 textLayout.addText(ForegroundColor.red, s); 10536 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 10537 textLayout.addText(" is the best!"); 10538 */ 10539 redraw(); 10540 } 10541 else static assert(false); 10542 } 10543 10544 void addText(string txt) { 10545 version(custom_widgets) { 10546 10547 textLayout.addText(txt); 10548 10549 { 10550 // FIXME: it should be able to get this info easier 10551 auto painter = draw(); 10552 textLayout.redoLayout(painter); 10553 } 10554 auto cbb = textLayout.contentBoundingBox(); 10555 setContentSize(cbb.width, cbb.height); 10556 10557 } else version(win32_widgets) { 10558 // get the current selection 10559 DWORD StartPos, EndPos; 10560 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 10561 10562 // move the caret to the end of the text 10563 int outLength = GetWindowTextLengthW(hwnd); 10564 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 10565 10566 // insert the text at the new caret position 10567 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 10568 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 10569 10570 // restore the previous selection 10571 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 10572 } else static assert(0); 10573 } 10574 10575 version(custom_widgets) 10576 override void paintFrameAndBackground(WidgetPainter painter) { 10577 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 10578 } 10579 10580 version(win32_widgets) { /* will do it with Windows calls in the classes */ } 10581 else version(custom_widgets) { 10582 // FIXME 10583 10584 static if(SimpledisplayTimerAvailable) 10585 Timer caretTimer; 10586 etc.TextLayout textLayout; 10587 10588 void setupCustomTextEditing() { 10589 textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4)); 10590 textLayout.selectionXorColor = getComputedStyle().activeListXorColor; 10591 } 10592 10593 override void paint(WidgetPainter painter) { 10594 if(parentWindow.win.closed) return; 10595 10596 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 10597 10598 /* 10599 painter.outlineColor = Color.white; 10600 painter.fillColor = Color.white; 10601 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 10602 */ 10603 10604 painter.outlineColor = Color.black; 10605 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 10606 10607 textLayout.caretShowingOnScreen = false; 10608 10609 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 10610 } 10611 10612 static class Style : Widget.Style { 10613 override MouseCursor cursor() { 10614 return GenericCursor.Text; 10615 } 10616 } 10617 mixin OverrideStyle!Style; 10618 } 10619 else static assert(false); 10620 10621 10622 10623 version(custom_widgets) 10624 override void defaultEventHandler_mousedown(MouseDownEvent ev) { 10625 super.defaultEventHandler_mousedown(ev); 10626 if(parentWindow.win.closed) return; 10627 if(ev.button == MouseButton.left) { 10628 if(textLayout.selectNone()) 10629 redraw(); 10630 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 10631 this.focus(); 10632 //this.parentWindow.win.grabInput(); 10633 } else if(ev.button == MouseButton.middle) { 10634 static if(UsingSimpledisplayX11) { 10635 getPrimarySelection(parentWindow.win, (txt) { 10636 textLayout.insert(txt); 10637 redraw(); 10638 10639 auto cbb = textLayout.contentBoundingBox(); 10640 setContentSize(cbb.width, cbb.height); 10641 }); 10642 } 10643 } 10644 } 10645 10646 version(custom_widgets) 10647 override void defaultEventHandler_mouseup(MouseUpEvent ev) { 10648 //this.parentWindow.win.releaseInputGrab(); 10649 super.defaultEventHandler_mouseup(ev); 10650 } 10651 10652 version(custom_widgets) 10653 override void defaultEventHandler_mousemove(MouseMoveEvent ev) { 10654 super.defaultEventHandler_mousemove(ev); 10655 if(ev.state & ModifierState.leftButtonDown) { 10656 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 10657 redraw(); 10658 } 10659 } 10660 10661 version(custom_widgets) 10662 override void defaultEventHandler_focus(Event ev) { 10663 super.defaultEventHandler_focus(ev); 10664 if(parentWindow.win.closed) return; 10665 auto painter = this.draw(); 10666 textLayout.drawCaret(painter); 10667 10668 static if(SimpledisplayTimerAvailable) 10669 if(caretTimer) { 10670 caretTimer.destroy(); 10671 caretTimer = null; 10672 } 10673 10674 bool blinkingCaret = true; 10675 static if(UsingSimpledisplayX11) 10676 if(!Image.impl.xshmAvailable) 10677 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 10678 10679 if(blinkingCaret) 10680 static if(SimpledisplayTimerAvailable) 10681 caretTimer = new Timer(500, { 10682 if(parentWindow.win.closed) { 10683 caretTimer.destroy(); 10684 return; 10685 } 10686 if(isFocused()) { 10687 auto painter = this.draw(); 10688 textLayout.drawCaret(painter); 10689 } else if(textLayout.caretShowingOnScreen) { 10690 auto painter = this.draw(); 10691 textLayout.eraseCaret(painter); 10692 } 10693 }); 10694 } 10695 10696 override void defaultEventHandler_blur(Event ev) { 10697 super.defaultEventHandler_blur(ev); 10698 if(parentWindow.win.closed) return; 10699 version(custom_widgets) { 10700 auto painter = this.draw(); 10701 textLayout.eraseCaret(painter); 10702 static if(SimpledisplayTimerAvailable) 10703 if(caretTimer) { 10704 caretTimer.destroy(); 10705 caretTimer = null; 10706 } 10707 } 10708 10709 auto evt = new ChangeEvent!string(this, &this.content); 10710 evt.dispatch(); 10711 } 10712 10713 version(custom_widgets) 10714 override void defaultEventHandler_char(CharEvent ev) { 10715 super.defaultEventHandler_char(ev); 10716 textLayout.insert(ev.character); 10717 redraw(); 10718 10719 // FIXME: too inefficient 10720 auto cbb = textLayout.contentBoundingBox(); 10721 setContentSize(cbb.width, cbb.height); 10722 } 10723 version(custom_widgets) 10724 override void defaultEventHandler_keydown(KeyDownEvent ev) { 10725 //super.defaultEventHandler_keydown(ev); 10726 switch(ev.key) { 10727 case Key.Delete: 10728 textLayout.delete_(); 10729 redraw(); 10730 break; 10731 case Key.Left: 10732 textLayout.moveLeft(); 10733 redraw(); 10734 break; 10735 case Key.Right: 10736 textLayout.moveRight(); 10737 redraw(); 10738 break; 10739 case Key.Up: 10740 textLayout.moveUp(); 10741 redraw(); 10742 break; 10743 case Key.Down: 10744 textLayout.moveDown(); 10745 redraw(); 10746 break; 10747 case Key.Home: 10748 textLayout.moveHome(); 10749 redraw(); 10750 break; 10751 case Key.End: 10752 textLayout.moveEnd(); 10753 redraw(); 10754 break; 10755 case Key.PageUp: 10756 foreach(i; 0 .. 32) 10757 textLayout.moveUp(); 10758 redraw(); 10759 break; 10760 case Key.PageDown: 10761 foreach(i; 0 .. 32) 10762 textLayout.moveDown(); 10763 redraw(); 10764 break; 10765 10766 default: 10767 {} // intentionally blank, let "char" handle it 10768 } 10769 /* 10770 if(ev.key == Key.Backspace) { 10771 textLayout.backspace(); 10772 redraw(); 10773 } 10774 */ 10775 ensureVisibleInScroll(textLayout.caretBoundingBox()); 10776 } 10777 10778 10779 } 10780 10781 /// 10782 class LineEdit : EditableTextWidget { 10783 // FIXME: hack 10784 version(custom_widgets) { 10785 override bool showingVerticalScroll() { return false; } 10786 override bool showingHorizontalScroll() { return false; } 10787 } 10788 10789 override int flexBasisWidth() { return 250; } 10790 10791 /// 10792 this(Widget parent) { 10793 super(parent); 10794 version(win32_widgets) { 10795 createWin32Window(this, "edit"w, "", 10796 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 10797 } else version(custom_widgets) { 10798 setupCustomTextEditing(); 10799 addEventListener(delegate(CharEvent ev) { 10800 if(ev.character == '\n') 10801 ev.preventDefault(); 10802 }); 10803 } else static assert(false); 10804 } 10805 override int maxHeight() { return defaultLineHeight + 4; } 10806 override int minHeight() { return defaultLineHeight + 4; } 10807 10808 /+ 10809 @property void passwordMode(bool p) { 10810 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 10811 } 10812 +/ 10813 } 10814 10815 /++ 10816 A [LineEdit] that displays `*` in place of the actual characters. 10817 10818 Alas, Windows requires the window to be created differently to use this style, 10819 so it had to be a new class instead of a toggle on and off on an existing object. 10820 10821 FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now. 10822 10823 History: 10824 Added January 24, 2021 10825 +/ 10826 class PasswordEdit : EditableTextWidget { 10827 version(custom_widgets) { 10828 override bool showingVerticalScroll() { return false; } 10829 override bool showingHorizontalScroll() { return false; } 10830 } 10831 10832 override int flexBasisWidth() { return 250; } 10833 10834 /// 10835 this(Widget parent) { 10836 super(parent); 10837 version(win32_widgets) { 10838 createWin32Window(this, "edit"w, "", 10839 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 10840 } else version(custom_widgets) { 10841 setupCustomTextEditing(); 10842 addEventListener(delegate(CharEvent ev) { 10843 if(ev.character == '\n') 10844 ev.preventDefault(); 10845 }); 10846 } else static assert(false); 10847 } 10848 override int maxHeight() { return defaultLineHeight + 4; } 10849 override int minHeight() { return defaultLineHeight + 4; } 10850 } 10851 10852 10853 /// 10854 class TextEdit : EditableTextWidget { 10855 /// 10856 this(Widget parent) { 10857 super(parent); 10858 version(win32_widgets) { 10859 createWin32Window(this, "edit"w, "", 10860 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 10861 } else version(custom_widgets) { 10862 setupCustomTextEditing(); 10863 } else static assert(false); 10864 } 10865 override int maxHeight() { return int.max; } 10866 override int heightStretchiness() { return 7; } 10867 10868 override int flexBasisWidth() { return 250; } 10869 override int flexBasisHeight() { return 250; } 10870 } 10871 10872 10873 /++ 10874 10875 +/ 10876 version(none) 10877 class RichTextDisplay : Widget { 10878 @property void content(string c) {} 10879 void appendContent(string c) {} 10880 } 10881 10882 /// 10883 class MessageBox : Window { 10884 private string message; 10885 MessageBoxButton buttonPressed = MessageBoxButton.None; 10886 /// 10887 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 10888 super(300, 100); 10889 10890 assert(buttons.length); 10891 assert(buttons.length == buttonIds.length); 10892 10893 this.message = message; 10894 10895 int buttonsWidth = cast(int) buttons.length * 50 + (cast(int) buttons.length - 1) * 16; 10896 10897 int x = this.width / 2 - buttonsWidth / 2; 10898 10899 foreach(idx, buttonText; buttons) { 10900 auto button = new Button(buttonText, this); 10901 button.x = x; 10902 button.y = height - (button.height + 10); 10903 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 10904 this.buttonPressed = buttonIds[idx]; 10905 win.close(); 10906 }; })(idx)); 10907 10908 button.registerMovement(); 10909 x += button.width; 10910 x += 16; 10911 if(idx == 0) 10912 button.focus(); 10913 } 10914 10915 win.show(); 10916 redraw(); 10917 } 10918 10919 override void paint(WidgetPainter painter) { 10920 super.paint(painter); 10921 10922 auto cs = getComputedStyle(); 10923 10924 painter.outlineColor = cs.foregroundColor(); 10925 painter.fillColor = cs.foregroundColor(); 10926 10927 painter.drawText(Point(0, 0), message, Point(width, height / 2), TextAlignment.Center | TextAlignment.VerticalCenter); 10928 } 10929 10930 // this one is all fixed position 10931 override void recomputeChildLayout() {} 10932 } 10933 10934 /// 10935 enum MessageBoxStyle { 10936 OK, /// 10937 OKCancel, /// 10938 RetryCancel, /// 10939 YesNo, /// 10940 YesNoCancel, /// 10941 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. 10942 } 10943 10944 /// 10945 enum MessageBoxIcon { 10946 None, /// 10947 Info, /// 10948 Warning, /// 10949 Error /// 10950 } 10951 10952 /// Identifies the button the user pressed on a message box. 10953 enum MessageBoxButton { 10954 None, /// The user closed the message box without clicking any of the buttons. 10955 OK, /// 10956 Cancel, /// 10957 Retry, /// 10958 Yes, /// 10959 No, /// 10960 Continue /// 10961 } 10962 10963 10964 /++ 10965 Displays a modal message box, blocking until the user dismisses it. 10966 10967 Returns: the button pressed. 10968 +/ 10969 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 10970 version(win32_widgets) { 10971 WCharzBuffer t = WCharzBuffer(title); 10972 WCharzBuffer m = WCharzBuffer(message); 10973 UINT type; 10974 with(MessageBoxStyle) 10975 final switch(style) { 10976 case OK: type |= MB_OK; break; 10977 case OKCancel: type |= MB_OKCANCEL; break; 10978 case RetryCancel: type |= MB_RETRYCANCEL; break; 10979 case YesNo: type |= MB_YESNO; break; 10980 case YesNoCancel: type |= MB_YESNOCANCEL; break; 10981 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 10982 } 10983 with(MessageBoxIcon) 10984 final switch(icon) { 10985 case None: break; 10986 case Info: type |= MB_ICONINFORMATION; break; 10987 case Warning: type |= MB_ICONWARNING; break; 10988 case Error: type |= MB_ICONERROR; break; 10989 } 10990 switch(MessageBoxW(null, m.ptr, t.ptr, type)) { 10991 case IDOK: return MessageBoxButton.OK; 10992 case IDCANCEL: return MessageBoxButton.Cancel; 10993 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 10994 case IDYES: return MessageBoxButton.Yes; 10995 case IDNO: return MessageBoxButton.No; 10996 case IDCONTINUE: return MessageBoxButton.Continue; 10997 default: return MessageBoxButton.None; 10998 } 10999 } else { 11000 string[] buttons; 11001 MessageBoxButton[] buttonIds; 11002 with(MessageBoxStyle) 11003 final switch(style) { 11004 case OK: 11005 buttons = ["OK"]; 11006 buttonIds = [MessageBoxButton.OK]; 11007 break; 11008 case OKCancel: 11009 buttons = ["OK", "Cancel"]; 11010 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 11011 break; 11012 case RetryCancel: 11013 buttons = ["Retry", "Cancel"]; 11014 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 11015 break; 11016 case YesNo: 11017 buttons = ["Yes", "No"]; 11018 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 11019 break; 11020 case YesNoCancel: 11021 buttons = ["Yes", "No", "Cancel"]; 11022 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 11023 break; 11024 case RetryCancelContinue: 11025 buttons = ["Try Again", "Cancel", "Continue"]; 11026 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 11027 break; 11028 } 11029 auto mb = new MessageBox(message, buttons, buttonIds); 11030 EventLoop el = EventLoop.get; 11031 el.run(() { return !mb.win.closed; }); 11032 return mb.buttonPressed; 11033 } 11034 } 11035 11036 /// ditto 11037 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 11038 return messageBox(null, message, style, icon); 11039 } 11040 11041 11042 11043 /// 11044 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 11045 11046 /++ 11047 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 11048 11049 History: 11050 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. 11051 +/ 11052 struct EventListener { 11053 private Widget widget; 11054 private string event; 11055 private EventHandler handler; 11056 private bool useCapture; 11057 11058 /// 11059 void disconnect() { 11060 widget.removeEventListener(this); 11061 } 11062 } 11063 11064 /++ 11065 The purpose of this enum was to give a compile-time checked version of various standard event strings. 11066 11067 Now, I recommend you use a statically typed event object instead. 11068 11069 See_Also: [Event] 11070 +/ 11071 enum EventType : string { 11072 click = "click", /// 11073 11074 mouseenter = "mouseenter", /// 11075 mouseleave = "mouseleave", /// 11076 mousein = "mousein", /// 11077 mouseout = "mouseout", /// 11078 mouseup = "mouseup", /// 11079 mousedown = "mousedown", /// 11080 mousemove = "mousemove", /// 11081 11082 keydown = "keydown", /// 11083 keyup = "keyup", /// 11084 char_ = "char", /// 11085 11086 focus = "focus", /// 11087 blur = "blur", /// 11088 11089 triggered = "triggered", /// 11090 11091 change = "change", /// 11092 } 11093 11094 /++ 11095 Represents an event that is currently being processed. 11096 11097 11098 Minigui's event model is based on the web browser. An event has a name, a target, 11099 and an associated data object. It starts from the window and works its way down through 11100 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 11101 then goes back up again all the way back to the window, triggering bubble phase handlers. At 11102 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 11103 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 11104 was called; that just stops it from calling other handlers in the widget tree, but the default happens 11105 whenever propagation is done, not only if it gets to the end of the chain). 11106 11107 This model has several nice points: 11108 11109 $(LIST 11110 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 11111 with event handlers set, then add/remove children as much as you want without needing 11112 to manage the event handlers on them - the parent alone can manage everything. 11113 11114 * It is easy to create new custom events in your application. 11115 11116 * It is familiar to many web developers. 11117 ) 11118 11119 There's a few downsides though: 11120 11121 $(LIST 11122 * There's not a lot of type safety. 11123 11124 * You don't get a static list of what events a widget can emit. 11125 11126 * Tracing where an event got cancelled along the chain can get difficult; the downside of 11127 the central delegation benefit is it can be lead to debugging of action at a distance. 11128 ) 11129 11130 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 11131 while keeping the benefits - and most compatibility with - the existing model. The main idea is 11132 to simply use a D object type which provides a static interface as well as a built-in event name. 11133 Then, a new static interface allows you to see what an event can emit and attach handlers to it 11134 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 11135 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 11136 to having a little more help from the D compiler and documentation generator. 11137 11138 Your code would change like this: 11139 11140 --- 11141 // old 11142 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 11143 11144 // new 11145 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 11146 --- 11147 11148 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. 11149 11150 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 11151 11152 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 11153 11154 Thus the family of functions are: 11155 11156 [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. 11157 11158 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 11159 11160 [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. 11161 11162 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 11163 11164 --- 11165 class MyCheckbox : Widget { 11166 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 11167 /// It is NOT actually required but should be used whenever possible. 11168 mixin Emits!(ChangeEvent!bool); 11169 11170 this(Widget parent) { 11171 super(parent); 11172 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 11173 } 11174 11175 private bool _checked; 11176 @property bool checked() { return _checked; } 11177 @property void checked(bool set) { 11178 _checked = set; 11179 emit!(ChangeEvent!bool)(&checked); 11180 } 11181 } 11182 --- 11183 11184 ## Creating Your Own Events 11185 11186 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. 11187 11188 --- 11189 class MyEvent : Event { 11190 this(Widget target) { super(EventString, target); } 11191 mixin Register; // adds EventString and other reflection information 11192 } 11193 --- 11194 11195 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 11196 11197 History: 11198 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. 11199 11200 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. 11201 +/ 11202 /+ 11203 11204 ## General Conventions 11205 11206 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. 11207 11208 11209 ## Qt-style signals and slots 11210 11211 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. 11212 11213 The intention is for events to be used when 11214 11215 --- 11216 class Demo : Widget { 11217 this() { 11218 myPropertyChanged = Signal!int(this); 11219 } 11220 @property myProperty(int v) { 11221 myPropertyChanged.emit(v); 11222 } 11223 11224 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 11225 // but it can just genuinely not care about `this` since that's not really passed. 11226 } 11227 11228 class Foo : Widget { 11229 // the slot uda is not necessary, but it helps the script and ui builder find it. 11230 @slot void setValue(int v) { ... } 11231 } 11232 11233 demo.myPropertyChanged.connect(&foo.setValue); 11234 --- 11235 11236 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 11237 11238 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 11239 11240 class StringChangeEvent : ChangeEvent, Signal!string { 11241 mixin SignalImpl 11242 } 11243 11244 +/ 11245 class Event : ReflectableProperties { 11246 /// Creates an event without populating any members and without sending it. See [dispatch] 11247 this(string eventName, Widget emittedBy) { 11248 this.eventName = eventName; 11249 this.srcElement = emittedBy; 11250 } 11251 11252 11253 /// Implementations for the [ReflectableProperties] interface/ 11254 void getPropertiesList(scope void delegate(string name) sink) const {} 11255 /// ditto 11256 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 11257 /// ditto 11258 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 11259 return SetPropertyResult.notPermitted; 11260 } 11261 11262 11263 /+ 11264 /++ 11265 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 11266 11267 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. 11268 +/ 11269 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 11270 if(value.length == 0) { 11271 finalSink(memberName, `""`); 11272 return; 11273 } 11274 11275 char[1024] bufferBacking; 11276 char[] buffer = bufferBacking; 11277 int bufferPosition; 11278 11279 void sink(char ch) { 11280 if(bufferPosition >= buffer.length) 11281 buffer.length = buffer.length + 1024; 11282 buffer[bufferPosition++] = ch; 11283 } 11284 11285 sink('"'); 11286 11287 foreach(ch; value) { 11288 switch(ch) { 11289 case '\\': 11290 sink('\\'); sink('\\'); 11291 break; 11292 case '"': 11293 sink('\\'); sink('"'); 11294 break; 11295 case '\n': 11296 sink('\\'); sink('n'); 11297 break; 11298 case '\r': 11299 sink('\\'); sink('r'); 11300 break; 11301 case '\t': 11302 sink('\\'); sink('t'); 11303 break; 11304 default: 11305 sink(ch); 11306 } 11307 } 11308 11309 sink('"'); 11310 11311 finalSink(memberName, buffer[0 .. bufferPosition]); 11312 } 11313 +/ 11314 11315 /+ 11316 enum EventInitiator { 11317 system, 11318 minigui, 11319 user 11320 } 11321 11322 immutable EventInitiator; initiatedBy; 11323 +/ 11324 11325 /++ 11326 Events should generally follow the propagation model, but there's some exceptions 11327 to that rule. If so, they should override this to return false. In that case, only 11328 bubbling event handlers on the target itself and capturing event handlers on the containing 11329 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 11330 capture -> target -> bubble process.) 11331 11332 History: 11333 Added May 12, 2021 11334 +/ 11335 bool propagates() const pure nothrow @nogc @safe { 11336 return true; 11337 } 11338 11339 /++ 11340 hints as to whether preventDefault will actually do anything. not entirely reliable. 11341 11342 History: 11343 Added May 14, 2021 11344 +/ 11345 bool cancelable() const pure nothrow @nogc @safe { 11346 return true; 11347 } 11348 11349 /++ 11350 You can mix this into child class to register some boilerplate. It includes the `EventString` 11351 member, a constructor, and implementations of the dynamic get data interfaces. 11352 11353 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 11354 11355 11356 You can override the default EventString by simply providing your own in the form of 11357 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 11358 which provides some namespace protection against conflicts in other libraries while still being fairly 11359 easy to use. 11360 11361 If you provide your own constructor, it will override the default constructor provided here. A constructor 11362 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 11363 first argument to your constructor. 11364 11365 History: 11366 Added May 13, 2021. 11367 +/ 11368 protected static mixin template Register() { 11369 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 11370 this(Widget target) { super(EventString, target); } 11371 11372 mixin ReflectableProperties.RegisterGetters; 11373 } 11374 11375 /++ 11376 This is the widget that emitted the event. 11377 11378 11379 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 11380 11381 History: 11382 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 11383 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 11384 so I don't intend to remove these aliases. 11385 +/ 11386 Widget source; 11387 /// ditto 11388 alias source target; 11389 /// ditto 11390 alias source srcElement; 11391 11392 Widget relatedTarget; /// Note: likely to be deprecated at some point. 11393 11394 /// Prevents the default event handler (if there is one) from being called 11395 void preventDefault() { 11396 lastDefaultPrevented = true; 11397 defaultPrevented = true; 11398 } 11399 11400 /// Stops the event propagation immediately. 11401 void stopPropagation() { 11402 propagationStopped = true; 11403 } 11404 11405 private bool defaultPrevented; 11406 private bool propagationStopped; 11407 private string eventName; 11408 11409 private bool isBubbling; 11410 11411 /// 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. 11412 protected void adjustScrolling() { } 11413 /// ditto 11414 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 11415 11416 /++ 11417 this sends it only to the target. If you want propagation, use dispatch() instead. 11418 11419 This should be made private!!! 11420 11421 +/ 11422 void sendDirectly() { 11423 if(srcElement is null) 11424 return; 11425 11426 // 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. 11427 11428 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 11429 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 11430 11431 adjustScrolling(); 11432 11433 if(auto e = target.parentWindow) { 11434 if(auto handlers = "*" in e.capturingEventHandlers) 11435 foreach(handler; *handlers) 11436 if(handler) handler(e, this); 11437 if(auto handlers = eventName in e.capturingEventHandlers) 11438 foreach(handler; *handlers) 11439 if(handler) handler(e, this); 11440 } 11441 11442 auto e = srcElement; 11443 11444 if(auto handlers = eventName in e.bubblingEventHandlers) 11445 foreach(handler; *handlers) 11446 if(handler) handler(e, this); 11447 11448 if(auto handlers = "*" in e.bubblingEventHandlers) 11449 foreach(handler; *handlers) 11450 if(handler) handler(e, this); 11451 11452 // there's never a default for a catch-all event 11453 if(!defaultPrevented) 11454 if(eventName in e.defaultEventHandlers) 11455 e.defaultEventHandlers[eventName](e, this); 11456 } 11457 11458 /// this dispatches the element using the capture -> target -> bubble process 11459 void dispatch() { 11460 if(srcElement is null) 11461 return; 11462 11463 if(!propagates) { 11464 sendDirectly; 11465 return; 11466 } 11467 11468 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 11469 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 11470 11471 adjustScrolling(); 11472 // first capture, then bubble 11473 11474 Widget[] chain; 11475 Widget curr = srcElement; 11476 while(curr) { 11477 auto l = curr; 11478 chain ~= l; 11479 curr = curr.parent; 11480 } 11481 11482 isBubbling = false; 11483 11484 foreach_reverse(e; chain) { 11485 if(auto handlers = "*" in e.capturingEventHandlers) 11486 foreach(handler; *handlers) if(handler !is null) handler(e, this); 11487 11488 if(propagationStopped) 11489 break; 11490 11491 if(auto handlers = eventName in e.capturingEventHandlers) 11492 foreach(handler; *handlers) if(handler !is null) handler(e, this); 11493 11494 // the default on capture should really be to always do nothing 11495 11496 //if(!defaultPrevented) 11497 // if(eventName in e.defaultEventHandlers) 11498 // e.defaultEventHandlers[eventName](e.element, this); 11499 11500 if(propagationStopped) 11501 break; 11502 } 11503 11504 int adjustX; 11505 int adjustY; 11506 11507 isBubbling = true; 11508 if(!propagationStopped) 11509 foreach(e; chain) { 11510 if(auto handlers = eventName in e.bubblingEventHandlers) 11511 foreach(handler; *handlers) if(handler !is null) handler(e, this); 11512 11513 if(propagationStopped) 11514 break; 11515 11516 if(auto handlers = "*" in e.bubblingEventHandlers) 11517 foreach(handler; *handlers) if(handler !is null) handler(e, this); 11518 11519 if(propagationStopped) 11520 break; 11521 11522 if(e.encapsulatedChildren()) { 11523 adjustClientCoordinates(adjustX, adjustY); 11524 target = e; 11525 } else { 11526 adjustX += e.x; 11527 adjustY += e.y; 11528 } 11529 } 11530 11531 if(!defaultPrevented) 11532 foreach(e; chain) { 11533 if(eventName in e.defaultEventHandlers) 11534 e.defaultEventHandlers[eventName](e, this); 11535 } 11536 } 11537 11538 11539 /* old compatibility things */ 11540 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 11541 final @property { 11542 Key key() { return (cast(KeyEventBase) this).key; } 11543 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 11544 11545 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 11546 bool altKey() { return (cast(KeyEventBase) this).altKey; } 11547 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 11548 } 11549 11550 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 11551 final @property { 11552 int clientX() { return (cast(MouseEventBase) this).clientX; } 11553 int clientY() { return (cast(MouseEventBase) this).clientY; } 11554 11555 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 11556 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 11557 11558 int button() { return (cast(MouseEventBase) this).button; } 11559 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 11560 } 11561 11562 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 11563 final @property { 11564 int state() { 11565 if(auto meb = cast(MouseEventBase) this) 11566 return meb.state; 11567 if(auto keb = cast(KeyEventBase) this) 11568 return keb.state; 11569 assert(0); 11570 } 11571 } 11572 11573 deprecated("Use a CharEvent instead of Event in your handler going forward") 11574 final @property { 11575 dchar character() { 11576 if(auto ce = cast(CharEvent) this) 11577 return ce.character; 11578 return dchar.init; 11579 } 11580 } 11581 11582 // for change events 11583 @property { 11584 /// 11585 int intValue() { return 0; } 11586 /// 11587 string stringValue() { return null; } 11588 } 11589 } 11590 11591 /++ 11592 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 11593 11594 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 11595 dynamic and custom events, but the static list helps ensure you get them right. 11596 11597 If this is declared, you can use [Widget.emit] to send the event. 11598 11599 All events work the same way though, following the capture->widget->bubble model described under [Event]. 11600 11601 History: 11602 Added May 4, 2021 11603 +/ 11604 mixin template Emits(EventType) { 11605 import arsd.minigui : EventString; 11606 static if(is(EventType : Event) && !is(EventType == Event)) 11607 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 11608 else 11609 static assert(0, "You can only emit subclasses of Event"); 11610 } 11611 11612 /// ditto 11613 mixin template Emits(string eventString) { 11614 mixin("private Event[0] emits_" ~ eventString ~";"); 11615 } 11616 11617 /* 11618 class SignalEvent(string name) : Event { 11619 11620 } 11621 */ 11622 11623 /++ 11624 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". 11625 11626 11627 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. 11628 11629 History: 11630 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 11631 +/ 11632 class CommandEvent : Event { 11633 enum EventString = "command"; 11634 this(Widget source, string CommandString = EventString) { 11635 super(CommandString, source); 11636 } 11637 } 11638 11639 /++ 11640 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 11641 +/ 11642 class CommandEventWithArgs(Args...) : CommandEvent { 11643 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 11644 Args args; 11645 } 11646 11647 /++ 11648 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. 11649 11650 See [CommandEvent] for more information. 11651 11652 Returns: 11653 The [EventListener] you can use to remove the handler. 11654 +/ 11655 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 11656 return w.addEventListener(CommandString, (Event ev) { 11657 if(ev.target is w) 11658 return; // it does not consume its own commands! 11659 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 11660 handler(cev.args); 11661 ev.stopPropagation(); 11662 } 11663 }); 11664 } 11665 11666 /++ 11667 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. 11668 +/ 11669 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 11670 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 11671 event.dispatch(); 11672 } 11673 11674 class ResizeEvent : Event { 11675 enum EventString = "resize"; 11676 11677 this(Widget target) { super(EventString, target); } 11678 11679 override bool propagates() const { return false; } 11680 } 11681 11682 /++ 11683 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 11684 11685 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. 11686 11687 History: 11688 Added June 21, 2021 (dub v10.1) 11689 +/ 11690 class ClosingEvent : Event { 11691 enum EventString = "closing"; 11692 11693 this(Widget target) { super(EventString, target); } 11694 11695 override bool propagates() const { return false; } 11696 override bool cancelable() const { return true; } 11697 } 11698 11699 /// ditto 11700 class ClosedEvent : Event { 11701 enum EventString = "closed"; 11702 11703 this(Widget target) { super(EventString, target); } 11704 11705 override bool propagates() const { return false; } 11706 override bool cancelable() const { return false; } 11707 } 11708 11709 /// 11710 class BlurEvent : Event { 11711 enum EventString = "blur"; 11712 11713 // FIXME: related target? 11714 this(Widget target) { super(EventString, target); } 11715 11716 override bool propagates() const { return false; } 11717 } 11718 11719 /// 11720 class FocusEvent : Event { 11721 enum EventString = "focus"; 11722 11723 // FIXME: related target? 11724 this(Widget target) { super(EventString, target); } 11725 11726 override bool propagates() const { return false; } 11727 } 11728 11729 /++ 11730 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 11731 11732 History: 11733 Added July 3, 2021 11734 +/ 11735 class FocusInEvent : Event { 11736 enum EventString = "focusin"; 11737 11738 // FIXME: related target? 11739 this(Widget target) { super(EventString, target); } 11740 11741 override bool cancelable() const { return false; } 11742 } 11743 11744 /// ditto 11745 class FocusOutEvent : Event { 11746 enum EventString = "focusout"; 11747 11748 // FIXME: related target? 11749 this(Widget target) { super(EventString, target); } 11750 11751 override bool cancelable() const { return false; } 11752 } 11753 11754 /// 11755 class ScrollEvent : Event { 11756 enum EventString = "scroll"; 11757 this(Widget target) { super(EventString, target); } 11758 11759 override bool cancelable() const { return false; } 11760 } 11761 11762 /++ 11763 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 11764 11765 History: 11766 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 11767 +/ 11768 class CharEvent : Event { 11769 enum EventString = "char"; 11770 this(Widget target, dchar ch) { 11771 character = ch; 11772 super(EventString, target); 11773 } 11774 11775 immutable dchar character; 11776 } 11777 11778 /++ 11779 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 11780 +/ 11781 abstract class ChangeEventBase : Event { 11782 enum EventString = "change"; 11783 this(Widget target) { 11784 super(EventString, target); 11785 } 11786 11787 /+ 11788 // idk where or how exactly i want to do this. 11789 // i might come back to it later. 11790 11791 // If a widget itself broadcasts one of theses itself, it stops propagation going down 11792 // this way the source doesn't get too confused (think of a nested scroll widget) 11793 // 11794 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 11795 // then you consume that command and change you scroll x position to whatever. then you do 11796 // some kind of change event that is broadcast back to the children and any horizontal scroll 11797 // listeners are now able to update, without having an explicit connection between them. 11798 void broadcastToChildren(string fieldName) { 11799 11800 } 11801 +/ 11802 } 11803 11804 /++ 11805 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. 11806 11807 11808 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). 11809 11810 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);` 11811 11812 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 11813 11814 History: 11815 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. 11816 +/ 11817 class ChangeEvent(T) : ChangeEventBase { 11818 this(Widget target, T delegate() getNewValue) { 11819 assert(getNewValue !is null); 11820 this.getNewValue = getNewValue; 11821 super(target); 11822 } 11823 11824 private T delegate() getNewValue; 11825 11826 /++ 11827 Gets the new value that just changed. 11828 +/ 11829 @property T value() { 11830 return getNewValue(); 11831 } 11832 11833 /// compatibility method for old generic Events 11834 static if(is(immutable T == immutable int)) 11835 override int intValue() { return value; } 11836 /// ditto 11837 static if(is(immutable T == immutable string)) 11838 override string stringValue() { return value; } 11839 } 11840 11841 /++ 11842 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 11843 11844 11845 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 11846 11847 History: 11848 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 11849 +/ 11850 abstract class KeyEventBase : Event { 11851 this(string name, Widget target) { 11852 super(name, target); 11853 } 11854 11855 // for key events 11856 Key key; /// 11857 11858 KeyEvent originalKeyEvent; 11859 11860 /++ 11861 Indicates the current state of the given keyboard modifier keys. 11862 11863 History: 11864 Added to events on April 15, 2020. 11865 +/ 11866 bool ctrlKey; 11867 11868 /// ditto 11869 bool altKey; 11870 11871 /// ditto 11872 bool shiftKey; 11873 11874 /++ 11875 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 11876 11877 See [arsd.simpledisplay.ModifierState] for other possible flags. 11878 +/ 11879 int state; 11880 11881 mixin Register; 11882 } 11883 11884 /++ 11885 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]. 11886 11887 11888 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 11889 11890 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. 11891 11892 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. 11893 11894 See_Also: [KeyUpEvent], [CharEvent] 11895 11896 History: 11897 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 11898 +/ 11899 class KeyDownEvent : KeyEventBase { 11900 enum EventString = "keydown"; 11901 this(Widget target) { super(EventString, target); } 11902 } 11903 11904 /++ 11905 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 11906 11907 11908 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 11909 11910 See_Also: [KeyDownEvent], [CharEvent] 11911 11912 History: 11913 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 11914 +/ 11915 class KeyUpEvent : KeyEventBase { 11916 enum EventString = "keyup"; 11917 this(Widget target) { super(EventString, target); } 11918 } 11919 11920 /++ 11921 Contains shared properties for various mouse events; 11922 11923 11924 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 11925 11926 History: 11927 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 11928 +/ 11929 abstract class MouseEventBase : Event { 11930 this(string name, Widget target) { 11931 super(name, target); 11932 } 11933 11934 // for mouse events 11935 int clientX; /// The mouse event location relative to the target widget 11936 int clientY; /// ditto 11937 11938 int viewportX; /// The mouse event location relative to the window origin 11939 int viewportY; /// ditto 11940 11941 int button; /// See: [MouseEvent.button] 11942 int buttonLinear; /// See: [MouseEvent.buttonLinear] 11943 11944 /++ 11945 Indicates the current state of the given keyboard modifier keys. 11946 11947 History: 11948 Added to mouse events on September 28, 2010. 11949 +/ 11950 bool ctrlKey; 11951 11952 /// ditto 11953 bool altKey; 11954 11955 /// ditto 11956 bool shiftKey; 11957 11958 11959 11960 int state; /// 11961 11962 /++ 11963 for consistent names with key event. 11964 11965 History: 11966 Added September 28, 2021 (dub v10.3) 11967 +/ 11968 alias modifierState = state; 11969 11970 /++ 11971 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 11972 11973 History: 11974 Added May 15, 2021 11975 +/ 11976 bool isMouseWheel() { 11977 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 11978 } 11979 11980 // private 11981 override void adjustClientCoordinates(int deltaX, int deltaY) { 11982 clientX += deltaX; 11983 clientY += deltaY; 11984 } 11985 11986 override void adjustScrolling() { 11987 version(custom_widgets) { // TEMP 11988 viewportX = clientX; 11989 viewportY = clientY; 11990 if(auto se = cast(ScrollableWidget) srcElement) { 11991 clientX += se.scrollOrigin.x; 11992 clientY += se.scrollOrigin.y; 11993 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 11994 //clientX += se.scrollX_; 11995 //clientY += se.scrollY_; 11996 } 11997 } 11998 } 11999 12000 mixin Register; 12001 } 12002 12003 /++ 12004 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 12005 12006 12007 $(WARNING 12008 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 12009 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 12010 behavior. 12011 ) 12012 12013 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 12014 12015 [MouseUpEvent] is sent when the user releases a mouse button. 12016 12017 [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.) 12018 12019 [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. 12020 12021 [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. 12022 12023 [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. 12024 12025 [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. 12026 12027 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 12028 12029 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 12030 12031 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 12032 12033 Rationale: 12034 12035 If you only want to do drag, mousedown/up works just fine being consistently sent. 12036 12037 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). 12038 12039 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. 12040 12041 History: 12042 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. 12043 +/ 12044 class MouseUpEvent : MouseEventBase { 12045 enum EventString = "mouseup"; /// 12046 this(Widget target) { super(EventString, target); } 12047 } 12048 /// ditto 12049 class MouseDownEvent : MouseEventBase { 12050 enum EventString = "mousedown"; /// 12051 this(Widget target) { super(EventString, target); } 12052 } 12053 /// ditto 12054 class MouseMoveEvent : MouseEventBase { 12055 enum EventString = "mousemove"; /// 12056 this(Widget target) { super(EventString, target); } 12057 } 12058 /// ditto 12059 class ClickEvent : MouseEventBase { 12060 enum EventString = "click"; /// 12061 this(Widget target) { super(EventString, target); } 12062 } 12063 /// ditto 12064 class DoubleClickEvent : MouseEventBase { 12065 enum EventString = "dblclick"; /// 12066 this(Widget target) { super(EventString, target); } 12067 } 12068 /// ditto 12069 class MouseOverEvent : Event { 12070 enum EventString = "mouseover"; /// 12071 this(Widget target) { super(EventString, target); } 12072 } 12073 /// ditto 12074 class MouseOutEvent : Event { 12075 enum EventString = "mouseout"; /// 12076 this(Widget target) { super(EventString, target); } 12077 } 12078 /// ditto 12079 class MouseEnterEvent : Event { 12080 enum EventString = "mouseenter"; /// 12081 this(Widget target) { super(EventString, target); } 12082 12083 override bool propagates() const { return false; } 12084 } 12085 /// ditto 12086 class MouseLeaveEvent : Event { 12087 enum EventString = "mouseleave"; /// 12088 this(Widget target) { super(EventString, target); } 12089 12090 override bool propagates() const { return false; } 12091 } 12092 12093 private bool isAParentOf(Widget a, Widget b) { 12094 if(a is null || b is null) 12095 return false; 12096 12097 while(b !is null) { 12098 if(a is b) 12099 return true; 12100 b = b.parent; 12101 } 12102 12103 return false; 12104 } 12105 12106 private struct WidgetAtPointResponse { 12107 Widget widget; 12108 12109 // x, y relative to the widget in the response. 12110 int x; 12111 int y; 12112 } 12113 12114 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 12115 assert(starting !is null); 12116 12117 starting.addScrollPosition(x, y); 12118 12119 auto child = starting.getChildAtPosition(x, y); 12120 while(child) { 12121 if(child.hidden) 12122 continue; 12123 starting = child; 12124 x -= child.x; 12125 y -= child.y; 12126 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 12127 child = r.widget; 12128 if(child is starting) 12129 break; 12130 } 12131 return WidgetAtPointResponse(starting, x, y); 12132 } 12133 12134 version(win32_widgets) { 12135 private: 12136 import core.sys.windows.commctrl; 12137 12138 pragma(lib, "comctl32"); 12139 shared static this() { 12140 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 12141 INITCOMMONCONTROLSEX ic; 12142 ic.dwSize = cast(DWORD) ic.sizeof; 12143 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 12144 if(!InitCommonControlsEx(&ic)) { 12145 //import std.stdio; writeln("ICC failed"); 12146 } 12147 } 12148 12149 12150 // everything from here is just win32 headers copy pasta 12151 private: 12152 extern(Windows): 12153 12154 alias HANDLE HMENU; 12155 HMENU CreateMenu(); 12156 bool SetMenu(HWND, HMENU); 12157 HMENU CreatePopupMenu(); 12158 enum MF_POPUP = 0x10; 12159 enum MF_STRING = 0; 12160 12161 12162 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 12163 struct INITCOMMONCONTROLSEX { 12164 DWORD dwSize; 12165 DWORD dwICC; 12166 } 12167 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 12168 enum { 12169 IDB_STD_SMALL_COLOR, 12170 IDB_STD_LARGE_COLOR, 12171 IDB_VIEW_SMALL_COLOR = 4, 12172 IDB_VIEW_LARGE_COLOR = 5 12173 } 12174 enum { 12175 STD_CUT, 12176 STD_COPY, 12177 STD_PASTE, 12178 STD_UNDO, 12179 STD_REDOW, 12180 STD_DELETE, 12181 STD_FILENEW, 12182 STD_FILEOPEN, 12183 STD_FILESAVE, 12184 STD_PRINTPRE, 12185 STD_PROPERTIES, 12186 STD_HELP, 12187 STD_FIND, 12188 STD_REPLACE, 12189 STD_PRINT // = 14 12190 } 12191 12192 alias HANDLE HIMAGELIST; 12193 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 12194 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 12195 BOOL ImageList_Destroy(HIMAGELIST); 12196 12197 uint MAKELONG(ushort a, ushort b) { 12198 return cast(uint) ((b << 16) | a); 12199 } 12200 12201 12202 struct TBBUTTON { 12203 int iBitmap; 12204 int idCommand; 12205 BYTE fsState; 12206 BYTE fsStyle; 12207 version(Win64) 12208 BYTE[6] bReserved; 12209 else 12210 BYTE[2] bReserved; 12211 DWORD dwData; 12212 INT_PTR iString; 12213 } 12214 12215 enum { 12216 TB_ADDBUTTONSA = WM_USER + 20, 12217 TB_INSERTBUTTONA = WM_USER + 21, 12218 TB_GETIDEALSIZE = WM_USER + 99, 12219 } 12220 12221 struct SIZE { 12222 LONG cx; 12223 LONG cy; 12224 } 12225 12226 12227 enum { 12228 TBSTATE_CHECKED = 1, 12229 TBSTATE_PRESSED = 2, 12230 TBSTATE_ENABLED = 4, 12231 TBSTATE_HIDDEN = 8, 12232 TBSTATE_INDETERMINATE = 16, 12233 TBSTATE_WRAP = 32 12234 } 12235 12236 12237 12238 enum { 12239 ILC_COLOR = 0, 12240 ILC_COLOR4 = 4, 12241 ILC_COLOR8 = 8, 12242 ILC_COLOR16 = 16, 12243 ILC_COLOR24 = 24, 12244 ILC_COLOR32 = 32, 12245 ILC_COLORDDB = 254, 12246 ILC_MASK = 1, 12247 ILC_PALETTE = 2048 12248 } 12249 12250 12251 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 12252 12253 12254 enum { 12255 TB_ENABLEBUTTON = WM_USER + 1, 12256 TB_CHECKBUTTON, 12257 TB_PRESSBUTTON, 12258 TB_HIDEBUTTON, 12259 TB_INDETERMINATE, // = WM_USER + 5, 12260 TB_ISBUTTONENABLED = WM_USER + 9, 12261 TB_ISBUTTONCHECKED, 12262 TB_ISBUTTONPRESSED, 12263 TB_ISBUTTONHIDDEN, 12264 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 12265 TB_SETSTATE = WM_USER + 17, 12266 TB_GETSTATE = WM_USER + 18, 12267 TB_ADDBITMAP = WM_USER + 19, 12268 TB_DELETEBUTTON = WM_USER + 22, 12269 TB_GETBUTTON, 12270 TB_BUTTONCOUNT, 12271 TB_COMMANDTOINDEX, 12272 TB_SAVERESTOREA, 12273 TB_CUSTOMIZE, 12274 TB_ADDSTRINGA, 12275 TB_GETITEMRECT, 12276 TB_BUTTONSTRUCTSIZE, 12277 TB_SETBUTTONSIZE, 12278 TB_SETBITMAPSIZE, 12279 TB_AUTOSIZE, // = WM_USER + 33, 12280 TB_GETTOOLTIPS = WM_USER + 35, 12281 TB_SETTOOLTIPS = WM_USER + 36, 12282 TB_SETPARENT = WM_USER + 37, 12283 TB_SETROWS = WM_USER + 39, 12284 TB_GETROWS, 12285 TB_GETBITMAPFLAGS, 12286 TB_SETCMDID, 12287 TB_CHANGEBITMAP, 12288 TB_GETBITMAP, 12289 TB_GETBUTTONTEXTA, 12290 TB_REPLACEBITMAP, // = WM_USER + 46, 12291 TB_GETBUTTONSIZE = WM_USER + 58, 12292 TB_SETBUTTONWIDTH = WM_USER + 59, 12293 TB_GETBUTTONTEXTW = WM_USER + 75, 12294 TB_SAVERESTOREW = WM_USER + 76, 12295 TB_ADDSTRINGW = WM_USER + 77, 12296 } 12297 12298 extern(Windows) 12299 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 12300 12301 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 12302 12303 12304 enum { 12305 TB_SETINDENT = WM_USER + 47, 12306 TB_SETIMAGELIST, 12307 TB_GETIMAGELIST, 12308 TB_LOADIMAGES, 12309 TB_GETRECT, 12310 TB_SETHOTIMAGELIST, 12311 TB_GETHOTIMAGELIST, 12312 TB_SETDISABLEDIMAGELIST, 12313 TB_GETDISABLEDIMAGELIST, 12314 TB_SETSTYLE, 12315 TB_GETSTYLE, 12316 //TB_GETBUTTONSIZE, 12317 //TB_SETBUTTONWIDTH, 12318 TB_SETMAXTEXTROWS, 12319 TB_GETTEXTROWS // = WM_USER + 61 12320 } 12321 12322 enum { 12323 CCM_FIRST = 0x2000, 12324 CCM_LAST = CCM_FIRST + 0x200, 12325 CCM_SETBKCOLOR = 8193, 12326 CCM_SETCOLORSCHEME = 8194, 12327 CCM_GETCOLORSCHEME = 8195, 12328 CCM_GETDROPTARGET = 8196, 12329 CCM_SETUNICODEFORMAT = 8197, 12330 CCM_GETUNICODEFORMAT = 8198, 12331 CCM_SETVERSION = 0x2007, 12332 CCM_GETVERSION = 0x2008, 12333 CCM_SETNOTIFYWINDOW = 0x2009 12334 } 12335 12336 12337 enum { 12338 PBM_SETRANGE = WM_USER + 1, 12339 PBM_SETPOS, 12340 PBM_DELTAPOS, 12341 PBM_SETSTEP, 12342 PBM_STEPIT, // = WM_USER + 5 12343 PBM_SETRANGE32 = 1030, 12344 PBM_GETRANGE, 12345 PBM_GETPOS, 12346 PBM_SETBARCOLOR, // = 1033 12347 PBM_SETBKCOLOR = CCM_SETBKCOLOR 12348 } 12349 12350 enum { 12351 PBS_SMOOTH = 1, 12352 PBS_VERTICAL = 4 12353 } 12354 12355 enum { 12356 ICC_LISTVIEW_CLASSES = 1, 12357 ICC_TREEVIEW_CLASSES = 2, 12358 ICC_BAR_CLASSES = 4, 12359 ICC_TAB_CLASSES = 8, 12360 ICC_UPDOWN_CLASS = 16, 12361 ICC_PROGRESS_CLASS = 32, 12362 ICC_HOTKEY_CLASS = 64, 12363 ICC_ANIMATE_CLASS = 128, 12364 ICC_WIN95_CLASSES = 255, 12365 ICC_DATE_CLASSES = 256, 12366 ICC_USEREX_CLASSES = 512, 12367 ICC_COOL_CLASSES = 1024, 12368 ICC_STANDARD_CLASSES = 0x00004000, 12369 } 12370 12371 enum WM_USER = 1024; 12372 } 12373 12374 version(win32_widgets) 12375 pragma(lib, "comdlg32"); 12376 12377 12378 /// 12379 enum GenericIcons : ushort { 12380 None, /// 12381 // these happen to match the win32 std icons numerically if you just subtract one from the value 12382 Cut, /// 12383 Copy, /// 12384 Paste, /// 12385 Undo, /// 12386 Redo, /// 12387 Delete, /// 12388 New, /// 12389 Open, /// 12390 Save, /// 12391 PrintPreview, /// 12392 Properties, /// 12393 Help, /// 12394 Find, /// 12395 Replace, /// 12396 Print, /// 12397 } 12398 12399 /// 12400 void getOpenFileName( 12401 void delegate(string) onOK, 12402 string prefilledName = null, 12403 string[] filters = null, 12404 void delegate() onCancel = null, 12405 ) 12406 { 12407 return getFileName(true, onOK, prefilledName, filters, onCancel); 12408 } 12409 12410 /++ 12411 History: 12412 onCancel was added November 6, 2021. 12413 +/ 12414 void getSaveFileName( 12415 void delegate(string) onOK, 12416 string prefilledName = null, 12417 string[] filters = null, 12418 void delegate() onCancel = null, 12419 ) 12420 { 12421 return getFileName(false, onOK, prefilledName, filters, onCancel); 12422 } 12423 12424 void getFileName( 12425 bool openOrSave, 12426 void delegate(string) onOK, 12427 string prefilledName = null, 12428 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"] 12429 void delegate() onCancel = null, 12430 ) 12431 { 12432 12433 version(win32_widgets) { 12434 import core.sys.windows.commdlg; 12435 /* 12436 Ofn.lStructSize = sizeof(OPENFILENAME); 12437 Ofn.hwndOwner = hWnd; 12438 Ofn.lpstrFilter = szFilter; 12439 Ofn.lpstrFile= szFile; 12440 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 12441 Ofn.lpstrFileTitle = szFileTitle; 12442 Ofn.nMaxFileTitle = sizeof(szFileTitle); 12443 Ofn.lpstrInitialDir = (LPSTR)NULL; 12444 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 12445 Ofn.lpstrTitle = szTitle; 12446 */ 12447 12448 12449 wchar[1024] file = 0; 12450 wchar[1024] filterBuffer = 0; 12451 makeWindowsString(prefilledName, file[]); 12452 OPENFILENAME ofn; 12453 ofn.lStructSize = ofn.sizeof; 12454 if(filters.length) { 12455 string filter; 12456 foreach(i, f; filters) { 12457 filter ~= f; 12458 filter ~= "\0"; 12459 } 12460 filter ~= "\0"; 12461 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 12462 } 12463 ofn.lpstrFile = file.ptr; 12464 ofn.nMaxFile = file.length; 12465 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) { 12466 onOK(makeUtf8StringFromWindowsString(ofn.lpstrFile)); 12467 } else { 12468 if(onCancel) 12469 onCancel(); 12470 } 12471 } else version(custom_widgets) { 12472 auto picker = new FilePicker(prefilledName); 12473 picker.onOK = onOK; 12474 picker.onCancel = onCancel; 12475 picker.show(); 12476 } 12477 } 12478 12479 version(custom_widgets) 12480 private 12481 class FilePicker : Dialog { 12482 void delegate(string) onOK; 12483 void delegate() onCancel; 12484 LineEdit lineEdit; 12485 this(string prefilledName, Window owner = null) { 12486 super(300, 200, "Choose File..."); // owner); 12487 12488 auto listWidget = new ListWidget(this); 12489 12490 lineEdit = new LineEdit(this); 12491 lineEdit.focus(); 12492 lineEdit.addEventListener(delegate(CharEvent event) { 12493 if(event.character == '\t' || event.character == '\n') 12494 event.preventDefault(); 12495 }); 12496 12497 listWidget.addEventListener(EventType.change, () { 12498 foreach(o; listWidget.options) 12499 if(o.selected) 12500 lineEdit.content = o.label; 12501 }); 12502 12503 //version(none) 12504 lineEdit.addEventListener((KeyDownEvent event) { 12505 if(event.key == Key.Tab) { 12506 listWidget.clear(); 12507 12508 string commonPrefix; 12509 auto cnt = lineEdit.content; 12510 if(cnt.length >= 2 && cnt[0 ..2] == "./") 12511 cnt = cnt[2 .. $]; 12512 12513 version(Windows) { 12514 WIN32_FIND_DATA data; 12515 WCharzBuffer search = WCharzBuffer("./" ~ cnt ~ "*"); 12516 auto handle = FindFirstFileW(search.ptr, &data); 12517 scope(exit) if(handle !is INVALID_HANDLE_VALUE) FindClose(handle); 12518 if(handle is INVALID_HANDLE_VALUE) { 12519 if(GetLastError() == ERROR_FILE_NOT_FOUND) 12520 goto file_not_found; 12521 throw new WindowsApiException("FindFirstFileW"); 12522 } 12523 } else version(Posix) { 12524 import core.sys.posix.dirent; 12525 auto dir = opendir("."); 12526 scope(exit) 12527 if(dir) closedir(dir); 12528 if(dir is null) 12529 throw new ErrnoApiException("opendir"); 12530 12531 auto dirent = readdir(dir); 12532 if(dirent is null) 12533 goto file_not_found; 12534 // filter those that don't start with it, since posix doesn't 12535 // do the * thing itself 12536 while(dirent.d_name[0 .. cnt.length] != cnt[]) { 12537 dirent = readdir(dir); 12538 if(dirent is null) 12539 goto file_not_found; 12540 } 12541 } else static assert(0); 12542 12543 while(true) { 12544 //foreach(string name; dirEntries(".", cnt ~ "*", SpanMode.shallow)) { 12545 version(Windows) { 12546 string name = makeUtf8StringFromWindowsString(data.cFileName[0 .. findIndexOfZero(data.cFileName[])]); 12547 } else version(Posix) { 12548 string name = dirent.d_name[0 .. findIndexOfZero(dirent.d_name[])].idup; 12549 } else static assert(0); 12550 12551 12552 listWidget.addOption(name); 12553 if(commonPrefix is null) 12554 commonPrefix = name; 12555 else { 12556 foreach(idx, char i; name) { 12557 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 12558 commonPrefix = commonPrefix[0 .. idx]; 12559 break; 12560 } 12561 } 12562 } 12563 12564 version(Windows) { 12565 auto ret = FindNextFileW(handle, &data); 12566 if(ret == 0) { 12567 if(GetLastError() == ERROR_NO_MORE_FILES) 12568 break; 12569 throw new WindowsApiException("FindNextFileW"); 12570 } 12571 } else version(Posix) { 12572 dirent = readdir(dir); 12573 if(dirent is null) 12574 break; 12575 12576 while(dirent.d_name[0 .. cnt.length] != cnt[]) { 12577 dirent = readdir(dir); 12578 if(dirent is null) 12579 break; 12580 } 12581 12582 if(dirent is null) 12583 break; 12584 } else static assert(0); 12585 } 12586 if(commonPrefix.length) 12587 lineEdit.content = commonPrefix; 12588 12589 file_not_found: 12590 event.preventDefault(); 12591 } 12592 }); 12593 12594 lineEdit.content = prefilledName; 12595 12596 auto hl = new HorizontalLayout(this); 12597 auto cancelButton = new Button("Cancel", hl); 12598 auto okButton = new Button("OK", hl); 12599 12600 recomputeChildLayout(); // FIXME hack 12601 12602 cancelButton.addEventListener(EventType.triggered, &Cancel); 12603 okButton.addEventListener(EventType.triggered, &OK); 12604 12605 this.addEventListener((KeyDownEvent event) { 12606 if(event.key == Key.Enter || event.key == Key.PadEnter) { 12607 event.preventDefault(); 12608 OK(); 12609 } 12610 if(event.key == Key.Escape) 12611 Cancel(); 12612 }); 12613 12614 } 12615 12616 override void OK() { 12617 if(onOK) 12618 onOK(lineEdit.content); 12619 close(); 12620 } 12621 12622 override void Cancel() { 12623 if(onCancel) 12624 onCancel(); 12625 close(); 12626 } 12627 } 12628 12629 /* 12630 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 12631 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 12632 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 12633 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 12634 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 12635 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 12636 http://www.sbin.org/doc/Xlib/chapt_03.html 12637 12638 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 12639 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 12640 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 12641 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 12642 */ 12643 12644 12645 // These are all for setMenuAndToolbarFromAnnotatedCode 12646 /// This item in the menu will be preceded by a separator line 12647 /// Group: generating_from_code 12648 struct separator {} 12649 deprecated("It was misspelled, use separator instead") alias seperator = separator; 12650 /// Program-wide keyboard shortcut to trigger the action 12651 /// Group: generating_from_code 12652 struct accelerator { string keyString; } 12653 /// tells which menu the action will be on 12654 /// Group: generating_from_code 12655 struct menu { string name; } 12656 /// Describes which toolbar section the action appears on 12657 /// Group: generating_from_code 12658 struct toolbar { string groupName; } 12659 /// 12660 /// Group: generating_from_code 12661 struct icon { ushort id; } 12662 /// 12663 /// Group: generating_from_code 12664 struct label { string label; } 12665 /// 12666 /// Group: generating_from_code 12667 struct hotkey { dchar ch; } 12668 /// 12669 /// Group: generating_from_code 12670 struct tip { string tip; } 12671 12672 12673 /++ 12674 Observes and allows inspection of an object via automatic gui 12675 +/ 12676 /// Group: generating_from_code 12677 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 12678 return new ObjectInspectionWindowImpl!(T)(t); 12679 } 12680 12681 class ObjectInspectionWindow : Window { 12682 this(int a, int b, string c) { 12683 super(a, b, c); 12684 } 12685 12686 abstract void readUpdatesFromObject(); 12687 } 12688 12689 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 12690 T t; 12691 this(T t) { 12692 this.t = t; 12693 12694 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 12695 12696 foreach(memberName; __traits(derivedMembers, T)) {{ 12697 alias member = I!(__traits(getMember, t, memberName))[0]; 12698 alias type = typeof(member); 12699 static if(is(type == int)) { 12700 auto le = new LabeledLineEdit(memberName ~ ": ", this); 12701 //le.addEventListener("char", (Event ev) { 12702 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 12703 //ev.preventDefault(); 12704 //}); 12705 le.addEventListener(EventType.change, (Event ev) { 12706 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 12707 }); 12708 12709 updateMemberDelegates[memberName] = () { 12710 le.content = toInternal!string(__traits(getMember, t, memberName)); 12711 }; 12712 } 12713 }} 12714 } 12715 12716 void delegate()[string] updateMemberDelegates; 12717 12718 override void readUpdatesFromObject() { 12719 foreach(k, v; updateMemberDelegates) 12720 v(); 12721 } 12722 } 12723 12724 /++ 12725 Creates a dialog based on a data structure. 12726 12727 --- 12728 dialog((YourStructure value) { 12729 // the user filled in the struct and clicked OK, 12730 // you can check the members now 12731 }); 12732 --- 12733 +/ 12734 /// Group: generating_from_code 12735 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 12736 auto dg = new AutomaticDialog!T(onOK, onCancel, title); 12737 dg.show(); 12738 } 12739 12740 private static template I(T...) { alias I = T; } 12741 12742 12743 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 12744 if(name == "id") 12745 return allLowerCase ? name : "ID"; 12746 12747 char[160] buffer; 12748 int bufferIndex = 0; 12749 bool shouldCap = true; 12750 bool shouldSpace; 12751 bool lastWasCap; 12752 foreach(idx, char ch; name) { 12753 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 12754 12755 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 12756 if(lastWasCap) { 12757 // two caps in a row, don't change. Prolly acronym. 12758 } else { 12759 if(idx) 12760 shouldSpace = true; // new word, add space 12761 } 12762 12763 lastWasCap = true; 12764 } else { 12765 lastWasCap = false; 12766 } 12767 12768 if(shouldSpace) { 12769 buffer[bufferIndex++] = space; 12770 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 12771 shouldSpace = false; 12772 } 12773 if(shouldCap) { 12774 if(ch >= 'a' && ch <= 'z') 12775 ch -= 32; 12776 shouldCap = false; 12777 } 12778 if(allLowerCase && ch >= 'A' && ch <= 'Z') 12779 ch += 32; 12780 buffer[bufferIndex++] = ch; 12781 } 12782 return buffer[0 .. bufferIndex].idup; 12783 } 12784 12785 /++ 12786 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. 12787 +/ 12788 class AutomaticDialog(T) : Dialog { 12789 T t; 12790 12791 void delegate(T) onOK; 12792 void delegate() onCancel; 12793 12794 override int paddingTop() { return defaultLineHeight; } 12795 override int paddingBottom() { return defaultLineHeight; } 12796 override int paddingRight() { return defaultLineHeight; } 12797 override int paddingLeft() { return defaultLineHeight; } 12798 12799 this(void delegate(T) onOK, void delegate() onCancel, string title) { 12800 assert(onOK !is null); 12801 static if(is(T == class)) 12802 t = new T(); 12803 this.onOK = onOK; 12804 this.onCancel = onCancel; 12805 super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + 4 + 2) + Window.lineHeight + 56, title); 12806 12807 static if(is(T == class)) 12808 this.addDataControllerWidget(t); 12809 else 12810 this.addDataControllerWidget(&t); 12811 12812 auto hl = new HorizontalLayout(this); 12813 auto stretch = new HorizontalSpacer(hl); // to right align 12814 auto ok = new CommandButton("OK", hl); 12815 auto cancel = new CommandButton("Cancel", hl); 12816 ok.addEventListener(EventType.triggered, &OK); 12817 cancel.addEventListener(EventType.triggered, &Cancel); 12818 12819 this.addEventListener((KeyDownEvent ev) { 12820 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 12821 ok.focus(); 12822 OK(); 12823 ev.preventDefault(); 12824 } 12825 if(ev.key == Key.Escape) { 12826 Cancel(); 12827 ev.preventDefault(); 12828 } 12829 }); 12830 12831 //this.children[0].focus(); 12832 } 12833 12834 override void OK() { 12835 onOK(t); 12836 close(); 12837 } 12838 12839 override void Cancel() { 12840 if(onCancel) 12841 onCancel(); 12842 close(); 12843 } 12844 } 12845 12846 private template baseClassCount(Class) { 12847 private int helper() { 12848 int count = 0; 12849 static if(is(Class bases == super)) { 12850 foreach(base; bases) 12851 static if(is(base == class)) 12852 count += 1 + baseClassCount!base; 12853 } 12854 return count; 12855 } 12856 12857 enum int baseClassCount = helper(); 12858 } 12859 12860 private long stringToLong(string s) { 12861 long ret; 12862 if(s.length == 0) 12863 return ret; 12864 bool negative = s[0] == '-'; 12865 if(negative) 12866 s = s[1 .. $]; 12867 foreach(ch; s) { 12868 if(ch >= '0' && ch <= '9') { 12869 ret *= 10; 12870 ret += ch - '0'; 12871 } 12872 } 12873 if(negative) 12874 ret = -ret; 12875 return ret; 12876 } 12877 12878 12879 interface ReflectableProperties { 12880 /++ 12881 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 12882 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 12883 json in the current implementation. 12884 12885 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 12886 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 12887 as of the June 2, 2021 release. 12888 12889 History: 12890 Added June 2, 2021. 12891 12892 See_Also: [getPropertyAsString], [setPropertyFromString] 12893 +/ 12894 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 12895 /++ 12896 Requests a property to be delivered to you as a string, through your `sink` delegate. 12897 12898 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 12899 be interpreted as json, otherwise, it is just a plain string. 12900 12901 The sink should always be called exactly once for each call (it is basically a return value, but it might 12902 use a local buffer it maintains instead of allocating a return value). 12903 12904 History: 12905 Added June 2, 2021. 12906 12907 See_Also: [getPropertiesList], [setPropertyFromString] 12908 +/ 12909 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 12910 /++ 12911 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. 12912 12913 History: 12914 Added June 2, 2021. 12915 12916 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 12917 +/ 12918 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 12919 12920 /// [setPropertyFromString] possible return values 12921 enum SetPropertyResult { 12922 success = 0, /// the property has been successfully set to the request value 12923 notPermitted = -1, /// the property exists but it cannot be changed at this time 12924 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 12925 noSuchProperty = -3, /// there is no property by that name 12926 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 12927 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) 12928 } 12929 12930 /++ 12931 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 12932 12933 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 12934 12935 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 12936 rarely need to use these building blocks directly. 12937 +/ 12938 mixin template RegisterSetters() { 12939 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 12940 switch(name) { 12941 foreach(memberName; __traits(derivedMembers, typeof(this))) { 12942 case memberName: 12943 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 12944 if(value != "true" && value != "false") 12945 return SetPropertyResult.wrongFormat; 12946 __traits(getMember, this, memberName) = value == "true" ? true : false; 12947 return SetPropertyResult.success; 12948 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 12949 import core.stdc.stdlib; 12950 char[128] zero = 0; 12951 if(buffer.length + 1 >= zero.length) 12952 return SetPropertyResult.wrongFormat; 12953 zero[0 .. buffer.length] = buffer[]; 12954 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 12955 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 12956 import core.stdc.stdlib; 12957 char[128] zero = 0; 12958 if(buffer.length + 1 >= zero.length) 12959 return SetPropertyResult.wrongFormat; 12960 zero[0 .. buffer.length] = buffer[]; 12961 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 12962 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 12963 __traits(getMember, this, memberName) = value.idup; 12964 } else { 12965 return SetPropertyResult.notImplemented; 12966 } 12967 12968 } 12969 default: 12970 return super.setPropertyFromString(name, value, valueIsJson); 12971 } 12972 } 12973 } 12974 12975 /++ 12976 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 12977 12978 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 12979 12980 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 12981 rarely need to use these building blocks directly. 12982 +/ 12983 mixin template RegisterGetters() { 12984 override void getPropertiesList(scope void delegate(string name) sink) const { 12985 super.getPropertiesList(sink); 12986 12987 foreach(memberName; __traits(derivedMembers, typeof(this))) { 12988 sink(memberName); 12989 } 12990 } 12991 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 12992 switch(name) { 12993 foreach(memberName; __traits(derivedMembers, typeof(this))) { 12994 case memberName: 12995 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 12996 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 12997 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 12998 import core.stdc.stdio; 12999 char[32] buffer; 13000 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 13001 sink(name, buffer[0 .. len], true); 13002 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 13003 import core.stdc.stdio; 13004 char[32] buffer; 13005 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 13006 sink(name, buffer[0 .. len], true); 13007 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 13008 sink(name, __traits(getMember, this, memberName), false); 13009 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 13010 } else { 13011 sink(name, null, true); 13012 } 13013 13014 return; 13015 } 13016 default: 13017 return super.getPropertyAsString(name, sink); 13018 } 13019 } 13020 } 13021 } 13022 13023 13024 /+ 13025 13026 I could fix up the hierarchy kinda like this 13027 13028 class Widget { 13029 Widget[] children() { return null; } 13030 } 13031 interface WidgetContainer { 13032 Widget asWidget(); 13033 void addChild(Widget w); 13034 13035 // alias asWidget this; // but meh 13036 } 13037 13038 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 13039 13040 class Layout : Widget, WidgetContainer {} 13041 13042 class Window : WidgetContainer {} 13043 13044 13045 All constructors that previously took Widgets should now take WidgetContainers instead 13046 13047 13048 13049 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". 13050 +/ 13051 13052 /+ 13053 LAYOUTS 2.0 13054 13055 can just be assigned as a function. assigning a new one will cause it to be immediately called. 13056 13057 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 13058 13059 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 13060 13061 and even Paint can just use computedStyle... 13062 13063 background color 13064 font 13065 border color and style 13066 13067 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 13068 please note that many widgets and in some modes will completely ignore properties as they will. 13069 they are just hints you set, not promises. 13070 13071 13072 13073 13074 13075 So generally the existing virtual functions are just the default for the class. But individual objects 13076 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 13077 +/ 13078 13079 /++ 13080 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. 13081 13082 History: 13083 Added May 24, 2021. 13084 +/ 13085 struct WidgetBackground { 13086 /++ 13087 A background with the given solid color. 13088 +/ 13089 this(Color color) { 13090 this.color = color; 13091 } 13092 13093 this(WidgetBackground bg) { 13094 this = bg; 13095 } 13096 13097 /++ 13098 Creates a widget from the string. 13099 13100 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 13101 +/ 13102 static WidgetBackground fromString(string s) { 13103 return WidgetBackground(Color.fromString(s)); 13104 } 13105 13106 private Color color; 13107 } 13108 13109 /++ 13110 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!) 13111 13112 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. 13113 13114 You should not inherit from this directly, but instead use [VisualTheme]. 13115 13116 History: 13117 Added May 8, 2021 13118 +/ 13119 abstract class BaseVisualTheme { 13120 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 13121 abstract void doPaint(Widget widget, WidgetPainter painter); 13122 13123 /+ 13124 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 13125 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 13126 +/ 13127 13128 /++ 13129 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 13130 where the interpretation of the string varies for each property and may include things like measurement units. 13131 +/ 13132 abstract string getPropertyString(Widget widget, string propertyName); 13133 13134 /++ 13135 Default background color of the window. Widgets also use this to simulate transparency. 13136 13137 Probably some shade of grey. 13138 +/ 13139 abstract Color windowBackgroundColor(); 13140 abstract Color widgetBackgroundColor(); 13141 abstract Color foregroundColor(); 13142 abstract Color lightAccentColor(); 13143 abstract Color darkAccentColor(); 13144 13145 /++ 13146 Color used to indicate active selections in lists and text boxes, etc. 13147 +/ 13148 abstract Color selectionColor(); 13149 13150 abstract OperatingSystemFont defaultFont(); 13151 13152 private OperatingSystemFont defaultFontCache_; 13153 private bool defaultFontCachePopulated; 13154 private OperatingSystemFont defaultFontCached() { 13155 if(!defaultFontCachePopulated) { 13156 // FIXME: set this to false if X disconnect or if visual theme changes 13157 defaultFontCache_ = defaultFont(); 13158 defaultFontCachePopulated = true; 13159 } 13160 return defaultFontCache_; 13161 } 13162 } 13163 13164 /+ 13165 A widget should have: 13166 classList 13167 dataset 13168 attributes 13169 computedStyles 13170 state (persistent) 13171 dynamic state (focused, hover, etc) 13172 +/ 13173 13174 // visualTheme.computedStyle(this).paddingLeft 13175 13176 13177 /++ 13178 This is your entry point to create your own visual theme for custom widgets. 13179 +/ 13180 abstract class VisualTheme(CRTP) : BaseVisualTheme { 13181 override string getPropertyString(Widget widget, string propertyName) { 13182 return null; 13183 } 13184 13185 /+ 13186 mixin StyleOverride!Widget 13187 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 13188 w.useStyleProperties(dg); 13189 } 13190 +/ 13191 13192 final override void doPaint(Widget widget, WidgetPainter painter) { 13193 auto derived = cast(CRTP) cast(void*) this; 13194 13195 scope void delegate(Widget, WidgetPainter) bestMatch; 13196 int bestMatchScore; 13197 13198 static if(__traits(hasMember, CRTP, "paint")) 13199 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 13200 static if(is(typeof(overload) Params == __parameters)) { 13201 static assert(Params.length == 2); 13202 static assert(is(Params[0] : Widget)); 13203 static assert(is(Params[1] == WidgetPainter)); 13204 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); 13205 13206 alias type = Params[0]; 13207 if(cast(type) widget) { 13208 auto score = baseClassCount!type; 13209 13210 if(score > bestMatchScore) { 13211 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 13212 bestMatchScore = score; 13213 } 13214 } 13215 } else static assert(0, "paint should be a method."); 13216 } 13217 13218 if(bestMatch) 13219 bestMatch(widget, painter); 13220 else 13221 widget.paint(painter); 13222 } 13223 13224 // 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 13225 override Color windowBackgroundColor() { return Color(212, 212, 212); } 13226 override Color widgetBackgroundColor() { return Color.white; } 13227 override Color foregroundColor() { return Color.black; } 13228 override Color darkAccentColor() { return Color(172, 172, 172); } 13229 override Color lightAccentColor() { return Color(223, 223, 223); } 13230 override Color selectionColor() { return Color(0, 0, 128); } 13231 override OperatingSystemFont defaultFont() { return null; } // will just use the default out of simpledisplay's xfontstr 13232 13233 private static struct Cached { 13234 // i prolly want to do this 13235 } 13236 } 13237 13238 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 13239 /+ 13240 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 13241 Color windowBackgroundColor() { return Color(242, 242, 242); } 13242 Color darkAccentColor() { return windowBackgroundColor; } 13243 Color lightAccentColor() { return windowBackgroundColor; } 13244 +/ 13245 } 13246 13247 /++ 13248 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 13249 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 13250 13251 History: 13252 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 13253 +/ 13254 class StateChanged(alias field) : Event { 13255 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 13256 override bool cancelable() const { return false; } 13257 this(Widget target, typeof(field) newValue) { 13258 this.newValue = newValue; 13259 super(EventString, target); 13260 } 13261 13262 typeof(field) newValue; 13263 } 13264 13265 /++ 13266 Convenience function to add a `triggered` event listener. 13267 13268 Its implementation is simply `w.addEventListener("triggered", dg);` 13269 13270 History: 13271 Added November 27, 2021 (dub v10.4) 13272 +/ 13273 void addWhenTriggered(Widget w, void delegate() dg) { 13274 w.addEventListener("triggered", dg); 13275 } 13276 13277 /++ 13278 Observable varables can be added to widgets and when they are changed, it fires 13279 off a [StateChanged] event so you can react to it. 13280 13281 It is implemented as a getter and setter property, along with another helper you 13282 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 13283 event through the usual means. Just give the name of the variable. See [StateChanged] for an 13284 example. 13285 13286 History: 13287 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 13288 +/ 13289 mixin template Observable(T, string name) { 13290 private T backing; 13291 13292 mixin(q{ 13293 void } ~ name ~ q{_changed (void delegate(T) dg) { 13294 this.addEventListener((StateChanged!this_thing ev) { 13295 dg(ev.newValue); 13296 }); 13297 } 13298 13299 @property T } ~ name ~ q{ () { 13300 return backing; 13301 } 13302 13303 @property void } ~ name ~ q{ (T t) { 13304 backing = t; 13305 auto event = new StateChanged!this_thing(this, t); 13306 event.dispatch(); 13307 } 13308 }); 13309 13310 mixin("private alias this_thing = " ~ name ~ ";"); 13311 } 13312 13313 13314 // still do layout delegation 13315 // and... split off Window from Widget. 13316 13317 version(minigui_demos) 13318 // https://github.com/adamdruppe/arsd/issues/310 13319 unittest { 13320 import arsd.minigui; 13321 13322 class ColorWidget : Widget { 13323 this(Color color, Widget parent) { 13324 this.color = color; 13325 super(parent); 13326 } 13327 Color color; 13328 class Style : Widget.Style { 13329 override WidgetBackground background() { return WidgetBackground(color); } 13330 } 13331 mixin OverrideStyle!Style; 13332 } 13333 13334 void main() { 13335 auto window = new Window; 13336 13337 auto header = new class HorizontalLayout { 13338 this() { super(window); } 13339 override int maxHeight() { return 50; } 13340 }; 13341 13342 auto bar = new HorizontalLayout(window); 13343 13344 auto left = new class VerticalLayout { 13345 this() { super(bar); } 13346 override int maxWidth() { return 100; } 13347 }; 13348 13349 auto container = new Widget(bar); 13350 13351 13352 auto headerColorBox = new ColorWidget(Color(0, 255, 255), header); 13353 auto leftColorBox = new ColorWidget(Color.green, left); 13354 auto rightColorBox = new ColorWidget(Color.purple, container); 13355 13356 window.loop(); 13357 } 13358 } 13359 13360 13361 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 13362 // FIXME: make multiple accelerators disambiguate based ona rgs 13363 // FIXME: MainWindow ctor should have same arg order as Window 13364 // FIXME: mainwindow ctor w/ client area size instead of total size. 13365 // 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. 13366 // FIXME: tri-state checkbox 13367 // FIXME: subordinate controls grouping...