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