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