1 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx 2 3 // me@arsd:~/.kde/share/config$ vim kdeglobals 4 5 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too. 6 7 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever. 8 9 // responsive minigui, menu search, and file open with a preview hook on the side. 10 11 // FIXME: add menu checkbox and menu icon eventually 12 13 /* 14 15 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 16 17 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 18 */ 19 20 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places 21 22 // FIXME: text label must be copyable to the clipboard, at least as a full chunk. 23 24 // FIXME: opt-in file picker widget with image support 25 26 // FIXME: number widget 27 28 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm 29 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles 30 31 // osx style menu search. 32 33 // would be cool for a scroll bar to have marking capabilities 34 // kinda like vim's marks just on clicks etc and visual representation 35 // generically. may be cool to add an up arrow to the bottom too 36 // 37 // leave a shadow of where you last were for going back easily 38 39 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 40 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 41 // the window. 42 43 // so what about context menus? 44 45 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 46 47 // FIXME: make the scroll thing go to bottom when the content changes. 48 49 // 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 50 51 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 52 53 54 // FIXME: add a command search thingy built in and implement tip. 55 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 56 57 // On Windows: 58 // FIXME: various labels look broken in high contrast mode 59 // FIXME: changing themes while the program is upen doesn't trigger a redraw 60 61 // add note about manifest to documentation. also icons. 62 63 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 64 // FIXME: clear the corner of scrollbars if they pop up 65 66 // minigui needs to have a stdout redirection for gui mode on windows writeln 67 68 // I kinda wanna do state reacting. sort of. idk tho 69 70 // need a viewer widget that works like a web page - arrows scroll down consistently 71 72 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 73 74 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 75 // and help info about menu items. 76 // and search in menus? 77 78 // FIXME: a scroll area event signaling when a thing comes into view might be good 79 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 80 81 // FIXME: unify Windows style line endings 82 83 /* 84 TODO: 85 86 pie menu 87 88 class Form with submit behavior -- see AutomaticDialog 89 90 disabled widgets and menu items 91 92 event cleanup 93 tooltips. 94 api improvements 95 96 margins are kinda broken, they don't collapse like they should. at least. 97 98 a table form btw would be a horizontal layout of vertical layouts holding each column 99 that would give the same width things 100 */ 101 102 /* 103 104 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 105 */ 106 107 /++ 108 minigui is a smallish GUI widget library, aiming to be on par with at least 109 HTML4 forms and a few other expected gui components. It uses native controls 110 on Windows and does its own thing on Linux (Mac is not currently supported but 111 may be later, and should use native controls) to keep size down. The Linux 112 appearance is similar to Windows 95 and avoids using images to maintain network 113 efficiency on remote X connections, though you can customize that. 114 115 116 minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color], 117 on which it is built. simpledisplay provides the low-level interfaces and minigui 118 builds the concept of widgets inside the windows on top of it. 119 120 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 121 It isn't hugely concerned with appearance - on Windows, it just uses the native 122 controls and native theme, and on Linux, it keeps it simple and I may change that 123 at any time, though after May 2021, you can customize some things with css-inspired 124 [Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`, 125 you can use the custom implementation there too, but... you shouldn't.) 126 127 The event model is similar to what you use in the browser with Javascript and the 128 layout engine tries to automatically fit things in, similar to a css flexbox. 129 130 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 131 `-L/SUBSYSTEM:WINDOWS:5.0`, for example, because otherwise you'll get a 132 console and other visual bugs. 133 134 HTML_To_Classes: 135 $(SMALL_TABLE 136 HTML Code | Minigui Class 137 138 `<input type="text">` | [LineEdit] 139 `<textarea>` | [TextEdit] 140 `<select>` | [DropDownSelection] 141 `<input type="checkbox">` | [Checkbox] 142 `<input type="radio">` | [Radiobox] 143 `<button>` | [Button] 144 ) 145 146 147 Stretchiness: 148 The default is 4. You can use larger numbers for things that should 149 consume a lot of space, and lower numbers for ones that are better at 150 smaller sizes. 151 152 Overlapped_input: 153 COMING EVENTUALLY: 154 minigui will include a little bit of I/O functionality that just works 155 with the event loop. If you want to get fancy, I suggest spinning up 156 another thread and posting events back and forth. 157 158 $(H2 Add ons) 159 See the `minigui_addons` directory in the arsd repo for some add on widgets 160 you can import separately too. 161 162 $(H3 XML definitions) 163 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 164 165 $(H3 Scriptability) 166 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 167 in this documentation, it means you can call it from the script language. 168 169 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 170 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 171 172 --- 173 import arsd.minigui_xml; 174 import arsd.script; 175 176 var globals = var.emptyObject; 177 globals.makeWidgetFromString = &makeWidgetFromString; 178 179 // this now works 180 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 181 --- 182 183 More to come. 184 185 History: 186 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 187 188 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 189 tag this as version 2.0. 190 191 Among the changes: 192 $(LIST 193 * 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. 194 195 See [Event] for details. 196 197 * 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. 198 199 See [DoubleClickEvent] for details. 200 201 * 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. 202 203 See [Widget.Style] for details. 204 205 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 206 207 * 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. 208 209 * 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. 210 211 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 212 213 * 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. 214 215 * Various non-breaking additions. 216 ) 217 +/ 218 module arsd.minigui; 219 220 import arsd.core; 221 222 /++ 223 This hello world sample will have an oversized button, but that's ok, you see your first window! 224 +/ 225 version(Demo) 226 unittest { 227 import arsd.minigui; 228 229 void main() { 230 auto window = new MainWindow(); 231 232 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 233 auto button = new Button("Close", window); 234 button.addWhenTriggered({ 235 window.close(); 236 }); 237 238 window.loop(); 239 } 240 241 main(); // exclude from docs 242 } 243 244 /++ 245 This example shows one way you can partition your window into a header 246 and sidebar. Here, the header and sidebar have a fixed width, while the 247 rest of the content sizes with the window. 248 249 It might be a new way of thinking about window layout to do things this 250 way - perhaps [GridLayout] more matches your style of thought - but the 251 concept here is to partition the window into sub-boxes with a particular 252 size, then partition those boxes into further boxes. 253 254 $(IMG //arsdnet.net/minigui-screenshots/windows/layout.png, The example window has a header across the top, then below it a sidebar to the left and a content area to the right.) 255 256 So to make the header, start with a child layout that has a max height. 257 It will use that space from the top, then the remaining children will 258 split the remaining area, meaning you can think of is as just being another 259 box you can split again. Keep splitting until you have the look you desire. 260 +/ 261 // https://github.com/adamdruppe/arsd/issues/310 262 version(minigui_screenshots) 263 @Screenshot("layout") 264 unittest { 265 import arsd.minigui; 266 267 // This helper class is just to help make the layout boxes visible. 268 // think of it like a <div style="background-color: whatever;"></div> in HTML. 269 class ColorWidget : Widget { 270 this(Color color, Widget parent) { 271 this.color = color; 272 super(parent); 273 } 274 Color color; 275 class Style : Widget.Style { 276 override WidgetBackground background() { return WidgetBackground(color); } 277 } 278 mixin OverrideStyle!Style; 279 } 280 281 void main() { 282 auto window = new Window; 283 284 // the key is to give it a max height. This is one way to do it: 285 auto header = new class HorizontalLayout { 286 this() { super(window); } 287 override int maxHeight() { return 50; } 288 }; 289 // this next line is a shortcut way of doing it too, but it only works 290 // for HorizontalLayout and VerticalLayout, and is less explicit, so it 291 // is good to know how to make a new class like above anyway. 292 // auto header = new HorizontalLayout(50, window); 293 294 auto bar = new HorizontalLayout(window); 295 296 // or since this is so common, VerticalLayout and HorizontalLayout both 297 // can just take an argument in their constructor for max width/height respectively 298 299 // (could have tone this above too, but I wanted to demo both techniques) 300 auto left = new VerticalLayout(100, bar); 301 302 // and this is the main section's container. A plain Widget instance is good enough here. 303 auto container = new Widget(bar); 304 305 // and these just add color to the containers we made above for the screenshot. 306 // in a real application, you can just add your actual controls instead of these. 307 auto headerColorBox = new ColorWidget(Color.teal, header); 308 auto leftColorBox = new ColorWidget(Color.green, left); 309 auto rightColorBox = new ColorWidget(Color.purple, container); 310 311 window.loop(); 312 } 313 314 main(); // exclude from docs 315 } 316 317 318 public import arsd.simpledisplay; 319 /++ 320 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 321 322 History: 323 Was private until May 15, 2021. 324 +/ 325 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 326 327 version(Windows) { 328 import core.sys.windows.winnls; 329 import core.sys.windows.windef; 330 import core.sys.windows.basetyps; 331 import core.sys.windows.winbase; 332 import core.sys.windows.winuser; 333 import core.sys.windows.wingdi; 334 static import gdi = core.sys.windows.wingdi; 335 } 336 337 version(Windows) { 338 version(minigui_manifest) {} else version=minigui_no_manifest; 339 340 version(minigui_no_manifest) {} else 341 static if(__VERSION__ >= 2_083) 342 version(CRuntime_Microsoft) { // FIXME: mingw? 343 // assume we want commctrl6 whenever possible since there's really no reason not to 344 // and this avoids some of the manifest hassle 345 pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""); 346 } 347 } 348 349 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 350 private bool lastDefaultPrevented; 351 352 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 353 alias scriptable = arsd_jsvar_compatible; 354 355 version(Windows) { 356 // use native widgets when available unless specifically asked otherwise 357 version(custom_widgets) { 358 enum bool UsingCustomWidgets = true; 359 enum bool UsingWin32Widgets = false; 360 } else { 361 version = win32_widgets; 362 enum bool UsingCustomWidgets = false; 363 enum bool UsingWin32Widgets = true; 364 } 365 // and native theming when needed 366 //version = win32_theming; 367 } else { 368 enum bool UsingCustomWidgets = true; 369 enum bool UsingWin32Widgets = false; 370 version=custom_widgets; 371 } 372 373 374 375 /* 376 377 The main goals of minigui.d are to: 378 1) Provide basic widgets that just work in a lightweight lib. 379 I basically want things comparable to a plain HTML form, 380 plus the easy and obvious things you expect from Windows 381 apps like a menu. 382 2) Use native things when possible for best functionality with 383 least library weight. 384 3) Give building blocks to provide easy extension for your 385 custom widgets, or hooking into additional native widgets 386 I didn't wrap. 387 4) Provide interfaces for easy interaction between third 388 party minigui extensions. (event model, perhaps 389 signals/slots, drop-in ease of use bits.) 390 5) Zero non-system dependencies, including Phobos as much as 391 I reasonably can. It must only import arsd.color and 392 my simpledisplay.d. If you need more, it will have to be 393 an extension module. 394 6) An easy layout system that generally works. 395 396 A stretch goal is to make it easy to make gui forms with code, 397 some kind of resource file (xml?) and even a wysiwyg designer. 398 399 Another stretch goal is to make it easy to hook data into the gui, 400 including from reflection. So like auto-generate a form from a 401 function signature or struct definition, or show a list from an 402 array that automatically updates as the array is changed. Then, 403 your program focuses on the data more than the gui interaction. 404 405 406 407 STILL NEEDED: 408 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 409 * slider 410 * listbox 411 * spinner 412 * label? 413 * rich text 414 */ 415 416 417 /+ 418 enum LayoutMethods { 419 verticalFlex, 420 horizontalFlex, 421 inlineBlock, // left to right, no stretch, goes to next line as needed 422 static, // just set to x, y 423 verticalNoStretch, // browser style default 424 425 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 426 427 grid, // magic 428 } 429 +/ 430 431 /++ 432 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. 433 434 435 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. 436 437 --- 438 class MinimalWidget : Widget { 439 this(Widget parent) { 440 super(parent); 441 } 442 } 443 --- 444 445 $(SIDEBAR 446 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. 447 ) 448 449 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. 450 451 Among the things you'll most likely want to change in your custom widget: 452 453 $(LIST 454 * 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.) 455 456 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. 457 458 Do this $(I after) calling the `super` constructor. 459 460 * 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. 461 462 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 463 464 * 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. 465 466 * 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. 467 ) 468 469 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. 470 471 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 472 473 Your own custom-drawn and native system controls can exist side-by-side. 474 475 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. 476 +/ 477 class Widget : ReflectableProperties { 478 479 private bool willDraw() { 480 return true; 481 } 482 483 /+ 484 /++ 485 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 486 487 History: 488 Added September 15, 2021 489 implemented.... ??? 490 +/ 491 void prepareReflection(this This)() { 492 493 } 494 +/ 495 496 private bool _enabled = true; 497 498 /++ 499 Determines whether the control is marked enabled. Disabled controls are generally displayed as greyed out and clicking on them does nothing. It is also possible for a control to be disabled because its parent is disabled, in which case this will still return `true`, but setting `enabled = true` may have no effect. Check [disabledBy] to see which parent caused it to be disabled. 500 501 I also recommend you set a [disabledReason] if you chose to set `enabled = false` to tell the user why the control does not work and what they can do to enable it. 502 503 History: 504 Added November 23, 2021 (dub v10.4) 505 506 Warning: the specific behavior of disabling with parents may change in the future. 507 Bugs: 508 Currently only implemented for widgets backed by native Windows controls. 509 510 See_Also: [disabledReason], [disabledBy] 511 +/ 512 @property bool enabled() { 513 return disabledBy() is null; 514 } 515 516 /// ditto 517 @property void enabled(bool yes) { 518 _enabled = yes; 519 version(win32_widgets) { 520 if(hwnd) 521 EnableWindow(hwnd, yes); 522 } 523 setDynamicState(DynamicState.disabled, yes); 524 } 525 526 private string disabledReason_; 527 528 /++ 529 If the widget is not [enabled] this string may be presented to the user when they try to use it. The exact manner and time it gets displayed is up to the implementation of the control. 530 531 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 532 533 History: 534 Added November 23, 2021 (dub v10.4) 535 See_Also: [enabled], [disabledBy] 536 +/ 537 @property string disabledReason() { 538 auto w = disabledBy(); 539 return (w is null) ? null : w.disabledReason_; 540 } 541 542 /// ditto 543 @property void disabledReason(string reason) { 544 disabledReason_ = reason; 545 } 546 547 /++ 548 Returns the widget that disabled this. It might be this or one of its parents all the way up the chain, or `null` if the widget is not disabled by anything. You can check [disabledReason] on the return value (after the null check!) to get a hint to display to the user. 549 550 History: 551 Added November 25, 2021 (dub v10.4) 552 See_Also: [enabled], [disabledReason] 553 +/ 554 Widget disabledBy() { 555 Widget p = this; 556 while(p) { 557 if(!p._enabled) 558 return p; 559 p = p.parent; 560 } 561 return null; 562 } 563 564 /// Implementations of [ReflectableProperties] interface. See the interface for details. 565 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 566 if(valueIsJson) 567 return SetPropertyResult.wrongFormat; 568 switch(name) { 569 case "name": 570 this.name = value.idup; 571 return SetPropertyResult.success; 572 case "statusTip": 573 this.statusTip = value.idup; 574 return SetPropertyResult.success; 575 default: 576 return SetPropertyResult.noSuchProperty; 577 } 578 } 579 /// ditto 580 void getPropertiesList(scope void delegate(string name) sink) const { 581 sink("name"); 582 sink("statusTip"); 583 } 584 /// ditto 585 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 586 switch(name) { 587 case "name": 588 sink(name, this.name, false); 589 return; 590 case "statusTip": 591 sink(name, this.statusTip, false); 592 return; 593 default: 594 sink(name, null, true); 595 } 596 } 597 598 /++ 599 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 600 601 History: 602 Added November 25, 2021 (dub v10.5) 603 `Point` overload added January 12, 2022 (dub v10.6) 604 +/ 605 int scaleWithDpi(int value, int assumedDpi = 96) { 606 // avoid potential overflow with common special values 607 if(value == int.max) 608 return int.max; 609 if(value == int.min) 610 return int.min; 611 if(value == 0) 612 return 0; 613 614 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 615 //divide = 138; 616 // for lower values it is something i don't really want changed anyway since it is an old monitor and you don't want to scale down. 617 // this also covers the case when actualDpi returns 0. 618 if(divide < 96) 619 divide = 96; 620 return value * divide / assumedDpi; 621 } 622 623 /// ditto 624 Point scaleWithDpi(Point value, int assumedDpi = 96) { 625 return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi)); 626 } 627 628 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 629 // I'll think up something better eventually 630 protected final int defaultLineHeight() { 631 auto cs = getComputedStyle(); 632 if(cs.font && !cs.font.isNull) 633 return cs.font.height() * 5 / 4; 634 else 635 return scaleWithDpi(Window.lineHeight * 5/4); 636 } 637 638 protected final int defaultTextWidth(const(char)[] text) { 639 auto cs = getComputedStyle(); 640 if(cs.font && !cs.font.isNull) 641 return cs.font.stringWidth(text); 642 else 643 return scaleWithDpi(Window.lineHeight * cast(int) text.length / 2); 644 } 645 646 /++ 647 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. 648 649 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. 650 651 History: 652 Added May 22, 2021 653 +/ 654 protected bool encapsulatedChildren() { 655 return false; 656 } 657 658 private void privateDpiChanged() { 659 dpiChanged(); 660 foreach(child; children) 661 child.privateDpiChanged(); 662 } 663 664 /++ 665 Virtual hook to update any caches or fonts you need on the event of a dpi scaling change. 666 667 History: 668 Added January 12, 2022 (dub v10.6) 669 +/ 670 protected void dpiChanged() { 671 672 } 673 674 // Default layout properties { 675 676 int minWidth() { return 0; } 677 int minHeight() { 678 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 679 int sum = this.paddingTop + this.paddingBottom; 680 foreach(child; children) { 681 if(child.hidden) 682 continue; 683 sum += child.minHeight(); 684 sum += child.marginTop(); 685 sum += child.marginBottom(); 686 } 687 688 return sum; 689 } 690 int maxWidth() { return int.max; } 691 int maxHeight() { return int.max; } 692 int widthStretchiness() { return 4; } 693 int heightStretchiness() { return 4; } 694 695 /++ 696 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 697 698 History: 699 Added June 15, 2021 (dub v10.1) 700 +/ 701 int widthShrinkiness() { return 0; } 702 /// ditto 703 int heightShrinkiness() { return 0; } 704 705 /++ 706 The initial size of the widget for layout calculations. Default is 0. 707 708 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 709 710 History: 711 Added June 15, 2021 (dub v10.1) 712 +/ 713 int flexBasisWidth() { return 0; } 714 /// ditto 715 int flexBasisHeight() { return 0; } 716 717 /++ 718 Not stable. 719 720 Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored. 721 722 So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6. 723 724 History: 725 Added January 5, 2023 726 +/ 727 Rectangle defaultMargin; 728 /// ditto 729 Rectangle defaultPadding; 730 731 int marginLeft() { return scaleWithDpi(defaultMargin.left); } 732 int marginRight() { return scaleWithDpi(defaultMargin.right); } 733 int marginTop() { return scaleWithDpi(defaultMargin.top); } 734 int marginBottom() { return scaleWithDpi(defaultMargin.bottom); } 735 int paddingLeft() { return scaleWithDpi(defaultPadding.left); } 736 int paddingRight() { return scaleWithDpi(defaultPadding.right); } 737 int paddingTop() { return scaleWithDpi(defaultPadding.top); } 738 int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); } 739 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 740 741 private bool recomputeChildLayoutRequired = true; 742 private static class RecomputeEvent {} 743 private __gshared rce = new RecomputeEvent(); 744 protected final void queueRecomputeChildLayout() { 745 recomputeChildLayoutRequired = true; 746 747 if(this.parentWindow) { 748 auto sw = this.parentWindow.win; 749 assert(sw !is null); 750 if(!sw.eventQueued!RecomputeEvent) { 751 sw.postEvent(rce); 752 // import std.stdio; writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 753 } 754 } 755 756 } 757 758 protected final void recomputeChildLayoutEntry() { 759 if(recomputeChildLayoutRequired) { 760 recomputeChildLayout(); 761 recomputeChildLayoutRequired = false; 762 redraw(); 763 } else { 764 // I still need to check the tree just in case one of them was queued up 765 // and the event came up here instead of there. 766 foreach(child; children) 767 child.recomputeChildLayoutEntry(); 768 } 769 } 770 771 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 772 void recomputeChildLayout() { 773 .recomputeChildLayout!"height"(this); 774 } 775 776 // } 777 778 779 /++ 780 Returns the style's tag name string this object uses. 781 782 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. 783 784 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 785 786 History: 787 Added May 10, 2021 788 +/ 789 string styleTagName() const { 790 string n = typeid(this).name; 791 foreach_reverse(idx, ch; n) 792 if(ch == '.') { 793 n = n[idx + 1 .. $]; 794 break; 795 } 796 return n; 797 } 798 799 /// API for the [styleClassList] 800 static struct ClassList { 801 private Widget widget; 802 803 /// 804 void add(string s) { 805 widget.styleClassList_ ~= s; 806 } 807 808 /// 809 void remove(string s) { 810 foreach(idx, s1; widget.styleClassList_) 811 if(s1 == s) { 812 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 813 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 814 widget.styleClassList_.assumeSafeAppend(); 815 return; 816 } 817 } 818 819 /// Returns true if it was added, false if it was removed. 820 bool toggle(string s) { 821 if(contains(s)) { 822 remove(s); 823 return false; 824 } else { 825 add(s); 826 return true; 827 } 828 } 829 830 /// 831 bool contains(string s) const { 832 foreach(s1; widget.styleClassList_) 833 if(s1 == s) 834 return true; 835 return false; 836 837 } 838 } 839 840 private string[] styleClassList_; 841 842 /++ 843 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. 844 845 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 846 847 History: 848 Added May 10, 2021 849 +/ 850 inout(ClassList) styleClassList() inout { 851 return cast(inout(ClassList)) ClassList(cast() this); 852 } 853 854 /++ 855 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. 856 857 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. 858 859 The upper 32 bits are available for your own extensions. 860 861 History: 862 Added May 10, 2021 863 +/ 864 enum DynamicState : ulong { 865 focus = (1 << 0), /// the widget currently has the keyboard focus 866 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 867 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!) 868 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!) 869 checked = (1 << 4), /// the widget is toggleable and currently toggled on 870 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 871 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 872 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 873 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. 874 875 USER_BEGIN = (1UL << 32), 876 } 877 878 // I want to add the primary and cancel styles to buttons at least at some point somehow. 879 880 /// ditto 881 @property ulong dynamicState() { return dynamicState_; } 882 /// ditto 883 @property ulong dynamicState(ulong newValue) { 884 if(dynamicState != newValue) { 885 auto old = dynamicState_; 886 dynamicState_ = newValue; 887 888 useStyleProperties((scope Widget.Style s) { 889 if(s.variesWithState(old ^ newValue)) 890 redraw(); 891 }); 892 } 893 return dynamicState_; 894 } 895 896 /// ditto 897 void setDynamicState(ulong flags, bool state) { 898 auto ds = dynamicState_; 899 if(state) 900 ds |= flags; 901 else 902 ds &= ~flags; 903 904 dynamicState = ds; 905 } 906 907 private ulong dynamicState_; 908 909 deprecated("Use dynamic styles instead now") { 910 Color backgroundColor() { return backgroundColor_; } 911 void backgroundColor(Color c){ this.backgroundColor_ = c; } 912 913 MouseCursor cursor() { return GenericCursor.Default; } 914 } private Color backgroundColor_ = Color.transparent; 915 916 917 /++ 918 Style properties are defined as an accessory class so they can be referenced and overridden independently, but they are nested so you can refer to them easily by name (e.g. generic `Widget.Style` vs `Button.Style` and such). 919 920 It is here so there can be a specificity switch. 921 922 See [OverrideStyle] for a helper function to use your own. 923 924 History: 925 Added May 11, 2021 926 +/ 927 static class Style/* : StyleProperties*/ { 928 public Widget widget; // public because the mixin template needs access to it 929 930 /++ 931 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 932 933 History: 934 Added May 11, 2021, but changed on July 2, 2021 to return false by default. You MUST override this if you want declarative hover effects etc to take effect. 935 +/ 936 bool variesWithState(ulong dynamicStateFlags) { 937 version(win32_widgets) { 938 if(widget.hwnd) 939 return false; 940 } 941 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 942 } 943 944 /// 945 Color foregroundColor() { 946 return WidgetPainter.visualTheme.foregroundColor; 947 } 948 949 /// 950 WidgetBackground background() { 951 // the default is a "transparent" background, which means 952 // it goes as far up as it can to get the color 953 if (widget.backgroundColor_ != Color.transparent) 954 return WidgetBackground(widget.backgroundColor_); 955 if (widget.parent) 956 return widget.parent.getComputedStyle.background; 957 return WidgetBackground(widget.backgroundColor_); 958 } 959 960 private static OperatingSystemFont fontCached_; 961 private OperatingSystemFont fontCached() { 962 if(fontCached_ is null) 963 fontCached_ = font(); 964 return fontCached_; 965 } 966 967 /++ 968 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. 969 +/ 970 OperatingSystemFont font() { 971 return null; 972 } 973 974 /++ 975 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. 976 977 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 978 979 History: 980 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 981 +/ 982 MouseCursor cursor() { 983 return GenericCursor.Default; 984 } 985 986 FrameStyle borderStyle() { 987 return FrameStyle.none; 988 } 989 990 /++ 991 +/ 992 Color borderColor() { 993 return Color.transparent; 994 } 995 996 FrameStyle outlineStyle() { 997 if(widget.dynamicState & DynamicState.focus) 998 return FrameStyle.dotted; 999 else 1000 return FrameStyle.none; 1001 } 1002 1003 Color outlineColor() { 1004 return foregroundColor; 1005 } 1006 } 1007 1008 /++ 1009 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 1010 The basic usage is simple: 1011 1012 --- 1013 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 1014 // override style hints as-needed here 1015 } 1016 OverrideStyle!Style; // add the method 1017 --- 1018 1019 $(TIP 1020 While the class is not forced to be `static`, for best results, it should be. A non-static class 1021 can not be inherited by other objects whereas the static one can. A property on the base class, 1022 called [Widget.Style.widget|widget], is available for you to access its properties. 1023 ) 1024 1025 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 1026 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 1027 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 1028 1029 1030 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 1031 You may also just override `variesWithState` when you use this flag. 1032 1033 --- 1034 mixin OverrideStyle!( 1035 DynamicState.focus, YourFocusedStyle, 1036 DynamicState.hover, YourHoverStyle, 1037 YourDefaultStyle 1038 ) 1039 --- 1040 1041 It checks if `dynamicState` matches the state and if so, returns the object given. 1042 1043 If there is no state mask given, the next one matches everything. The first match given is used. 1044 1045 However, since in most cases you'll want check state inside your individual methods, you probably won't 1046 find much use for this whole-class swap out. 1047 1048 History: 1049 Added May 16, 2021 1050 +/ 1051 static protected mixin template OverrideStyle(S...) { 1052 static import amg = arsd.minigui; 1053 override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) { 1054 ulong mask = 0; 1055 foreach(idx, thing; S) { 1056 static if(is(typeof(thing) : ulong)) { 1057 mask = thing; 1058 } else { 1059 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 1060 //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."); 1061 scope amg.Widget.Style s = new thing(); 1062 s.widget = this; 1063 dg(s); 1064 return; 1065 } 1066 } 1067 } 1068 } 1069 } 1070 /++ 1071 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 1072 +/ 1073 void useStyleProperties(scope void delegate(scope Style props) dg) { 1074 scope Style s = new Style(); 1075 s.widget = this; 1076 dg(s); 1077 } 1078 1079 1080 protected void sendResizeEvent() { 1081 this.emit!ResizeEvent(); 1082 } 1083 1084 Menu contextMenu(int x, int y) { return null; } 1085 1086 final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) { 1087 if(parentWindow is null || parentWindow.win is null) return false; 1088 1089 auto menu = this.contextMenu(x, y); 1090 if(menu is null) 1091 return false; 1092 1093 version(win32_widgets) { 1094 // FIXME: if it is -1, -1, do it at the current selection location instead 1095 // tho the corner of the window, whcih it does now, isn't the literal worst. 1096 1097 if(screenX < 0 && screenY < 0) { 1098 auto p = this.globalCoordinates(); 1099 if(screenX == -2) 1100 p.x += x; 1101 if(screenY == -2) 1102 p.y += y; 1103 1104 screenX = p.x; 1105 screenY = p.y; 1106 } 1107 1108 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1109 throw new Exception("TrackContextMenuEx"); 1110 } else version(custom_widgets) { 1111 menu.popup(this, x, y); 1112 } 1113 1114 return true; 1115 } 1116 1117 /++ 1118 Removes this widget from its parent. 1119 1120 History: 1121 `removeWidget` was made `final` on May 11, 2021. 1122 +/ 1123 @scriptable 1124 final void removeWidget() { 1125 auto p = this.parent; 1126 if(p) { 1127 int item; 1128 for(item = 0; item < p._children.length; item++) 1129 if(p._children[item] is this) 1130 break; 1131 auto idx = item; 1132 for(; item < p._children.length - 1; item++) 1133 p._children[item] = p._children[item + 1]; 1134 p._children = p._children[0 .. $-1]; 1135 1136 this.parent.widgetRemoved(idx, this); 1137 //this.parent = null; 1138 1139 p.queueRecomputeChildLayout(); 1140 } 1141 version(win32_widgets) { 1142 removeAllChildren(); 1143 if(hwnd) { 1144 DestroyWindow(hwnd); 1145 hwnd = null; 1146 } 1147 } 1148 } 1149 1150 /++ 1151 Notifies the subclass that a widget was removed. If you keep auxillary data about your children, you can override this to help keep that data in sync. 1152 1153 History: 1154 Added September 19, 2021 1155 +/ 1156 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 1157 1158 /++ 1159 Removes all child widgets from `this`. You should not use the removed widgets again. 1160 1161 Note that on Windows, it also destroys the native handles for the removed children recursively. 1162 1163 History: 1164 Added July 1, 2021 (dub v10.2) 1165 +/ 1166 void removeAllChildren() { 1167 version(win32_widgets) 1168 foreach(child; _children) { 1169 child.removeAllChildren(); 1170 if(child.hwnd) { 1171 DestroyWindow(child.hwnd); 1172 child.hwnd = null; 1173 } 1174 } 1175 auto orig = this._children; 1176 this._children = null; 1177 foreach(idx, w; orig) 1178 this.widgetRemoved(idx, w); 1179 1180 queueRecomputeChildLayout(); 1181 } 1182 1183 /++ 1184 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1185 +/ 1186 @scriptable 1187 Widget getChildByName(string name) { 1188 return getByName(name); 1189 } 1190 /++ 1191 Finds the nearest descendant with the requested type and [name]. May return `this`. 1192 +/ 1193 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1194 if(this.name == name) 1195 if(auto c = cast(WidgetClass) this) 1196 return c; 1197 foreach(child; children) { 1198 auto w = child.getByName(name); 1199 if(auto c = cast(WidgetClass) w) 1200 return c; 1201 } 1202 return null; 1203 } 1204 1205 /++ 1206 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. 1207 Names should be unique in a window. 1208 1209 See_Also: [getByName], [getChildByName] 1210 +/ 1211 @scriptable string name; 1212 1213 private EventHandler[][string] bubblingEventHandlers; 1214 private EventHandler[][string] capturingEventHandlers; 1215 1216 /++ 1217 Default event handlers. These are called on the appropriate 1218 event unless [Event.preventDefault] is called on the event at 1219 some point through the bubbling process. 1220 1221 1222 If you are implementing your own widget and want to add custom 1223 events, you should follow the same pattern here: create a virtual 1224 function named `defaultEventHandler_eventname` with the implementation, 1225 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1226 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1227 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1228 This ensures virtual dispatch based on the correct subclass. 1229 1230 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1231 overridden version. 1232 1233 You only need to do that on parent classes adding NEW event types. If you 1234 just want to change the default behavior of an existing event type in a subclass, 1235 you override the function (and optionally call `super.method_name`) like normal. 1236 1237 +/ 1238 protected EventHandler[string] defaultEventHandlers; 1239 1240 /// ditto 1241 void setupDefaultEventHandlers() { 1242 defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); }; 1243 defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); }; 1244 defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); }; 1245 defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); }; 1246 defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); }; 1247 defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); }; 1248 defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); }; 1249 defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); }; 1250 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); }; 1251 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); }; 1252 defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); }; 1253 defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); }; 1254 defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); }; 1255 defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); }; 1256 defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); }; 1257 defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); }; 1258 defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); }; 1259 defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); }; 1260 } 1261 1262 /// ditto 1263 void defaultEventHandler_click(ClickEvent event) {} 1264 /// ditto 1265 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1266 /// ditto 1267 void defaultEventHandler_keydown(KeyDownEvent event) {} 1268 /// ditto 1269 void defaultEventHandler_keyup(KeyUpEvent event) {} 1270 /// ditto 1271 void defaultEventHandler_mousedown(MouseDownEvent event) { 1272 if(event.button == MouseButton.left) { 1273 if(this.tabStop) 1274 this.focus(); 1275 } 1276 } 1277 /// ditto 1278 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1279 /// ditto 1280 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1281 /// ditto 1282 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1283 /// ditto 1284 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1285 /// ditto 1286 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1287 /// ditto 1288 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1289 /// ditto 1290 void defaultEventHandler_char(CharEvent event) {} 1291 /// ditto 1292 void defaultEventHandler_triggered(Event event) {} 1293 /// ditto 1294 void defaultEventHandler_change(Event event) {} 1295 /// ditto 1296 void defaultEventHandler_focus(Event event) {} 1297 /// ditto 1298 void defaultEventHandler_blur(Event event) {} 1299 /// ditto 1300 void defaultEventHandler_focusin(Event event) {} 1301 /// ditto 1302 void defaultEventHandler_focusout(Event event) {} 1303 1304 /++ 1305 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1306 1307 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1308 1309 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1310 of participating in handler delegation. 1311 1312 $(TIP 1313 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. 1314 ) 1315 +/ 1316 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1317 return addEventListener(event, (Widget, scope Event e) { 1318 if(e.srcElement is this) 1319 handler(); 1320 }, useCapture); 1321 } 1322 1323 /// ditto 1324 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1325 return addEventListener(event, (Widget, Event e) { 1326 if(e.srcElement is this) 1327 handler(e); 1328 }, useCapture); 1329 } 1330 1331 /// ditto 1332 EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) { 1333 static if(is(Handler Fn == delegate)) { 1334 static if(is(Fn Params == __parameters)) { 1335 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1336 if(e.srcElement !is this) 1337 return; 1338 auto ty = cast(Params[0]) e; 1339 if(ty !is null) 1340 handler(ty); 1341 }, useCapture); 1342 } else static assert(0); 1343 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1344 } 1345 1346 /// ditto 1347 @scriptable 1348 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1349 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1350 } 1351 1352 /// ditto 1353 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1354 static if(is(Handler Fn == delegate)) { 1355 static if(is(Fn Params == __parameters)) { 1356 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1357 auto ty = cast(Params[0]) e; 1358 if(ty !is null) 1359 handler(ty); 1360 }, useCapture); 1361 } else static assert(0); 1362 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1363 } 1364 1365 /// ditto 1366 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1367 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1368 } 1369 1370 /// ditto 1371 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1372 if(event.length > 2 && event[0..2] == "on") 1373 event = event[2 .. $]; 1374 1375 if(useCapture) 1376 capturingEventHandlers[event] ~= handler; 1377 else 1378 bubblingEventHandlers[event] ~= handler; 1379 1380 return EventListener(this, event, handler, useCapture); 1381 } 1382 1383 /// ditto 1384 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1385 if(event.length > 2 && event[0..2] == "on") 1386 event = event[2 .. $]; 1387 1388 if(useCapture) { 1389 if(event in capturingEventHandlers) 1390 foreach(ref evt; capturingEventHandlers[event]) 1391 if(evt is handler) evt = null; 1392 } else { 1393 if(event in bubblingEventHandlers) 1394 foreach(ref evt; bubblingEventHandlers[event]) 1395 if(evt is handler) evt = null; 1396 } 1397 } 1398 1399 /// ditto 1400 void removeEventListener(EventListener listener) { 1401 removeEventListener(listener.event, listener.handler, listener.useCapture); 1402 } 1403 1404 static if(UsingSimpledisplayX11) { 1405 void discardXConnectionState() { 1406 foreach(child; children) 1407 child.discardXConnectionState(); 1408 } 1409 1410 void recreateXConnectionState() { 1411 foreach(child; children) 1412 child.recreateXConnectionState(); 1413 redraw(); 1414 } 1415 } 1416 1417 /++ 1418 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1419 1420 History: 1421 `globalCoordinates` was made `final` on May 11, 2021. 1422 +/ 1423 Point globalCoordinates() { 1424 int x = this.x; 1425 int y = this.y; 1426 auto p = this.parent; 1427 while(p) { 1428 x += p.x; 1429 y += p.y; 1430 p = p.parent; 1431 } 1432 1433 static if(UsingSimpledisplayX11) { 1434 auto dpy = XDisplayConnection.get; 1435 arsd.simpledisplay.Window dummyw; 1436 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1437 } else { 1438 POINT pt; 1439 pt.x = x; 1440 pt.y = y; 1441 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1442 x = pt.x; 1443 y = pt.y; 1444 } 1445 1446 return Point(x, y); 1447 } 1448 1449 version(win32_widgets) 1450 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1451 1452 version(win32_widgets) 1453 /// Called when a WM_COMMAND is sent to the associated hwnd. 1454 void handleWmCommand(ushort cmd, ushort id) {} 1455 1456 version(win32_widgets) 1457 /++ 1458 Called when a WM_NOTIFY is sent to the associated hwnd. 1459 1460 History: 1461 +/ 1462 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1463 1464 version(win32_widgets) 1465 deprecated("This overload is problematic since it is liable to discard return values. Add the `out int mustReturn` to your override as the last parameter and set it to 1 when you must forward the return value to Windows. Otherwise, you can just add the parameter then ignore it and use the default value of 0 to maintain the status quo.") int handleWmNotify(NMHDR* hdr, int code) { int ignored; return handleWmNotify(hdr, code, ignored); } 1466 1467 /++ 1468 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1469 1470 Updates to this variable will only be made visible on the next mouse enter event. 1471 +/ 1472 @scriptable string statusTip; 1473 // string toolTip; 1474 // string helpText; 1475 1476 /++ 1477 If true, this widget can be focused via keyboard control with the tab key. 1478 1479 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1480 +/ 1481 bool tabStop = true; 1482 /++ 1483 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.) 1484 +/ 1485 int tabOrder; 1486 1487 version(win32_widgets) { 1488 static Widget[HWND] nativeMapping; 1489 /// The native handle, if there is one. 1490 HWND hwnd; 1491 WNDPROC originalWindowProcedure; 1492 1493 SimpleWindow simpleWindowWrappingHwnd; 1494 1495 // please note it IGNORES your return value and does NOT forward it to Windows! 1496 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1497 return 0; 1498 } 1499 } 1500 private bool implicitlyCreated; 1501 1502 /// 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. 1503 int x; 1504 /// ditto 1505 int y; 1506 private int _width; 1507 private int _height; 1508 private Widget[] _children; 1509 private Widget _parent; 1510 private Window _parentWindow; 1511 1512 /++ 1513 Returns the window to which this widget is attached. 1514 1515 History: 1516 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. 1517 +/ 1518 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1519 private @property void parentWindow(Window parent) { 1520 _parentWindow = parent; 1521 foreach(child; children) 1522 child.parentWindow = parent; // please note that this is recursive 1523 } 1524 1525 /++ 1526 Returns the list of the widget's children. 1527 1528 History: 1529 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1530 1531 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1532 +/ 1533 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1534 1535 /++ 1536 Returns the widget's parent. 1537 1538 History: 1539 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1540 1541 The parent should only be managed by the [addChild] and [removeWidget] method. 1542 +/ 1543 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1544 1545 /// The widget's current size. 1546 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1547 /// ditto 1548 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1549 1550 /// Only the layout manager should be calling these. 1551 final protected @property int width(int a) @safe { return _width = a; } 1552 /// ditto 1553 final protected @property int height(int a) @safe { return _height = a; } 1554 1555 /++ 1556 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. 1557 1558 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 1559 +/ 1560 protected void registerMovement() { 1561 version(win32_widgets) { 1562 if(hwnd) { 1563 auto pos = getChildPositionRelativeToParentHwnd(this); 1564 MoveWindow(hwnd, pos[0], pos[1], width, height, false); // setting this to false can sometimes speed things up but only if it is actually drawn later and that's kinda iffy to do right here so being slower but safer rn 1565 } 1566 } 1567 sendResizeEvent(); 1568 } 1569 1570 /// Creates the widget and adds it to the parent. 1571 this(Widget parent) { 1572 if(parent !is null) 1573 parent.addChild(this); 1574 setupDefaultEventHandlers(); 1575 } 1576 1577 /// 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. 1578 @scriptable 1579 bool isFocused() { 1580 return parentWindow && parentWindow.focusedWidget is this; 1581 } 1582 1583 private bool showing_ = true; 1584 /// 1585 bool showing() { return showing_; } 1586 /// 1587 bool hidden() { return !showing_; } 1588 /++ 1589 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. 1590 +/ 1591 void showing(bool s, bool recalculate = true) { 1592 auto so = showing_; 1593 showing_ = s; 1594 if(s != so) { 1595 version(win32_widgets) 1596 if(hwnd) 1597 ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE); 1598 1599 if(parent && recalculate) { 1600 parent.queueRecomputeChildLayout(); 1601 parent.redraw(); 1602 } 1603 1604 foreach(child; children) 1605 child.showing(s, false); 1606 1607 } 1608 queueRecomputeChildLayout(); 1609 redraw(); 1610 } 1611 /// Convenience method for `showing = true` 1612 @scriptable 1613 void show() { 1614 showing = true; 1615 } 1616 /// Convenience method for `showing = false` 1617 @scriptable 1618 void hide() { 1619 showing = false; 1620 } 1621 1622 /// 1623 @scriptable 1624 void focus() { 1625 assert(parentWindow !is null); 1626 if(isFocused()) 1627 return; 1628 1629 if(parentWindow.focusedWidget) { 1630 // FIXME: more details here? like from and to 1631 auto from = parentWindow.focusedWidget; 1632 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 1633 parentWindow.focusedWidget = null; 1634 from.emit!BlurEvent(); 1635 this.emit!FocusOutEvent(); 1636 } 1637 1638 1639 version(win32_widgets) { 1640 if(this.hwnd !is null) 1641 SetFocus(this.hwnd); 1642 } 1643 //else static if(UsingSimpledisplayX11) 1644 //this.parentWindow.win.focus(); 1645 1646 parentWindow.focusedWidget = this; 1647 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 1648 this.emit!FocusEvent(); 1649 this.emit!FocusInEvent(); 1650 } 1651 1652 /+ 1653 /++ 1654 Unfocuses the widget. This may reset 1655 +/ 1656 @scriptable 1657 void blur() { 1658 1659 } 1660 +/ 1661 1662 1663 /++ 1664 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 1665 1666 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 1667 +/ 1668 void attachedToWindow(Window w) {} 1669 /++ 1670 Callback when the widget is added to another widget. 1671 1672 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 1673 +/ 1674 void addedTo(Widget w) {} 1675 1676 /++ 1677 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. 1678 1679 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 1680 +/ 1681 protected void addChild(Widget w, int position = int.max) { 1682 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 1683 assert(w !is this, "Child cannot be its own parent!"); 1684 w._parent = this; 1685 if(position == int.max || position == children.length) { 1686 _children ~= w; 1687 } else { 1688 assert(position < _children.length); 1689 _children.length = _children.length + 1; 1690 for(int i = cast(int) _children.length - 1; i > position; i--) 1691 _children[i] = _children[i - 1]; 1692 _children[position] = w; 1693 } 1694 1695 this.parentWindow = this._parentWindow; 1696 1697 w.addedTo(this); 1698 1699 if(this.hidden) 1700 w.showing = false; 1701 1702 if(parentWindow !is null) { 1703 w.attachedToWindow(parentWindow); 1704 parentWindow.queueRecomputeChildLayout(); 1705 parentWindow.redraw(); 1706 } 1707 } 1708 1709 /++ 1710 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. 1711 +/ 1712 Widget getChildAtPosition(int x, int y) { 1713 // it goes backward so the last one to show gets picked first 1714 // might use z-index later 1715 foreach_reverse(child; children) { 1716 if(child.hidden) 1717 continue; 1718 if(child.x <= x && child.y <= y 1719 && ((x - child.x) < child.width) 1720 && ((y - child.y) < child.height)) 1721 { 1722 return child; 1723 } 1724 } 1725 1726 return null; 1727 } 1728 1729 /++ 1730 If the widget is a scrollable container, this should add the current scroll position to the given coordinates so the mouse events can be dispatched correctly. 1731 1732 History: 1733 Added July 2, 2021 (v10.2) 1734 +/ 1735 protected void addScrollPosition(ref int x, ref int y) {}; 1736 1737 /++ 1738 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. 1739 1740 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. 1741 1742 [paint] is not called for system widgets as the OS library draws them instead. 1743 1744 1745 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. 1746 1747 You should also look at [WidgetPainter.visualTheme] to be theme aware. 1748 1749 History: 1750 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. 1751 +/ 1752 void paint(WidgetPainter painter) { 1753 version(win32_widgets) 1754 if(hwnd) { 1755 return; 1756 } 1757 painter.drawThemed(&paintContent); // note this refers to the following overload 1758 } 1759 1760 /++ 1761 Responsible for drawing the content as the theme engine is responsible for other elements. 1762 1763 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 1764 1765 Params: 1766 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. 1767 1768 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. 1769 1770 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. 1771 1772 Returns: 1773 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. 1774 1775 History: 1776 Added May 15, 2021 1777 +/ 1778 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 1779 return bounds; 1780 } 1781 1782 deprecated("Change ScreenPainter to WidgetPainter") 1783 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 1784 1785 /// I don't actually like the name of this 1786 /// this draws a background on it 1787 void erase(WidgetPainter painter) { 1788 version(win32_widgets) 1789 if(hwnd) return; // Windows will do it. I think. 1790 1791 auto c = getComputedStyle().background.color; 1792 painter.fillColor = c; 1793 painter.outlineColor = c; 1794 1795 version(win32_widgets) { 1796 HANDLE b, p; 1797 if(c.a == 0 && parent is parentWindow) { 1798 // I don't remember why I had this really... 1799 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 1800 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 1801 } 1802 } 1803 painter.drawRectangle(Point(0, 0), width, height); 1804 version(win32_widgets) { 1805 if(c.a == 0 && parent is parentWindow) { 1806 SelectObject(painter.impl.hdc, p); 1807 SelectObject(painter.impl.hdc, b); 1808 } 1809 } 1810 } 1811 1812 /// 1813 WidgetPainter draw() { 1814 int x = this.x, y = this.y; 1815 auto parent = this.parent; 1816 while(parent) { 1817 x += parent.x; 1818 y += parent.y; 1819 parent = parent.parent; 1820 } 1821 1822 auto painter = parentWindow.win.draw(true); 1823 painter.originX = x; 1824 painter.originY = y; 1825 painter.setClipRectangle(Point(0, 0), width, height); 1826 return WidgetPainter(painter, this); 1827 } 1828 1829 /// This can be overridden by scroll things. It is responsible for actually calling [paint]. Do not override unless you've studied minigui.d's source code. There are no stability guarantees if you do override this; it can (and likely will) break without notice. 1830 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 1831 if(hidden) 1832 return; 1833 1834 int paintX = x; 1835 int paintY = y; 1836 if(this.useNativeDrawing()) { 1837 paintX = 0; 1838 paintY = 0; 1839 lox = 0; 1840 loy = 0; 1841 containment = Rectangle(0, 0, int.max, int.max); 1842 } 1843 1844 painter.originX = lox + paintX; 1845 painter.originY = loy + paintY; 1846 1847 bool actuallyPainted = false; 1848 1849 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 1850 if(clip == Rectangle.init) { 1851 //import std.stdio; writeln(this, " clipped out"); 1852 return; 1853 } 1854 1855 bool invalidateChildren = invalidate; 1856 1857 if(redrawRequested || force) { 1858 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 1859 1860 painter.drawingUpon = this; 1861 1862 erase(painter); 1863 if(painter.visualTheme) 1864 painter.visualTheme.doPaint(this, painter); 1865 else 1866 paint(painter); 1867 1868 if(invalidate) { 1869 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 1870 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 1871 painter.invalidateRect(region); 1872 // children are contained inside this, so no need to do extra work 1873 invalidateChildren = false; 1874 } 1875 1876 redrawRequested = false; 1877 actuallyPainted = true; 1878 } 1879 1880 foreach(child; children) { 1881 version(win32_widgets) 1882 if(child.useNativeDrawing()) continue; 1883 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 1884 } 1885 1886 version(win32_widgets) 1887 foreach(child; children) { 1888 if(child.useNativeDrawing) { 1889 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 1890 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, true); // have to reset the invalidate flag since these are not necessarily affected the same way, being native children with a clip 1891 } 1892 } 1893 } 1894 1895 protected bool useNativeDrawing() nothrow { 1896 version(win32_widgets) 1897 return hwnd !is null; 1898 else 1899 return false; 1900 } 1901 1902 private static class RedrawEvent {} 1903 private __gshared re = new RedrawEvent(); 1904 1905 private bool redrawRequested; 1906 /// 1907 final void redraw(string file = __FILE__, size_t line = __LINE__) { 1908 redrawRequested = true; 1909 1910 if(this.parentWindow) { 1911 auto sw = this.parentWindow.win; 1912 assert(sw !is null); 1913 if(!sw.eventQueued!RedrawEvent) { 1914 sw.postEvent(re); 1915 // import std.stdio; writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1916 } 1917 } 1918 } 1919 1920 private SimpleWindow drawableWindow; 1921 1922 /++ 1923 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. 1924 1925 Returns: 1926 `true` if you should do your default behavior. 1927 1928 History: 1929 Added May 5, 2021 1930 1931 Bugs: 1932 It does not do the static checks on gdc right now. 1933 +/ 1934 final protected bool emit(EventType, this This, Args...)(Args args) { 1935 version(GNU) {} else 1936 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1937 auto e = new EventType(this, args); 1938 e.dispatch(); 1939 return !e.defaultPrevented; 1940 } 1941 /// ditto 1942 final protected bool emit(string eventString, this This)() { 1943 auto e = new Event(eventString, this); 1944 e.dispatch(); 1945 return !e.defaultPrevented; 1946 } 1947 1948 /++ 1949 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. 1950 1951 History: 1952 Added May 5, 2021 1953 +/ 1954 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 1955 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1956 return addEventListener(handler); 1957 } 1958 1959 /++ 1960 Gets the computed style properties from the visual theme. 1961 1962 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].) 1963 1964 History: 1965 Added May 8, 2021 1966 +/ 1967 final StyleInformation getComputedStyle() { 1968 return StyleInformation(this); 1969 } 1970 1971 int focusableWidgets(scope int delegate(Widget) dg) { 1972 foreach(widget; WidgetStream(this)) { 1973 if(widget.tabStop && !widget.hidden) { 1974 int result = dg(widget); 1975 if (result) 1976 return result; 1977 } 1978 } 1979 return 0; 1980 } 1981 1982 /++ 1983 Calculates the border box (that is, the full width/height of the widget, from border edge to border edge) 1984 for the given content box (the area between the padding) 1985 1986 History: 1987 Added January 4, 2023 (dub v11.0) 1988 +/ 1989 Rectangle borderBoxForContentBox(Rectangle contentBox) { 1990 auto cs = getComputedStyle(); 1991 1992 auto borderWidth = getBorderWidth(cs.borderStyle); 1993 1994 auto rect = contentBox; 1995 1996 rect.left -= borderWidth; 1997 rect.right += borderWidth; 1998 rect.top -= borderWidth; 1999 rect.bottom += borderWidth; 2000 2001 auto insideBorderRect = rect; 2002 2003 rect.left -= cs.paddingLeft; 2004 rect.right += cs.paddingRight; 2005 rect.top -= cs.paddingTop; 2006 rect.bottom += cs.paddingBottom; 2007 2008 return rect; 2009 } 2010 2011 2012 // FIXME: I kinda want to hide events from implementation widgets 2013 // so it just catches them all and stops propagation... 2014 // i guess i can do it with a event listener on star. 2015 2016 mixin Emits!KeyDownEvent; /// 2017 mixin Emits!KeyUpEvent; /// 2018 mixin Emits!CharEvent; /// 2019 2020 mixin Emits!MouseDownEvent; /// 2021 mixin Emits!MouseUpEvent; /// 2022 mixin Emits!ClickEvent; /// 2023 mixin Emits!DoubleClickEvent; /// 2024 mixin Emits!MouseMoveEvent; /// 2025 mixin Emits!MouseOverEvent; /// 2026 mixin Emits!MouseOutEvent; /// 2027 mixin Emits!MouseEnterEvent; /// 2028 mixin Emits!MouseLeaveEvent; /// 2029 2030 mixin Emits!ResizeEvent; /// 2031 2032 mixin Emits!BlurEvent; /// 2033 mixin Emits!FocusEvent; /// 2034 2035 mixin Emits!FocusInEvent; /// 2036 mixin Emits!FocusOutEvent; /// 2037 } 2038 2039 /+ 2040 /++ 2041 Interface to indicate that the widget has a simple value property. 2042 2043 History: 2044 Added August 26, 2021 2045 +/ 2046 interface HasValue!T { 2047 /// Getter 2048 @property T value(); 2049 /// Setter 2050 @property void value(T); 2051 } 2052 2053 /++ 2054 Interface to indicate that the widget has a range of possible values for its simple value property. 2055 This would be present on something like a slider or possibly a number picker. 2056 2057 History: 2058 Added September 11, 2021 2059 +/ 2060 interface HasRangeOfValues!T : HasValue!T { 2061 /// The minimum and maximum values in the range, inclusive. 2062 @property T minValue(); 2063 @property void minValue(T); /// ditto 2064 @property T maxValue(); /// ditto 2065 @property void maxValue(T); /// ditto 2066 2067 /// The smallest step the user interface allows. User may still type in values without this limitation. 2068 @property void step(T); 2069 @property T step(); /// ditto 2070 } 2071 2072 /++ 2073 Interface to indicate that the widget has a list of possible values the user can choose from. 2074 This would be present on something like a drop-down selector. 2075 2076 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2077 combobox. 2078 2079 History: 2080 Added September 11, 2021 2081 +/ 2082 interface HasListOfValues!T : HasValue!T { 2083 @property T[] values; 2084 @property void values(T[]); 2085 2086 @property int selectedIndex(); // note it may return -1! 2087 @property void selectedIndex(int); 2088 } 2089 +/ 2090 2091 /++ 2092 History: 2093 Added September 2021 (dub v10.4) 2094 +/ 2095 class GridLayout : Layout { 2096 2097 // FIXME: grid padding around edges and also cell spacing between units. even though you could do that by just specifying some gutter yourself in the layout. 2098 2099 /++ 2100 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2101 +/ 2102 enum Gravity { 2103 Center = 0, 2104 NorthWest = North | West, 2105 North = 0b10_00, 2106 NorthEast = North | East, 2107 West = 0b00_10, 2108 East = 0b00_01, 2109 SouthWest = South | West, 2110 South = 0b01_00, 2111 SouthEast = South | East, 2112 } 2113 2114 /++ 2115 The width and height are in some proportional units and can often just be 12. 2116 +/ 2117 this(int width, int height, Widget parent) { 2118 this.gridWidth = width; 2119 this.gridHeight = height; 2120 super(parent); 2121 } 2122 2123 /++ 2124 Sets the position of the given child. 2125 2126 The units of these arguments are in the proportional grid units you set in the constructor. 2127 +/ 2128 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2129 // ensure it is in bounds 2130 // then ensure no overlaps 2131 2132 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2133 2134 foreach(ref position; positions) { 2135 if(position.widget is child) { 2136 position = p; 2137 goto set; 2138 } 2139 } 2140 2141 positions ~= p; 2142 2143 set: 2144 2145 // FIXME: should this batch? 2146 queueRecomputeChildLayout(); 2147 2148 return child; 2149 } 2150 2151 override void addChild(Widget w, int position = int.max) { 2152 super.addChild(w, position); 2153 //positions ~= ChildPosition(w); 2154 if(position != int.max) { 2155 // FIXME: align it so they actually match. 2156 } 2157 } 2158 2159 override void widgetRemoved(size_t idx, Widget w) { 2160 // FIXME: keep the positions array aligned 2161 // positions[idx].widget = null; 2162 } 2163 2164 override void recomputeChildLayout() { 2165 registerMovement(); 2166 int onGrid = cast(int) positions.length; 2167 c: foreach(child; children) { 2168 // just snap it to the grid 2169 if(onGrid) 2170 foreach(position; positions) 2171 if(position.widget is child) { 2172 child.x = this.width * position.x / this.gridWidth; 2173 child.y = this.height * position.y / this.gridHeight; 2174 child.width = this.width * position.width / this.gridWidth; 2175 child.height = this.height * position.height / this.gridHeight; 2176 2177 auto diff = child.width - child.maxWidth(); 2178 // FIXME: gravity? 2179 if(diff > 0) { 2180 child.width = child.width - diff; 2181 2182 if(position.gravity & Gravity.West) { 2183 // nothing needed, already aligned 2184 } else if(position.gravity & Gravity.East) { 2185 child.x += diff; 2186 } else { 2187 child.x += diff / 2; 2188 } 2189 } 2190 2191 diff = child.height - child.maxHeight(); 2192 // FIXME: gravity? 2193 if(diff > 0) { 2194 child.height = child.height - diff; 2195 2196 if(position.gravity & Gravity.North) { 2197 // nothing needed, already aligned 2198 } else if(position.gravity & Gravity.South) { 2199 child.y += diff; 2200 } else { 2201 child.y += diff / 2; 2202 } 2203 } 2204 2205 2206 child.recomputeChildLayout(); 2207 onGrid--; 2208 continue c; 2209 } 2210 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2211 } 2212 } 2213 2214 private struct ChildPosition { 2215 Widget widget; 2216 int x; 2217 int y; 2218 int width; 2219 int height; 2220 Gravity gravity; 2221 } 2222 private ChildPosition[] positions; 2223 2224 int gridWidth = 12; 2225 int gridHeight = 12; 2226 } 2227 2228 /// 2229 abstract class ComboboxBase : Widget { 2230 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2231 // or to always show the list, we want CBS_SIMPLE == 1 2232 version(win32_widgets) 2233 this(uint style, Widget parent) { 2234 super(parent); 2235 createWin32Window(this, "ComboBox"w, null, style); 2236 } 2237 else version(custom_widgets) 2238 this(Widget parent) { 2239 super(parent); 2240 2241 addEventListener((KeyDownEvent event) { 2242 if(event.key == Key.Up) { 2243 if(selection_ > -1) { // -1 means select blank 2244 selection_--; 2245 fireChangeEvent(); 2246 } 2247 event.preventDefault(); 2248 } 2249 if(event.key == Key.Down) { 2250 if(selection_ + 1 < options.length) { 2251 selection_++; 2252 fireChangeEvent(); 2253 } 2254 event.preventDefault(); 2255 } 2256 2257 }); 2258 2259 } 2260 else static assert(false); 2261 2262 /++ 2263 Returns the current list of options in the selection. 2264 2265 History: 2266 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2267 +/ 2268 final @property string[] options() const { 2269 return cast(string[]) options_; 2270 } 2271 2272 private string[] options_; 2273 private int selection_ = -1; 2274 2275 /++ 2276 Adds an option to the end of options array. 2277 +/ 2278 void addOption(string s) { 2279 options_ ~= s; 2280 version(win32_widgets) 2281 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2282 } 2283 2284 /++ 2285 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2286 +/ 2287 int getSelection() { 2288 return selection_; 2289 } 2290 2291 /++ 2292 Returns the current selection as a string. 2293 2294 History: 2295 Added November 17, 2021 2296 +/ 2297 string getSelectionString() { 2298 return selection_ == -1 ? null : options[selection_]; 2299 } 2300 2301 /++ 2302 Sets the current selection to an index in the options array, or to the given option if present. 2303 Please note that the string version may do a linear lookup. 2304 2305 Returns: 2306 the index you passed in 2307 2308 History: 2309 The `string` based overload was added on March 1, 2022 (dub v10.7). 2310 2311 The return value was `void` prior to March 1, 2022. 2312 +/ 2313 int setSelection(int idx) { 2314 selection_ = idx; 2315 version(win32_widgets) 2316 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2317 2318 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2319 t.dispatch(); 2320 2321 return idx; 2322 } 2323 2324 /// ditto 2325 int setSelection(string s) { 2326 if(s !is null) 2327 foreach(idx, item; options) 2328 if(item == s) { 2329 return setSelection(cast(int) idx); 2330 } 2331 return setSelection(-1); 2332 } 2333 2334 /++ 2335 This event is fired when the selection changes. Note it inherits 2336 from ChangeEvent!string, meaning you can use that as well, and it also 2337 fills in [Event.intValue]. 2338 +/ 2339 static class SelectionChangedEvent : ChangeEvent!string { 2340 this(Widget target, int iv, string sv) { 2341 super(target, &stringValue); 2342 this.iv = iv; 2343 this.sv = sv; 2344 } 2345 immutable int iv; 2346 immutable string sv; 2347 2348 override @property string stringValue() { return sv; } 2349 override @property int intValue() { return iv; } 2350 } 2351 2352 version(win32_widgets) 2353 override void handleWmCommand(ushort cmd, ushort id) { 2354 if(cmd == CBN_SELCHANGE) { 2355 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2356 fireChangeEvent(); 2357 } 2358 } 2359 2360 private void fireChangeEvent() { 2361 if(selection_ >= options.length) 2362 selection_ = -1; 2363 2364 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2365 t.dispatch(); 2366 } 2367 2368 version(win32_widgets) { 2369 override int minHeight() { return defaultLineHeight + 6; } 2370 override int maxHeight() { return defaultLineHeight + 6; } 2371 } else { 2372 override int minHeight() { return defaultLineHeight + 4; } 2373 override int maxHeight() { return defaultLineHeight + 4; } 2374 } 2375 2376 version(custom_widgets) { 2377 2378 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2379 2380 SimpleWindow dropDown; 2381 void popup() { 2382 auto w = width; 2383 // FIXME: suggestedDropdownHeight see below 2384 auto h = cast(int) this.options.length * defaultLineHeight + 8; 2385 2386 auto coord = this.globalCoordinates(); 2387 auto dropDown = new SimpleWindow( 2388 w, h, 2389 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null); 2390 2391 dropDown.move(coord.x, coord.y + this.height); 2392 2393 { 2394 auto cs = getComputedStyle(); 2395 auto painter = dropDown.draw(); 2396 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2397 auto p = Point(4, 4); 2398 painter.outlineColor = cs.foregroundColor; 2399 foreach(option; options) { 2400 painter.drawText(p, option); 2401 p.y += defaultLineHeight; 2402 } 2403 } 2404 2405 dropDown.setEventHandlers( 2406 (MouseEvent event) { 2407 if(event.type == MouseEventType.buttonReleased) { 2408 dropDown.close(); 2409 auto element = (event.y - 4) / defaultLineHeight; 2410 if(element >= 0 && element <= options.length) { 2411 selection_ = element; 2412 2413 fireChangeEvent(); 2414 } 2415 } 2416 } 2417 ); 2418 2419 dropDown.visibilityChanged = (bool visible) { 2420 if(visible) { 2421 this.redraw(); 2422 dropDown.grabInput(); 2423 } else { 2424 dropDown.releaseInputGrab(); 2425 } 2426 }; 2427 2428 dropDown.show(); 2429 } 2430 2431 } 2432 } 2433 2434 /++ 2435 A drop-down list where the user must select one of the 2436 given options. Like `<select>` in HTML. 2437 +/ 2438 class DropDownSelection : ComboboxBase { 2439 this(Widget parent) { 2440 version(win32_widgets) 2441 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 2442 else version(custom_widgets) { 2443 super(parent); 2444 2445 addEventListener("focus", () { this.redraw; }); 2446 addEventListener("blur", () { this.redraw; }); 2447 addEventListener(EventType.change, () { this.redraw; }); 2448 addEventListener("mousedown", () { this.focus(); this.popup(); }); 2449 addEventListener((KeyDownEvent event) { 2450 if(event.key == Key.Space) 2451 popup(); 2452 }); 2453 } else static assert(false); 2454 } 2455 2456 mixin Padding!q{2}; 2457 static class Style : Widget.Style { 2458 override FrameStyle borderStyle() { return FrameStyle.risen; } 2459 } 2460 mixin OverrideStyle!Style; 2461 2462 version(custom_widgets) 2463 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2464 auto cs = getComputedStyle(); 2465 2466 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 2467 2468 painter.outlineColor = cs.foregroundColor; 2469 painter.fillColor = cs.foregroundColor; 2470 Point[4] triangle; 2471 enum padding = 6; 2472 enum paddingV = 7; 2473 enum triangleWidth = 10; 2474 triangle[0] = Point(width - padding - triangleWidth, paddingV); 2475 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 2476 triangle[2] = Point(width - padding - 0, paddingV); 2477 triangle[3] = triangle[0]; 2478 painter.drawPolygon(triangle[]); 2479 2480 return bounds; 2481 } 2482 2483 version(win32_widgets) 2484 override void registerMovement() { 2485 version(win32_widgets) { 2486 if(hwnd) { 2487 auto pos = getChildPositionRelativeToParentHwnd(this); 2488 // the height given to this from Windows' perspective is supposed 2489 // to include the drop down's height. so I add to it to give some 2490 // room for that. 2491 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 2492 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 2493 } 2494 } 2495 sendResizeEvent(); 2496 } 2497 } 2498 2499 /++ 2500 A text box with a drop down arrow listing selections. 2501 The user can choose from the list, or type their own. 2502 +/ 2503 class FreeEntrySelection : ComboboxBase { 2504 this(Widget parent) { 2505 version(win32_widgets) 2506 super(2 /* CBS_DROPDOWN */, parent); 2507 else version(custom_widgets) { 2508 super(parent); 2509 auto hl = new HorizontalLayout(this); 2510 lineEdit = new LineEdit(hl); 2511 2512 tabStop = false; 2513 2514 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2515 2516 auto btn = new class ArrowButton { 2517 this() { 2518 super(ArrowDirection.down, hl); 2519 } 2520 override int maxHeight() { 2521 return int.max; 2522 } 2523 }; 2524 //btn.addDirectEventListener("focus", &lineEdit.focus); 2525 btn.addEventListener("triggered", &this.popup); 2526 addEventListener(EventType.change, (Event event) { 2527 lineEdit.content = event.stringValue; 2528 lineEdit.focus(); 2529 redraw(); 2530 }); 2531 } 2532 else static assert(false); 2533 } 2534 2535 version(custom_widgets) { 2536 LineEdit lineEdit; 2537 } 2538 } 2539 2540 /++ 2541 A combination of free entry with a list below it. 2542 +/ 2543 class ComboBox : ComboboxBase { 2544 this(Widget parent) { 2545 version(win32_widgets) 2546 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 2547 else version(custom_widgets) { 2548 super(parent); 2549 lineEdit = new LineEdit(this); 2550 listWidget = new ListWidget(this); 2551 listWidget.multiSelect = false; 2552 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 2553 string c = null; 2554 foreach(option; listWidget.options) 2555 if(option.selected) { 2556 c = option.label; 2557 break; 2558 } 2559 lineEdit.content = c; 2560 }); 2561 2562 listWidget.tabStop = false; 2563 this.tabStop = false; 2564 listWidget.addEventListener("focus", &lineEdit.focus); 2565 this.addEventListener("focus", &lineEdit.focus); 2566 2567 addDirectEventListener(EventType.change, { 2568 listWidget.setSelection(selection_); 2569 if(selection_ != -1) 2570 lineEdit.content = options[selection_]; 2571 lineEdit.focus(); 2572 redraw(); 2573 }); 2574 2575 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2576 2577 listWidget.addDirectEventListener(EventType.change, { 2578 int set = -1; 2579 foreach(idx, opt; listWidget.options) 2580 if(opt.selected) { 2581 set = cast(int) idx; 2582 break; 2583 } 2584 if(set != selection_) 2585 this.setSelection(set); 2586 }); 2587 } else static assert(false); 2588 } 2589 2590 override int minHeight() { return defaultLineHeight * 3; } 2591 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 2592 override int heightStretchiness() { return 5; } 2593 2594 version(custom_widgets) { 2595 LineEdit lineEdit; 2596 ListWidget listWidget; 2597 2598 override void addOption(string s) { 2599 listWidget.options ~= ListWidget.Option(s); 2600 ComboboxBase.addOption(s); 2601 } 2602 } 2603 } 2604 2605 /+ 2606 class Spinner : Widget { 2607 version(win32_widgets) 2608 this(Widget parent) { 2609 super(parent); 2610 parentWindow = parent.parentWindow; 2611 auto hlayout = new HorizontalLayout(this); 2612 lineEdit = new LineEdit(hlayout); 2613 upDownControl = new UpDownControl(hlayout); 2614 } 2615 2616 LineEdit lineEdit; 2617 UpDownControl upDownControl; 2618 } 2619 2620 class UpDownControl : Widget { 2621 version(win32_widgets) 2622 this(Widget parent) { 2623 super(parent); 2624 parentWindow = parent.parentWindow; 2625 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 2626 } 2627 2628 override int minHeight() { return defaultLineHeight; } 2629 override int maxHeight() { return defaultLineHeight * 3/2; } 2630 2631 override int minWidth() { return defaultLineHeight * 3/2; } 2632 override int maxWidth() { return defaultLineHeight * 3/2; } 2633 } 2634 +/ 2635 2636 /+ 2637 class DataView : Widget { 2638 // this is the omnibus data viewer 2639 // the internal data layout is something like: 2640 // string[string][] but also each node can have parents 2641 } 2642 +/ 2643 2644 2645 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 2646 2647 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 2648 2649 // FIXME: menus should prolly capture the mouse. ugh i kno. 2650 /* 2651 TextEdit needs: 2652 2653 * caret manipulation 2654 * selection control 2655 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 2656 2657 For example: 2658 2659 connect(paste, &textEdit.insertTextAtCaret); 2660 2661 would be nice. 2662 2663 2664 2665 I kinda want an omnibus dataview that combines list, tree, 2666 and table - it can be switched dynamically between them. 2667 2668 Flattening policy: only show top level, show recursive, show grouped 2669 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 2670 2671 Single select, multi select, organization, drag+drop 2672 */ 2673 2674 //static if(UsingSimpledisplayX11) 2675 version(win32_widgets) {} 2676 else version(custom_widgets) { 2677 enum scrollClickRepeatInterval = 50; 2678 2679 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 2680 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 2681 enum activeTabColor = lightAccentColor; 2682 enum hoveringColor = Color(228, 228, 228); 2683 enum buttonColor = windowBackgroundColor; 2684 enum depressedButtonColor = darkAccentColor; 2685 enum activeListXorColor = Color(255, 255, 127); 2686 enum progressBarColor = Color(0, 0, 128); 2687 enum activeMenuItemColor = Color(0, 0, 128); 2688 2689 }} 2690 else static assert(false); 2691 deprecated("Get these properties off the `visualTheme` instead.") { 2692 // these are used by horizontal rule so not just custom_widgets. for now at least. 2693 enum darkAccentColor = Color(172, 172, 172); 2694 enum lightAccentColor = Color(223, 223, 223); // used to be 223 2695 } 2696 2697 private const(wchar)* toWstringzInternal(in char[] s) { 2698 wchar[] str; 2699 str.reserve(s.length + 1); 2700 foreach(dchar ch; s) 2701 str ~= ch; 2702 str ~= '\0'; 2703 return str.ptr; 2704 } 2705 2706 static if(SimpledisplayTimerAvailable) 2707 void setClickRepeat(Widget w, int interval, int delay = 250) { 2708 Timer timer; 2709 int delayRemaining = delay / interval; 2710 if(delayRemaining <= 1) 2711 delayRemaining = 2; 2712 2713 immutable originalDelayRemaining = delayRemaining; 2714 2715 w.addDirectEventListener((scope MouseDownEvent ev) { 2716 if(ev.srcElement !is w) 2717 return; 2718 if(timer !is null) { 2719 timer.destroy(); 2720 timer = null; 2721 } 2722 delayRemaining = originalDelayRemaining; 2723 timer = new Timer(interval, () { 2724 if(delayRemaining > 0) 2725 delayRemaining--; 2726 else { 2727 auto ev = new Event("triggered", w); 2728 ev.sendDirectly(); 2729 } 2730 }); 2731 }); 2732 2733 w.addDirectEventListener((scope MouseUpEvent ev) { 2734 if(ev.srcElement !is w) 2735 return; 2736 if(timer !is null) { 2737 timer.destroy(); 2738 timer = null; 2739 } 2740 }); 2741 2742 w.addDirectEventListener((scope MouseLeaveEvent ev) { 2743 if(ev.srcElement !is w) 2744 return; 2745 if(timer !is null) { 2746 timer.destroy(); 2747 timer = null; 2748 } 2749 }); 2750 2751 } 2752 else 2753 void setClickRepeat(Widget w, int interval, int delay = 250) {} 2754 2755 enum FrameStyle { 2756 none, /// 2757 risen, /// a 3d pop-out effect (think Windows 95 button) 2758 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 2759 solid, /// 2760 dotted, /// 2761 fantasy, /// a style based on a popular fantasy video game 2762 } 2763 2764 version(custom_widgets) 2765 deprecated 2766 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 2767 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2768 } 2769 2770 version(custom_widgets) 2771 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 2772 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 2773 } 2774 2775 version(custom_widgets) 2776 deprecated 2777 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 2778 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2779 } 2780 2781 int getBorderWidth(FrameStyle style) { 2782 final switch(style) { 2783 case FrameStyle.sunk, FrameStyle.risen: 2784 return 2; 2785 case FrameStyle.none: 2786 return 0; 2787 case FrameStyle.solid: 2788 return 1; 2789 case FrameStyle.dotted: 2790 return 1; 2791 case FrameStyle.fantasy: 2792 return 3; 2793 } 2794 } 2795 2796 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 2797 int borderWidth = getBorderWidth(style); 2798 final switch(style) { 2799 case FrameStyle.sunk, FrameStyle.risen: 2800 // outer layer 2801 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 2802 break; 2803 case FrameStyle.none: 2804 painter.outlineColor = background; 2805 break; 2806 case FrameStyle.solid: 2807 painter.pen = Pen(border, 1); 2808 break; 2809 case FrameStyle.dotted: 2810 painter.pen = Pen(border, 1, Pen.Style.Dotted); 2811 break; 2812 case FrameStyle.fantasy: 2813 painter.pen = Pen(border, 3); 2814 break; 2815 } 2816 2817 painter.fillColor = background; 2818 painter.drawRectangle(Point(x + 0, y + 0), width, height); 2819 2820 2821 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 2822 // 3d effect 2823 auto vt = WidgetPainter.visualTheme; 2824 2825 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 2826 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 2827 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 2828 2829 // inner layer 2830 //right, bottom 2831 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 2832 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 2833 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 2834 // left, top 2835 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 2836 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 2837 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 2838 } else if(style == FrameStyle.fantasy) { 2839 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 2840 painter.fillColor = Color.transparent; 2841 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 2842 } 2843 2844 return borderWidth; 2845 } 2846 2847 /++ 2848 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. 2849 2850 See_Also: 2851 [MenuItem] 2852 [ToolButton] 2853 [Menu.addItem] 2854 +/ 2855 class Action { 2856 version(win32_widgets) { 2857 private int id; 2858 private static int lastId = 9000; 2859 private static Action[int] mapping; 2860 } 2861 2862 KeyEvent accelerator; 2863 2864 // FIXME: disable message 2865 // and toggle thing? 2866 // ??? and trigger arguments too ??? 2867 2868 /++ 2869 Params: 2870 label = the textual label 2871 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 2872 triggered = initial handler, more can be added via the [triggered] member. 2873 +/ 2874 this(string label, ushort icon = 0, void delegate() triggered = null) { 2875 this.label = label; 2876 this.iconId = icon; 2877 if(triggered !is null) 2878 this.triggered ~= triggered; 2879 version(win32_widgets) { 2880 id = ++lastId; 2881 mapping[id] = this; 2882 } 2883 } 2884 2885 private string label; 2886 private ushort iconId; 2887 // icon 2888 2889 // when it is triggered, the triggered event is fired on the window 2890 /// The list of handlers when it is triggered. 2891 void delegate()[] triggered; 2892 } 2893 2894 /* 2895 plan: 2896 keyboard accelerators 2897 2898 * menus (and popups and tooltips) 2899 * status bar 2900 * toolbars and buttons 2901 2902 sortable table view 2903 2904 maybe notification area icons 2905 basic clipboard 2906 2907 * radio box 2908 splitter 2909 toggle buttons (optionally mutually exclusive, like in Paint) 2910 label, rich text display, multi line plain text (selectable) 2911 * fieldset 2912 * nestable grid layout 2913 single line text input 2914 * multi line text input 2915 slider 2916 spinner 2917 list box 2918 drop down 2919 combo box 2920 auto complete box 2921 * progress bar 2922 2923 terminal window/widget (on unix it might even be a pty but really idk) 2924 2925 ok button 2926 cancel button 2927 2928 keyboard hotkeys 2929 2930 scroll widget 2931 2932 event redirections and network transparency 2933 script integration 2934 */ 2935 2936 2937 /* 2938 MENUS 2939 2940 auto bar = new MenuBar(window); 2941 window.menuBar = bar; 2942 2943 auto fileMenu = bar.addItem(new Menu("&File")); 2944 fileMenu.addItem(new MenuItem("&Exit")); 2945 2946 2947 EVENTS 2948 2949 For controls, you should usually use "triggered" rather than "click", etc., because 2950 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 2951 This is the case on menus and pushbuttons. 2952 2953 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 2954 */ 2955 2956 2957 /* 2958 enum LinePreference { 2959 AlwaysOnOwnLine, // always on its own line 2960 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 2961 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 2962 } 2963 */ 2964 2965 /++ 2966 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. 2967 2968 --- 2969 class MyWidget : Widget { 2970 this(Widget parent) { super(parent); } 2971 2972 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 2973 mixin Padding!q{4}; 2974 2975 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 2976 mixin Margin!q{8}; 2977 2978 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 2979 // while Top/Bottom/Right remain 8 from the mixin above. 2980 override int marginLeft() { return 2; } 2981 } 2982 --- 2983 2984 2985 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]). 2986 2987 Padding is the area inside a widget where its background is drawn, but the content avoids. 2988 2989 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!). 2990 2991 * 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. 2992 +/ 2993 mixin template Padding(string code) { 2994 override int paddingLeft() { return mixin(code);} 2995 override int paddingRight() { return mixin(code);} 2996 override int paddingTop() { return mixin(code);} 2997 override int paddingBottom() { return mixin(code);} 2998 } 2999 3000 /// ditto 3001 mixin template Margin(string code) { 3002 override int marginLeft() { return mixin(code);} 3003 override int marginRight() { return mixin(code);} 3004 override int marginTop() { return mixin(code);} 3005 override int marginBottom() { return mixin(code);} 3006 } 3007 3008 private 3009 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3010 enum calcingV = relevantMeasure == "height"; 3011 3012 parent.registerMovement(); 3013 3014 if(parent.children.length == 0) 3015 return; 3016 3017 auto parentStyle = parent.getComputedStyle(); 3018 3019 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3020 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3021 3022 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3023 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3024 3025 // my own width and height should already be set by the caller of this function... 3026 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3027 mixin("parentStyle.padding"~firstThingy~"()") - 3028 mixin("parentStyle.padding"~secondThingy~"()"); 3029 3030 int stretchinessSum; 3031 int stretchyChildSum; 3032 int lastMargin = 0; 3033 3034 int shrinkinessSum; 3035 int shrinkyChildSum; 3036 3037 // set initial size 3038 foreach(child; parent.children) { 3039 3040 auto childStyle = child.getComputedStyle(); 3041 3042 if(cast(StaticPosition) child) 3043 continue; 3044 if(child.hidden) 3045 continue; 3046 3047 const iw = child.flexBasisWidth(); 3048 const ih = child.flexBasisHeight(); 3049 3050 static if(calcingV) { 3051 child.width = parent.width - 3052 mixin("childStyle.margin"~otherFirstThingy~"()") - 3053 mixin("childStyle.margin"~otherSecondThingy~"()") - 3054 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3055 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3056 3057 if(child.width < 0) 3058 child.width = 0; 3059 if(child.width > childStyle.maxWidth()) 3060 child.width = childStyle.maxWidth(); 3061 3062 if(iw > 0) { 3063 auto totalPossible = child.width; 3064 if(child.width > iw && child.widthStretchiness() == 0) 3065 child.width = iw; 3066 } 3067 3068 child.height = mymax(childStyle.minHeight(), ih); 3069 } else { 3070 // set to take all the space 3071 child.height = parent.height - 3072 mixin("childStyle.margin"~firstThingy~"()") - 3073 mixin("childStyle.margin"~secondThingy~"()") - 3074 mixin("parentStyle.padding"~firstThingy~"()") - 3075 mixin("parentStyle.padding"~secondThingy~"()"); 3076 3077 // then clamp it 3078 if(child.height < 0) 3079 child.height = 0; 3080 if(child.height > childStyle.maxHeight()) 3081 child.height = childStyle.maxHeight(); 3082 3083 // and if possible, respect the ideal target 3084 if(ih > 0) { 3085 auto totalPossible = child.height; 3086 if(child.height > ih && child.heightStretchiness() == 0) 3087 child.height = ih; 3088 } 3089 3090 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3091 child.width = mymax(childStyle.minWidth(), iw); 3092 } 3093 3094 spaceRemaining -= mixin("child." ~ relevantMeasure); 3095 3096 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3097 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3098 lastMargin = margin; 3099 spaceRemaining -= thisMargin + margin; 3100 3101 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3102 stretchinessSum += s; 3103 if(s > 0) 3104 stretchyChildSum++; 3105 3106 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3107 shrinkinessSum += s2; 3108 if(s2 > 0) 3109 shrinkyChildSum++; 3110 } 3111 3112 if(spaceRemaining < 0 && shrinkyChildSum) { 3113 // shrink to get into the space if it is possible 3114 auto toRemove = -spaceRemaining; 3115 auto removalPerItem = toRemove * shrinkinessSum / shrinkyChildSum; 3116 auto remainder = toRemove * shrinkinessSum % shrinkyChildSum; 3117 3118 // FIXME: wtf why am i shrinking things with no shrinkiness? 3119 3120 foreach(child; parent.children) { 3121 auto childStyle = child.getComputedStyle(); 3122 if(cast(StaticPosition) child) 3123 continue; 3124 if(child.hidden) 3125 continue; 3126 static if(calcingV) { 3127 auto maximum = childStyle.maxHeight(); 3128 } else { 3129 auto maximum = childStyle.maxWidth(); 3130 } 3131 3132 mixin("child._" ~ relevantMeasure) -= removalPerItem + remainder; // this is removing more than needed to trigger the next thing. ugh. 3133 3134 spaceRemaining += removalPerItem + remainder; 3135 } 3136 } 3137 3138 // stretch to fill space 3139 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3140 auto spacePerChild = spaceRemaining / stretchinessSum; 3141 bool spreadEvenly; 3142 bool giveToBiggest; 3143 if(spacePerChild <= 0) { 3144 spacePerChild = spaceRemaining / stretchyChildSum; 3145 spreadEvenly = true; 3146 } 3147 if(spacePerChild <= 0) { 3148 giveToBiggest = true; 3149 } 3150 int previousSpaceRemaining = spaceRemaining; 3151 stretchinessSum = 0; 3152 Widget mostStretchy; 3153 int mostStretchyS; 3154 foreach(child; parent.children) { 3155 auto childStyle = child.getComputedStyle(); 3156 if(cast(StaticPosition) child) 3157 continue; 3158 if(child.hidden) 3159 continue; 3160 static if(calcingV) { 3161 auto maximum = childStyle.maxHeight(); 3162 } else { 3163 auto maximum = childStyle.maxWidth(); 3164 } 3165 3166 if(mixin("child." ~ relevantMeasure) >= maximum) { 3167 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3168 mixin("child._" ~ relevantMeasure) -= adj; 3169 spaceRemaining += adj; 3170 continue; 3171 } 3172 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3173 if(s <= 0) 3174 continue; 3175 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3176 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3177 spaceRemaining -= spaceAdjustment; 3178 if(mixin("child." ~ relevantMeasure) > maximum) { 3179 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3180 mixin("child._" ~ relevantMeasure) -= diff; 3181 spaceRemaining += diff; 3182 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3183 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3184 if(mostStretchy is null || s >= mostStretchyS) { 3185 mostStretchy = child; 3186 mostStretchyS = s; 3187 } 3188 } 3189 } 3190 3191 if(giveToBiggest && mostStretchy !is null) { 3192 auto child = mostStretchy; 3193 auto childStyle = child.getComputedStyle(); 3194 int spaceAdjustment = spaceRemaining; 3195 3196 static if(calcingV) 3197 auto maximum = childStyle.maxHeight(); 3198 else 3199 auto maximum = childStyle.maxWidth(); 3200 3201 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3202 spaceRemaining -= spaceAdjustment; 3203 if(mixin("child._" ~ relevantMeasure) > maximum) { 3204 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3205 mixin("child._" ~ relevantMeasure) -= diff; 3206 spaceRemaining += diff; 3207 } 3208 } 3209 3210 if(spaceRemaining == previousSpaceRemaining) { 3211 if(mostStretchy !is null) { 3212 static if(calcingV) 3213 auto maximum = mostStretchy.maxHeight(); 3214 else 3215 auto maximum = mostStretchy.maxWidth(); 3216 3217 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3218 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3219 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3220 } 3221 break; // apparently nothing more we can do 3222 } 3223 3224 } 3225 3226 // position 3227 lastMargin = 0; 3228 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3229 foreach(child; parent.children) { 3230 auto childStyle = child.getComputedStyle(); 3231 if(cast(StaticPosition) child) { 3232 child.recomputeChildLayout(); 3233 continue; 3234 } 3235 if(child.hidden) 3236 continue; 3237 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3238 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3239 currentPos += thisMargin; 3240 static if(calcingV) { 3241 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3242 child.y = currentPos; 3243 } else { 3244 child.x = currentPos; 3245 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3246 3247 } 3248 currentPos += mixin("child." ~ relevantMeasure); 3249 currentPos += margin; 3250 lastMargin = margin; 3251 3252 child.recomputeChildLayout(); 3253 } 3254 } 3255 3256 int mymax(int a, int b) { return a > b ? a : b; } 3257 int mymax(int a, int b, int c) { 3258 auto d = mymax(a, b); 3259 return c > d ? c : d; 3260 } 3261 3262 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3263 // and here, it must be integrable with the layout, the event system, and not be painted over. 3264 version(win32_widgets) { 3265 3266 // this function just does stuff that a parent window needs for redirection 3267 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3268 this_.hookedWndProc(msg, wParam, lParam); 3269 3270 switch(msg) { 3271 3272 case WM_VSCROLL, WM_HSCROLL: 3273 auto pos = HIWORD(wParam); 3274 auto m = LOWORD(wParam); 3275 3276 auto scrollbarHwnd = cast(HWND) lParam; 3277 3278 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3279 3280 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3281 3282 switch(m) { 3283 /+ 3284 // I don't think those messages are ever actually sent normally by the widget itself, 3285 // they are more used for the keyboard interface. methinks. 3286 case SB_BOTTOM: 3287 //import std.stdio; writeln("end"); 3288 auto event = new Event("scrolltoend", *widgetp); 3289 event.dispatch(); 3290 //if(!event.defaultPrevented) 3291 break; 3292 case SB_TOP: 3293 //import std.stdio; writeln("top"); 3294 auto event = new Event("scrolltobeginning", *widgetp); 3295 event.dispatch(); 3296 break; 3297 case SB_ENDSCROLL: 3298 // idk 3299 break; 3300 +/ 3301 case SB_LINEDOWN: 3302 (*widgetp).emitCommand!"scrolltonextline"(); 3303 return 0; 3304 case SB_LINEUP: 3305 (*widgetp).emitCommand!"scrolltopreviousline"(); 3306 return 0; 3307 case SB_PAGEDOWN: 3308 (*widgetp).emitCommand!"scrolltonextpage"(); 3309 return 0; 3310 case SB_PAGEUP: 3311 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3312 return 0; 3313 case SB_THUMBPOSITION: 3314 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3315 ev.dispatch(); 3316 return 0; 3317 case SB_THUMBTRACK: 3318 // eh kinda lying but i like the real time update display 3319 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3320 ev.dispatch(); 3321 3322 // the event loop doesn't seem to carry on with a requested redraw.. 3323 // so we request it to get our dirty bit set... 3324 // then we need to immediately actually redraw it too for instant feedback to user 3325 SimpleWindow.processAllCustomEvents(); 3326 SimpleWindow.processAllCustomEvents(); 3327 //if(this_.parentWindow) 3328 //this_.parentWindow.actualRedraw(); 3329 3330 // and this ensures the WM_PAINT message is sent fairly quickly 3331 // still seems to lag a little in large windows but meh it basically works. 3332 if(this_.parentWindow) { 3333 // FIXME: if painting is slow, this does still lag 3334 // we probably will want to expose some user hook to ScrollWindowEx 3335 // or something. 3336 UpdateWindow(this_.parentWindow.hwnd); 3337 } 3338 return 0; 3339 default: 3340 } 3341 } 3342 break; 3343 3344 case WM_CONTEXTMENU: 3345 auto hwndFrom = cast(HWND) wParam; 3346 3347 auto xPos = cast(short) LOWORD(lParam); 3348 auto yPos = cast(short) HIWORD(lParam); 3349 3350 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3351 POINT p; 3352 p.x = xPos; 3353 p.y = yPos; 3354 ScreenToClient(hwnd, &p); 3355 auto clientX = cast(ushort) p.x; 3356 auto clientY = cast(ushort) p.y; 3357 3358 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 3359 3360 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 3361 return 0; 3362 } 3363 } 3364 break; 3365 3366 case WM_DRAWITEM: 3367 auto dis = cast(DRAWITEMSTRUCT*) lParam; 3368 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 3369 return (*widgetp).handleWmDrawItem(dis); 3370 } 3371 break; 3372 3373 case WM_NOTIFY: 3374 auto hdr = cast(NMHDR*) lParam; 3375 auto hwndFrom = hdr.hwndFrom; 3376 auto code = hdr.code; 3377 3378 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3379 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 3380 } 3381 break; 3382 case WM_COMMAND: 3383 auto handle = cast(HWND) lParam; 3384 auto cmd = HIWORD(wParam); 3385 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 3386 3387 default: 3388 // pass it on 3389 } 3390 return 0; 3391 } 3392 3393 3394 3395 extern(Windows) 3396 private 3397 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 3398 // but can i merge them?! 3399 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3400 //import std.stdio; try { writeln(iMessage); } catch(Exception e) {}; 3401 3402 if(auto te = hWnd in Widget.nativeMapping) { 3403 try { 3404 3405 te.hookedWndProc(iMessage, wParam, lParam); 3406 3407 int mustReturn; 3408 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 3409 if(mustReturn) 3410 return ret; 3411 3412 if(iMessage == WM_SETFOCUS) { 3413 auto lol = *te; 3414 while(lol !is null && lol.implicitlyCreated) 3415 lol = lol.parent; 3416 lol.focus(); 3417 //(*te).parentWindow.focusedWidget = lol; 3418 } 3419 3420 3421 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 3422 SetBkMode(cast(HDC) wParam, TRANSPARENT); 3423 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 3424 //GetStockObject(NULL_BRUSH); 3425 } 3426 3427 auto pos = getChildPositionRelativeToParentOrigin(*te); 3428 lastDefaultPrevented = false; 3429 // try {import std.stdio; writeln(typeid(*te)); } catch(Exception e) {} 3430 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 3431 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 3432 else { 3433 // it was something we recognized, should only call the window procedure if the default was not prevented 3434 } 3435 } catch(Exception e) { 3436 assert(0, e.toString()); 3437 } 3438 return 0; 3439 } 3440 assert(0, "shouldn't be receiving messages for this window...."); 3441 //import std.conv; 3442 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 3443 } 3444 3445 extern(Windows) 3446 private 3447 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 3448 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3449 if(iMessage == WM_ERASEBKGND) { 3450 auto dc = GetDC(hWnd); 3451 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 3452 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 3453 RECT r; 3454 GetWindowRect(hWnd, &r); 3455 // since the pen is null, to fill the whole space, we need the +1 on both. 3456 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 3457 SelectObject(dc, p); 3458 SelectObject(dc, b); 3459 ReleaseDC(hWnd, dc); 3460 InvalidateRect(hWnd, null, false); // redraw the border 3461 return 1; 3462 } 3463 return HookedWndProc(hWnd, iMessage, wParam, lParam); 3464 } 3465 3466 /++ 3467 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 3468 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 3469 of minigui's expectations. 3470 3471 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 3472 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 3473 3474 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 3475 3476 To check if you can use this, use `static if(UsingWin32Widgets)`. 3477 +/ 3478 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 3479 assert(p.parentWindow !is null); 3480 assert(p.parentWindow.win.impl.hwnd !is null); 3481 3482 auto bsgroupbox = style == BS_GROUPBOX; 3483 3484 HWND phwnd; 3485 3486 auto wtf = p.parent; 3487 while(wtf) { 3488 if(wtf.hwnd !is null) { 3489 phwnd = wtf.hwnd; 3490 break; 3491 } 3492 wtf = wtf.parent; 3493 } 3494 3495 if(phwnd is null) 3496 phwnd = p.parentWindow.win.impl.hwnd; 3497 3498 assert(phwnd !is null); 3499 3500 WCharzBuffer wt = WCharzBuffer(windowText); 3501 3502 style |= WS_VISIBLE | WS_CHILD; 3503 //if(className != WC_TABCONTROL) 3504 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 3505 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 3506 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 3507 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 3508 3509 assert(p.hwnd !is null); 3510 3511 3512 static HFONT font; 3513 if(font is null) { 3514 NONCLIENTMETRICS params; 3515 params.cbSize = params.sizeof; 3516 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 3517 font = CreateFontIndirect(¶ms.lfMessageFont); 3518 } 3519 } 3520 3521 if(font) 3522 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 3523 3524 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 3525 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 3526 Widget.nativeMapping[p.hwnd] = p; 3527 3528 if(bsgroupbox) 3529 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 3530 else 3531 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3532 3533 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 3534 3535 p.registerMovement(); 3536 } 3537 } 3538 3539 version(win32_widgets) 3540 private 3541 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 3542 if(hwnd is null || hwnd in Widget.nativeMapping) 3543 return true; 3544 auto parent = cast(Widget) cast(void*) lparam; 3545 Widget p = new Widget(null); 3546 p._parent = parent; 3547 p.parentWindow = parent.parentWindow; 3548 p.hwnd = hwnd; 3549 p.implicitlyCreated = true; 3550 Widget.nativeMapping[p.hwnd] = p; 3551 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3552 return true; 3553 } 3554 3555 /++ 3556 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 3557 +/ 3558 struct WidgetPainter { 3559 this(ScreenPainter screenPainter, Widget drawingUpon) { 3560 this.drawingUpon = drawingUpon; 3561 this.screenPainter = screenPainter; 3562 if(auto font = visualTheme.defaultFontCached) 3563 this.screenPainter.setFont(font); 3564 } 3565 3566 /++ 3567 EXPERIMENTAL. subject to change. 3568 3569 When you draw a cursor, you can draw this to notify your window of where it is, 3570 for IME systems to use. 3571 +/ 3572 void notifyCursorPosition(int x, int y, int width, int height) { 3573 if(auto a = drawingUpon.parentWindow) 3574 if(auto w = a.inputProxy) { 3575 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 3576 } 3577 } 3578 3579 3580 /// 3581 ScreenPainter screenPainter; 3582 /// Forward to the screen painter for other methods 3583 alias screenPainter this; 3584 3585 private Widget drawingUpon; 3586 3587 /++ 3588 This is the list of rectangles that actually need to be redrawn. 3589 3590 Not actually implemented yet. 3591 +/ 3592 Rectangle[] invalidatedRectangles; 3593 3594 private static BaseVisualTheme _visualTheme; 3595 3596 /++ 3597 Functions to access the visual theme and helpers to easily use it. 3598 3599 These are aware of the current widget's computed style out of the theme. 3600 +/ 3601 static @property BaseVisualTheme visualTheme() { 3602 if(_visualTheme is null) 3603 _visualTheme = new DefaultVisualTheme(); 3604 return _visualTheme; 3605 } 3606 3607 /// ditto 3608 static @property void visualTheme(BaseVisualTheme theme) { 3609 _visualTheme = theme; 3610 3611 // FIXME: notify all windows about the new theme 3612 } 3613 3614 /// ditto 3615 Color themeForeground() { 3616 return drawingUpon.getComputedStyle().foregroundColor(); 3617 } 3618 3619 /// ditto 3620 Color themeBackground() { 3621 return drawingUpon.getComputedStyle().background.color; 3622 } 3623 3624 int isDarkTheme() { 3625 return 0; // unspecified, yes, no as enum. FIXME 3626 } 3627 3628 /++ 3629 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. 3630 3631 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 3632 3633 If you change teh clip rectangle, you should change it back before you return. 3634 3635 3636 The sequence it uses is: 3637 background 3638 content (delegated to you) 3639 border 3640 focused outline 3641 selected overlay 3642 3643 Example code: 3644 3645 --- 3646 void paint(WidgetPainter painter) { 3647 painter.drawThemed((bounds) { 3648 return bounds; // if the selection overlay should be contained, you can return it here. 3649 }); 3650 } 3651 --- 3652 +/ 3653 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 3654 drawThemed((WidgetPainter painter, const Rectangle bounds) { 3655 return drawBody(bounds); 3656 }); 3657 } 3658 // this overload is actually mroe for setting the delegate to a virtual function 3659 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 3660 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 3661 3662 auto cs = drawingUpon.getComputedStyle(); 3663 3664 auto bg = cs.background.color; 3665 3666 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 3667 3668 rect.left += borderWidth; 3669 rect.right -= borderWidth; 3670 rect.top += borderWidth; 3671 rect.bottom -= borderWidth; 3672 3673 auto insideBorderRect = rect; 3674 3675 rect.left += cs.paddingLeft; 3676 rect.right -= cs.paddingRight; 3677 rect.top += cs.paddingTop; 3678 rect.bottom -= cs.paddingBottom; 3679 3680 this.outlineColor = this.themeForeground; 3681 this.fillColor = bg; 3682 3683 auto widgetFont = cs.fontCached; 3684 if(widgetFont !is null) 3685 this.setFont(widgetFont); 3686 3687 rect = drawBody(this, rect); 3688 3689 if(widgetFont !is null) { 3690 if(auto vtFont = visualTheme.defaultFontCached) 3691 this.setFont(vtFont); 3692 else 3693 this.setFont(null); 3694 } 3695 3696 if(auto os = cs.outlineStyle()) { 3697 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 3698 this.fillColor = Color.transparent; 3699 this.drawRectangle(insideBorderRect); 3700 } 3701 } 3702 3703 /++ 3704 First, draw the background. 3705 Then draw your content. 3706 Next, draw the border. 3707 And the focused indicator. 3708 And the is-selected box. 3709 3710 If it is focused i can draw the outline too... 3711 3712 If selected i can even do the xor action but that's at the end. 3713 +/ 3714 void drawThemeBackground() { 3715 3716 } 3717 3718 void drawThemeBorder() { 3719 3720 } 3721 3722 // all this stuff is a dangerous experiment.... 3723 static class ScriptableVersion { 3724 ScreenPainterImplementation* p; 3725 int originX, originY; 3726 3727 @scriptable: 3728 void drawRectangle(int x, int y, int width, int height) { 3729 p.drawRectangle(x + originX, y + originY, width, height); 3730 } 3731 void drawLine(int x1, int y1, int x2, int y2) { 3732 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 3733 } 3734 void drawText(int x, int y, string text) { 3735 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 3736 } 3737 void setOutlineColor(int r, int g, int b) { 3738 p.pen = Pen(Color(r,g,b), 1); 3739 } 3740 void setFillColor(int r, int g, int b) { 3741 p.fillColor = Color(r,g,b); 3742 } 3743 } 3744 3745 ScriptableVersion toArsdJsvar() { 3746 auto sv = new ScriptableVersion; 3747 sv.p = this.screenPainter.impl; 3748 sv.originX = this.screenPainter.originX; 3749 sv.originY = this.screenPainter.originY; 3750 return sv; 3751 } 3752 3753 static WidgetPainter fromJsVar(T)(T t) { 3754 return WidgetPainter.init; 3755 } 3756 // done.......... 3757 } 3758 3759 3760 struct Style { 3761 static struct helper(string m, T) { 3762 enum method = m; 3763 T v; 3764 3765 mixin template MethodOverride(typeof(this) v) { 3766 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 3767 } 3768 } 3769 3770 static auto opDispatch(string method, T)(T value) { 3771 return helper!(method, T)(value); 3772 } 3773 } 3774 3775 /++ 3776 Implementation detail of the [ControlledBy] UDA. 3777 3778 History: 3779 Added Oct 28, 2020 3780 +/ 3781 struct ControlledBy_(T, Args...) { 3782 Args args; 3783 3784 static if(Args.length) 3785 this(Args args) { 3786 this.args = args; 3787 } 3788 3789 private T construct(Widget parent) { 3790 return new T(args, parent); 3791 } 3792 } 3793 3794 /++ 3795 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 3796 3797 History: 3798 Added Oct 28, 2020 3799 +/ 3800 auto ControlledBy(T, Args...)(Args args) { 3801 return ControlledBy_!(T, Args)(args); 3802 } 3803 3804 struct ContainerMeta { 3805 string name; 3806 ContainerMeta[] children; 3807 Widget function(Widget parent) factory; 3808 3809 Widget instantiate(Widget parent) { 3810 auto n = factory(parent); 3811 n.name = name; 3812 foreach(child; children) 3813 child.instantiate(n); 3814 return n; 3815 } 3816 } 3817 3818 /++ 3819 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 3820 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 3821 3822 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 3823 structures. It works fine on structs declared inside functions though. 3824 3825 See: https://issues.dlang.org/show_bug.cgi?id=21984 3826 +/ 3827 template Container(CArgs...) { 3828 static if(CArgs.length && is(CArgs[0] : Widget)) { 3829 private alias Super = CArgs[0]; 3830 private alias CArgs2 = CArgs[1 .. $]; 3831 } else { 3832 private alias Super = Layout; 3833 private alias CArgs2 = CArgs; 3834 } 3835 3836 class Container : Super { 3837 this(Widget parent) { super(parent); } 3838 3839 // just to partially support old gdc versions 3840 version(GNU) { 3841 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 3842 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 3843 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 3844 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 3845 } else mixin(q{ 3846 static foreach(Arg; CArgs2) { 3847 mixin Arg.MethodOverride!(Arg); 3848 } 3849 }); 3850 3851 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 3852 return ContainerMeta( 3853 name, 3854 children.dup, 3855 function (Widget parent) { return new typeof(this)(parent); } 3856 ); 3857 } 3858 3859 static ContainerMeta opCall(ContainerMeta[] children...) { 3860 return opCall(null, children); 3861 } 3862 } 3863 } 3864 3865 /++ 3866 The data controller widget is created by reflecting over the given 3867 data type. You can use [ControlledBy] as a UDA on a struct or 3868 just let it create things automatically. 3869 3870 Unlike [dialog], this uses real-time updating of the data and 3871 you add it to another window yourself. 3872 3873 --- 3874 struct Test { 3875 int x; 3876 int y; 3877 } 3878 3879 auto window = new Window(); 3880 auto dcw = new DataControllerWidget!Test(new Test, window); 3881 --- 3882 3883 The way it works is any public members are given a widget based 3884 on their data type, and public methods trigger an action button 3885 if no relevant parameters or a dialog action if it does have 3886 parameters, similar to the [menu] facility. 3887 3888 If you change data programmatically, without going through the 3889 DataControllerWidget methods, you will have to tell it something 3890 has changed and it needs to redraw. This is done with the `invalidate` 3891 method. 3892 3893 History: 3894 Added Oct 28, 2020 3895 +/ 3896 /// Group: generating_from_code 3897 class DataControllerWidget(T) : WidgetContainer { 3898 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 3899 private alias Tref = T; 3900 else 3901 private alias Tref = T*; 3902 3903 Tref datum; 3904 3905 /++ 3906 See_also: [addDataControllerWidget] 3907 +/ 3908 this(Tref datum, Widget parent) { 3909 this.datum = datum; 3910 3911 Widget cp = this; 3912 3913 super(parent); 3914 3915 foreach(attr; __traits(getAttributes, T)) 3916 static if(is(typeof(attr) == ContainerMeta)) { 3917 cp = attr.instantiate(this); 3918 } 3919 3920 auto def = this.getByName("default"); 3921 if(def !is null) 3922 cp = def; 3923 3924 Widget helper(string name) { 3925 auto maybe = this.getByName(name); 3926 if(maybe is null) 3927 return cp; 3928 return maybe; 3929 3930 } 3931 3932 foreach(member; __traits(allMembers, T)) 3933 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 3934 static if(is(typeof(__traits(getMember, this.datum, member)))) 3935 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 3936 void delegate() update; 3937 3938 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 3939 3940 if(update) 3941 updaters ~= update; 3942 3943 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 3944 w.addEventListener("triggered", delegate() { 3945 makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))(); 3946 notifyDataUpdated(); 3947 }); 3948 } else static if(is(typeof(w.isChecked) == bool)) { 3949 w.addEventListener(EventType.change, (Event ev) { 3950 __traits(getMember, this.datum, member) = w.isChecked; 3951 }); 3952 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 3953 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 3954 } else static if(is(typeof(w.value) == int)) { 3955 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 3956 } else static if(is(typeof(w) == DropDownSelection)) { 3957 // special case for this to kinda support enums and such. coudl be better though 3958 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 3959 } else { 3960 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 3961 } 3962 } 3963 } 3964 3965 /++ 3966 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 3967 3968 History: 3969 Added May 28, 2021 3970 +/ 3971 void notifyDataUpdated() { 3972 foreach(updater; updaters) 3973 updater(); 3974 3975 this.emit!(ChangeEvent!void)(delegate{}); 3976 } 3977 3978 private Widget[string] memberWidgets; 3979 private void delegate()[] updaters; 3980 3981 mixin Emits!(ChangeEvent!void); 3982 } 3983 3984 private int saturatedSum(int[] values...) { 3985 int sum; 3986 foreach(value; values) { 3987 if(value == int.max) 3988 return int.max; 3989 sum += value; 3990 } 3991 return sum; 3992 } 3993 3994 void genericSetValue(T, W)(T* where, W what) { 3995 import std.conv; 3996 *where = to!T(what); 3997 //*where = cast(T) stringToLong(what); 3998 } 3999 4000 /++ 4001 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4002 4003 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4004 4005 Note that this creates the widget but does not attach any event handlers to it. 4006 +/ 4007 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 4008 4009 string displayName = __traits(identifier, tt).beautify; 4010 4011 static if(controlledByCount!tt == 1) { 4012 foreach(i, attr; __traits(getAttributes, tt)) { 4013 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4014 auto w = attr.construct(parent); 4015 static if(__traits(compiles, w.setPosition(*valptr))) 4016 update = () { w.setPosition(*valptr); }; 4017 else static if(__traits(compiles, w.setValue(*valptr))) 4018 update = () { w.setValue(*valptr); }; 4019 4020 if(update) 4021 update(); 4022 return w; 4023 } 4024 } 4025 } else static if(controlledByCount!tt == 0) { 4026 static if(is(typeof(tt) == enum)) { 4027 // FIXME: update 4028 auto dds = new DropDownSelection(parent); 4029 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4030 dds.addOption(option); 4031 if(__traits(getMember, typeof(tt), option) == *valptr) 4032 dds.setSelection(cast(int) idx); 4033 } 4034 return dds; 4035 } else static if(is(typeof(tt) == bool)) { 4036 auto box = new Checkbox(displayName, parent); 4037 update = () { box.isChecked = *valptr; }; 4038 update(); 4039 return box; 4040 } else static if(is(typeof(tt) : const long)) { 4041 auto le = new LabeledLineEdit(displayName, parent); 4042 update = () { le.content = toInternal!string(*valptr); }; 4043 update(); 4044 return le; 4045 } else static if(is(typeof(tt) : const double)) { 4046 auto le = new LabeledLineEdit(displayName, parent); 4047 import std.conv; 4048 update = () { le.content = to!string(*valptr); }; 4049 update(); 4050 return le; 4051 } else static if(is(typeof(tt) : const string)) { 4052 auto le = new LabeledLineEdit(displayName, parent); 4053 update = () { le.content = *valptr; }; 4054 update(); 4055 return le; 4056 } else static if(is(typeof(tt) == function)) { 4057 auto w = new Button(displayName, parent); 4058 return w; 4059 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4060 return parent.addDataControllerWidget(tt); 4061 } else static assert(0, typeof(tt).stringof); 4062 } else static assert(0, "multiple controllers not yet supported"); 4063 } 4064 4065 private template controlledByCount(alias tt) { 4066 static int helper() { 4067 int count; 4068 foreach(i, attr; __traits(getAttributes, tt)) 4069 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 4070 count++; 4071 return count; 4072 } 4073 4074 enum controlledByCount = helper; 4075 } 4076 4077 /++ 4078 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 4079 4080 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 4081 4082 History: 4083 The `redrawOnChange` parameter was added on May 28, 2021. 4084 +/ 4085 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 4086 auto dcw = new DataControllerWidget!T(t, parent); 4087 initializeDataControllerWidget(dcw, redrawOnChange); 4088 return dcw; 4089 } 4090 4091 /// ditto 4092 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 4093 auto dcw = new DataControllerWidget!T(t, parent); 4094 initializeDataControllerWidget(dcw, redrawOnChange); 4095 return dcw; 4096 } 4097 4098 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 4099 if(redrawOnChange !is null) 4100 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 4101 } 4102 4103 /++ 4104 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. 4105 4106 History: 4107 Finalized on June 3, 2021 for the dub v10.0 release 4108 +/ 4109 struct StyleInformation { 4110 private Widget w; 4111 private BaseVisualTheme visualTheme; 4112 4113 private this(Widget w) { 4114 this.w = w; 4115 this.visualTheme = WidgetPainter.visualTheme; 4116 } 4117 4118 /++ 4119 Forwards to [Widget.Style] 4120 4121 Bugs: 4122 It is supposed to fall back to the [VisualTheme] if 4123 the style doesn't override the default, but that is 4124 not generally implemented. Many of them may end up 4125 being explicit overloads instead of the generic 4126 opDispatch fallback, like [font] is now. 4127 +/ 4128 public @property opDispatch(string name)() { 4129 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4130 w.useStyleProperties((scope Widget.Style props) { 4131 //visualTheme.useStyleProperties(w, (props) { 4132 prop = __traits(getMember, props, name); 4133 }); 4134 return prop; 4135 } 4136 4137 /++ 4138 Returns the cached font object associated with the widget, 4139 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 4140 4141 History: 4142 Prior to March 21, 2022 (dub v10.7), `font` went through 4143 [opDispatch], which did not use the cache. You can now call it 4144 repeatedly without guilt. 4145 +/ 4146 public @property OperatingSystemFont font() { 4147 OperatingSystemFont prop; 4148 w.useStyleProperties((scope Widget.Style props) { 4149 prop = props.fontCached; 4150 }); 4151 if(prop is null) 4152 prop = visualTheme.defaultFontCached; 4153 return prop; 4154 } 4155 4156 @property { 4157 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 4158 /** */ int paddingLeft() { return w.paddingLeft(); } 4159 /** */ int paddingRight() { return w.paddingRight(); } 4160 /** */ int paddingTop() { return w.paddingTop(); } 4161 /** */ int paddingBottom() { return w.paddingBottom(); } 4162 4163 /** */ int marginLeft() { return w.marginLeft(); } 4164 /** */ int marginRight() { return w.marginRight(); } 4165 /** */ int marginTop() { return w.marginTop(); } 4166 /** */ int marginBottom() { return w.marginBottom(); } 4167 4168 /** */ int maxHeight() { return w.maxHeight(); } 4169 /** */ int minHeight() { return w.minHeight(); } 4170 4171 /** */ int maxWidth() { return w.maxWidth(); } 4172 /** */ int minWidth() { return w.minWidth(); } 4173 4174 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 4175 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 4176 4177 /** */ int heightStretchiness() { return w.heightStretchiness(); } 4178 /** */ int widthStretchiness() { return w.widthStretchiness(); } 4179 4180 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 4181 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 4182 4183 // Global helpers some of these are unstable. 4184 static: 4185 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4186 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4187 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4188 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4189 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 4190 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4191 4192 /** */ Color activeTabColor() { return lightAccentColor; } 4193 /** */ Color buttonColor() { return windowBackgroundColor; } 4194 /** */ Color depressedButtonColor() { return darkAccentColor; } 4195 /** */ Color hoveringColor() { return lightAccentColor; } 4196 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 4197 auto c = WidgetPainter.visualTheme.selectionColor(); 4198 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4199 } 4200 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4201 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4202 } 4203 4204 4205 4206 /+ 4207 4208 private static auto extractStyleProperty(string name)(Widget w) { 4209 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4210 w.useStyleProperties((props) { 4211 prop = __traits(getMember, props, name); 4212 }); 4213 return prop; 4214 } 4215 4216 // FIXME: clear this upon a X server disconnect 4217 private static OperatingSystemFont[string] fontCache; 4218 4219 T getProperty(T)(string name, lazy T default_) { 4220 if(visualTheme !is null) { 4221 auto str = visualTheme.getPropertyString(w, name); 4222 if(str is null) 4223 return default_; 4224 static if(is(T == Color)) 4225 return Color.fromString(str); 4226 else static if(is(T == Measurement)) 4227 return Measurement(cast(int) toInternal!int(str)); 4228 else static if(is(T == WidgetBackground)) 4229 return WidgetBackground.fromString(str); 4230 else static if(is(T == OperatingSystemFont)) { 4231 if(auto f = str in fontCache) 4232 return *f; 4233 else 4234 return fontCache[str] = new OperatingSystemFont(str); 4235 } else static if(is(T == FrameStyle)) { 4236 switch(str) { 4237 default: 4238 return FrameStyle.none; 4239 foreach(style; __traits(allMembers, FrameStyle)) 4240 case style: 4241 return __traits(getMember, FrameStyle, style); 4242 } 4243 } else static assert(0); 4244 } else 4245 return default_; 4246 } 4247 4248 static struct Measurement { 4249 int value; 4250 alias value this; 4251 } 4252 4253 @property: 4254 4255 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 4256 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 4257 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 4258 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 4259 4260 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 4261 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 4262 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 4263 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 4264 4265 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 4266 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 4267 4268 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 4269 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 4270 4271 4272 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 4273 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 4274 4275 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 4276 4277 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 4278 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 4279 4280 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 4281 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 4282 4283 4284 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4285 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4286 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4287 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4288 4289 Color activeTabColor() { return lightAccentColor; } 4290 Color buttonColor() { return windowBackgroundColor; } 4291 Color depressedButtonColor() { return darkAccentColor; } 4292 Color hoveringColor() { return Color(228, 228, 228); } 4293 Color activeListXorColor() { 4294 auto c = WidgetPainter.visualTheme.selectionColor(); 4295 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4296 } 4297 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4298 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4299 +/ 4300 } 4301 4302 4303 4304 // pragma(msg, __traits(classInstanceSize, Widget)); 4305 4306 /*private*/ template EventString(E) { 4307 static if(is(typeof(E.EventString))) 4308 enum EventString = E.EventString; 4309 else 4310 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 4311 } 4312 4313 /*private*/ template EventStringIdentifier(E) { 4314 string helper() { 4315 auto es = EventString!E; 4316 char[] id = new char[](es.length * 2); 4317 size_t idx; 4318 foreach(char ch; es) { 4319 id[idx++] = cast(char)('a' + (ch >> 4)); 4320 id[idx++] = cast(char)('a' + (ch & 0x0f)); 4321 } 4322 return cast(string) id; 4323 } 4324 4325 enum EventStringIdentifier = helper(); 4326 } 4327 4328 4329 template classStaticallyEmits(This, EventType) { 4330 static if(is(This Base == super)) 4331 static if(is(Base : Widget)) 4332 enum baseEmits = classStaticallyEmits!(Base, EventType); 4333 else 4334 enum baseEmits = false; 4335 else 4336 enum baseEmits = false; 4337 4338 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 4339 4340 enum classStaticallyEmits = thisEmits || baseEmits; 4341 } 4342 4343 /++ 4344 A helper to make widgets out of other native windows. 4345 4346 History: 4347 Factored out of OpenGlWidget on November 5, 2021 4348 +/ 4349 class NestedChildWindowWidget : Widget { 4350 SimpleWindow win; 4351 4352 /++ 4353 Used on X to send focus to the appropriate child window when requested by the window manager. 4354 4355 Normally returns its own nested window. Can also return another child or null to revert to the parent 4356 if you override it in a child class. 4357 4358 History: 4359 Added April 2, 2022 (dub v10.8) 4360 +/ 4361 SimpleWindow focusableWindow() { 4362 return win; 4363 } 4364 4365 /// 4366 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4367 this(SimpleWindow win, Widget parent) { 4368 this.parentWindow = parent.parentWindow; 4369 this.win = win; 4370 4371 super(parent); 4372 windowsetup(win); 4373 } 4374 4375 static protected SimpleWindow getParentWindow(Widget parent) { 4376 assert(parent !is null); 4377 SimpleWindow pwin = parent.parentWindow.win; 4378 4379 version(win32_widgets) { 4380 HWND phwnd; 4381 auto wtf = parent; 4382 while(wtf) { 4383 if(wtf.hwnd) { 4384 phwnd = wtf.hwnd; 4385 break; 4386 } 4387 wtf = wtf.parent; 4388 } 4389 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 4390 if(phwnd) 4391 pwin = new SimpleWindow(phwnd); 4392 } 4393 4394 return pwin; 4395 } 4396 4397 /++ 4398 Called upon the nested window being destroyed. 4399 Remember the window has already been destroyed at 4400 this point, so don't use the native handle for anything. 4401 4402 History: 4403 Added April 3, 2022 (dub v10.8) 4404 +/ 4405 protected void dispose() { 4406 4407 } 4408 4409 protected void windowsetup(SimpleWindow w) { 4410 /* 4411 win.onFocusChange = (bool getting) { 4412 if(getting) 4413 this.focus(); 4414 }; 4415 */ 4416 4417 /+ 4418 win.onFocusChange = (bool getting) { 4419 if(getting) { 4420 this.parentWindow.focusedWidget = this; 4421 this.emit!FocusEvent(); 4422 this.emit!FocusInEvent(); 4423 } else { 4424 this.emit!BlurEvent(); 4425 this.emit!FocusOutEvent(); 4426 } 4427 }; 4428 +/ 4429 4430 win.onDestroyed = () { 4431 this.dispose(); 4432 }; 4433 4434 version(win32_widgets) { 4435 Widget.nativeMapping[win.hwnd] = this; 4436 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4437 } else { 4438 win.setEventHandlers( 4439 (MouseEvent e) { 4440 Widget p = this; 4441 while(p ! is parentWindow) { 4442 e.x += p.x; 4443 e.y += p.y; 4444 p = p.parent; 4445 } 4446 parentWindow.dispatchMouseEvent(e); 4447 }, 4448 (KeyEvent e) { 4449 //import std.stdio; writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 4450 parentWindow.dispatchKeyEvent(e); 4451 }, 4452 (dchar e) { 4453 parentWindow.dispatchCharEvent(e); 4454 }, 4455 ); 4456 } 4457 4458 } 4459 4460 override void showing(bool s, bool recalc) { 4461 auto cur = hidden; 4462 win.hidden = !s; 4463 if(cur != s && s) 4464 redraw(); 4465 } 4466 4467 /// OpenGL widgets cannot have child widgets. Do not call this. 4468 /* @disable */ final override void addChild(Widget, int) { 4469 throw new Error("cannot add children to OpenGL widgets"); 4470 } 4471 4472 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 4473 /// Keep in mind that events like mouse coordinates are still relative to your size. 4474 override void registerMovement() { 4475 //import std.stdio; writefln("%d %d %d %d", x,y,width,height); 4476 version(win32_widgets) 4477 auto pos = getChildPositionRelativeToParentHwnd(this); 4478 else 4479 auto pos = getChildPositionRelativeToParentOrigin(this); 4480 win.moveResize(pos[0], pos[1], width, height); 4481 4482 registerMovementAdditionalWork(); 4483 sendResizeEvent(); 4484 } 4485 4486 abstract void registerMovementAdditionalWork(); 4487 } 4488 4489 /++ 4490 Nests an opengl capable window inside this window as a widget. 4491 4492 You may also just want to create an additional [SimpleWindow] with 4493 [OpenGlOptions.yes] yourself. 4494 4495 An OpenGL widget cannot have child widgets. It will throw if you try. 4496 +/ 4497 static if(OpenGlEnabled) 4498 class OpenGlWidget : NestedChildWindowWidget { 4499 4500 override void registerMovementAdditionalWork() { 4501 win.setAsCurrentOpenGlContext(); 4502 } 4503 4504 /// 4505 this(Widget parent) { 4506 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4507 super(win, parent); 4508 } 4509 4510 override void paint(WidgetPainter painter) { 4511 win.setAsCurrentOpenGlContext(); 4512 glViewport(0, 0, this.width, this.height); 4513 win.redrawOpenGlSceneNow(); 4514 } 4515 4516 void redrawOpenGlScene(void delegate() dg) { 4517 win.redrawOpenGlScene = dg; 4518 } 4519 } 4520 4521 /++ 4522 This demo shows how to draw text in an opengl scene. 4523 +/ 4524 unittest { 4525 import arsd.minigui; 4526 import arsd.ttf; 4527 4528 void main() { 4529 auto window = new Window(); 4530 4531 auto widget = new OpenGlWidget(window); 4532 4533 // old means non-shader code so compatible with glBegin etc. 4534 // tbh I haven't implemented new one in font yet... 4535 // anyway, declaring here, will construct soon. 4536 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 4537 4538 // this is a little bit awkward, calling some methods through 4539 // the underlying SimpleWindow `win` method, and you can't do this 4540 // on a nanovega widget due to conflicts so I should probably fix 4541 // the api to be a bit easier. But here it will work. 4542 // 4543 // Alternatively, you could load the font on the first draw, inside 4544 // the redrawOpenGlScene, and keep a flag so you don't do it every 4545 // time. That'd be a bit easier since the lib sets up the context 4546 // by then guaranteed. 4547 // 4548 // But still, I wanna show this. 4549 widget.win.visibleForTheFirstTime = delegate { 4550 // must set the opengl context 4551 widget.win.setAsCurrentOpenGlContext(); 4552 4553 // if you were doing a OpenGL 3+ shader, this 4554 // gets especially important to do in order. With 4555 // old-style opengl, I think you can even do it 4556 // in main(), but meh, let's show it more correctly. 4557 4558 // Anyway, now it is time to load the font from the 4559 // OS (you can alternatively load one from a .ttf file 4560 // you bundle with the application), then load the 4561 // font into texture for drawing. 4562 4563 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 4564 4565 assert(!osfont.isNull()); // make sure it actually loaded 4566 4567 // using typeof to avoid repeating the long name lol 4568 glfont = new typeof(glfont)( 4569 // get the raw data from the font for loading in here 4570 // since it doesn't use the OS function to draw the 4571 // text, we gotta treat it more as a file than as 4572 // a drawing api. 4573 osfont.getTtfBytes(), 4574 18, // need to respecify size since opengl world is different coordinate system 4575 4576 // these last two numbers are why it is called 4577 // "Limited" font. It only loads the characters 4578 // in the given range, since the texture atlas 4579 // it references is all a big image generated ahead 4580 // of time. You could maybe do the whole thing but 4581 // idk how much memory that is. 4582 // 4583 // But here, 0-128 represents the ASCII range, so 4584 // good enough for most English things, numeric labels, 4585 // etc. 4586 0, 4587 128 4588 ); 4589 }; 4590 4591 widget.redrawOpenGlScene = () { 4592 // now we can use the glfont's drawString function 4593 4594 // first some opengl setup. You can do this in one place 4595 // on window first visible too in many cases, just showing 4596 // here cuz it is easier for me. 4597 4598 // gonna need some alpha blending or it just looks awful 4599 glEnable(GL_BLEND); 4600 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 4601 glClearColor(0,0,0,0); 4602 glDepthFunc(GL_LEQUAL); 4603 4604 // Also need to enable 2d textures, since it draws the 4605 // font characters as images baked in 4606 glMatrixMode(GL_MODELVIEW); 4607 glLoadIdentity(); 4608 glDisable(GL_DEPTH_TEST); 4609 glEnable(GL_TEXTURE_2D); 4610 4611 // the orthographic matrix is best for 2d things like text 4612 // so let's set that up. This matrix makes the coordinates 4613 // in the opengl scene be one-to-one with the actual pixels 4614 // on screen. (Not necessarily best, you may wish to scale 4615 // things, but it does help keep fonts looking normal.) 4616 glMatrixMode(GL_PROJECTION); 4617 glLoadIdentity(); 4618 glOrtho(0, widget.width, widget.height, 0, 0, 1); 4619 4620 // you can do other glScale, glRotate, glTranslate, etc 4621 // to the matrix here of course if you want. 4622 4623 // note the x,y coordinates here are for the text baseline 4624 // NOT the upper-left corner. The baseline is like the line 4625 // in the notebook you write on. Most the letters are actually 4626 // above it, but some, like p and q, dip a bit below it. 4627 // 4628 // So if you're used to the upper left coordinate like the 4629 // rest of simpledisplay/minigui usually do, do the 4630 // y + glfont.ascent to bring it down a little. So this 4631 // example puts the string in the upper left of the window. 4632 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 4633 4634 // re color btw: the function sets a solid color internally, 4635 // but you actually COULD do your own thing for rainbow effects 4636 // and the sort if you wanted too, by pulling its guts out. 4637 // Just view its source for an idea of how it actually draws: 4638 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 4639 4640 // it gets a bit complicated with the character positioning, 4641 // but the opengl parts are fairly simple: bind a texture, 4642 // set the color, draw a quad for each letter. 4643 4644 4645 // the last optional argument there btw is a bounding box 4646 // it will/ use to word wrap and return an object you can 4647 // use to implement scrolling or pagination; it tells how 4648 // much of the string didn't fit in the box. But for simple 4649 // labels we can just ignore that. 4650 4651 4652 // I'd suggest drawing text as the last step, after you 4653 // do your other drawing. You might use the push/pop matrix 4654 // stuff to keep your place. You, in theory, should be able 4655 // to do text in a 3d space but I've never actually tried 4656 // that.... 4657 }; 4658 4659 window.loop(); 4660 } 4661 } 4662 4663 version(custom_widgets) 4664 private alias ListWidgetBase = ScrollableWidget; 4665 else 4666 private alias ListWidgetBase = Widget; 4667 4668 /++ 4669 A list widget contains a list of strings that the user can examine and select. 4670 4671 4672 In the future, items in the list may be possible to be more than just strings. 4673 4674 See_Also: 4675 [TableView] 4676 +/ 4677 class ListWidget : ListWidgetBase { 4678 /// 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. 4679 mixin Emits!(ChangeEvent!void); 4680 4681 static struct Option { 4682 string label; 4683 bool selected; 4684 void* tag; 4685 } 4686 4687 /++ 4688 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 4689 +/ 4690 void setSelection(int y) { 4691 if(!multiSelect) 4692 foreach(ref opt; options) 4693 opt.selected = false; 4694 if(y >= 0 && y < options.length) 4695 options[y].selected = !options[y].selected; 4696 4697 this.emit!(ChangeEvent!void)(delegate {}); 4698 4699 version(custom_widgets) 4700 redraw(); 4701 } 4702 4703 /++ 4704 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 4705 Returns -1 if nothing is selected. 4706 +/ 4707 int getSelection() 4708 { 4709 foreach(i, opt; options) { 4710 if (opt.selected) 4711 return cast(int) i; 4712 } 4713 return -1; 4714 } 4715 4716 version(custom_widgets) 4717 override void defaultEventHandler_click(ClickEvent event) { 4718 this.focus(); 4719 if(event.button == MouseButton.left) { 4720 auto y = (event.clientY - 4) / defaultLineHeight; 4721 if(y >= 0 && y < options.length) { 4722 setSelection(y); 4723 } 4724 } 4725 super.defaultEventHandler_click(event); 4726 } 4727 4728 this(Widget parent) { 4729 tabStop = false; 4730 super(parent); 4731 version(win32_widgets) 4732 createWin32Window(this, WC_LISTBOX, "", 4733 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 4734 } 4735 4736 version(win32_widgets) 4737 override void handleWmCommand(ushort code, ushort id) { 4738 switch(code) { 4739 case LBN_SELCHANGE: 4740 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 4741 setSelection(cast(int) sel); 4742 break; 4743 default: 4744 } 4745 } 4746 4747 4748 version(custom_widgets) 4749 override void paintFrameAndBackground(WidgetPainter painter) { 4750 draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor); 4751 } 4752 4753 version(custom_widgets) 4754 override void paint(WidgetPainter painter) { 4755 auto cs = getComputedStyle(); 4756 auto pos = Point(4, 4); 4757 foreach(idx, option; options) { 4758 painter.fillColor = painter.visualTheme.widgetBackgroundColor; 4759 painter.outlineColor = painter.visualTheme.widgetBackgroundColor; 4760 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4761 if(option.selected) { 4762 //painter.rasterOp = RasterOp.xor; 4763 painter.outlineColor = cs.selectionForegroundColor; 4764 painter.fillColor = cs.selectionBackgroundColor; 4765 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4766 //painter.rasterOp = RasterOp.normal; 4767 } 4768 painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor; 4769 painter.drawText(pos, option.label); 4770 pos.y += defaultLineHeight; 4771 } 4772 } 4773 4774 static class Style : Widget.Style { 4775 override WidgetBackground background() { 4776 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 4777 } 4778 } 4779 mixin OverrideStyle!Style; 4780 //mixin Padding!q{2}; 4781 4782 void addOption(string text, void* tag = null) { 4783 options ~= Option(text, false, tag); 4784 version(win32_widgets) { 4785 WCharzBuffer buffer = WCharzBuffer(text); 4786 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 4787 } 4788 version(custom_widgets) { 4789 setContentSize(width, cast(int) (options.length * defaultLineHeight)); 4790 redraw(); 4791 } 4792 } 4793 4794 void clear() { 4795 options = null; 4796 version(win32_widgets) { 4797 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 4798 {} 4799 4800 } else version(custom_widgets) { 4801 scrollTo(Point(0, 0)); 4802 redraw(); 4803 } 4804 } 4805 4806 Option[] options; 4807 version(win32_widgets) 4808 enum multiSelect = false; /// not implemented yet 4809 else 4810 bool multiSelect; 4811 4812 override int heightStretchiness() { return 6; } 4813 } 4814 4815 4816 4817 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 4818 enum ScrollBarShowPolicy { 4819 automatic, /// automatically show the scroll bar if it is necessary 4820 never, /// never show the scroll bar (scrolling must be done programmatically) 4821 always /// always show the scroll bar, even if it is disabled 4822 } 4823 4824 /++ 4825 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 4826 4827 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 4828 +/ 4829 // FIXME ScrollBarShowPolicy 4830 // FIXME: use the ScrollMessageWidget in here now that it exists 4831 class ScrollableWidget : Widget { 4832 // FIXME: make line size configurable 4833 // FIXME: add keyboard controls 4834 version(win32_widgets) { 4835 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 4836 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 4837 auto pos = HIWORD(wParam); 4838 auto m = LOWORD(wParam); 4839 4840 // FIXME: I can reintroduce the 4841 // scroll bars now by using this 4842 // in the top-level window handler 4843 // to forward comamnds 4844 auto scrollbarHwnd = lParam; 4845 switch(m) { 4846 case SB_BOTTOM: 4847 if(msg == WM_HSCROLL) 4848 horizontalScrollTo(contentWidth_); 4849 else 4850 verticalScrollTo(contentHeight_); 4851 break; 4852 case SB_TOP: 4853 if(msg == WM_HSCROLL) 4854 horizontalScrollTo(0); 4855 else 4856 verticalScrollTo(0); 4857 break; 4858 case SB_ENDSCROLL: 4859 // idk 4860 break; 4861 case SB_LINEDOWN: 4862 if(msg == WM_HSCROLL) 4863 horizontalScroll(scaleWithDpi(16)); 4864 else 4865 verticalScroll(scaleWithDpi(16)); 4866 break; 4867 case SB_LINEUP: 4868 if(msg == WM_HSCROLL) 4869 horizontalScroll(scaleWithDpi(-16)); 4870 else 4871 verticalScroll(scaleWithDpi(-16)); 4872 break; 4873 case SB_PAGEDOWN: 4874 if(msg == WM_HSCROLL) 4875 horizontalScroll(scaleWithDpi(100)); 4876 else 4877 verticalScroll(scaleWithDpi(100)); 4878 break; 4879 case SB_PAGEUP: 4880 if(msg == WM_HSCROLL) 4881 horizontalScroll(scaleWithDpi(-100)); 4882 else 4883 verticalScroll(scaleWithDpi(-100)); 4884 break; 4885 case SB_THUMBPOSITION: 4886 case SB_THUMBTRACK: 4887 if(msg == WM_HSCROLL) 4888 horizontalScrollTo(pos); 4889 else 4890 verticalScrollTo(pos); 4891 4892 if(m == SB_THUMBTRACK) { 4893 // the event loop doesn't seem to carry on with a requested redraw.. 4894 // so we request it to get our dirty bit set... 4895 redraw(); 4896 4897 // then we need to immediately actually redraw it too for instant feedback to user 4898 4899 SimpleWindow.processAllCustomEvents(); 4900 //if(parentWindow) 4901 //parentWindow.actualRedraw(); 4902 } 4903 break; 4904 default: 4905 } 4906 } 4907 return super.hookedWndProc(msg, wParam, lParam); 4908 } 4909 } 4910 /// 4911 this(Widget parent) { 4912 this.parentWindow = parent.parentWindow; 4913 4914 version(win32_widgets) { 4915 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 4916 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 4917 super(parent); 4918 } else version(custom_widgets) { 4919 outerContainer = new InternalScrollableContainerWidget(this, parent); 4920 super(outerContainer); 4921 } else static assert(0); 4922 } 4923 4924 version(custom_widgets) 4925 InternalScrollableContainerWidget outerContainer; 4926 4927 override void defaultEventHandler_click(ClickEvent event) { 4928 if(event.button == MouseButton.wheelUp) 4929 verticalScroll(scaleWithDpi(-16)); 4930 if(event.button == MouseButton.wheelDown) 4931 verticalScroll(scaleWithDpi(16)); 4932 super.defaultEventHandler_click(event); 4933 } 4934 4935 override void defaultEventHandler_keydown(KeyDownEvent event) { 4936 switch(event.key) { 4937 case Key.Left: 4938 horizontalScroll(scaleWithDpi(-16)); 4939 break; 4940 case Key.Right: 4941 horizontalScroll(scaleWithDpi(16)); 4942 break; 4943 case Key.Up: 4944 verticalScroll(scaleWithDpi(-16)); 4945 break; 4946 case Key.Down: 4947 verticalScroll(scaleWithDpi(16)); 4948 break; 4949 case Key.Home: 4950 verticalScrollTo(0); 4951 break; 4952 case Key.End: 4953 verticalScrollTo(contentHeight); 4954 break; 4955 case Key.PageUp: 4956 verticalScroll(scaleWithDpi(-160)); 4957 break; 4958 case Key.PageDown: 4959 verticalScroll(scaleWithDpi(160)); 4960 break; 4961 default: 4962 } 4963 super.defaultEventHandler_keydown(event); 4964 } 4965 4966 4967 version(win32_widgets) 4968 override void recomputeChildLayout() { 4969 super.recomputeChildLayout(); 4970 SCROLLINFO info; 4971 info.cbSize = info.sizeof; 4972 info.nPage = viewportHeight; 4973 info.fMask = SIF_PAGE | SIF_RANGE; 4974 info.nMin = 0; 4975 info.nMax = contentHeight_; 4976 SetScrollInfo(hwnd, SB_VERT, &info, true); 4977 4978 info.cbSize = info.sizeof; 4979 info.nPage = viewportWidth; 4980 info.fMask = SIF_PAGE | SIF_RANGE; 4981 info.nMin = 0; 4982 info.nMax = contentWidth_; 4983 SetScrollInfo(hwnd, SB_HORZ, &info, true); 4984 } 4985 4986 /* 4987 Scrolling 4988 ------------ 4989 4990 You are assigned a width and a height by the layout engine, which 4991 is your viewport box. However, you may draw more than that by setting 4992 a contentWidth and contentHeight. 4993 4994 If these can be contained by the viewport, no scrollbar is displayed. 4995 If they cannot fit though, it will automatically show scroll as necessary. 4996 4997 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 4998 is zero, no vertical scrolling is performed. 4999 5000 If scrolling is necessary, the lib will automatically work with the bars. 5001 When you redraw, the origin and clipping info in the painter is set so if 5002 you just draw everything, it will work, but you can be more efficient by checking 5003 the viewportWidth, viewportHeight, and scrollOrigin members. 5004 */ 5005 5006 /// 5007 final @property int viewportWidth() { 5008 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 5009 } 5010 /// 5011 final @property int viewportHeight() { 5012 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 5013 } 5014 5015 // FIXME property 5016 Point scrollOrigin_; 5017 5018 /// 5019 final const(Point) scrollOrigin() { 5020 return scrollOrigin_; 5021 } 5022 5023 // the user sets these two 5024 private int contentWidth_ = 0; 5025 private int contentHeight_ = 0; 5026 5027 /// 5028 int contentWidth() { return contentWidth_; } 5029 /// 5030 int contentHeight() { return contentHeight_; } 5031 5032 /// 5033 void setContentSize(int width, int height) { 5034 contentWidth_ = width; 5035 contentHeight_ = height; 5036 5037 version(custom_widgets) { 5038 if(showingVerticalScroll || showingHorizontalScroll) { 5039 outerContainer.recomputeChildLayout(); 5040 } 5041 5042 if(showingVerticalScroll()) 5043 outerContainer.verticalScrollBar.redraw(); 5044 if(showingHorizontalScroll()) 5045 outerContainer.horizontalScrollBar.redraw(); 5046 } else version(win32_widgets) { 5047 recomputeChildLayout(); 5048 } else static assert(0); 5049 } 5050 5051 /// 5052 void verticalScroll(int delta) { 5053 verticalScrollTo(scrollOrigin.y + delta); 5054 } 5055 /// 5056 void verticalScrollTo(int pos) { 5057 scrollOrigin_.y = pos; 5058 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 5059 scrollOrigin_.y = contentHeight - viewportHeight; 5060 5061 if(scrollOrigin_.y < 0) 5062 scrollOrigin_.y = 0; 5063 5064 version(win32_widgets) { 5065 SCROLLINFO info; 5066 info.cbSize = info.sizeof; 5067 info.fMask = SIF_POS; 5068 info.nPos = scrollOrigin_.y; 5069 SetScrollInfo(hwnd, SB_VERT, &info, true); 5070 } else version(custom_widgets) { 5071 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 5072 } else static assert(0); 5073 5074 redraw(); 5075 } 5076 5077 /// 5078 void horizontalScroll(int delta) { 5079 horizontalScrollTo(scrollOrigin.x + delta); 5080 } 5081 /// 5082 void horizontalScrollTo(int pos) { 5083 scrollOrigin_.x = pos; 5084 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 5085 scrollOrigin_.x = contentWidth - viewportWidth; 5086 5087 if(scrollOrigin_.x < 0) 5088 scrollOrigin_.x = 0; 5089 5090 version(win32_widgets) { 5091 SCROLLINFO info; 5092 info.cbSize = info.sizeof; 5093 info.fMask = SIF_POS; 5094 info.nPos = scrollOrigin_.x; 5095 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5096 } else version(custom_widgets) { 5097 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 5098 } else static assert(0); 5099 5100 redraw(); 5101 } 5102 /// 5103 void scrollTo(Point p) { 5104 verticalScrollTo(p.y); 5105 horizontalScrollTo(p.x); 5106 } 5107 5108 /// 5109 void ensureVisibleInScroll(Point p) { 5110 auto rect = viewportRectangle(); 5111 if(rect.contains(p)) 5112 return; 5113 if(p.x < rect.left) 5114 horizontalScroll(p.x - rect.left); 5115 else if(p.x > rect.right) 5116 horizontalScroll(p.x - rect.right); 5117 5118 if(p.y < rect.top) 5119 verticalScroll(p.y - rect.top); 5120 else if(p.y > rect.bottom) 5121 verticalScroll(p.y - rect.bottom); 5122 } 5123 5124 /// 5125 void ensureVisibleInScroll(Rectangle rect) { 5126 ensureVisibleInScroll(rect.upperLeft); 5127 ensureVisibleInScroll(rect.lowerRight); 5128 } 5129 5130 /// 5131 Rectangle viewportRectangle() { 5132 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 5133 } 5134 5135 /// 5136 bool showingHorizontalScroll() { 5137 return contentWidth > width; 5138 } 5139 /// 5140 bool showingVerticalScroll() { 5141 return contentHeight > height; 5142 } 5143 5144 /// This is called before the ordinary paint delegate, 5145 /// giving you a chance to draw the window frame, etc, 5146 /// before the scroll clip takes effect 5147 void paintFrameAndBackground(WidgetPainter painter) { 5148 version(win32_widgets) { 5149 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5150 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5151 // since the pen is null, to fill the whole space, we need the +1 on both. 5152 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5153 SelectObject(painter.impl.hdc, p); 5154 SelectObject(painter.impl.hdc, b); 5155 } 5156 5157 } 5158 5159 // make space for the scroll bar, and that's it. 5160 final override int paddingRight() { return scaleWithDpi(16); } 5161 final override int paddingBottom() { return scaleWithDpi(16); } 5162 5163 /* 5164 END SCROLLING 5165 */ 5166 5167 override WidgetPainter draw() { 5168 int x = this.x, y = this.y; 5169 auto parent = this.parent; 5170 while(parent) { 5171 x += parent.x; 5172 y += parent.y; 5173 parent = parent.parent; 5174 } 5175 5176 //version(win32_widgets) { 5177 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5178 //} else { 5179 auto painter = parentWindow.win.draw(true); 5180 //} 5181 painter.originX = x; 5182 painter.originY = y; 5183 5184 painter.originX = painter.originX - scrollOrigin.x; 5185 painter.originY = painter.originY - scrollOrigin.y; 5186 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 5187 5188 return WidgetPainter(painter, this); 5189 } 5190 5191 mixin ScrollableChildren; 5192 } 5193 5194 // you need to have a Point scrollOrigin in the class somewhere 5195 // and a paintFrameAndBackground 5196 private mixin template ScrollableChildren() { 5197 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5198 if(hidden) 5199 return; 5200 5201 //version(win32_widgets) 5202 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5203 5204 painter.originX = lox + x; 5205 painter.originY = loy + y; 5206 5207 bool actuallyPainted = false; 5208 5209 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 5210 if(clip == Rectangle.init) 5211 return; 5212 5213 if(force || redrawRequested) { 5214 //painter.setClipRectangle(scrollOrigin, width, height); 5215 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5216 paintFrameAndBackground(painter); 5217 } 5218 5219 painter.originX = painter.originX - scrollOrigin.x; 5220 painter.originY = painter.originY - scrollOrigin.y; 5221 if(force || redrawRequested) { 5222 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 5223 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 5224 5225 //erase(painter); // we paintFrameAndBackground above so no need 5226 if(painter.visualTheme) 5227 painter.visualTheme.doPaint(this, painter); 5228 else 5229 paint(painter); 5230 5231 if(invalidate) { 5232 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5233 // children are contained inside this, so no need to do extra work 5234 invalidate = false; 5235 } 5236 5237 5238 actuallyPainted = true; 5239 redrawRequested = false; 5240 } 5241 foreach(child; children) { 5242 if(cast(FixedPosition) child) 5243 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5244 else 5245 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5246 } 5247 } 5248 } 5249 5250 private class InternalScrollableContainerInsideWidget : ContainerWidget { 5251 ScrollableContainerWidget scw; 5252 5253 this(ScrollableContainerWidget parent) { 5254 scw = parent; 5255 super(parent); 5256 } 5257 5258 version(custom_widgets) 5259 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5260 if(hidden) 5261 return; 5262 5263 bool actuallyPainted = false; 5264 5265 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 5266 5267 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 5268 if(clip == Rectangle.init) 5269 return; 5270 5271 painter.originX = lox + x - scrollOrigin.x; 5272 painter.originY = loy + y - scrollOrigin.y; 5273 if(force || redrawRequested) { 5274 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5275 5276 erase(painter); 5277 if(painter.visualTheme) 5278 painter.visualTheme.doPaint(this, painter); 5279 else 5280 paint(painter); 5281 5282 if(invalidate) { 5283 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5284 // children are contained inside this, so no need to do extra work 5285 invalidate = false; 5286 } 5287 5288 actuallyPainted = true; 5289 redrawRequested = false; 5290 } 5291 foreach(child; children) { 5292 if(cast(FixedPosition) child) 5293 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5294 else 5295 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5296 } 5297 } 5298 5299 version(custom_widgets) 5300 override protected void addScrollPosition(ref int x, ref int y) { 5301 x += scw.scrollX_; 5302 y += scw.scrollY_; 5303 } 5304 } 5305 5306 /++ 5307 A widget meant to contain other widgets that may need to scroll. 5308 5309 Currently buggy. 5310 5311 History: 5312 Added July 1, 2021 (dub v10.2) 5313 5314 On January 3, 2022, I tried to use it in a few other cases 5315 and found it only worked well in the original test case. Since 5316 it still sucks, I think I'm going to rewrite it again. 5317 +/ 5318 class ScrollableContainerWidget : ContainerWidget { 5319 /// 5320 this(Widget parent) { 5321 super(parent); 5322 5323 container = new InternalScrollableContainerInsideWidget(this); 5324 hsb = new HorizontalScrollbar(this); 5325 vsb = new VerticalScrollbar(this); 5326 5327 tabStop = false; 5328 container.tabStop = false; 5329 magic = true; 5330 5331 5332 vsb.addEventListener("scrolltonextline", () { 5333 scrollBy(0, scaleWithDpi(16)); 5334 }); 5335 vsb.addEventListener("scrolltopreviousline", () { 5336 scrollBy(0,scaleWithDpi( -16)); 5337 }); 5338 vsb.addEventListener("scrolltonextpage", () { 5339 scrollBy(0, container.height); 5340 }); 5341 vsb.addEventListener("scrolltopreviouspage", () { 5342 scrollBy(0, -container.height); 5343 }); 5344 vsb.addEventListener((scope ScrollToPositionEvent spe) { 5345 scrollTo(scrollX_, spe.value); 5346 }); 5347 5348 this.addEventListener(delegate (scope ClickEvent e) { 5349 if(e.button == MouseButton.wheelUp) { 5350 if(!e.defaultPrevented) 5351 scrollBy(0, scaleWithDpi(-16)); 5352 e.stopPropagation(); 5353 } else if(e.button == MouseButton.wheelDown) { 5354 if(!e.defaultPrevented) 5355 scrollBy(0, scaleWithDpi(16)); 5356 e.stopPropagation(); 5357 } 5358 }); 5359 } 5360 5361 /+ 5362 override void defaultEventHandler_click(ClickEvent e) { 5363 } 5364 +/ 5365 5366 override void removeAllChildren() { 5367 container.removeAllChildren(); 5368 } 5369 5370 void scrollTo(int x, int y) { 5371 scrollBy(x - scrollX_, y - scrollY_); 5372 } 5373 5374 void scrollBy(int x, int y) { 5375 auto ox = scrollX_; 5376 auto oy = scrollY_; 5377 5378 auto nx = ox + x; 5379 auto ny = oy + y; 5380 5381 if(nx < 0) 5382 nx = 0; 5383 if(ny < 0) 5384 ny = 0; 5385 5386 auto maxX = hsb.max - container.width; 5387 if(maxX < 0) maxX = 0; 5388 auto maxY = vsb.max - container.height; 5389 if(maxY < 0) maxY = 0; 5390 5391 if(nx > maxX) 5392 nx = maxX; 5393 if(ny > maxY) 5394 ny = maxY; 5395 5396 auto dx = nx - ox; 5397 auto dy = ny - oy; 5398 5399 if(dx || dy) { 5400 version(win32_widgets) 5401 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 5402 else { 5403 redraw(); 5404 } 5405 5406 hsb.setPosition = nx; 5407 vsb.setPosition = ny; 5408 5409 scrollX_ = nx; 5410 scrollY_ = ny; 5411 } 5412 } 5413 5414 private int scrollX_; 5415 private int scrollY_; 5416 5417 void setTotalArea(int width, int height) { 5418 hsb.setMax(width); 5419 vsb.setMax(height); 5420 } 5421 5422 /// 5423 void setViewableArea(int width, int height) { 5424 hsb.setViewableArea(width); 5425 vsb.setViewableArea(height); 5426 } 5427 5428 private bool magic; 5429 override void addChild(Widget w, int position = int.max) { 5430 if(magic) 5431 container.addChild(w, position); 5432 else 5433 super.addChild(w, position); 5434 } 5435 5436 override void recomputeChildLayout() { 5437 if(hsb is null || vsb is null || container is null) return; 5438 5439 /+ 5440 import std.stdio; writeln(x, " ", y , " ", width, " ", height); 5441 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 5442 +/ 5443 5444 registerMovement(); 5445 5446 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 5447 hsb.x = 0; 5448 hsb.y = this.height - hsb.height; 5449 hsb.width = this.width - scaleWithDpi(16); 5450 hsb.recomputeChildLayout(); 5451 5452 vsb.width = scaleWithDpi(16); // FIXME? 5453 vsb.x = this.width - vsb.width; 5454 vsb.y = 0; 5455 vsb.height = this.height - scaleWithDpi(16); 5456 vsb.recomputeChildLayout(); 5457 5458 container.x = 0; 5459 container.y = 0; 5460 container.width = this.width - vsb.width; 5461 container.height = this.height - hsb.height; 5462 container.recomputeChildLayout(); 5463 5464 scrollX_ = 0; 5465 scrollY_ = 0; 5466 5467 hsb.setPosition(0); 5468 vsb.setPosition(0); 5469 5470 int mw, mh; 5471 Widget c = container; 5472 // FIXME: hack here to handle a layout inside... 5473 if(c.children.length == 1 && cast(Layout) c.children[0]) 5474 c = c.children[0]; 5475 foreach(child; c.children) { 5476 auto w = child.x + child.width; 5477 auto h = child.y + child.height; 5478 5479 if(w > mw) mw = w; 5480 if(h > mh) mh = h; 5481 } 5482 5483 setTotalArea(mw, mh); 5484 setViewableArea(width, height); 5485 } 5486 5487 override int minHeight() { return scaleWithDpi(64); } 5488 5489 HorizontalScrollbar hsb; 5490 VerticalScrollbar vsb; 5491 ContainerWidget container; 5492 } 5493 5494 5495 version(custom_widgets) 5496 private class InternalScrollableContainerWidget : Widget { 5497 5498 ScrollableWidget sw; 5499 5500 VerticalScrollbar verticalScrollBar; 5501 HorizontalScrollbar horizontalScrollBar; 5502 5503 this(ScrollableWidget sw, Widget parent) { 5504 this.sw = sw; 5505 5506 this.tabStop = false; 5507 5508 horizontalScrollBar = new HorizontalScrollbar(this); 5509 verticalScrollBar = new VerticalScrollbar(this); 5510 5511 horizontalScrollBar.showing_ = false; 5512 verticalScrollBar.showing_ = false; 5513 5514 horizontalScrollBar.addEventListener("scrolltonextline", { 5515 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 5516 sw.horizontalScrollTo(horizontalScrollBar.position); 5517 }); 5518 horizontalScrollBar.addEventListener("scrolltopreviousline", { 5519 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 5520 sw.horizontalScrollTo(horizontalScrollBar.position); 5521 }); 5522 verticalScrollBar.addEventListener("scrolltonextline", { 5523 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 5524 sw.verticalScrollTo(verticalScrollBar.position); 5525 }); 5526 verticalScrollBar.addEventListener("scrolltopreviousline", { 5527 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 5528 sw.verticalScrollTo(verticalScrollBar.position); 5529 }); 5530 horizontalScrollBar.addEventListener("scrolltonextpage", { 5531 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 5532 sw.horizontalScrollTo(horizontalScrollBar.position); 5533 }); 5534 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 5535 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 5536 sw.horizontalScrollTo(horizontalScrollBar.position); 5537 }); 5538 verticalScrollBar.addEventListener("scrolltonextpage", { 5539 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 5540 sw.verticalScrollTo(verticalScrollBar.position); 5541 }); 5542 verticalScrollBar.addEventListener("scrolltopreviouspage", { 5543 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 5544 sw.verticalScrollTo(verticalScrollBar.position); 5545 }); 5546 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 5547 horizontalScrollBar.setPosition(event.intValue); 5548 sw.horizontalScrollTo(horizontalScrollBar.position); 5549 }); 5550 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 5551 verticalScrollBar.setPosition(event.intValue); 5552 sw.verticalScrollTo(verticalScrollBar.position); 5553 }); 5554 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 5555 horizontalScrollBar.setPosition(event.intValue); 5556 sw.horizontalScrollTo(horizontalScrollBar.position); 5557 }); 5558 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 5559 verticalScrollBar.setPosition(event.intValue); 5560 }); 5561 5562 super(parent); 5563 } 5564 5565 // this is supposed to be basically invisible... 5566 override int minWidth() { return sw.minWidth; } 5567 override int minHeight() { return sw.minHeight; } 5568 override int maxWidth() { return sw.maxWidth; } 5569 override int maxHeight() { return sw.maxHeight; } 5570 override int widthStretchiness() { return sw.widthStretchiness; } 5571 override int heightStretchiness() { return sw.heightStretchiness; } 5572 override int marginLeft() { return sw.marginLeft; } 5573 override int marginRight() { return sw.marginRight; } 5574 override int marginTop() { return sw.marginTop; } 5575 override int marginBottom() { return sw.marginBottom; } 5576 override int paddingLeft() { return sw.paddingLeft; } 5577 override int paddingRight() { return sw.paddingRight; } 5578 override int paddingTop() { return sw.paddingTop; } 5579 override int paddingBottom() { return sw.paddingBottom; } 5580 override void focus() { sw.focus(); } 5581 5582 5583 override void recomputeChildLayout() { 5584 // The stupid thing needs to calculate if a scroll bar is needed... 5585 recomputeChildLayoutHelper(); 5586 // then running it again will position things correctly if the bar is NOT needed 5587 recomputeChildLayoutHelper(); 5588 5589 // this sucks but meh it barely works 5590 } 5591 5592 private void recomputeChildLayoutHelper() { 5593 if(sw is null) return; 5594 5595 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 5596 if(horizontalScrollBar && verticalScrollBar) { 5597 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 5598 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 5599 horizontalScrollBar.x = 0; 5600 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 5601 5602 verticalScrollBar.width = verticalScrollBar.minWidth(); 5603 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 5604 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 5605 verticalScrollBar.y = 0 + 2; 5606 5607 sw.x = 0; 5608 sw.y = 0; 5609 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 5610 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 5611 5612 if(sw.contentWidth_ <= this.width) 5613 sw.scrollOrigin_.x = 0; 5614 if(sw.contentHeight_ <= this.height) 5615 sw.scrollOrigin_.y = 0; 5616 5617 horizontalScrollBar.recomputeChildLayout(); 5618 verticalScrollBar.recomputeChildLayout(); 5619 sw.recomputeChildLayout(); 5620 } 5621 5622 if(sw.contentWidth_ <= this.width) 5623 sw.scrollOrigin_.x = 0; 5624 if(sw.contentHeight_ <= this.height) 5625 sw.scrollOrigin_.y = 0; 5626 5627 if(sw.showingHorizontalScroll()) 5628 horizontalScrollBar.showing(true, false); 5629 else 5630 horizontalScrollBar.showing(false, false); 5631 if(sw.showingVerticalScroll()) 5632 verticalScrollBar.showing(true, false); 5633 else 5634 verticalScrollBar.showing(false, false); 5635 5636 verticalScrollBar.setViewableArea(sw.viewportHeight()); 5637 verticalScrollBar.setMax(sw.contentHeight); 5638 verticalScrollBar.setPosition(sw.scrollOrigin.y); 5639 5640 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 5641 horizontalScrollBar.setMax(sw.contentWidth); 5642 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 5643 } 5644 } 5645 5646 /* 5647 class ScrollableClientWidget : Widget { 5648 this(Widget parent) { 5649 super(parent); 5650 } 5651 override void paint(WidgetPainter p) { 5652 parent.paint(p); 5653 } 5654 } 5655 */ 5656 5657 /++ 5658 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. 5659 +/ 5660 abstract class Slider : Widget { 5661 this(int min, int max, int step, Widget parent) { 5662 min_ = min; 5663 max_ = max; 5664 step_ = step; 5665 page_ = step; 5666 super(parent); 5667 } 5668 5669 private int min_; 5670 private int max_; 5671 private int step_; 5672 private int position_; 5673 private int page_; 5674 5675 // selection start and selection end 5676 // tics 5677 // tooltip? 5678 // some way to see and just type the value 5679 // win32 buddy controls are labels 5680 5681 /// 5682 void setMin(int a) { 5683 min_ = a; 5684 version(custom_widgets) 5685 redraw(); 5686 version(win32_widgets) 5687 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 5688 } 5689 /// 5690 int min() { 5691 return min_; 5692 } 5693 /// 5694 void setMax(int a) { 5695 max_ = a; 5696 version(custom_widgets) 5697 redraw(); 5698 version(win32_widgets) 5699 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 5700 } 5701 /// 5702 int max() { 5703 return max_; 5704 } 5705 /// 5706 void setPosition(int a) { 5707 if(a > max) 5708 a = max; 5709 if(a < min) 5710 a = min; 5711 position_ = a; 5712 version(custom_widgets) 5713 setPositionCustom(a); 5714 5715 version(win32_widgets) 5716 setPositionWindows(a); 5717 } 5718 version(win32_widgets) { 5719 protected abstract void setPositionWindows(int a); 5720 } 5721 5722 protected abstract int win32direction(); 5723 5724 /++ 5725 Alias for [position] for better compatibility with generic code. 5726 5727 History: 5728 Added October 5, 2021 5729 +/ 5730 @property int value() { 5731 return position; 5732 } 5733 5734 /// 5735 int position() { 5736 return position_; 5737 } 5738 /// 5739 void setStep(int a) { 5740 step_ = a; 5741 version(win32_widgets) 5742 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 5743 } 5744 /// 5745 int step() { 5746 return step_; 5747 } 5748 /// 5749 void setPageSize(int a) { 5750 page_ = a; 5751 version(win32_widgets) 5752 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 5753 } 5754 /// 5755 int pageSize() { 5756 return page_; 5757 } 5758 5759 private void notify() { 5760 auto event = new ChangeEvent!int(this, &this.position); 5761 event.dispatch(); 5762 } 5763 5764 version(win32_widgets) 5765 void win32Setup(int style) { 5766 createWin32Window(this, TRACKBAR_CLASS, "", 5767 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 5768 5769 // the trackbar sends the same messages as scroll, which 5770 // our other layer sends as these... just gonna translate 5771 // here 5772 this.addDirectEventListener("scrolltoposition", (Event event) { 5773 event.stopPropagation(); 5774 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 5775 notify(); 5776 }); 5777 this.addDirectEventListener("scrolltonextline", (Event event) { 5778 event.stopPropagation(); 5779 this.setPosition(this.position + this.step_ * this.win32direction); 5780 notify(); 5781 }); 5782 this.addDirectEventListener("scrolltopreviousline", (Event event) { 5783 event.stopPropagation(); 5784 this.setPosition(this.position - this.step_ * this.win32direction); 5785 notify(); 5786 }); 5787 this.addDirectEventListener("scrolltonextpage", (Event event) { 5788 event.stopPropagation(); 5789 this.setPosition(this.position + this.page_ * this.win32direction); 5790 notify(); 5791 }); 5792 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 5793 event.stopPropagation(); 5794 this.setPosition(this.position - this.page_ * this.win32direction); 5795 notify(); 5796 }); 5797 5798 setMin(min_); 5799 setMax(max_); 5800 setStep(step_); 5801 setPageSize(page_); 5802 } 5803 5804 version(custom_widgets) { 5805 protected MouseTrackingWidget thumb; 5806 5807 protected abstract void setPositionCustom(int a); 5808 5809 override void defaultEventHandler_keydown(KeyDownEvent event) { 5810 switch(event.key) { 5811 case Key.Up: 5812 case Key.Right: 5813 setPosition(position() - step() * win32direction); 5814 changed(); 5815 break; 5816 case Key.Down: 5817 case Key.Left: 5818 setPosition(position() + step() * win32direction); 5819 changed(); 5820 break; 5821 case Key.Home: 5822 setPosition(win32direction > 0 ? min() : max()); 5823 changed(); 5824 break; 5825 case Key.End: 5826 setPosition(win32direction > 0 ? max() : min()); 5827 changed(); 5828 break; 5829 case Key.PageUp: 5830 setPosition(position() - pageSize() * win32direction); 5831 changed(); 5832 break; 5833 case Key.PageDown: 5834 setPosition(position() + pageSize() * win32direction); 5835 changed(); 5836 break; 5837 default: 5838 } 5839 super.defaultEventHandler_keydown(event); 5840 } 5841 5842 protected void changed() { 5843 auto ev = new ChangeEvent!int(this, &position); 5844 ev.dispatch(); 5845 } 5846 } 5847 } 5848 5849 /++ 5850 5851 +/ 5852 class VerticalSlider : Slider { 5853 this(int min, int max, int step, Widget parent) { 5854 version(custom_widgets) 5855 initialize(); 5856 5857 super(min, max, step, parent); 5858 5859 version(win32_widgets) 5860 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 5861 } 5862 5863 protected override int win32direction() { 5864 return -1; 5865 } 5866 5867 version(win32_widgets) 5868 protected override void setPositionWindows(int a) { 5869 // the windows thing makes the top 0 and i don't like that. 5870 SendMessage(hwnd, TBM_SETPOS, true, max - a); 5871 } 5872 5873 version(custom_widgets) 5874 private void initialize() { 5875 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 5876 5877 thumb.tabStop = false; 5878 5879 thumb.thumbWidth = width; 5880 thumb.thumbHeight = scaleWithDpi(16); 5881 5882 thumb.addEventListener(EventType.change, () { 5883 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 5884 sx = max - sx; 5885 //informProgramThatUserChangedPosition(sx); 5886 5887 position_ = sx; 5888 5889 changed(); 5890 }); 5891 } 5892 5893 version(custom_widgets) 5894 override void recomputeChildLayout() { 5895 thumb.thumbWidth = this.width; 5896 super.recomputeChildLayout(); 5897 setPositionCustom(position_); 5898 } 5899 5900 version(custom_widgets) 5901 protected override void setPositionCustom(int a) { 5902 if(max()) 5903 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 5904 redraw(); 5905 } 5906 } 5907 5908 /++ 5909 5910 +/ 5911 class HorizontalSlider : Slider { 5912 this(int min, int max, int step, Widget parent) { 5913 version(custom_widgets) 5914 initialize(); 5915 5916 super(min, max, step, parent); 5917 5918 version(win32_widgets) 5919 win32Setup(TBS_HORZ); 5920 } 5921 5922 version(win32_widgets) 5923 protected override void setPositionWindows(int a) { 5924 SendMessage(hwnd, TBM_SETPOS, true, a); 5925 } 5926 5927 protected override int win32direction() { 5928 return 1; 5929 } 5930 5931 version(custom_widgets) 5932 private void initialize() { 5933 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 5934 5935 thumb.tabStop = false; 5936 5937 thumb.thumbWidth = scaleWithDpi(16); 5938 thumb.thumbHeight = height; 5939 5940 thumb.addEventListener(EventType.change, () { 5941 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 5942 //informProgramThatUserChangedPosition(sx); 5943 5944 position_ = sx; 5945 5946 changed(); 5947 }); 5948 } 5949 5950 version(custom_widgets) 5951 override void recomputeChildLayout() { 5952 thumb.thumbHeight = this.height; 5953 super.recomputeChildLayout(); 5954 setPositionCustom(position_); 5955 } 5956 5957 version(custom_widgets) 5958 protected override void setPositionCustom(int a) { 5959 if(max()) 5960 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 5961 redraw(); 5962 } 5963 } 5964 5965 5966 /// 5967 abstract class ScrollbarBase : Widget { 5968 /// 5969 this(Widget parent) { 5970 super(parent); 5971 tabStop = false; 5972 step_ = scaleWithDpi(16); 5973 } 5974 5975 private int viewableArea_; 5976 private int max_; 5977 private int step_;// = 16; 5978 private int position_; 5979 5980 /// 5981 bool atEnd() { 5982 return position_ + viewableArea_ >= max_; 5983 } 5984 5985 /// 5986 bool atStart() { 5987 return position_ == 0; 5988 } 5989 5990 /// 5991 void setViewableArea(int a) { 5992 viewableArea_ = a; 5993 version(custom_widgets) 5994 redraw(); 5995 } 5996 /// 5997 void setMax(int a) { 5998 max_ = a; 5999 version(custom_widgets) 6000 redraw(); 6001 } 6002 /// 6003 int max() { 6004 return max_; 6005 } 6006 /// 6007 void setPosition(int a) { 6008 auto logicalMax = max_ - viewableArea_; 6009 if(a == int.max) 6010 a = logicalMax; 6011 6012 if(a > logicalMax) 6013 a = logicalMax; 6014 if(a < 0) 6015 a = 0; 6016 6017 position_ = a; 6018 6019 version(custom_widgets) 6020 redraw(); 6021 } 6022 /// 6023 int position() { 6024 return position_; 6025 } 6026 /// 6027 void setStep(int a) { 6028 step_ = a; 6029 } 6030 /// 6031 int step() { 6032 return step_; 6033 } 6034 6035 // FIXME: remove this.... maybe 6036 /+ 6037 protected void informProgramThatUserChangedPosition(int n) { 6038 position_ = n; 6039 auto evt = new Event(EventType.change, this); 6040 evt.intValue = n; 6041 evt.dispatch(); 6042 } 6043 +/ 6044 6045 version(custom_widgets) { 6046 enum MIN_THUMB_SIZE = 8; 6047 6048 abstract protected int getBarDim(); 6049 int thumbSize() { 6050 if(viewableArea_ >= max_ || max_ == 0) 6051 return getBarDim(); 6052 6053 int res = viewableArea_ * getBarDim() / max_; 6054 6055 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 6056 res = scaleWithDpi(MIN_THUMB_SIZE); 6057 6058 return res; 6059 } 6060 6061 int thumbPosition() { 6062 /* 6063 viewableArea_ is the viewport height/width 6064 position_ is where we are 6065 */ 6066 //if(position_ + viewableArea_ >= max_) 6067 //return getBarDim - thumbSize; 6068 6069 auto maximumPossibleValue = getBarDim() - thumbSize; 6070 auto maximiumLogicalValue = max_ - viewableArea_; 6071 6072 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 6073 6074 return p; 6075 } 6076 } 6077 } 6078 6079 //public import mgt; 6080 6081 /++ 6082 A mouse tracking widget is one that follows the mouse when dragged inside it. 6083 6084 Concrete subclasses may include a scrollbar thumb and a volume control. 6085 +/ 6086 //version(custom_widgets) 6087 class MouseTrackingWidget : Widget { 6088 6089 /// 6090 int positionX() { return positionX_; } 6091 /// 6092 int positionY() { return positionY_; } 6093 6094 /// 6095 void positionX(int p) { positionX_ = p; } 6096 /// 6097 void positionY(int p) { positionY_ = p; } 6098 6099 private int positionX_; 6100 private int positionY_; 6101 6102 /// 6103 enum Orientation { 6104 horizontal, /// 6105 vertical, /// 6106 twoDimensional, /// 6107 } 6108 6109 private int thumbWidth_; 6110 private int thumbHeight_; 6111 6112 /// 6113 int thumbWidth() { return thumbWidth_; } 6114 /// 6115 int thumbHeight() { return thumbHeight_; } 6116 /// 6117 int thumbWidth(int a) { return thumbWidth_ = a; } 6118 /// 6119 int thumbHeight(int a) { return thumbHeight_ = a; } 6120 6121 private bool dragging; 6122 private bool hovering; 6123 private int startMouseX, startMouseY; 6124 6125 /// 6126 this(Orientation orientation, Widget parent) { 6127 super(parent); 6128 6129 //assert(parentWindow !is null); 6130 6131 addEventListener((MouseDownEvent event) { 6132 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6133 dragging = true; 6134 startMouseX = event.clientX - positionX; 6135 startMouseY = event.clientY - positionY; 6136 parentWindow.captureMouse(this); 6137 } else { 6138 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6139 positionX = event.clientX - thumbWidth / 2; 6140 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6141 positionY = event.clientY - thumbHeight / 2; 6142 6143 if(positionX + thumbWidth > this.width) 6144 positionX = this.width - thumbWidth; 6145 if(positionY + thumbHeight > this.height) 6146 positionY = this.height - thumbHeight; 6147 6148 if(positionX < 0) 6149 positionX = 0; 6150 if(positionY < 0) 6151 positionY = 0; 6152 6153 6154 // this.emit!(ChangeEvent!void)(); 6155 auto evt = new Event(EventType.change, this); 6156 evt.sendDirectly(); 6157 6158 redraw(); 6159 6160 } 6161 }); 6162 6163 addEventListener(EventType.mouseup, (Event event) { 6164 dragging = false; 6165 parentWindow.releaseMouseCapture(); 6166 }); 6167 6168 addEventListener(EventType.mouseout, (Event event) { 6169 if(!hovering) 6170 return; 6171 hovering = false; 6172 redraw(); 6173 }); 6174 6175 int lpx, lpy; 6176 6177 addEventListener((MouseMoveEvent event) { 6178 auto oh = hovering; 6179 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6180 hovering = true; 6181 } else { 6182 hovering = false; 6183 } 6184 if(!dragging) { 6185 if(hovering != oh) 6186 redraw(); 6187 return; 6188 } 6189 6190 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6191 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 6192 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6193 positionY = event.clientY - startMouseY; 6194 6195 if(positionX + thumbWidth > this.width) 6196 positionX = this.width - thumbWidth; 6197 if(positionY + thumbHeight > this.height) 6198 positionY = this.height - thumbHeight; 6199 6200 if(positionX < 0) 6201 positionX = 0; 6202 if(positionY < 0) 6203 positionY = 0; 6204 6205 if(positionX != lpx || positionY != lpy) { 6206 lpx = positionX; 6207 lpy = positionY; 6208 6209 auto evt = new Event(EventType.change, this); 6210 evt.sendDirectly(); 6211 } 6212 6213 redraw(); 6214 }); 6215 } 6216 6217 version(custom_widgets) 6218 override void paint(WidgetPainter painter) { 6219 auto cs = getComputedStyle(); 6220 auto c = darken(cs.windowBackgroundColor, 0.2); 6221 painter.outlineColor = c; 6222 painter.fillColor = c; 6223 painter.drawRectangle(Point(0, 0), this.width, this.height); 6224 6225 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 6226 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 6227 } 6228 } 6229 6230 //version(custom_widgets) 6231 //private 6232 class HorizontalScrollbar : ScrollbarBase { 6233 6234 version(custom_widgets) { 6235 private MouseTrackingWidget thumb; 6236 6237 override int getBarDim() { 6238 return thumb.width; 6239 } 6240 } 6241 6242 override void setViewableArea(int a) { 6243 super.setViewableArea(a); 6244 6245 version(win32_widgets) { 6246 SCROLLINFO info; 6247 info.cbSize = info.sizeof; 6248 info.nPage = a + 1; 6249 info.fMask = SIF_PAGE; 6250 SetScrollInfo(hwnd, SB_CTL, &info, true); 6251 } else version(custom_widgets) { 6252 thumb.positionX = thumbPosition; 6253 thumb.thumbWidth = thumbSize; 6254 thumb.redraw(); 6255 } else static assert(0); 6256 6257 } 6258 6259 override void setMax(int a) { 6260 super.setMax(a); 6261 version(win32_widgets) { 6262 SCROLLINFO info; 6263 info.cbSize = info.sizeof; 6264 info.nMin = 0; 6265 info.nMax = max; 6266 info.fMask = SIF_RANGE; 6267 SetScrollInfo(hwnd, SB_CTL, &info, true); 6268 } else version(custom_widgets) { 6269 thumb.positionX = thumbPosition; 6270 thumb.thumbWidth = thumbSize; 6271 thumb.redraw(); 6272 } 6273 } 6274 6275 override void setPosition(int a) { 6276 super.setPosition(a); 6277 version(win32_widgets) { 6278 SCROLLINFO info; 6279 info.cbSize = info.sizeof; 6280 info.fMask = SIF_POS; 6281 info.nPos = position; 6282 SetScrollInfo(hwnd, SB_CTL, &info, true); 6283 } else version(custom_widgets) { 6284 thumb.positionX = thumbPosition(); 6285 thumb.thumbWidth = thumbSize; 6286 thumb.redraw(); 6287 } else static assert(0); 6288 } 6289 6290 this(Widget parent) { 6291 super(parent); 6292 6293 version(win32_widgets) { 6294 createWin32Window(this, "Scrollbar"w, "", 6295 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 6296 } else version(custom_widgets) { 6297 auto vl = new HorizontalLayout(this); 6298 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 6299 leftButton.setClickRepeat(scrollClickRepeatInterval); 6300 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 6301 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 6302 rightButton.setClickRepeat(scrollClickRepeatInterval); 6303 6304 leftButton.tabStop = false; 6305 rightButton.tabStop = false; 6306 thumb.tabStop = false; 6307 6308 leftButton.addEventListener(EventType.triggered, () { 6309 this.emitCommand!"scrolltopreviousline"(); 6310 //informProgramThatUserChangedPosition(position - step()); 6311 }); 6312 rightButton.addEventListener(EventType.triggered, () { 6313 this.emitCommand!"scrolltonextline"(); 6314 //informProgramThatUserChangedPosition(position + step()); 6315 }); 6316 6317 thumb.thumbWidth = this.minWidth; 6318 thumb.thumbHeight = scaleWithDpi(16); 6319 6320 thumb.addEventListener(EventType.change, () { 6321 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 6322 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 6323 6324 //informProgramThatUserChangedPosition(sx); 6325 6326 auto ev = new ScrollToPositionEvent(this, sx); 6327 ev.dispatch(); 6328 }); 6329 } 6330 } 6331 6332 override int minHeight() { return scaleWithDpi(16); } 6333 override int maxHeight() { return scaleWithDpi(16); } 6334 override int minWidth() { return scaleWithDpi(48); } 6335 } 6336 6337 class ScrollToPositionEvent : Event { 6338 enum EventString = "scrolltoposition"; 6339 6340 this(Widget target, int value) { 6341 this.value = value; 6342 super(EventString, target); 6343 } 6344 6345 immutable int value; 6346 6347 override @property int intValue() { 6348 return value; 6349 } 6350 } 6351 6352 //version(custom_widgets) 6353 //private 6354 class VerticalScrollbar : ScrollbarBase { 6355 6356 version(custom_widgets) { 6357 override int getBarDim() { 6358 return thumb.height; 6359 } 6360 6361 private MouseTrackingWidget thumb; 6362 } 6363 6364 override void setViewableArea(int a) { 6365 super.setViewableArea(a); 6366 6367 version(win32_widgets) { 6368 SCROLLINFO info; 6369 info.cbSize = info.sizeof; 6370 info.nPage = a + 1; 6371 info.fMask = SIF_PAGE; 6372 SetScrollInfo(hwnd, SB_CTL, &info, true); 6373 } else version(custom_widgets) { 6374 thumb.positionY = thumbPosition; 6375 thumb.thumbHeight = thumbSize; 6376 thumb.redraw(); 6377 } else static assert(0); 6378 6379 } 6380 6381 override void setMax(int a) { 6382 super.setMax(a); 6383 version(win32_widgets) { 6384 SCROLLINFO info; 6385 info.cbSize = info.sizeof; 6386 info.nMin = 0; 6387 info.nMax = max; 6388 info.fMask = SIF_RANGE; 6389 SetScrollInfo(hwnd, SB_CTL, &info, true); 6390 } else version(custom_widgets) { 6391 thumb.positionY = thumbPosition; 6392 thumb.thumbHeight = thumbSize; 6393 thumb.redraw(); 6394 } 6395 } 6396 6397 override void setPosition(int a) { 6398 super.setPosition(a); 6399 version(win32_widgets) { 6400 SCROLLINFO info; 6401 info.cbSize = info.sizeof; 6402 info.fMask = SIF_POS; 6403 info.nPos = position; 6404 SetScrollInfo(hwnd, SB_CTL, &info, true); 6405 } else version(custom_widgets) { 6406 thumb.positionY = thumbPosition; 6407 thumb.thumbHeight = thumbSize; 6408 thumb.redraw(); 6409 } else static assert(0); 6410 } 6411 6412 this(Widget parent) { 6413 super(parent); 6414 6415 version(win32_widgets) { 6416 createWin32Window(this, "Scrollbar"w, "", 6417 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 6418 } else version(custom_widgets) { 6419 auto vl = new VerticalLayout(this); 6420 auto upButton = new ArrowButton(ArrowDirection.up, vl); 6421 upButton.setClickRepeat(scrollClickRepeatInterval); 6422 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 6423 auto downButton = new ArrowButton(ArrowDirection.down, vl); 6424 downButton.setClickRepeat(scrollClickRepeatInterval); 6425 6426 upButton.addEventListener(EventType.triggered, () { 6427 this.emitCommand!"scrolltopreviousline"(); 6428 //informProgramThatUserChangedPosition(position - step()); 6429 }); 6430 downButton.addEventListener(EventType.triggered, () { 6431 this.emitCommand!"scrolltonextline"(); 6432 //informProgramThatUserChangedPosition(position + step()); 6433 }); 6434 6435 thumb.thumbWidth = this.minWidth; 6436 thumb.thumbHeight = scaleWithDpi(16); 6437 6438 thumb.addEventListener(EventType.change, () { 6439 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 6440 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 6441 6442 auto ev = new ScrollToPositionEvent(this, sy); 6443 ev.dispatch(); 6444 6445 //informProgramThatUserChangedPosition(sy); 6446 }); 6447 6448 upButton.tabStop = false; 6449 downButton.tabStop = false; 6450 thumb.tabStop = false; 6451 } 6452 } 6453 6454 override int minWidth() { return scaleWithDpi(16); } 6455 override int maxWidth() { return scaleWithDpi(16); } 6456 override int minHeight() { return scaleWithDpi(48); } 6457 } 6458 6459 6460 /++ 6461 EXPERIMENTAL 6462 6463 A widget specialized for being a container for other widgets. 6464 6465 History: 6466 Added May 29, 2021. Not stabilized at this time. 6467 +/ 6468 class WidgetContainer : Widget { 6469 this(Widget parent) { 6470 tabStop = false; 6471 super(parent); 6472 } 6473 6474 override int maxHeight() { 6475 if(this.children.length == 1) { 6476 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 6477 } else { 6478 return int.max; 6479 } 6480 } 6481 6482 override int maxWidth() { 6483 if(this.children.length == 1) { 6484 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 6485 } else { 6486 return int.max; 6487 } 6488 } 6489 6490 /+ 6491 6492 override int minHeight() { 6493 int largest = 0; 6494 int margins = 0; 6495 int lastMargin = 0; 6496 foreach(child; children) { 6497 auto mh = child.minHeight(); 6498 if(mh > largest) 6499 largest = mh; 6500 margins += mymax(lastMargin, child.marginTop()); 6501 lastMargin = child.marginBottom(); 6502 } 6503 return largest + margins; 6504 } 6505 6506 override int maxHeight() { 6507 int largest = 0; 6508 int margins = 0; 6509 int lastMargin = 0; 6510 foreach(child; children) { 6511 auto mh = child.maxHeight(); 6512 if(mh == int.max) 6513 return int.max; 6514 if(mh > largest) 6515 largest = mh; 6516 margins += mymax(lastMargin, child.marginTop()); 6517 lastMargin = child.marginBottom(); 6518 } 6519 return largest + margins; 6520 } 6521 6522 override int minWidth() { 6523 int min; 6524 foreach(child; children) { 6525 auto cm = child.minWidth; 6526 if(cm > min) 6527 min = cm; 6528 } 6529 return min + paddingLeft + paddingRight; 6530 } 6531 6532 override int minHeight() { 6533 int min; 6534 foreach(child; children) { 6535 auto cm = child.minHeight; 6536 if(cm > min) 6537 min = cm; 6538 } 6539 return min + paddingTop + paddingBottom; 6540 } 6541 6542 override int maxHeight() { 6543 int largest = 0; 6544 int margins = 0; 6545 int lastMargin = 0; 6546 foreach(child; children) { 6547 auto mh = child.maxHeight(); 6548 if(mh == int.max) 6549 return int.max; 6550 if(mh > largest) 6551 largest = mh; 6552 margins += mymax(lastMargin, child.marginTop()); 6553 lastMargin = child.marginBottom(); 6554 } 6555 return largest + margins; 6556 } 6557 6558 override int heightStretchiness() { 6559 int max; 6560 foreach(child; children) { 6561 auto c = child.heightStretchiness; 6562 if(c > max) 6563 max = c; 6564 } 6565 return max; 6566 } 6567 6568 override int marginTop() { 6569 if(this.children.length) 6570 return this.children[0].marginTop; 6571 return 0; 6572 } 6573 +/ 6574 } 6575 6576 /// 6577 abstract class Layout : Widget { 6578 this(Widget parent) { 6579 tabStop = false; 6580 super(parent); 6581 } 6582 } 6583 6584 /++ 6585 Makes all children minimum width and height, placing them down 6586 left to right, top to bottom. 6587 6588 Useful if you want to make a list of buttons that automatically 6589 wrap to a new line when necessary. 6590 +/ 6591 class InlineBlockLayout : Layout { 6592 /// 6593 this(Widget parent) { super(parent); } 6594 6595 override void recomputeChildLayout() { 6596 registerMovement(); 6597 6598 int x = this.paddingLeft, y = this.paddingTop; 6599 6600 int lineHeight; 6601 int previousMargin = 0; 6602 int previousMarginBottom = 0; 6603 6604 foreach(child; children) { 6605 if(child.hidden) 6606 continue; 6607 if(cast(FixedPosition) child) { 6608 child.recomputeChildLayout(); 6609 continue; 6610 } 6611 child.width = child.flexBasisWidth(); 6612 if(child.width == 0) 6613 child.width = child.minWidth(); 6614 if(child.width == 0) 6615 child.width = 32; 6616 6617 child.height = child.flexBasisHeight(); 6618 if(child.height == 0) 6619 child.height = child.minHeight(); 6620 if(child.height == 0) 6621 child.height = 32; 6622 6623 if(x + child.width + paddingRight > this.width) { 6624 x = this.paddingLeft; 6625 y += lineHeight; 6626 lineHeight = 0; 6627 previousMargin = 0; 6628 previousMarginBottom = 0; 6629 } 6630 6631 auto margin = child.marginLeft; 6632 if(previousMargin > margin) 6633 margin = previousMargin; 6634 6635 x += margin; 6636 6637 child.x = x; 6638 child.y = y; 6639 6640 int marginTopApplied; 6641 if(child.marginTop > previousMarginBottom) { 6642 child.y += child.marginTop; 6643 marginTopApplied = child.marginTop; 6644 } 6645 6646 x += child.width; 6647 previousMargin = child.marginRight; 6648 6649 if(child.marginBottom > previousMarginBottom) 6650 previousMarginBottom = child.marginBottom; 6651 6652 auto h = child.height + previousMarginBottom + marginTopApplied; 6653 if(h > lineHeight) 6654 lineHeight = h; 6655 6656 child.recomputeChildLayout(); 6657 } 6658 6659 } 6660 6661 override int minWidth() { 6662 int min; 6663 foreach(child; children) { 6664 auto cm = child.minWidth; 6665 if(cm > min) 6666 min = cm; 6667 } 6668 return min + paddingLeft + paddingRight; 6669 } 6670 6671 override int minHeight() { 6672 int min; 6673 foreach(child; children) { 6674 auto cm = child.minHeight; 6675 if(cm > min) 6676 min = cm; 6677 } 6678 return min + paddingTop + paddingBottom; 6679 } 6680 } 6681 6682 /++ 6683 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 6684 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 6685 the [TabWidget] will automatically change pages of child widgets. 6686 6687 This allows you to react to it however you see fit rather than having to 6688 be tied to just the new sets of child widgets. 6689 6690 It sends the message in the form of `this.emitCommand!"changetab"();`. 6691 6692 History: 6693 Added December 24, 2021 (dub v10.5) 6694 +/ 6695 class TabMessageWidget : Widget { 6696 6697 protected void tabIndexClicked(int item) { 6698 this.emitCommand!"changetab"(); 6699 } 6700 6701 /++ 6702 Adds the a new tab to the control with the given title. 6703 6704 Returns: 6705 The index of the newly added tab. You will need to know 6706 this index to refer to it later and to know which tab to 6707 change to when you get a changetab message. 6708 +/ 6709 int addTab(string title, int pos = int.max) { 6710 version(win32_widgets) { 6711 TCITEM item; 6712 item.mask = TCIF_TEXT; 6713 WCharzBuffer buf = WCharzBuffer(title); 6714 item.pszText = buf.ptr; 6715 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 6716 } else version(custom_widgets) { 6717 if(pos >= tabs.length) { 6718 tabs ~= title; 6719 redraw(); 6720 return cast(int) tabs.length - 1; 6721 } else if(pos <= 0) { 6722 tabs = title ~ tabs; 6723 redraw(); 6724 return 0; 6725 } else { 6726 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 6727 redraw(); 6728 return pos; 6729 } 6730 } 6731 } 6732 6733 override void addChild(Widget child, int pos = int.max) { 6734 if(container) 6735 container.addChild(child, pos); 6736 else 6737 super.addChild(child, pos); 6738 } 6739 6740 protected Widget makeContainer() { 6741 return new Widget(this); 6742 } 6743 6744 private Widget container; 6745 6746 override void recomputeChildLayout() { 6747 version(win32_widgets) { 6748 this.registerMovement(); 6749 6750 RECT rect; 6751 GetWindowRect(hwnd, &rect); 6752 6753 auto left = rect.left; 6754 auto top = rect.top; 6755 6756 TabCtrl_AdjustRect(hwnd, false, &rect); 6757 foreach(child; children) { 6758 if(!child.showing) continue; 6759 child.x = rect.left - left; 6760 child.y = rect.top - top; 6761 child.width = rect.right - rect.left; 6762 child.height = rect.bottom - rect.top; 6763 child.recomputeChildLayout(); 6764 } 6765 } else version(custom_widgets) { 6766 this.registerMovement(); 6767 foreach(child; children) { 6768 if(!child.showing) continue; 6769 child.x = 2; 6770 child.y = tabBarHeight + 2; // for the border 6771 child.width = width - 4; // for the border 6772 child.height = height - tabBarHeight - 2 - 2; // for the border 6773 child.recomputeChildLayout(); 6774 } 6775 } else static assert(0); 6776 } 6777 6778 version(custom_widgets) 6779 string[] tabs; 6780 6781 this(Widget parent) { 6782 super(parent); 6783 6784 tabStop = false; 6785 6786 version(win32_widgets) { 6787 createWin32Window(this, WC_TABCONTROL, "", 0); 6788 } else version(custom_widgets) { 6789 addEventListener((ClickEvent event) { 6790 if(event.target !is this && this.container !is null && event.target !is this.container) return; 6791 if(event.clientY < tabBarHeight) { 6792 auto t = (event.clientX / tabWidth); 6793 if(t >= 0 && t < tabs.length) { 6794 currentTab_ = t; 6795 tabIndexClicked(t); 6796 redraw(); 6797 } 6798 } 6799 }); 6800 } else static assert(0); 6801 6802 this.container = makeContainer(); 6803 } 6804 6805 override int marginTop() { return 4; } 6806 override int paddingBottom() { return 4; } 6807 6808 override int minHeight() { 6809 int max = 0; 6810 foreach(child; children) 6811 max = mymax(child.minHeight, max); 6812 6813 6814 version(win32_widgets) { 6815 RECT rect; 6816 rect.right = this.width; 6817 rect.bottom = max; 6818 TabCtrl_AdjustRect(hwnd, true, &rect); 6819 6820 max = rect.bottom; 6821 } else { 6822 max += defaultLineHeight + 4; 6823 } 6824 6825 6826 return max; 6827 } 6828 6829 version(win32_widgets) 6830 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 6831 switch(code) { 6832 case TCN_SELCHANGE: 6833 auto sel = TabCtrl_GetCurSel(hwnd); 6834 tabIndexClicked(sel); 6835 break; 6836 default: 6837 } 6838 return 0; 6839 } 6840 6841 version(custom_widgets) { 6842 private int currentTab_; 6843 private int tabBarHeight() { return defaultLineHeight; } 6844 int tabWidth = 80; 6845 } 6846 6847 version(win32_widgets) 6848 override void paint(WidgetPainter painter) {} 6849 6850 version(custom_widgets) 6851 override void paint(WidgetPainter painter) { 6852 auto cs = getComputedStyle(); 6853 6854 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 6855 6856 int posX = 0; 6857 foreach(idx, title; tabs) { 6858 auto isCurrent = idx == getCurrentTab(); 6859 6860 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 6861 6862 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 6863 painter.outlineColor = cs.foregroundColor; 6864 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 6865 6866 if(isCurrent) { 6867 painter.outlineColor = cs.windowBackgroundColor; 6868 painter.fillColor = Color.transparent; 6869 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 6870 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 6871 6872 painter.outlineColor = Color.white; 6873 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 6874 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 6875 painter.outlineColor = cs.activeTabColor; 6876 painter.drawPixel(Point(posX, tabBarHeight - 1)); 6877 } 6878 6879 posX += tabWidth - 2; 6880 } 6881 } 6882 6883 /// 6884 @scriptable 6885 void setCurrentTab(int item) { 6886 version(win32_widgets) 6887 TabCtrl_SetCurSel(hwnd, item); 6888 else version(custom_widgets) 6889 currentTab_ = item; 6890 else static assert(0); 6891 6892 tabIndexClicked(item); 6893 } 6894 6895 /// 6896 @scriptable 6897 int getCurrentTab() { 6898 version(win32_widgets) 6899 return TabCtrl_GetCurSel(hwnd); 6900 else version(custom_widgets) 6901 return currentTab_; // FIXME 6902 else static assert(0); 6903 } 6904 6905 /// 6906 @scriptable 6907 void removeTab(int item) { 6908 if(item && item == getCurrentTab()) 6909 setCurrentTab(item - 1); 6910 6911 version(win32_widgets) { 6912 TabCtrl_DeleteItem(hwnd, item); 6913 } 6914 6915 for(int a = item; a < children.length - 1; a++) 6916 this._children[a] = this._children[a + 1]; 6917 this._children = this._children[0 .. $-1]; 6918 } 6919 6920 } 6921 6922 6923 /++ 6924 A tab widget is a set of clickable tab buttons followed by a content area. 6925 6926 6927 Tabs can change existing content or can be new pages. 6928 6929 When the user picks a different tab, a `change` message is generated. 6930 +/ 6931 class TabWidget : TabMessageWidget { 6932 this(Widget parent) { 6933 super(parent); 6934 } 6935 6936 override protected Widget makeContainer() { 6937 return null; 6938 } 6939 6940 override void addChild(Widget child, int pos = int.max) { 6941 if(auto twp = cast(TabWidgetPage) child) { 6942 Widget.addChild(child, pos); 6943 if(pos == int.max) 6944 pos = cast(int) this.children.length - 1; 6945 6946 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 6947 6948 if(pos != getCurrentTab) { 6949 child.showing = false; 6950 } 6951 } else { 6952 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 6953 } 6954 } 6955 6956 // FIXME: add tab icons at some point, Windows supports them 6957 /++ 6958 Adds a page and its associated tab with the given label to the widget. 6959 6960 Returns: 6961 The added page object, to which you can add other widgets. 6962 +/ 6963 @scriptable 6964 TabWidgetPage addPage(string title) { 6965 return new TabWidgetPage(title, this); 6966 } 6967 6968 /++ 6969 Gets the page at the given tab index, or `null` if the index is bad. 6970 6971 History: 6972 Added December 24, 2021. 6973 +/ 6974 TabWidgetPage getPage(int index) { 6975 if(index < this.children.length) 6976 return null; 6977 return cast(TabWidgetPage) this.children[index]; 6978 } 6979 6980 /++ 6981 While you can still use the addTab from the parent class, 6982 *strongly* recommend you use [addPage] insteaad. 6983 6984 History: 6985 Added December 24, 2021 to fulful the interface 6986 requirement that came from adding [TabMessageWidget]. 6987 6988 You should not use it though since the [addPage] function 6989 is much easier to use here. 6990 +/ 6991 override int addTab(string title, int pos = int.max) { 6992 auto p = addPage(title); 6993 foreach(idx, child; this.children) 6994 if(child is p) 6995 return cast(int) idx; 6996 return -1; 6997 } 6998 6999 protected override void tabIndexClicked(int item) { 7000 foreach(idx, child; children) { 7001 child.showing(false, false); // batch the recalculates for the end 7002 } 7003 7004 foreach(idx, child; children) { 7005 if(idx == item) { 7006 child.showing(true, false); 7007 if(parentWindow) { 7008 auto f = parentWindow.getFirstFocusable(child); 7009 if(f) 7010 f.focus(); 7011 } 7012 recomputeChildLayout(); 7013 } 7014 } 7015 7016 version(win32_widgets) { 7017 InvalidateRect(hwnd, null, true); 7018 } else version(custom_widgets) { 7019 this.redraw(); 7020 } 7021 } 7022 7023 } 7024 7025 /++ 7026 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 7027 7028 You add [TabWidgetPage]s to it. 7029 +/ 7030 class PageWidget : Widget { 7031 this(Widget parent) { 7032 super(parent); 7033 } 7034 7035 override int minHeight() { 7036 int max = 0; 7037 foreach(child; children) 7038 max = mymax(child.minHeight, max); 7039 7040 return max; 7041 } 7042 7043 7044 override void addChild(Widget child, int pos = int.max) { 7045 if(auto twp = cast(TabWidgetPage) child) { 7046 super.addChild(child, pos); 7047 if(pos == int.max) 7048 pos = cast(int) this.children.length - 1; 7049 7050 if(pos != getCurrentTab) { 7051 child.showing = false; 7052 } 7053 } else { 7054 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 7055 } 7056 } 7057 7058 override void recomputeChildLayout() { 7059 this.registerMovement(); 7060 foreach(child; children) { 7061 child.x = 0; 7062 child.y = 0; 7063 child.width = width; 7064 child.height = height; 7065 child.recomputeChildLayout(); 7066 } 7067 } 7068 7069 private int currentTab_; 7070 7071 /// 7072 @scriptable 7073 void setCurrentTab(int item) { 7074 currentTab_ = item; 7075 7076 showOnly(item); 7077 } 7078 7079 /// 7080 @scriptable 7081 int getCurrentTab() { 7082 return currentTab_; 7083 } 7084 7085 /// 7086 @scriptable 7087 void removeTab(int item) { 7088 if(item && item == getCurrentTab()) 7089 setCurrentTab(item - 1); 7090 7091 for(int a = item; a < children.length - 1; a++) 7092 this._children[a] = this._children[a + 1]; 7093 this._children = this._children[0 .. $-1]; 7094 } 7095 7096 /// 7097 @scriptable 7098 TabWidgetPage addPage(string title) { 7099 return new TabWidgetPage(title, this); 7100 } 7101 7102 private void showOnly(int item) { 7103 foreach(idx, child; children) 7104 if(idx == item) { 7105 child.show(); 7106 child.recomputeChildLayout(); 7107 } else { 7108 child.hide(); 7109 } 7110 } 7111 7112 } 7113 7114 /++ 7115 7116 +/ 7117 class TabWidgetPage : Widget { 7118 string title; 7119 this(string title, Widget parent) { 7120 this.title = title; 7121 this.tabStop = false; 7122 super(parent); 7123 7124 ///* 7125 version(win32_widgets) { 7126 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 7127 } 7128 //*/ 7129 } 7130 7131 override int minHeight() { 7132 int sum = 0; 7133 foreach(child; children) 7134 sum += child.minHeight(); 7135 return sum; 7136 } 7137 } 7138 7139 version(none) 7140 /++ 7141 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 7142 7143 I think I need to modify the layout algorithms to support this. 7144 +/ 7145 class CollapsableSidebar : Widget { 7146 7147 } 7148 7149 /// Stacks the widgets vertically, taking all the available width for each child. 7150 class VerticalLayout : Layout { 7151 // most of this is intentionally blank - widget's default is vertical layout right now 7152 /// 7153 this(Widget parent) { super(parent); } 7154 7155 /++ 7156 Sets a max width for the layout so you don't have to subclass. The max width 7157 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7158 7159 History: 7160 Added November 29, 2021 (dub v10.5) 7161 +/ 7162 this(int maxWidth, Widget parent) { 7163 this.mw = maxWidth; 7164 super(parent); 7165 } 7166 7167 private int mw = int.max; 7168 7169 override int maxWidth() { return scaleWithDpi(mw); } 7170 } 7171 7172 /// Stacks the widgets horizontally, taking all the available height for each child. 7173 class HorizontalLayout : Layout { 7174 /// 7175 this(Widget parent) { super(parent); } 7176 7177 /++ 7178 Sets a max height for the layout so you don't have to subclass. The max height 7179 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7180 7181 History: 7182 Added November 29, 2021 (dub v10.5) 7183 +/ 7184 this(int maxHeight, Widget parent) { 7185 this.mh = maxHeight; 7186 super(parent); 7187 } 7188 7189 private int mh = 0; 7190 7191 7192 7193 override void recomputeChildLayout() { 7194 .recomputeChildLayout!"width"(this); 7195 } 7196 7197 override int minHeight() { 7198 int largest = 0; 7199 int margins = 0; 7200 int lastMargin = 0; 7201 foreach(child; children) { 7202 auto mh = child.minHeight(); 7203 if(mh > largest) 7204 largest = mh; 7205 margins += mymax(lastMargin, child.marginTop()); 7206 lastMargin = child.marginBottom(); 7207 } 7208 return largest + margins; 7209 } 7210 7211 override int maxHeight() { 7212 if(mh != 0) 7213 return mymax(minHeight, scaleWithDpi(mh)); 7214 7215 int largest = 0; 7216 int margins = 0; 7217 int lastMargin = 0; 7218 foreach(child; children) { 7219 auto mh = child.maxHeight(); 7220 if(mh == int.max) 7221 return int.max; 7222 if(mh > largest) 7223 largest = mh; 7224 margins += mymax(lastMargin, child.marginTop()); 7225 lastMargin = child.marginBottom(); 7226 } 7227 return largest + margins; 7228 } 7229 7230 override int heightStretchiness() { 7231 int max; 7232 foreach(child; children) { 7233 auto c = child.heightStretchiness; 7234 if(c > max) 7235 max = c; 7236 } 7237 return max; 7238 } 7239 7240 } 7241 7242 version(win32_widgets) 7243 private 7244 extern(Windows) 7245 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 7246 Widget* pwin = hwnd in Widget.nativeMapping; 7247 if(pwin is null) 7248 return DefWindowProc(hwnd, message, wparam, lparam); 7249 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 7250 if(win is null) 7251 return DefWindowProc(hwnd, message, wparam, lparam); 7252 7253 switch(message) { 7254 case WM_SIZE: 7255 auto width = LOWORD(lparam); 7256 auto height = HIWORD(lparam); 7257 7258 auto hdc = GetDC(hwnd); 7259 auto hdcBmp = CreateCompatibleDC(hdc); 7260 7261 // FIXME: could this be more efficient? it never relinquishes a large bitmap 7262 if(width > win.bmpWidth || height > win.bmpHeight) { 7263 auto oldBuffer = win.buffer; 7264 win.buffer = CreateCompatibleBitmap(hdc, width, height); 7265 7266 if(oldBuffer) 7267 DeleteObject(oldBuffer); 7268 7269 win.bmpWidth = width; 7270 win.bmpHeight = height; 7271 } 7272 7273 // just always erase it upon resizing so minigui can draw over with a clean slate 7274 auto oldBmp = SelectObject(hdcBmp, win.buffer); 7275 7276 auto brush = GetSysColorBrush(COLOR_3DFACE); 7277 RECT r; 7278 r.left = 0; 7279 r.top = 0; 7280 r.right = width; 7281 r.bottom = height; 7282 FillRect(hdcBmp, &r, brush); 7283 7284 SelectObject(hdcBmp, oldBmp); 7285 DeleteDC(hdcBmp); 7286 ReleaseDC(hwnd, hdc); 7287 break; 7288 case WM_PAINT: 7289 if(win.buffer is null) 7290 goto default; 7291 7292 BITMAP bm; 7293 PAINTSTRUCT ps; 7294 7295 HDC hdc = BeginPaint(hwnd, &ps); 7296 7297 HDC hdcMem = CreateCompatibleDC(hdc); 7298 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 7299 7300 GetObject(win.buffer, bm.sizeof, &bm); 7301 7302 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 7303 7304 SelectObject(hdcMem, hbmOld); 7305 DeleteDC(hdcMem); 7306 EndPaint(hwnd, &ps); 7307 break; 7308 default: 7309 return DefWindowProc(hwnd, message, wparam, lparam); 7310 } 7311 7312 return 0; 7313 } 7314 7315 private wstring Win32Class(wstring name)() { 7316 static bool classRegistered; 7317 if(!classRegistered) { 7318 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 7319 WNDCLASSEX wc; 7320 wc.cbSize = wc.sizeof; 7321 wc.hInstance = hInstance; 7322 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 7323 wc.lpfnWndProc = &DoubleBufferWndProc; 7324 wc.lpszClassName = name.ptr; 7325 if(!RegisterClassExW(&wc)) 7326 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 7327 classRegistered = true; 7328 } 7329 7330 return name; 7331 } 7332 7333 /+ 7334 version(win32_widgets) 7335 extern(Windows) 7336 private 7337 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 7338 switch(iMessage) { 7339 case WM_PAINT: 7340 if(auto te = hWnd in Widget.nativeMapping) { 7341 try { 7342 //te.redraw(); 7343 import std.stdio; writeln(te, " drawing"); 7344 } catch(Exception) {} 7345 } 7346 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7347 default: 7348 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7349 } 7350 } 7351 +/ 7352 7353 7354 /++ 7355 A widget specifically designed to hold other widgets. 7356 7357 History: 7358 Added July 1, 2021 7359 +/ 7360 class ContainerWidget : Widget { 7361 this(Widget parent) { 7362 super(parent); 7363 this.tabStop = false; 7364 7365 version(win32_widgets) { 7366 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 7367 } 7368 } 7369 } 7370 7371 /++ 7372 A widget that takes your widget, puts scroll bars around it, and sends 7373 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 7374 no effort to automatically scroll or clip its child widgets - it just sends 7375 the messages. 7376 7377 7378 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 7379 The scroll coordinates are all given in a unit you interpret as you wish. One 7380 of these units is moved on each press of the arrow buttons and represents the 7381 smallest amount the user can scroll. The intention is for this to be one line, 7382 one item in a list, one row in a table, etc. Whatever makes sense for your widget 7383 in each direction that the user might be interested in. 7384 7385 You can set a "page size" with the [step] property. (Yes, I regret the name...) 7386 This is the amount it jumps when the user pressed page up and page down, or clicks 7387 in the exposed part of the scroll bar. 7388 7389 You should add child content to the ScrollMessageWidget. However, it is important to 7390 note that the coordinates are always independent of the scroll position! It is YOUR 7391 responsibility to do any necessary transforms, clipping, etc., while drawing the 7392 content and interpreting mouse events if they are supposed to change with the scroll. 7393 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 7394 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 7395 you more control (which can be considerably more efficient and adapted to your actual data) 7396 at the expense of you also needing to be aware of its reality. 7397 7398 Please note that it does NOT react to mouse wheel events or various keyboard events as of 7399 version 10.3. Maybe this will change in the future.... but for now you must call 7400 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 7401 +/ 7402 class ScrollMessageWidget : Widget { 7403 this(Widget parent) { 7404 super(parent); 7405 7406 container = new Widget(this); 7407 hsb = new HorizontalScrollbar(this); 7408 vsb = new VerticalScrollbar(this); 7409 7410 hsb.addEventListener("scrolltonextline", { 7411 hsb.setPosition(hsb.position + movementPerButtonClickH_); 7412 notify(); 7413 }); 7414 hsb.addEventListener("scrolltopreviousline", { 7415 hsb.setPosition(hsb.position - movementPerButtonClickH_); 7416 notify(); 7417 }); 7418 vsb.addEventListener("scrolltonextline", { 7419 vsb.setPosition(vsb.position + movementPerButtonClickV_); 7420 notify(); 7421 }); 7422 vsb.addEventListener("scrolltopreviousline", { 7423 vsb.setPosition(vsb.position - movementPerButtonClickV_); 7424 notify(); 7425 }); 7426 hsb.addEventListener("scrolltonextpage", { 7427 hsb.setPosition(hsb.position + hsb.step_); 7428 notify(); 7429 }); 7430 hsb.addEventListener("scrolltopreviouspage", { 7431 hsb.setPosition(hsb.position - hsb.step_); 7432 notify(); 7433 }); 7434 vsb.addEventListener("scrolltonextpage", { 7435 vsb.setPosition(vsb.position + vsb.step_); 7436 notify(); 7437 }); 7438 vsb.addEventListener("scrolltopreviouspage", { 7439 vsb.setPosition(vsb.position - vsb.step_); 7440 notify(); 7441 }); 7442 hsb.addEventListener("scrolltoposition", (Event event) { 7443 hsb.setPosition(event.intValue); 7444 notify(); 7445 }); 7446 vsb.addEventListener("scrolltoposition", (Event event) { 7447 vsb.setPosition(event.intValue); 7448 notify(); 7449 }); 7450 7451 7452 tabStop = false; 7453 container.tabStop = false; 7454 magic = true; 7455 } 7456 7457 private int movementPerButtonClickH_ = 1; 7458 private int movementPerButtonClickV_ = 1; 7459 public void movementPerButtonClick(int h, int v) { 7460 movementPerButtonClickH_ = h; 7461 movementPerButtonClickV_ = v; 7462 } 7463 7464 /++ 7465 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 7466 7467 7468 The defaults for [addDefaultWheelListeners] are: 7469 7470 $(LIST 7471 * Mouse wheel scrolls vertically 7472 * Alt key + mouse wheel scrolls horiontally 7473 * Shift + mouse wheel scrolls faster. 7474 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 7475 ) 7476 7477 The defaults for [addDefaultKeyboardListeners] are: 7478 7479 $(LIST 7480 * Arrow keys scroll by the given amounts 7481 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 7482 * Page up and down scroll by the vertical viewable area 7483 * Home and end scroll to the start and end of the verticle viewable area. 7484 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 7485 ) 7486 7487 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 7488 7489 Params: 7490 horizontalArrowScrollAmount = 7491 verticalArrowScrollAmount = 7492 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 7493 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 7494 shiftMultiplier = multiplies the scroll amount by this when shift is held 7495 +/ 7496 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 7497 auto _this = this; 7498 7499 container.addEventListener((scope KeyDownEvent ke) { 7500 switch(ke.key) { 7501 case Key.Left: 7502 _this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7503 break; 7504 case Key.Right: 7505 _this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7506 break; 7507 case Key.Up: 7508 _this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7509 break; 7510 case Key.Down: 7511 _this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7512 break; 7513 case Key.PageUp: 7514 if(ke.altKey) 7515 _this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7516 else 7517 _this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7518 break; 7519 case Key.PageDown: 7520 if(ke.altKey) 7521 _this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7522 else 7523 _this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7524 break; 7525 case Key.Home: 7526 if(ke.altKey) 7527 _this.scrollLeft(short.max * 16); 7528 else 7529 _this.scrollUp(short.max * 16); 7530 break; 7531 case Key.End: 7532 if(ke.altKey) 7533 _this.scrollRight(short.max * 16); 7534 else 7535 _this.scrollDown(short.max * 16); 7536 break; 7537 7538 default: 7539 // ignore, not for us. 7540 } 7541 7542 }); 7543 } 7544 7545 /// ditto 7546 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 7547 auto _this = this; 7548 container.addEventListener((scope ClickEvent ce) { 7549 7550 if(ce.target && ce.target.tabStop) 7551 ce.target.focus(); 7552 7553 // ctrl is reserved for the application 7554 if(ce.ctrlKey) 7555 return; 7556 7557 if(horizontalWheelScrollAmount == 0 && ce.altKey) 7558 return; 7559 7560 if(shiftMultiplier == 0 && ce.shiftKey) 7561 return; 7562 7563 if(ce.button == MouseButton.wheelDown) { 7564 if(ce.altKey) 7565 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7566 else 7567 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7568 } else if(ce.button == MouseButton.wheelUp) { 7569 if(ce.altKey) 7570 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7571 else 7572 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7573 } 7574 }); 7575 } 7576 7577 /++ 7578 Scrolls the given amount. 7579 7580 History: 7581 The scroll up and down functions was here in the initial release of the class, but the `amount` parameter and left/right functions were added on September 28, 2021. 7582 +/ 7583 void scrollUp(int amount = 1) { 7584 vsb.setPosition(vsb.position - amount); 7585 notify(); 7586 } 7587 /// ditto 7588 void scrollDown(int amount = 1) { 7589 vsb.setPosition(vsb.position + amount); 7590 notify(); 7591 } 7592 /// ditto 7593 void scrollLeft(int amount = 1) { 7594 hsb.setPosition(hsb.position - amount); 7595 notify(); 7596 } 7597 /// ditto 7598 void scrollRight(int amount = 1) { 7599 hsb.setPosition(hsb.position + amount); 7600 notify(); 7601 } 7602 7603 /// 7604 VerticalScrollbar verticalScrollBar() { return vsb; } 7605 /// 7606 HorizontalScrollbar horizontalScrollBar() { return hsb; } 7607 7608 void notify() { 7609 static bool insideNotify; 7610 7611 if(insideNotify) 7612 return; // avoid the recursive call, even if it isn't strictly correct 7613 7614 insideNotify = true; 7615 scope(exit) insideNotify = false; 7616 7617 this.emit!ScrollEvent(); 7618 } 7619 7620 mixin Emits!ScrollEvent; 7621 7622 /// 7623 Point position() { 7624 return Point(hsb.position, vsb.position); 7625 } 7626 7627 /// 7628 void setPosition(int x, int y) { 7629 hsb.setPosition(x); 7630 vsb.setPosition(y); 7631 } 7632 7633 /// 7634 void setPageSize(int unitsX, int unitsY) { 7635 hsb.setStep(unitsX); 7636 vsb.setStep(unitsY); 7637 } 7638 7639 /// Always call this BEFORE setViewableArea 7640 void setTotalArea(int width, int height) { 7641 hsb.setMax(width); 7642 vsb.setMax(height); 7643 } 7644 7645 /++ 7646 Always set the viewable area AFTER setitng the total area if you are going to change both. 7647 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 7648 If you need to do that, use [queueRecomputeChildLayout]. 7649 +/ 7650 void setViewableArea(int width, int height) { 7651 7652 // actually there IS A need to dothis cuz the max might have changed since then 7653 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 7654 //return; // no need to do what is already done 7655 hsb.setViewableArea(width); 7656 vsb.setViewableArea(height); 7657 7658 bool needsNotify = false; 7659 7660 // FIXME: if at any point the rhs is outside the scrollbar, we need 7661 // to reset to 0. but it should remember the old position in case the 7662 // window resizes again, so it can kinda return ot where it was. 7663 // 7664 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 7665 if(width >= hsb.max) { 7666 // there's plenty of room to display it all so we need to reset to zero 7667 // FIXME: adjust so it matches the note above 7668 hsb.setPosition(0); 7669 needsNotify = true; 7670 } 7671 if(height >= vsb.max) { 7672 // there's plenty of room to display it all so we need to reset to zero 7673 // FIXME: adjust so it matches the note above 7674 vsb.setPosition(0); 7675 needsNotify = true; 7676 } 7677 if(needsNotify) 7678 notify(); 7679 } 7680 7681 private bool magic; 7682 override void addChild(Widget w, int position = int.max) { 7683 if(magic) 7684 container.addChild(w, position); 7685 else 7686 super.addChild(w, position); 7687 } 7688 7689 override void recomputeChildLayout() { 7690 if(hsb is null || vsb is null || container is null) return; 7691 7692 registerMovement(); 7693 7694 enum BUTTON_SIZE = 16; 7695 7696 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 7697 hsb.x = 0; 7698 hsb.y = this.height - hsb.height; 7699 7700 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 7701 vsb.x = this.width - vsb.width; 7702 vsb.y = 0; 7703 7704 auto vsb_width = vsb.showing ? vsb.width : 0; 7705 auto hsb_height = hsb.showing ? hsb.height : 0; 7706 7707 hsb.width = this.width - vsb_width; 7708 vsb.height = this.height - hsb_height; 7709 7710 hsb.recomputeChildLayout(); 7711 vsb.recomputeChildLayout(); 7712 7713 if(this.header is null) { 7714 container.x = 0; 7715 container.y = 0; 7716 container.width = this.width - vsb_width; 7717 container.height = this.height - hsb_height; 7718 container.recomputeChildLayout(); 7719 } else { 7720 header.x = 0; 7721 header.y = 0; 7722 header.width = this.width - vsb_width; 7723 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 7724 header.recomputeChildLayout(); 7725 7726 container.x = 0; 7727 container.y = scaleWithDpi(BUTTON_SIZE); 7728 container.width = this.width - vsb_width; 7729 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 7730 container.recomputeChildLayout(); 7731 } 7732 } 7733 7734 private HorizontalScrollbar hsb; 7735 private VerticalScrollbar vsb; 7736 Widget container; 7737 private Widget header; 7738 7739 /++ 7740 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 7741 7742 History: 7743 Added September 27, 2021 (dub v10.3) 7744 +/ 7745 Widget getHeader() { 7746 if(this.header is null) { 7747 magic = false; 7748 scope(exit) magic = true; 7749 this.header = new Widget(this); 7750 recomputeChildLayout(); 7751 } 7752 return this.header; 7753 } 7754 7755 /++ 7756 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 7757 7758 History: 7759 Added January 3, 2023 (dub v11.0) 7760 +/ 7761 void scrollIntoView(Rectangle rect) { 7762 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 7763 7764 // import std.stdio; writeln(viewRectangle, " ", rect, " ", viewRectangle.contains(rect.lowerRight)); 7765 7766 if(!viewRectangle.contains(rect.lowerRight)) 7767 setPosition(rect.upperLeft.tupleof); 7768 7769 } 7770 7771 override int minHeight() { 7772 int min = container ? container.minHeight : 0; 7773 if(header !is null) 7774 min += header.minHeight; 7775 if(horizontalScrollBar.showing) 7776 min += horizontalScrollBar.minHeight; 7777 return min; 7778 } 7779 7780 override int maxHeight() { 7781 int max = container ? container.maxHeight : int.max; 7782 if(max == int.max) 7783 return max; 7784 if(horizontalScrollBar.showing) 7785 max += horizontalScrollBar.minHeight; 7786 return max; 7787 } 7788 } 7789 7790 /++ 7791 $(IMG //arsdnet.net/minigui-screenshots/windows/ScrollMessageWidget.png, A box saying "baby will" with three round buttons inside it for the options of "eat", "cry", and "sleep") 7792 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 7793 +/ 7794 version(minigui_screenshots) 7795 @Screenshot("ScrollMessageWidget") 7796 unittest { 7797 auto window = new Window("ScrollMessageWidget"); 7798 7799 auto smw = new ScrollMessageWidget(window); 7800 smw.addDefaultKeyboardListeners(); 7801 smw.addDefaultWheelListeners(); 7802 7803 window.loop(); 7804 } 7805 7806 /++ 7807 Bypasses automatic layout for its children, using manual positioning and sizing only. 7808 While you need to manually position them, you must ensure they are inside the StaticLayout's 7809 bounding box to avoid undefined behavior. 7810 7811 You should almost never use this. 7812 +/ 7813 class StaticLayout : Layout { 7814 /// 7815 this(Widget parent) { super(parent); } 7816 override void recomputeChildLayout() { 7817 registerMovement(); 7818 foreach(child; children) 7819 child.recomputeChildLayout(); 7820 } 7821 } 7822 7823 /++ 7824 Bypasses automatic positioning when being laid out. It is your responsibility to make 7825 room for this widget in the parent layout. 7826 7827 Its children are laid out normally, unless there is exactly one, in which case it takes 7828 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 7829 can do that with `padding`). 7830 +/ 7831 class StaticPosition : Layout { 7832 /// 7833 this(Widget parent) { super(parent); } 7834 7835 override void recomputeChildLayout() { 7836 registerMovement(); 7837 if(this.children.length == 1) { 7838 auto child = children[0]; 7839 child.x = 0; 7840 child.y = 0; 7841 child.width = this.width; 7842 child.height = this.height; 7843 child.recomputeChildLayout(); 7844 } else 7845 foreach(child; children) 7846 child.recomputeChildLayout(); 7847 } 7848 7849 alias width = typeof(super).width; 7850 alias height = typeof(super).height; 7851 7852 @property int width(int w) @nogc pure @safe nothrow { 7853 return this._width = w; 7854 } 7855 7856 @property int height(int w) @nogc pure @safe nothrow { 7857 return this._height = w; 7858 } 7859 7860 } 7861 7862 /++ 7863 FixedPosition is like [StaticPosition], but its coordinates 7864 are always relative to the viewport, meaning they do not scroll with 7865 the parent content. 7866 +/ 7867 class FixedPosition : StaticPosition { 7868 /// 7869 this(Widget parent) { super(parent); } 7870 } 7871 7872 version(win32_widgets) 7873 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 7874 if(true) { 7875 // cmd == 0 = menu, cmd == 1 = accelerator 7876 if(auto item = idm in Action.mapping) { 7877 foreach(handler; (*item).triggered) 7878 handler(); 7879 /* 7880 auto event = new Event("triggered", *item); 7881 event.button = idm; 7882 event.dispatch(); 7883 */ 7884 return 0; 7885 } 7886 } 7887 if(handle) 7888 if(auto widgetp = handle in Widget.nativeMapping) { 7889 (*widgetp).handleWmCommand(cmd, idm); 7890 return 0; 7891 } 7892 return 1; 7893 } 7894 7895 7896 /// 7897 class Window : Widget { 7898 int mouseCaptureCount = 0; 7899 Widget mouseCapturedBy; 7900 void captureMouse(Widget byWhom) { 7901 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 7902 mouseCaptureCount++; 7903 mouseCapturedBy = byWhom; 7904 win.grabInput(); 7905 } 7906 void releaseMouseCapture() { 7907 mouseCaptureCount--; 7908 mouseCapturedBy = null; 7909 win.releaseInputGrab(); 7910 } 7911 7912 /++ 7913 Sets the window icon which is often seen in title bars and taskbars. 7914 7915 History: 7916 Added April 5, 2022 (dub v10.8) 7917 +/ 7918 @property void icon(MemoryImage icon) { 7919 if(win && icon) 7920 win.icon = icon; 7921 } 7922 7923 /// 7924 @scriptable 7925 @property bool focused() { 7926 return win.focused; 7927 } 7928 7929 static class Style : Widget.Style { 7930 override WidgetBackground background() { 7931 version(custom_widgets) 7932 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 7933 else version(win32_widgets) 7934 return WidgetBackground(Color.transparent); 7935 else static assert(0); 7936 } 7937 } 7938 mixin OverrideStyle!Style; 7939 7940 /++ 7941 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. 7942 +/ 7943 static int lineHeight() { 7944 OperatingSystemFont font; 7945 if(auto vt = WidgetPainter.visualTheme) { 7946 font = vt.defaultFontCached(); 7947 } 7948 7949 if(font is null) { 7950 static int defaultHeightCache; 7951 if(defaultHeightCache == 0) { 7952 font = new OperatingSystemFont; 7953 font.loadDefault; 7954 defaultHeightCache = font.height();// * 5 / 4; 7955 } 7956 return defaultHeightCache; 7957 } 7958 7959 return font.height();// * 5 / 4; 7960 } 7961 7962 Widget focusedWidget; 7963 7964 private SimpleWindow win_; 7965 7966 @property { 7967 /++ 7968 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 7969 7970 History: 7971 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 7972 +/ 7973 public SimpleWindow win() { 7974 return win_; 7975 } 7976 /// 7977 protected void win(SimpleWindow w) { 7978 win_ = w; 7979 } 7980 } 7981 7982 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 7983 this(Widget p) { 7984 tabStop = false; 7985 super(p); 7986 } 7987 7988 private void actualRedraw() { 7989 if(recomputeChildLayoutRequired) 7990 recomputeChildLayoutEntry(); 7991 if(!showing) return; 7992 7993 assert(parentWindow !is null); 7994 7995 auto w = drawableWindow; 7996 if(w is null) 7997 w = parentWindow.win; 7998 7999 if(w.closed()) 8000 return; 8001 8002 auto ugh = this.parent; 8003 int lox, loy; 8004 while(ugh) { 8005 lox += ugh.x; 8006 loy += ugh.y; 8007 ugh = ugh.parent; 8008 } 8009 auto painter = w.draw(true); 8010 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 8011 // RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN); 8012 } 8013 8014 8015 private bool skipNextChar = false; 8016 8017 /++ 8018 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 8019 8020 This constructor is intended primarily for internal use and may be changed to `protected` later. 8021 +/ 8022 this(SimpleWindow win) { 8023 8024 static if(UsingSimpledisplayX11) { 8025 win.discardAdditionalConnectionState = &discardXConnectionState; 8026 win.recreateAdditionalConnectionState = &recreateXConnectionState; 8027 } 8028 8029 tabStop = false; 8030 super(null); 8031 this.win = win; 8032 8033 win.addEventListener((Widget.RedrawEvent) { 8034 if(win.eventQueued!RecomputeEvent) { 8035 // import std.stdio; writeln("skipping"); 8036 return; // let the recompute event do the actual redraw 8037 } 8038 this.actualRedraw(); 8039 }); 8040 8041 win.addEventListener((Widget.RecomputeEvent) { 8042 recomputeChildLayoutEntry(); 8043 if(win.eventQueued!RedrawEvent) 8044 return; // let the queued one do it 8045 else { 8046 // import std.stdio; writeln("drawing"); 8047 this.actualRedraw(); // if not queued, it needs to be done now anyway 8048 } 8049 }); 8050 8051 this.width = win.width; 8052 this.height = win.height; 8053 this.parentWindow = this; 8054 8055 win.closeQuery = () { 8056 if(this.emit!ClosingEvent()) 8057 win.close(); 8058 }; 8059 win.onClosing = () { 8060 this.emit!ClosedEvent(); 8061 }; 8062 8063 win.windowResized = (int w, int h) { 8064 this.width = w; 8065 this.height = h; 8066 recomputeChildLayout(); 8067 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 8068 //version(win32_widgets) 8069 //InvalidateRect(hwnd, null, true); 8070 redraw(); 8071 }; 8072 8073 win.onFocusChange = (bool getting) { 8074 if(this.focusedWidget) { 8075 if(getting) { 8076 this.focusedWidget.emit!FocusEvent(); 8077 this.focusedWidget.emit!FocusInEvent(); 8078 } else { 8079 this.focusedWidget.emit!BlurEvent(); 8080 this.focusedWidget.emit!FocusOutEvent(); 8081 } 8082 } 8083 8084 if(getting) { 8085 this.emit!FocusEvent(); 8086 this.emit!FocusInEvent(); 8087 } else { 8088 this.emit!BlurEvent(); 8089 this.emit!FocusOutEvent(); 8090 } 8091 }; 8092 8093 win.onDpiChanged = { 8094 this.queueRecomputeChildLayout(); 8095 auto event = new DpiChangedEvent(this); 8096 event.sendDirectly(); 8097 8098 privateDpiChanged(); 8099 }; 8100 8101 win.setEventHandlers( 8102 (MouseEvent e) { 8103 dispatchMouseEvent(e); 8104 }, 8105 (KeyEvent e) { 8106 //import std.stdio; 8107 //writefln("%x %s", cast(uint) e.key, e.key); 8108 dispatchKeyEvent(e); 8109 }, 8110 (dchar e) { 8111 if(e == 13) e = 10; // hack? 8112 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8113 dispatchCharEvent(e); 8114 }, 8115 ); 8116 8117 addEventListener("char", (Widget, Event ev) { 8118 if(skipNextChar) { 8119 ev.preventDefault(); 8120 skipNextChar = false; 8121 } 8122 }); 8123 8124 version(win32_widgets) 8125 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 8126 if(hwnd !is this.win.impl.hwnd) 8127 return 1; // we don't care... pass it on 8128 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 8129 if(mustReturn) 8130 return ret; 8131 return 1; // pass it on 8132 }; 8133 8134 if(Window.newWindowCreated) 8135 Window.newWindowCreated(this); 8136 } 8137 8138 version(custom_widgets) 8139 override void defaultEventHandler_click(ClickEvent event) { 8140 if(event.target && event.target.tabStop) 8141 event.target.focus(); 8142 } 8143 8144 private static void delegate(Window) newWindowCreated; 8145 8146 version(win32_widgets) 8147 override void paint(WidgetPainter painter) { 8148 /* 8149 RECT rect; 8150 rect.right = this.width; 8151 rect.bottom = this.height; 8152 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 8153 */ 8154 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 8155 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 8156 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 8157 // since the pen is null, to fill the whole space, we need the +1 on both. 8158 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 8159 SelectObject(painter.impl.hdc, p); 8160 SelectObject(painter.impl.hdc, b); 8161 } 8162 version(custom_widgets) 8163 override void paint(WidgetPainter painter) { 8164 auto cs = getComputedStyle(); 8165 painter.fillColor = cs.windowBackgroundColor; 8166 painter.outlineColor = cs.windowBackgroundColor; 8167 painter.drawRectangle(Point(0, 0), this.width, this.height); 8168 } 8169 8170 8171 override void defaultEventHandler_keydown(KeyDownEvent event) { 8172 Widget _this = event.target; 8173 8174 if(event.key == Key.Tab) { 8175 /* Window tab ordering is a recursive thingy with each group */ 8176 8177 // FIXME inefficient 8178 Widget[] helper(Widget p) { 8179 if(p.hidden) 8180 return null; 8181 Widget[] childOrdering; 8182 8183 auto children = p.children.dup; 8184 8185 while(true) { 8186 // UIs should be generally small, so gonna brute force it a little 8187 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 8188 8189 Widget smallestTab; 8190 foreach(ref c; children) { 8191 if(c is null) continue; 8192 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 8193 smallestTab = c; 8194 c = null; 8195 } 8196 } 8197 if(smallestTab !is null) { 8198 if(smallestTab.tabStop && !smallestTab.hidden) 8199 childOrdering ~= smallestTab; 8200 if(!smallestTab.hidden) 8201 childOrdering ~= helper(smallestTab); 8202 } else 8203 break; 8204 8205 } 8206 8207 return childOrdering; 8208 } 8209 8210 Widget[] tabOrdering = helper(this); 8211 8212 Widget recipient; 8213 8214 if(tabOrdering.length) { 8215 bool seenThis = false; 8216 Widget previous; 8217 foreach(idx, child; tabOrdering) { 8218 if(child is focusedWidget) { 8219 8220 if(event.shiftKey) { 8221 if(idx == 0) 8222 recipient = tabOrdering[$-1]; 8223 else 8224 recipient = tabOrdering[idx - 1]; 8225 break; 8226 } 8227 8228 seenThis = true; 8229 if(idx + 1 == tabOrdering.length) { 8230 // we're at the end, either move to the next group 8231 // or start back over 8232 recipient = tabOrdering[0]; 8233 } 8234 continue; 8235 } 8236 if(seenThis) { 8237 recipient = child; 8238 break; 8239 } 8240 previous = child; 8241 } 8242 } 8243 8244 if(recipient !is null) { 8245 // import std.stdio; writeln(typeid(recipient)); 8246 recipient.focus(); 8247 8248 skipNextChar = true; 8249 } 8250 } 8251 8252 debug if(event.key == Key.F12) { 8253 if(devTools) { 8254 devTools.close(); 8255 devTools = null; 8256 } else { 8257 devTools = new DevToolWindow(this); 8258 devTools.show(); 8259 } 8260 } 8261 } 8262 8263 debug DevToolWindow devTools; 8264 8265 8266 /++ 8267 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 8268 8269 History: 8270 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 8271 8272 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 8273 +/ 8274 this(int width = 500, int height = 500, string title = null) { 8275 if(title is null) { 8276 import core.runtime; 8277 if(Runtime.args.length) 8278 title = Runtime.args[0]; 8279 } 8280 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus); 8281 8282 static if(UsingSimpledisplayX11) { 8283 ///+ 8284 // for input proxy 8285 auto display = XDisplayConnection.get; 8286 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 8287 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 8288 XMapWindow(display, inputProxy); 8289 //import std.stdio; writefln("input proxy: 0x%0x", inputProxy); 8290 this.inputProxy = new SimpleWindow(inputProxy); 8291 8292 XEvent lastEvent; 8293 this.inputProxy.handleNativeEvent = (XEvent ev) { 8294 lastEvent = ev; 8295 return 1; 8296 }; 8297 this.inputProxy.setEventHandlers( 8298 (MouseEvent e) { 8299 dispatchMouseEvent(e); 8300 }, 8301 (KeyEvent e) { 8302 //import std.stdio; 8303 //writefln("%x %s", cast(uint) e.key, e.key); 8304 if(dispatchKeyEvent(e)) { 8305 // FIXME: i should trap error 8306 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 8307 auto thing = nw.focusableWindow(); 8308 if(thing && thing.window) { 8309 lastEvent.xkey.window = thing.window; 8310 // import std.stdio; writeln("sending event ", lastEvent.xkey); 8311 trapXErrors( { 8312 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 8313 }); 8314 } 8315 } 8316 } 8317 }, 8318 (dchar e) { 8319 if(e == 13) e = 10; // hack? 8320 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8321 dispatchCharEvent(e); 8322 }, 8323 ); 8324 8325 this.inputProxy.populateXic(); 8326 // done 8327 //+/ 8328 } 8329 8330 8331 8332 win.setRequestedInputFocus = &this.setRequestedInputFocus; 8333 8334 this(win); 8335 } 8336 8337 SimpleWindow inputProxy; 8338 8339 private SimpleWindow setRequestedInputFocus() { 8340 return inputProxy; 8341 } 8342 8343 /// ditto 8344 this(string title, int width = 500, int height = 500) { 8345 this(width, height, title); 8346 } 8347 8348 /// 8349 @property string title() { return parentWindow.win.title; } 8350 /// 8351 @property void title(string title) { parentWindow.win.title = title; } 8352 8353 /// 8354 @scriptable 8355 void close() { 8356 win.close(); 8357 // I synchronize here upon window closing to ensure all child windows 8358 // get updated too before the event loop. This avoids some random X errors. 8359 static if(UsingSimpledisplayX11) { 8360 runInGuiThread( { 8361 XSync(XDisplayConnection.get, false); 8362 }); 8363 } 8364 } 8365 8366 bool dispatchKeyEvent(KeyEvent ev) { 8367 auto wid = focusedWidget; 8368 if(wid is null) 8369 wid = this; 8370 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 8371 event.originalKeyEvent = ev; 8372 event.key = ev.key; 8373 event.state = ev.modifierState; 8374 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8375 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8376 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8377 event.dispatch(); 8378 8379 return !event.propagationStopped; 8380 } 8381 8382 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 8383 bool dispatchCharEvent(dchar ch) { 8384 if(focusedWidget) { 8385 auto event = new CharEvent(focusedWidget, ch); 8386 event.dispatch(); 8387 return !event.propagationStopped; 8388 } 8389 return true; 8390 } 8391 8392 Widget mouseLastOver; 8393 Widget mouseLastDownOn; 8394 bool lastWasDoubleClick; 8395 bool dispatchMouseEvent(MouseEvent ev) { 8396 auto eleR = widgetAtPoint(this, ev.x, ev.y); 8397 auto ele = eleR.widget; 8398 8399 auto captureEle = ele; 8400 8401 if(mouseCapturedBy !is null) { 8402 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 8403 captureEle = mouseCapturedBy; 8404 } 8405 8406 // a hack to get it relative to the widget. 8407 eleR.x = ev.x; 8408 eleR.y = ev.y; 8409 auto pain = captureEle; 8410 while(pain) { 8411 eleR.x -= pain.x; 8412 eleR.y -= pain.y; 8413 pain.addScrollPosition(eleR.x, eleR.y); 8414 pain = pain.parent; 8415 } 8416 8417 void populateMouseEventBase(MouseEventBase event) { 8418 event.button = ev.button; 8419 event.buttonLinear = ev.buttonLinear; 8420 event.state = ev.modifierState; 8421 event.clientX = eleR.x; 8422 event.clientY = eleR.y; 8423 8424 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8425 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8426 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8427 } 8428 8429 if(ev.type == MouseEventType.buttonPressed) { 8430 { 8431 auto event = new MouseDownEvent(captureEle); 8432 populateMouseEventBase(event); 8433 event.dispatch(); 8434 } 8435 8436 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 8437 auto event = new DoubleClickEvent(captureEle); 8438 populateMouseEventBase(event); 8439 event.dispatch(); 8440 lastWasDoubleClick = ev.doubleClick; 8441 } else { 8442 lastWasDoubleClick = false; 8443 } 8444 8445 mouseLastDownOn = ele; 8446 } else if(ev.type == MouseEventType.buttonReleased) { 8447 { 8448 auto event = new MouseUpEvent(captureEle); 8449 populateMouseEventBase(event); 8450 event.dispatch(); 8451 } 8452 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 8453 auto event = new ClickEvent(captureEle); 8454 populateMouseEventBase(event); 8455 event.dispatch(); 8456 } 8457 } else if(ev.type == MouseEventType.motion) { 8458 // motion 8459 { 8460 auto event = new MouseMoveEvent(captureEle); 8461 populateMouseEventBase(event); // fills in button which is meaningless but meh 8462 event.dispatch(); 8463 } 8464 8465 if(mouseLastOver !is ele) { 8466 if(ele !is null) { 8467 if(!isAParentOf(ele, mouseLastOver)) { 8468 ele.setDynamicState(DynamicState.hover, true); 8469 auto event = new MouseEnterEvent(ele); 8470 event.relatedTarget = mouseLastOver; 8471 event.sendDirectly(); 8472 8473 ele.useStyleProperties((scope Widget.Style s) { 8474 ele.parentWindow.win.cursor = s.cursor; 8475 }); 8476 } 8477 } 8478 8479 if(mouseLastOver !is null) { 8480 if(!isAParentOf(mouseLastOver, ele)) { 8481 mouseLastOver.setDynamicState(DynamicState.hover, false); 8482 auto event = new MouseLeaveEvent(mouseLastOver); 8483 event.relatedTarget = ele; 8484 event.sendDirectly(); 8485 } 8486 } 8487 8488 if(ele !is null) { 8489 auto event = new MouseOverEvent(ele); 8490 event.relatedTarget = mouseLastOver; 8491 event.dispatch(); 8492 } 8493 8494 if(mouseLastOver !is null) { 8495 auto event = new MouseOutEvent(mouseLastOver); 8496 event.relatedTarget = ele; 8497 event.dispatch(); 8498 } 8499 8500 mouseLastOver = ele; 8501 } 8502 } 8503 8504 return true; // FIXME: the event default prevented? 8505 } 8506 8507 /++ 8508 Shows the window and runs the application event loop. 8509 8510 Blocks until this window is closed. 8511 8512 Bugs: 8513 8514 $(PITFALL 8515 You should always have one event loop live for your application. 8516 If you make two windows in sequence, the second call to loop (or 8517 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 8518 might fail: 8519 8520 --- 8521 // don't do this! 8522 auto window = new Window(); 8523 window.loop(); 8524 8525 // or new Window or new MainWindow, all the same 8526 auto window2 = new SimpleWindow(); 8527 window2.eventLoop(0); // problematic! might crash 8528 --- 8529 8530 simpledisplay's current implementation assumes that final cleanup is 8531 done when the event loop refcount reaches zero. So after the first 8532 eventLoop returns, when there isn't already another one active, it assumes 8533 the program will exit soon and cleans up. 8534 8535 This is arguably a bug that it doesn't reinitialize, and I'll probably change 8536 it eventually, but in the mean time, there's an easy solution: 8537 8538 --- 8539 // do this 8540 EventLoop mainEventLoop = EventLoop.get; // just add this line 8541 8542 auto window = new Window(); 8543 window.loop(); 8544 8545 // or any other type of Window etc. 8546 auto window2 = new Window(); 8547 window2.loop(); // perfectly fine since mainEventLoop still alive 8548 --- 8549 8550 By adding a top-level reference to the event loop, it ensures the final cleanup 8551 is not performed until it goes out of scope too, letting the individual window loops 8552 work without trouble despite the bug. 8553 ) 8554 8555 History: 8556 The [BlockingMode] parameter was added on December 8, 2021. 8557 The default behavior is to block until the application quits 8558 (so all windows have been closed), unless another minigui or 8559 simpledisplay event loop is already running, in which case it 8560 will block until this window closes specifically. 8561 +/ 8562 @scriptable 8563 void loop(BlockingMode bm = BlockingMode.automatic) { 8564 if(win.closed) 8565 return; // otherwise show will throw 8566 show(); 8567 win.eventLoopWithBlockingMode(bm, 0); 8568 } 8569 8570 private bool firstShow = true; 8571 8572 @scriptable 8573 override void show() { 8574 bool rd = false; 8575 if(firstShow) { 8576 firstShow = false; 8577 recomputeChildLayout(); 8578 auto f = getFirstFocusable(this); // FIXME: autofocus? 8579 if(f) 8580 f.focus(); 8581 redraw(); 8582 } 8583 win.show(); 8584 super.show(); 8585 } 8586 @scriptable 8587 override void hide() { 8588 win.hide(); 8589 super.hide(); 8590 } 8591 8592 static Widget getFirstFocusable(Widget start) { 8593 if(start is null) 8594 return null; 8595 8596 foreach(widget; &start.focusableWidgets) { 8597 return widget; 8598 } 8599 8600 return null; 8601 } 8602 8603 static Widget getLastFocusable(Widget start) { 8604 if(start is null) 8605 return null; 8606 8607 Widget last; 8608 foreach(widget; &start.focusableWidgets) { 8609 last = widget; 8610 } 8611 8612 return last; 8613 } 8614 8615 8616 mixin Emits!ClosingEvent; 8617 mixin Emits!ClosedEvent; 8618 } 8619 8620 /++ 8621 History: 8622 Added January 12, 2022 8623 +/ 8624 class DpiChangedEvent : Event { 8625 enum EventString = "dpichanged"; 8626 8627 this(Widget target) { 8628 super(EventString, target); 8629 } 8630 } 8631 8632 debug private class DevToolWindow : Window { 8633 Window p; 8634 8635 TextEdit parentList; 8636 TextEdit logWindow; 8637 TextLabel clickX, clickY; 8638 8639 this(Window p) { 8640 this.p = p; 8641 super(400, 300, "Developer Toolbox"); 8642 8643 logWindow = new TextEdit(this); 8644 parentList = new TextEdit(this); 8645 8646 auto hl = new HorizontalLayout(this); 8647 clickX = new TextLabel("", TextAlignment.Right, hl); 8648 clickY = new TextLabel("", TextAlignment.Right, hl); 8649 8650 parentListeners ~= p.addEventListener("*", (Event ev) { 8651 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 8652 }); 8653 8654 parentListeners ~= p.addEventListener((ClickEvent ev) { 8655 auto s = ev.srcElement; 8656 8657 string list; 8658 8659 void addInfo(Widget s) { 8660 list ~= s.toString(); 8661 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 8662 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 8663 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 8664 list ~= "\n\theight: " ~ toInternal!string(s.height); 8665 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 8666 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 8667 } 8668 8669 addInfo(s); 8670 8671 s = s.parent; 8672 while(s) { 8673 list ~= "\n"; 8674 addInfo(s); 8675 s = s.parent; 8676 } 8677 parentList.content = list; 8678 8679 clickX.label = toInternal!string(ev.clientX); 8680 clickY.label = toInternal!string(ev.clientY); 8681 }); 8682 } 8683 8684 EventListener[] parentListeners; 8685 8686 override void close() { 8687 assert(p !is null); 8688 foreach(p; parentListeners) 8689 p.disconnect(); 8690 parentListeners = null; 8691 p.devTools = null; 8692 p = null; 8693 super.close(); 8694 } 8695 8696 override void defaultEventHandler_keydown(KeyDownEvent ev) { 8697 if(ev.key == Key.F12) { 8698 this.close(); 8699 if(p) 8700 p.devTools = null; 8701 } else { 8702 super.defaultEventHandler_keydown(ev); 8703 } 8704 } 8705 8706 void log(T...)(T t) { 8707 string str; 8708 import std.conv; 8709 foreach(i; t) 8710 str ~= to!string(i); 8711 str ~= "\n"; 8712 logWindow.addText(str); 8713 8714 //version(custom_widgets) 8715 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 8716 } 8717 } 8718 8719 /++ 8720 A dialog is a transient window that intends to get information from 8721 the user before being dismissed. 8722 +/ 8723 abstract class Dialog : Window { 8724 /// 8725 this(int width, int height, string title = null) { 8726 super(width, height, title); 8727 } 8728 8729 /// 8730 abstract void OK(); 8731 8732 /// 8733 void Cancel() { 8734 this.close(); 8735 } 8736 } 8737 8738 /++ 8739 A custom widget similar to the HTML5 <details> tag. 8740 +/ 8741 version(none) 8742 class DetailsView : Widget { 8743 8744 } 8745 8746 // FIXME: maybe i should expose the other list views Windows offers too 8747 8748 /++ 8749 A TableView is a widget made to display a table of data strings. 8750 8751 8752 Future_Directions: 8753 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 8754 8755 I will add a selection changed event at some point, as well as item clicked events. 8756 History: 8757 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 8758 See_Also: 8759 [ListWidget] which displays a list of strings without additional columns. 8760 +/ 8761 class TableView : Widget { 8762 /++ 8763 8764 +/ 8765 this(Widget parent) { 8766 super(parent); 8767 8768 version(win32_widgets) { 8769 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 8770 } else version(custom_widgets) { 8771 auto smw = new ScrollMessageWidget(this); 8772 smw.addDefaultKeyboardListeners(); 8773 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 8774 tvwi = new TableViewWidgetInner(this, smw); 8775 } 8776 } 8777 8778 // FIXME: auto-size columns on double click of header thing like in Windows 8779 // it need only make the currently displayed things fit well. 8780 8781 8782 private ColumnInfo[] columns; 8783 private int itemCount; 8784 8785 version(custom_widgets) private { 8786 TableViewWidgetInner tvwi; 8787 } 8788 8789 /// Passed to [setColumnInfo] 8790 static struct ColumnInfo { 8791 const(char)[] name; /// the name displayed in the header 8792 /++ 8793 The default width, in pixels. As a special case, you can set this to -1 8794 if you want the system to try to automatically size the width to fit visible 8795 content. If it can't, it will try to pick a sensible default size. 8796 8797 Any other negative value is not allowed and may lead to unpredictable results. 8798 8799 History: 8800 The -1 behavior was specified on December 3, 2021. It actually worked before 8801 anyway on Win32 but now it is a formal feature with partial Linux support. 8802 8803 Bugs: 8804 It doesn't actually attempt to calculate a best-fit width on Linux as of 8805 December 3, 2021. I do plan to fix this in the future, but Windows is the 8806 priority right now. At least it doesn't break things when you use it now. 8807 +/ 8808 int width; 8809 8810 /++ 8811 Alignment of the text in the cell. Applies to the header as well as all data in this 8812 column. 8813 8814 Bugs: 8815 On Windows, the first column ignores this member and is always left aligned. 8816 You can work around this by inserting a dummy first column with width = 0 8817 then putting your actual data in the second column, which does respect the 8818 alignment. 8819 8820 This is a quirk of the operating system's implementation going back a very 8821 long time and is unlikely to ever be fixed. 8822 +/ 8823 TextAlignment alignment; 8824 8825 /++ 8826 After all the pixel widths have been assigned, any left over 8827 space is divided up among all columns and distributed to according 8828 to the widthPercent field. 8829 8830 8831 For example, if you have two fields, both with width 50 and one with 8832 widthPercent of 25 and the other with widthPercent of 75, and the 8833 container is 200 pixels wide, first both get their width of 50. 8834 then the 100 remaining pixels are split up, so the one gets a total 8835 of 75 pixels and the other gets a total of 125. 8836 8837 This is automatically applied as the window is resized. 8838 8839 If there is not enough space - that is, when a horizontal scrollbar 8840 needs to appear - there are 0 pixels divided up, and thus everyone 8841 gets 0. This can cause a column to shrink out of proportion when 8842 passing the scroll threshold. 8843 8844 It is important to still set a fixed width (that is, to populate the 8845 `width` field) even if you use the percents because that will be the 8846 default minimum in the event of a scroll bar appearing. 8847 8848 The percents total in the column can never exceed 100 or be less than 0. 8849 Doing this will trigger an assert error. 8850 8851 Implementation note: 8852 8853 Please note that percentages are only recalculated 1) upon original 8854 construction and 2) upon resizing the control. If the user adjusts the 8855 width of a column, the percentage items will not be updated. 8856 8857 On the other hand, if the user adjusts the width of a percentage column 8858 then resizes the window, it is recalculated, meaning their hand adjustment 8859 is discarded. This specific behavior may change in the future as it is 8860 arguably a bug, but I'm not certain yet. 8861 8862 History: 8863 Added November 10, 2021 (dub v10.4) 8864 +/ 8865 int widthPercent; 8866 8867 8868 private int calculatedWidth; 8869 } 8870 /++ 8871 Sets the number of columns along with information about the headers. 8872 8873 Please note: on Windows, the first column ignores your alignment preference 8874 and is always left aligned. 8875 +/ 8876 void setColumnInfo(ColumnInfo[] columns...) { 8877 8878 foreach(ref c; columns) { 8879 c.name = c.name.idup; 8880 } 8881 this.columns = columns.dup; 8882 8883 updateCalculatedWidth(false); 8884 8885 version(custom_widgets) { 8886 tvwi.header.updateHeaders(); 8887 tvwi.updateScrolls(); 8888 } else version(win32_widgets) 8889 foreach(i, column; this.columns) { 8890 LVCOLUMN lvColumn; 8891 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 8892 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 8893 8894 auto bfr = WCharzBuffer(column.name); 8895 lvColumn.pszText = bfr.ptr; 8896 8897 if(column.alignment & TextAlignment.Center) 8898 lvColumn.fmt = LVCFMT_CENTER; 8899 else if(column.alignment & TextAlignment.Right) 8900 lvColumn.fmt = LVCFMT_RIGHT; 8901 else 8902 lvColumn.fmt = LVCFMT_LEFT; 8903 8904 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 8905 throw new WindowsApiException("Insert Column Fail", GetLastError()); 8906 } 8907 } 8908 8909 private int getActualSetSize(size_t i, bool askWindows) { 8910 version(win32_widgets) 8911 if(askWindows) 8912 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 8913 auto w = columns[i].width; 8914 if(w == -1) 8915 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 8916 return w; 8917 } 8918 8919 private void updateCalculatedWidth(bool informWindows) { 8920 int padding; 8921 version(win32_widgets) 8922 padding = 4; 8923 int remaining = this.width; 8924 foreach(i, column; columns) 8925 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 8926 remaining -= padding; 8927 if(remaining < 0) 8928 remaining = 0; 8929 8930 int percentTotal; 8931 foreach(i, ref column; columns) { 8932 percentTotal += column.widthPercent; 8933 8934 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 8935 8936 column.calculatedWidth = c; 8937 8938 version(win32_widgets) 8939 if(informWindows) 8940 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 8941 } 8942 8943 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 8944 assert(percentTotal <= 100, "The total percents in your column definitions exceeded 100. They must add up to no more than 100 (can be less though)."); 8945 8946 8947 } 8948 8949 override void registerMovement() { 8950 super.registerMovement(); 8951 8952 updateCalculatedWidth(true); 8953 } 8954 8955 /++ 8956 Tells the view how many items are in it. It uses this to set the scroll bar, but the items are not added per se; it calls [getData] as-needed. 8957 +/ 8958 void setItemCount(int count) { 8959 this.itemCount = count; 8960 version(custom_widgets) { 8961 tvwi.updateScrolls(); 8962 redraw(); 8963 } else version(win32_widgets) { 8964 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 8965 } 8966 } 8967 8968 /++ 8969 Clears all items; 8970 +/ 8971 void clear() { 8972 this.itemCount = 0; 8973 this.columns = null; 8974 version(custom_widgets) { 8975 tvwi.header.updateHeaders(); 8976 tvwi.updateScrolls(); 8977 redraw(); 8978 } else version(win32_widgets) { 8979 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 8980 } 8981 } 8982 8983 /+ 8984 version(win32_widgets) 8985 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 8986 auto itemId = dis.itemID; 8987 auto hdc = dis.hDC; 8988 auto rect = dis.rcItem; 8989 switch(dis.itemAction) { 8990 case ODA_DRAWENTIRE: 8991 8992 // FIXME: do other items 8993 // FIXME: do the focus rectangle i guess 8994 // FIXME: alignment 8995 // FIXME: column width 8996 // FIXME: padding left 8997 // FIXME: check dpi scaling 8998 // FIXME: don't owner draw unless it is necessary. 8999 9000 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 9001 RECT itemRect; 9002 itemRect.top = 1; // subitem idx, 1-based 9003 itemRect.left = LVIR_BOUNDS; 9004 9005 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 9006 itemRect.left += padding; 9007 9008 getData(itemId, 0, (in char[] data) { 9009 auto wdata = WCharzBuffer(data); 9010 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 9011 9012 }); 9013 goto case; 9014 case ODA_FOCUS: 9015 if(dis.itemState & ODS_FOCUS) 9016 DrawFocusRect(hdc, &rect); 9017 break; 9018 case ODA_SELECT: 9019 // itemState & ODS_SELECTED 9020 break; 9021 default: 9022 } 9023 return 1; 9024 } 9025 +/ 9026 9027 version(win32_widgets) { 9028 CellStyle last; 9029 COLORREF defaultColor; 9030 COLORREF defaultBackground; 9031 } 9032 9033 version(win32_widgets) 9034 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 9035 switch(code) { 9036 case NM_CUSTOMDRAW: 9037 auto s = cast(NMLVCUSTOMDRAW*) hdr; 9038 switch(s.nmcd.dwDrawStage) { 9039 case CDDS_PREPAINT: 9040 if(getCellStyle is null) 9041 return 0; 9042 9043 mustReturn = true; 9044 return CDRF_NOTIFYITEMDRAW; 9045 case CDDS_ITEMPREPAINT: 9046 mustReturn = true; 9047 return CDRF_NOTIFYSUBITEMDRAW; 9048 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 9049 mustReturn = true; 9050 9051 if(getCellStyle is null) // this SHOULD never happen... 9052 return 0; 9053 9054 if(s.iSubItem == 0) { 9055 // Windows resets it per row so we'll use item 0 as a chance 9056 // to capture these for later 9057 defaultColor = s.clrText; 9058 defaultBackground = s.clrTextBk; 9059 } 9060 9061 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 9062 // if no special style and no reset needed... 9063 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 9064 return 0; // allow default processing to continue 9065 9066 last = style; 9067 9068 // might still need to reset or use the preference. 9069 9070 if(style.flags & CellStyle.Flags.textColorSet) 9071 s.clrText = style.textColor.asWindowsColorRef; 9072 else 9073 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 9074 if(style.flags & CellStyle.Flags.backgroundColorSet) 9075 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 9076 else 9077 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 9078 9079 return CDRF_NEWFONT; 9080 default: 9081 return 0; 9082 9083 } 9084 case NM_RETURN: // no need since i subclass keydown 9085 break; 9086 case LVN_COLUMNCLICK: 9087 auto info = cast(LPNMLISTVIEW) hdr; 9088 this.emit!HeaderClickedEvent(info.iSubItem); 9089 break; 9090 case NM_CLICK: 9091 case NM_DBLCLK: 9092 case NM_RCLICK: 9093 case NM_RDBLCLK: 9094 // the item/subitem is set here and that can be a useful notification 9095 // even beyond the normal click notification 9096 break; 9097 case LVN_GETDISPINFO: 9098 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 9099 if(info.item.mask & LVIF_TEXT) { 9100 if(getData) { 9101 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 9102 auto bfr = WCharzBuffer(dataReceived); 9103 auto len = info.item.cchTextMax; 9104 if(bfr.length < len) 9105 len = cast(typeof(len)) bfr.length; 9106 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 9107 info.item.pszText[len] = 0; 9108 }); 9109 } else { 9110 info.item.pszText[0] = 0; 9111 } 9112 //info.item.iItem 9113 //if(info.item.iSubItem) 9114 } 9115 break; 9116 default: 9117 } 9118 return 0; 9119 } 9120 9121 override bool encapsulatedChildren() { 9122 return true; 9123 } 9124 9125 /++ 9126 Informs the control that content has changed. 9127 9128 History: 9129 Added November 10, 2021 (dub v10.4) 9130 +/ 9131 void update() { 9132 version(custom_widgets) 9133 redraw(); 9134 else { 9135 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 9136 UpdateWindow(hwnd); 9137 } 9138 9139 9140 } 9141 9142 /++ 9143 Called by the system to request the text content of an individual cell. You 9144 should pass the text into the provided `sink` delegate. This function will be 9145 called for each visible cell as-needed when drawing. 9146 +/ 9147 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 9148 9149 /++ 9150 Available per-cell style customization options. Use one of the constructors 9151 provided to set the values conveniently, or default construct it and set individual 9152 values yourself. Just remember to set the `flags` so your values are actually used. 9153 If the flag isn't set, the field is ignored and the system default is used instead. 9154 9155 This is returned by the [getCellStyle] delegate. 9156 9157 Examples: 9158 --- 9159 // assumes you have a variables called `my_data` which is an array of arrays of numbers 9160 auto table = new TableView(window); 9161 // snip: you would set up columns here 9162 9163 // this is how you provide data to the table view class 9164 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 9165 import std.conv; 9166 sink(to!string(my_data[row][column])); 9167 }; 9168 9169 // and this is how you customize the colors 9170 table.getCellStyle = delegate(int row, int column) { 9171 return (my_data[row][column] < 0) ? 9172 TableView.CellStyle(Color.red); // make negative numbers red 9173 : TableView.CellStyle.init; // leave the rest alone 9174 }; 9175 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 9176 --- 9177 9178 History: 9179 Added November 27, 2021 (dub v10.4) 9180 +/ 9181 struct CellStyle { 9182 /// Sets just a custom text color, leaving the background as the default. Use caution with certain colors as it may have illeglible contrast on the (unknown to you) background color. 9183 this(Color textColor) { 9184 this.textColor = textColor; 9185 this.flags |= Flags.textColorSet; 9186 } 9187 /// Sets a custom text and background color. 9188 this(Color textColor, Color backgroundColor) { 9189 this.textColor = textColor; 9190 this.backgroundColor = backgroundColor; 9191 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 9192 } 9193 9194 Color textColor; 9195 Color backgroundColor; 9196 int flags; /// bitmask of [Flags] 9197 /// available options to combine into [flags] 9198 enum Flags { 9199 textColorSet = 1 << 0, 9200 backgroundColorSet = 1 << 1, 9201 } 9202 } 9203 /++ 9204 Companion delegate to [getData] that allows you to custom style each 9205 cell of the table. 9206 9207 Returns: 9208 A [CellStyle] structure that describes the desired style for the 9209 given cell. `return CellStyle.init` if you want the default style. 9210 9211 History: 9212 Added November 27, 2021 (dub v10.4) 9213 +/ 9214 CellStyle delegate(int row, int column) getCellStyle; 9215 9216 // i want to be able to do things like draw little colored things to show red for negative numbers 9217 // or background color indicators or even in-cell charts 9218 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 9219 9220 /++ 9221 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 9222 +/ 9223 mixin Emits!HeaderClickedEvent; 9224 } 9225 9226 /++ 9227 This is emitted by the [TableView] when a user clicks on a column header. 9228 9229 Its member `columnIndex` has the zero-based index of the column that was clicked. 9230 9231 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 9232 9233 History: 9234 Added November 27, 2021 (dub v10.4) 9235 +/ 9236 class HeaderClickedEvent : Event { 9237 enum EventString = "HeaderClicked"; 9238 this(Widget target, int columnIndex) { 9239 this.columnIndex = columnIndex; 9240 super(EventString, target); 9241 } 9242 9243 /// The index of the column 9244 int columnIndex; 9245 9246 /// 9247 override @property int intValue() { 9248 return columnIndex; 9249 } 9250 } 9251 9252 version(custom_widgets) 9253 private class TableViewWidgetInner : Widget { 9254 9255 // wrap this thing in a ScrollMessageWidget 9256 9257 TableView tvw; 9258 ScrollMessageWidget smw; 9259 HeaderWidget header; 9260 9261 this(TableView tvw, ScrollMessageWidget smw) { 9262 this.tvw = tvw; 9263 this.smw = smw; 9264 super(smw); 9265 9266 this.tabStop = true; 9267 9268 header = new HeaderWidget(this, smw.getHeader()); 9269 9270 smw.addEventListener("scroll", () { 9271 this.redraw(); 9272 header.redraw(); 9273 }); 9274 9275 9276 // I need headers outside the scroll area but rendered on the same line as the up arrow 9277 // FIXME: add a fixed header to the SMW 9278 } 9279 9280 enum padding = 3; 9281 9282 void updateScrolls() { 9283 int w; 9284 foreach(idx, column; tvw.columns) { 9285 if(column.width == 0) continue; 9286 w += tvw.getActualSetSize(idx, false);// + padding; 9287 } 9288 smw.setTotalArea(w, tvw.itemCount); 9289 columnsWidth = w; 9290 } 9291 9292 private int columnsWidth; 9293 9294 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 9295 9296 override void registerMovement() { 9297 super.registerMovement(); 9298 // FIXME: actual column width. it might need to be done per-pixel instead of per-colum 9299 smw.setViewableArea(this.width, this.height / lh); 9300 } 9301 9302 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 9303 int x; 9304 int y; 9305 9306 int row = smw.position.y; 9307 9308 foreach(lol; 0 .. this.height / lh) { 9309 if(row >= tvw.itemCount) 9310 break; 9311 x = 0; 9312 foreach(columnNumber, column; tvw.columns) { 9313 auto x2 = x + column.calculatedWidth; 9314 auto smwx = smw.position.x; 9315 9316 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 9317 auto startX = x; 9318 auto endX = x + column.calculatedWidth; 9319 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 9320 case TextAlignment.Left: startX += padding; break; 9321 case TextAlignment.Center: startX += padding; endX -= padding; break; 9322 case TextAlignment.Right: endX -= padding; break; 9323 default: /* broken */ break; 9324 } 9325 if(column.width != 0) // no point drawing an invisible column 9326 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 9327 // auto clip = painter.setClipRectangle( 9328 9329 void dotext(WidgetPainter painter) { 9330 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 9331 } 9332 9333 if(tvw.getCellStyle !is null) { 9334 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 9335 9336 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 9337 auto tempPainter = painter; 9338 tempPainter.fillColor = style.backgroundColor; 9339 tempPainter.outlineColor = style.backgroundColor; 9340 9341 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 9342 Point(endX - smw.position.x, y + lh)); 9343 } 9344 auto tempPainter = painter; 9345 if(style.flags & TableView.CellStyle.Flags.textColorSet) 9346 tempPainter.outlineColor = style.textColor; 9347 9348 dotext(tempPainter); 9349 } else { 9350 dotext(painter); 9351 } 9352 }); 9353 } 9354 9355 x += column.calculatedWidth; 9356 } 9357 row++; 9358 y += lh; 9359 } 9360 return bounds; 9361 } 9362 9363 static class Style : Widget.Style { 9364 override WidgetBackground background() { 9365 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 9366 } 9367 } 9368 mixin OverrideStyle!Style; 9369 9370 private static class HeaderWidget : Widget { 9371 this(TableViewWidgetInner tvw, Widget parent) { 9372 super(parent); 9373 this.tvw = tvw; 9374 9375 this.remainder = new Button("", this); 9376 9377 this.addEventListener((scope ClickEvent ev) { 9378 int header = -1; 9379 foreach(idx, child; this.children[1 .. $]) { 9380 if(child is ev.target) { 9381 header = cast(int) idx; 9382 break; 9383 } 9384 } 9385 9386 if(header != -1) { 9387 auto hce = new HeaderClickedEvent(tvw.tvw, header); 9388 hce.dispatch(); 9389 } 9390 9391 }); 9392 } 9393 9394 void updateHeaders() { 9395 foreach(child; children[1 .. $]) 9396 child.removeWidget(); 9397 9398 foreach(column; tvw.tvw.columns) { 9399 // the cast is ok because I dup it above, just the type is never changed. 9400 // all this is private so it should never get messed up. 9401 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 9402 } 9403 } 9404 9405 Button remainder; 9406 TableViewWidgetInner tvw; 9407 9408 override void recomputeChildLayout() { 9409 registerMovement(); 9410 int pos; 9411 foreach(idx, child; children[1 .. $]) { 9412 if(idx >= tvw.tvw.columns.length) 9413 continue; 9414 child.x = pos; 9415 child.y = 0; 9416 child.width = tvw.tvw.columns[idx].calculatedWidth; 9417 child.height = scaleWithDpi(16);// this.height; 9418 pos += child.width; 9419 9420 child.recomputeChildLayout(); 9421 } 9422 9423 if(remainder is null) 9424 return; 9425 9426 remainder.x = pos; 9427 remainder.y = 0; 9428 if(pos < this.width) 9429 remainder.width = this.width - pos;// + 4; 9430 else 9431 remainder.width = 0; 9432 remainder.height = scaleWithDpi(16); 9433 9434 remainder.recomputeChildLayout(); 9435 } 9436 9437 // for the scrollable children mixin 9438 Point scrollOrigin() { 9439 return Point(tvw.smw.position.x, 0); 9440 } 9441 void paintFrameAndBackground(WidgetPainter painter) { } 9442 9443 mixin ScrollableChildren; 9444 } 9445 } 9446 9447 /+ 9448 9449 // given struct / array / number / string / etc, make it viewable and editable 9450 class DataViewerWidget : Widget { 9451 9452 } 9453 +/ 9454 9455 /++ 9456 A line edit box with an associated label. 9457 9458 History: 9459 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 9460 9461 ``` 9462 Old: ________ 9463 9464 New: 9465 ____________ 9466 ``` 9467 9468 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 9469 9470 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 9471 horizontal label but left aligned. You may also consider a [GridLayout]. 9472 +/ 9473 alias LabeledLineEdit = Labeled!LineEdit; 9474 9475 /++ 9476 History: 9477 Added May 19, 2021 9478 +/ 9479 class Labeled(T) : Widget { 9480 /// 9481 this(string label, Widget parent) { 9482 super(parent); 9483 initialize!VerticalLayout(label, TextAlignment.Left, parent); 9484 } 9485 9486 /++ 9487 History: 9488 The alignment parameter was added May 17, 2021 9489 +/ 9490 this(string label, TextAlignment alignment, Widget parent) { 9491 super(parent); 9492 initialize!HorizontalLayout(label, alignment, parent); 9493 } 9494 9495 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 9496 tabStop = false; 9497 horizontal = is(L == HorizontalLayout); 9498 auto hl = new L(this); 9499 this.label = new TextLabel(label, alignment, hl); 9500 this.lineEdit = new T(hl); 9501 9502 this.label.labelFor = this.lineEdit; 9503 } 9504 9505 private bool horizontal; 9506 9507 TextLabel label; /// 9508 T lineEdit; /// 9509 9510 override int flexBasisWidth() { return 250; } 9511 9512 override int minHeight() { 9513 return this.children[0].minHeight; 9514 } 9515 override int maxHeight() { return minHeight(); } 9516 override int marginTop() { return 4; } 9517 override int marginBottom() { return 4; } 9518 9519 // FIXME: i should prolly call it value as well as content tbh 9520 9521 /// 9522 @property string content() { 9523 return lineEdit.content; 9524 } 9525 /// 9526 @property void content(string c) { 9527 return lineEdit.content(c); 9528 } 9529 9530 /// 9531 void selectAll() { 9532 lineEdit.selectAll(); 9533 } 9534 9535 override void focus() { 9536 lineEdit.focus(); 9537 } 9538 } 9539 9540 /++ 9541 A labeled password edit. 9542 9543 History: 9544 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 9545 9546 The default parameters for the constructors were also removed on May 19, 2021 9547 +/ 9548 alias LabeledPasswordEdit = Labeled!PasswordEdit; 9549 9550 private string toMenuLabel(string s) { 9551 string n; 9552 n.reserve(s.length); 9553 foreach(c; s) 9554 if(c == '_') 9555 n ~= ' '; 9556 else 9557 n ~= c; 9558 return n; 9559 } 9560 9561 private void autoExceptionHandler(Exception e) { 9562 messageBox(e.msg); 9563 } 9564 9565 private void delegate() makeAutomaticHandler(alias fn, T)(T t) { 9566 static if(is(T : void delegate())) { 9567 return () { 9568 try 9569 t(); 9570 catch(Exception e) 9571 autoExceptionHandler(e); 9572 }; 9573 } else static if(is(typeof(fn) Params == __parameters)) { 9574 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 9575 return () { 9576 void onOK(string s) { 9577 member = s; 9578 try 9579 t(Params[0](s)); 9580 catch(Exception e) 9581 autoExceptionHandler(e); 9582 } 9583 9584 if( 9585 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 9586 || type == FileDialogType.Save) 9587 { 9588 getSaveFileName(&onOK, member, filters, null); 9589 } else 9590 getOpenFileName(&onOK, member, filters, null); 9591 }; 9592 } else { 9593 struct S { 9594 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 9595 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 9596 } else mixin(q{ 9597 static foreach(idx, ignore; Params) { 9598 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 9599 } 9600 }); 9601 } 9602 return () { 9603 dialog((S s) { 9604 try { 9605 static if(is(typeof(t) Ret == return)) { 9606 static if(is(Ret == void)) { 9607 t(s.tupleof); 9608 } else { 9609 auto ret = t(s.tupleof); 9610 import std.conv; 9611 messageBox(to!string(ret), "Returned Value"); 9612 } 9613 } 9614 } catch(Exception e) 9615 autoExceptionHandler(e); 9616 }, null, __traits(identifier, fn)); 9617 }; 9618 } 9619 } 9620 } 9621 9622 private template hasAnyRelevantAnnotations(a...) { 9623 bool helper() { 9624 bool any; 9625 foreach(attr; a) { 9626 static if(is(typeof(attr) == .menu)) 9627 any = true; 9628 else static if(is(typeof(attr) == .toolbar)) 9629 any = true; 9630 else static if(is(attr == .separator)) 9631 any = true; 9632 else static if(is(typeof(attr) == .accelerator)) 9633 any = true; 9634 else static if(is(typeof(attr) == .hotkey)) 9635 any = true; 9636 else static if(is(typeof(attr) == .icon)) 9637 any = true; 9638 else static if(is(typeof(attr) == .label)) 9639 any = true; 9640 else static if(is(typeof(attr) == .tip)) 9641 any = true; 9642 } 9643 return any; 9644 } 9645 9646 enum bool hasAnyRelevantAnnotations = helper(); 9647 } 9648 9649 /++ 9650 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. 9651 +/ 9652 class MainWindow : Window { 9653 /// 9654 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 9655 super(initialWidth, initialHeight, title); 9656 9657 _clientArea = new ClientAreaWidget(); 9658 _clientArea.x = 0; 9659 _clientArea.y = 0; 9660 _clientArea.width = this.width; 9661 _clientArea.height = this.height; 9662 _clientArea.tabStop = false; 9663 9664 super.addChild(_clientArea); 9665 9666 statusBar = new StatusBar(this); 9667 } 9668 9669 /++ 9670 Adds a menu and toolbar from annotated functions. 9671 9672 --- 9673 struct Commands { 9674 @menu("File") { 9675 void New() {} 9676 void Open() {} 9677 void Save() {} 9678 @separator 9679 void Exit() @accelerator("Alt+F4") @hotkey('x') { 9680 window.close(); 9681 } 9682 } 9683 9684 @menu("Edit") { 9685 void Undo() { 9686 undo(); 9687 } 9688 @separator 9689 void Cut() {} 9690 void Copy() {} 9691 void Paste() {} 9692 } 9693 9694 @menu("Help") { 9695 void About() {} 9696 } 9697 } 9698 9699 Commands commands; 9700 9701 window.setMenuAndToolbarFromAnnotatedCode(commands); 9702 --- 9703 9704 Note that you can call this function multiple times and it will add the items in order to the given items. 9705 9706 +/ 9707 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 9708 setMenuAndToolbarFromAnnotatedCode_internal(t); 9709 } 9710 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 9711 setMenuAndToolbarFromAnnotatedCode_internal(t); 9712 } 9713 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 9714 Action[] toolbarActions; 9715 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 9716 Menu[string] mcs; 9717 9718 foreach(menu; menuBar.subMenus) { 9719 mcs[menu.label] = menu; 9720 } 9721 9722 foreach(memberName; __traits(derivedMembers, T)) { 9723 static if(memberName != "this") 9724 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 9725 .menu menu; 9726 .toolbar toolbar; 9727 bool separator; 9728 .accelerator accelerator; 9729 .hotkey hotkey; 9730 .icon icon; 9731 string label; 9732 string tip; 9733 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 9734 static if(is(typeof(attr) == .menu)) 9735 menu = attr; 9736 else static if(is(typeof(attr) == .toolbar)) 9737 toolbar = attr; 9738 else static if(is(attr == .separator)) 9739 separator = true; 9740 else static if(is(typeof(attr) == .accelerator)) 9741 accelerator = attr; 9742 else static if(is(typeof(attr) == .hotkey)) 9743 hotkey = attr; 9744 else static if(is(typeof(attr) == .icon)) 9745 icon = attr; 9746 else static if(is(typeof(attr) == .label)) 9747 label = attr.label; 9748 else static if(is(typeof(attr) == .tip)) 9749 tip = attr.tip; 9750 } 9751 9752 if(menu !is .menu.init || toolbar !is .toolbar.init) { 9753 ushort correctIcon = icon.id; // FIXME 9754 if(label.length == 0) 9755 label = memberName.toMenuLabel; 9756 9757 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName)); 9758 9759 auto action = new Action(label, correctIcon, handler); 9760 9761 if(accelerator.keyString.length) { 9762 auto ke = KeyEvent.parse(accelerator.keyString); 9763 action.accelerator = ke; 9764 accelerators[ke.toStr] = handler; 9765 } 9766 9767 if(toolbar !is .toolbar.init) 9768 toolbarActions ~= action; 9769 if(menu !is .menu.init) { 9770 Menu mc; 9771 if(menu.name in mcs) { 9772 mc = mcs[menu.name]; 9773 } else { 9774 mc = new Menu(menu.name, this); 9775 menuBar.addItem(mc); 9776 mcs[menu.name] = mc; 9777 } 9778 9779 if(separator) 9780 mc.addSeparator(); 9781 mc.addItem(new MenuItem(action)); 9782 } 9783 } 9784 } 9785 } 9786 9787 this.menuBar = menuBar; 9788 9789 if(toolbarActions.length) { 9790 auto tb = new ToolBar(toolbarActions, this); 9791 } 9792 } 9793 9794 void delegate()[string] accelerators; 9795 9796 override void defaultEventHandler_keydown(KeyDownEvent event) { 9797 auto str = event.originalKeyEvent.toStr; 9798 if(auto acl = str in accelerators) 9799 (*acl)(); 9800 super.defaultEventHandler_keydown(event); 9801 } 9802 9803 override void defaultEventHandler_mouseover(MouseOverEvent event) { 9804 super.defaultEventHandler_mouseover(event); 9805 if(this.statusBar !is null && event.target.statusTip.length) 9806 this.statusBar.parts[0].content = event.target.statusTip; 9807 else if(this.statusBar !is null && this.statusTip.length) 9808 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 9809 } 9810 9811 override void addChild(Widget c, int position = int.max) { 9812 if(auto tb = cast(ToolBar) c) 9813 version(win32_widgets) 9814 super.addChild(c, 0); 9815 else version(custom_widgets) 9816 super.addChild(c, menuBar ? 1 : 0); 9817 else static assert(0); 9818 else 9819 clientArea.addChild(c, position); 9820 } 9821 9822 ToolBar _toolBar; 9823 /// 9824 ToolBar toolBar() { return _toolBar; } 9825 /// 9826 ToolBar toolBar(ToolBar t) { 9827 _toolBar = t; 9828 foreach(child; this.children) 9829 if(child is t) 9830 return t; 9831 version(win32_widgets) 9832 super.addChild(t, 0); 9833 else version(custom_widgets) 9834 super.addChild(t, menuBar ? 1 : 0); 9835 else static assert(0); 9836 return t; 9837 } 9838 9839 MenuBar _menu; 9840 /// 9841 MenuBar menuBar() { return _menu; } 9842 /// 9843 MenuBar menuBar(MenuBar m) { 9844 if(m is _menu) { 9845 version(custom_widgets) 9846 recomputeChildLayout(); 9847 return m; 9848 } 9849 9850 if(_menu !is null) { 9851 // make sure it is sanely removed 9852 // FIXME 9853 } 9854 9855 _menu = m; 9856 9857 version(win32_widgets) { 9858 SetMenu(parentWindow.win.impl.hwnd, m.handle); 9859 } else version(custom_widgets) { 9860 super.addChild(m, 0); 9861 9862 // clientArea.y = menu.height; 9863 // clientArea.height = this.height - menu.height; 9864 9865 recomputeChildLayout(); 9866 } else static assert(false); 9867 9868 return _menu; 9869 } 9870 private Widget _clientArea; 9871 /// 9872 @property Widget clientArea() { return _clientArea; } 9873 protected @property void clientArea(Widget wid) { 9874 _clientArea = wid; 9875 } 9876 9877 private StatusBar _statusBar; 9878 /++ 9879 Returns the window's [StatusBar]. Be warned it may be `null`. 9880 +/ 9881 @property StatusBar statusBar() { return _statusBar; } 9882 /// ditto 9883 @property void statusBar(StatusBar bar) { 9884 if(_statusBar !is null) 9885 _statusBar.removeWidget(); 9886 _statusBar = bar; 9887 if(bar !is null) 9888 super.addChild(_statusBar); 9889 } 9890 } 9891 9892 /+ 9893 This is really an implementation detail of [MainWindow] 9894 +/ 9895 private class ClientAreaWidget : Widget { 9896 this() { 9897 this.tabStop = false; 9898 super(null); 9899 //sa = new ScrollableWidget(this); 9900 } 9901 /* 9902 ScrollableWidget sa; 9903 override void addChild(Widget w, int position) { 9904 if(sa is null) 9905 super.addChild(w, position); 9906 else { 9907 sa.addChild(w, position); 9908 sa.setContentSize(this.minWidth + 1, this.minHeight); 9909 import std.stdio; writeln(sa.contentWidth, "x", sa.contentHeight); 9910 } 9911 } 9912 */ 9913 } 9914 9915 /** 9916 Toolbars are lists of buttons (typically icons) that appear under the menu. 9917 Each button ought to correspond to a menu item, represented by [Action] objects. 9918 */ 9919 class ToolBar : Widget { 9920 version(win32_widgets) { 9921 private const int idealHeight; 9922 override int minHeight() { return idealHeight; } 9923 override int maxHeight() { return idealHeight; } 9924 } else version(custom_widgets) { 9925 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 9926 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 9927 } else static assert(false); 9928 override int heightStretchiness() { return 0; } 9929 9930 version(win32_widgets) 9931 HIMAGELIST imageList; 9932 9933 this(Widget parent) { 9934 this(null, parent); 9935 } 9936 9937 /// 9938 this(Action[] actions, Widget parent) { 9939 super(parent); 9940 9941 tabStop = false; 9942 9943 version(win32_widgets) { 9944 // so i like how the flat thing looks on windows, but not on wine 9945 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 9946 // leave it commented 9947 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 9948 9949 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 9950 9951 imageList = ImageList_Create( 9952 // width, height 9953 16, 16, 9954 ILC_COLOR16 | ILC_MASK, 9955 16 /*numberOfButtons*/, 0); 9956 9957 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageList); 9958 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 9959 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 9960 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 9961 9962 TBBUTTON[] buttons; 9963 9964 // FIXME: I_IMAGENONE is if here is no icon 9965 foreach(action; actions) 9966 buttons ~= TBBUTTON( 9967 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 9968 action.id, 9969 TBSTATE_ENABLED, // state 9970 0, // style 9971 0, // reserved array, just zero it out 9972 0, // dwData 9973 cast(size_t) toWstringzInternal(action.label) // INT_PTR 9974 ); 9975 9976 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 9977 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 9978 9979 SIZE size; 9980 import core.sys.windows.commctrl; 9981 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 9982 idealHeight = size.cy + 4; // the plus 4 is a hack 9983 9984 /* 9985 RECT rect; 9986 GetWindowRect(hwnd, &rect); 9987 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 9988 */ 9989 9990 assert(idealHeight); 9991 } else version(custom_widgets) { 9992 foreach(action; actions) 9993 new ToolButton(action, this); 9994 } else static assert(false); 9995 } 9996 9997 override void recomputeChildLayout() { 9998 .recomputeChildLayout!"width"(this); 9999 } 10000 } 10001 10002 enum toolbarIconSize = 24; 10003 10004 /// 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. 10005 class ToolButton : Button { 10006 /// 10007 this(string label, Widget parent) { 10008 super(label, parent); 10009 tabStop = false; 10010 } 10011 /// 10012 this(Action action, Widget parent) { 10013 super(action.label, parent); 10014 tabStop = false; 10015 this.action = action; 10016 } 10017 10018 version(custom_widgets) 10019 override void defaultEventHandler_click(ClickEvent event) { 10020 foreach(handler; action.triggered) 10021 handler(); 10022 } 10023 10024 Action action; 10025 10026 override int maxWidth() { return toolbarIconSize; } 10027 override int minWidth() { return toolbarIconSize; } 10028 override int maxHeight() { return toolbarIconSize; } 10029 override int minHeight() { return toolbarIconSize; } 10030 10031 version(custom_widgets) 10032 override void paint(WidgetPainter painter) { 10033 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 10034 painter.outlineColor = Color.black; 10035 10036 // I want to get from 16 to 24. that's * 3 / 2 10037 static assert(toolbarIconSize >= 16); 10038 enum multiplier = toolbarIconSize / 8; 10039 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 10040 switch(action.iconId) { 10041 case GenericIcons.New: 10042 painter.fillColor = Color.white; 10043 painter.drawPolygon( 10044 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 10045 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 10046 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 10047 ); 10048 break; 10049 case GenericIcons.Save: 10050 painter.fillColor = Color.white; 10051 painter.outlineColor = Color.black; 10052 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10053 10054 // the label 10055 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 10056 10057 // the slider 10058 painter.fillColor = Color.black; 10059 painter.outlineColor = Color.black; 10060 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 10061 10062 painter.fillColor = Color.white; 10063 painter.outlineColor = Color.white; 10064 // the disc window 10065 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 10066 break; 10067 case GenericIcons.Open: 10068 painter.fillColor = Color.white; 10069 painter.drawPolygon( 10070 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 10071 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 10072 painter.drawPolygon( 10073 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 10074 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 10075 Point(2, 6) * multiplier / divisor); 10076 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 10077 break; 10078 case GenericIcons.Copy: 10079 painter.fillColor = Color.white; 10080 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 10081 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 10082 break; 10083 case GenericIcons.Cut: 10084 painter.fillColor = Color.transparent; 10085 painter.outlineColor = getComputedStyle.foregroundColor(); 10086 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 10087 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 10088 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 10089 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 10090 break; 10091 case GenericIcons.Paste: 10092 painter.fillColor = Color.white; 10093 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 10094 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10095 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 10096 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 10097 painter.fillColor = Color.black; 10098 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 10099 break; 10100 case GenericIcons.Help: 10101 painter.outlineColor = getComputedStyle.foregroundColor(); 10102 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10103 break; 10104 case GenericIcons.Undo: 10105 painter.fillColor = Color.transparent; 10106 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10107 painter.outlineColor = Color.black; 10108 painter.fillColor = Color.black; 10109 painter.drawPolygon( 10110 Point(4, 4) * multiplier / divisor, 10111 Point(8, 2) * multiplier / divisor, 10112 Point(8, 6) * multiplier / divisor, 10113 Point(4, 4) * multiplier / divisor, 10114 ); 10115 break; 10116 case GenericIcons.Redo: 10117 painter.fillColor = Color.transparent; 10118 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10119 painter.outlineColor = Color.black; 10120 painter.fillColor = Color.black; 10121 painter.drawPolygon( 10122 Point(10, 4) * multiplier / divisor, 10123 Point(6, 2) * multiplier / divisor, 10124 Point(6, 6) * multiplier / divisor, 10125 Point(10, 4) * multiplier / divisor, 10126 ); 10127 break; 10128 default: 10129 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10130 } 10131 return bounds; 10132 }); 10133 } 10134 10135 } 10136 10137 10138 /// 10139 class MenuBar : Widget { 10140 MenuItem[] items; 10141 Menu[] subMenus; 10142 10143 version(win32_widgets) { 10144 HMENU handle; 10145 /// 10146 this(Widget parent = null) { 10147 super(parent); 10148 10149 handle = CreateMenu(); 10150 tabStop = false; 10151 } 10152 } else version(custom_widgets) { 10153 /// 10154 this(Widget parent = null) { 10155 tabStop = false; // these are selected some other way 10156 super(parent); 10157 } 10158 10159 mixin Padding!q{2}; 10160 } else static assert(false); 10161 10162 version(custom_widgets) 10163 override void paint(WidgetPainter painter) { 10164 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 10165 } 10166 10167 /// 10168 MenuItem addItem(MenuItem item) { 10169 this.addChild(item); 10170 items ~= item; 10171 version(win32_widgets) { 10172 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10173 } 10174 return item; 10175 } 10176 10177 10178 /// 10179 Menu addItem(Menu item) { 10180 10181 subMenus ~= item; 10182 10183 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 10184 10185 addChild(mbItem); 10186 items ~= mbItem; 10187 10188 version(win32_widgets) { 10189 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 10190 } else version(custom_widgets) { 10191 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 10192 item.popup(mbItem); 10193 }; 10194 } else static assert(false); 10195 10196 return item; 10197 } 10198 10199 override void recomputeChildLayout() { 10200 .recomputeChildLayout!"width"(this); 10201 } 10202 10203 override int maxHeight() { return defaultLineHeight + 4; } 10204 override int minHeight() { return defaultLineHeight + 4; } 10205 } 10206 10207 10208 /** 10209 Status bars appear at the bottom of a MainWindow. 10210 They are made out of Parts, with a width and content. 10211 10212 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 10213 10214 10215 sb.parts[0].content = "Status bar text!"; 10216 */ 10217 class StatusBar : Widget { 10218 private Part[] partsArray; 10219 /// 10220 struct Parts { 10221 @disable this(); 10222 this(StatusBar owner) { this.owner = owner; } 10223 //@disable this(this); 10224 /// 10225 @property int length() { return cast(int) owner.partsArray.length; } 10226 private StatusBar owner; 10227 private this(StatusBar owner, Part[] parts) { 10228 this.owner.partsArray = parts; 10229 this.owner = owner; 10230 } 10231 /// 10232 Part opIndex(int p) { 10233 if(owner.partsArray.length == 0) 10234 this ~= new StatusBar.Part(300); 10235 return owner.partsArray[p]; 10236 } 10237 10238 /// 10239 Part opOpAssign(string op : "~" )(Part p) { 10240 assert(owner.partsArray.length < 255); 10241 p.owner = this.owner; 10242 p.idx = cast(int) owner.partsArray.length; 10243 owner.partsArray ~= p; 10244 version(win32_widgets) { 10245 int[256] pos; 10246 int cpos = 0; 10247 foreach(idx, part; owner.partsArray) { 10248 if(part.width) 10249 cpos += part.width; 10250 else 10251 cpos += 100; 10252 10253 if(idx + 1 == owner.partsArray.length) 10254 pos[idx] = -1; 10255 else 10256 pos[idx] = cpos; 10257 } 10258 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 10259 } else version(custom_widgets) { 10260 owner.redraw(); 10261 } else static assert(false); 10262 10263 return p; 10264 } 10265 } 10266 10267 private Parts _parts; 10268 /// 10269 final @property Parts parts() { 10270 return _parts; 10271 } 10272 10273 /// 10274 static class Part { 10275 int width; 10276 StatusBar owner; 10277 10278 /// 10279 this(int w = 100) { width = w; } 10280 10281 private int idx; 10282 private string _content; 10283 /// 10284 @property string content() { return _content; } 10285 /// 10286 @property void content(string s) { 10287 version(win32_widgets) { 10288 _content = s; 10289 WCharzBuffer bfr = WCharzBuffer(s); 10290 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 10291 } else version(custom_widgets) { 10292 if(_content != s) { 10293 _content = s; 10294 owner.redraw(); 10295 } 10296 } else static assert(false); 10297 } 10298 } 10299 string simpleModeContent; 10300 bool inSimpleMode; 10301 10302 10303 /// 10304 this(Widget parent) { 10305 super(null); // FIXME 10306 _parts = Parts(this); 10307 tabStop = false; 10308 version(win32_widgets) { 10309 parentWindow = parent.parentWindow; 10310 createWin32Window(this, "msctls_statusbar32"w, "", 0); 10311 10312 RECT rect; 10313 GetWindowRect(hwnd, &rect); 10314 idealHeight = rect.bottom - rect.top; 10315 assert(idealHeight); 10316 } else version(custom_widgets) { 10317 } else static assert(false); 10318 } 10319 10320 version(win32_widgets) 10321 override protected void dpiChanged() { 10322 RECT rect; 10323 GetWindowRect(hwnd, &rect); 10324 idealHeight = rect.bottom - rect.top; 10325 assert(idealHeight); 10326 } 10327 10328 version(custom_widgets) 10329 override void paint(WidgetPainter painter) { 10330 auto cs = getComputedStyle(); 10331 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10332 int cpos = 0; 10333 int remainingLength = this.width; 10334 foreach(idx, part; this.partsArray) { 10335 auto partWidth = part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 10336 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 10337 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 10338 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 10339 10340 painter.outlineColor = cs.foregroundColor(); 10341 painter.fillColor = cs.foregroundColor(); 10342 10343 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 10344 cpos += partWidth; 10345 remainingLength -= partWidth; 10346 } 10347 } 10348 10349 10350 version(win32_widgets) { 10351 private int idealHeight; 10352 override int maxHeight() { return idealHeight; } 10353 override int minHeight() { return idealHeight; } 10354 } else version(custom_widgets) { 10355 override int maxHeight() { return defaultLineHeight + 4; } 10356 override int minHeight() { return defaultLineHeight + 4; } 10357 } else static assert(false); 10358 } 10359 10360 /// Displays an in-progress indicator without known values 10361 version(none) 10362 class IndefiniteProgressBar : Widget { 10363 version(win32_widgets) 10364 this(Widget parent) { 10365 super(parent); 10366 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 10367 tabStop = false; 10368 } 10369 override int minHeight() { return 10; } 10370 } 10371 10372 /// A progress bar with a known endpoint and completion amount 10373 class ProgressBar : Widget { 10374 /++ 10375 History: 10376 Added March 16, 2022 (dub v10.7) 10377 +/ 10378 this(int min, int max, Widget parent) { 10379 this(parent); 10380 setRange(cast(ushort) min, cast(ushort) max); // FIXME 10381 } 10382 this(Widget parent) { 10383 version(win32_widgets) { 10384 super(parent); 10385 createWin32Window(this, "msctls_progress32"w, "", 0); 10386 tabStop = false; 10387 } else version(custom_widgets) { 10388 super(parent); 10389 max = 100; 10390 step = 10; 10391 tabStop = false; 10392 } else static assert(0); 10393 } 10394 10395 version(custom_widgets) 10396 override void paint(WidgetPainter painter) { 10397 auto cs = getComputedStyle(); 10398 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10399 painter.fillColor = cs.progressBarColor; 10400 painter.drawRectangle(Point(0, 0), width * current / max, height); 10401 } 10402 10403 10404 version(custom_widgets) { 10405 int current; 10406 int max; 10407 int step; 10408 } 10409 10410 /// 10411 void advanceOneStep() { 10412 version(win32_widgets) 10413 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 10414 else version(custom_widgets) 10415 addToPosition(step); 10416 else static assert(false); 10417 } 10418 10419 /// 10420 void setStepIncrement(int increment) { 10421 version(win32_widgets) 10422 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 10423 else version(custom_widgets) 10424 step = increment; 10425 else static assert(false); 10426 } 10427 10428 /// 10429 void addToPosition(int amount) { 10430 version(win32_widgets) 10431 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 10432 else version(custom_widgets) 10433 setPosition(current + amount); 10434 else static assert(false); 10435 } 10436 10437 /// 10438 void setPosition(int pos) { 10439 version(win32_widgets) 10440 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 10441 else version(custom_widgets) { 10442 current = pos; 10443 if(current > max) 10444 current = max; 10445 redraw(); 10446 } 10447 else static assert(false); 10448 } 10449 10450 /// 10451 void setRange(ushort min, ushort max) { 10452 version(win32_widgets) 10453 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 10454 else version(custom_widgets) { 10455 this.max = max; 10456 } 10457 else static assert(false); 10458 } 10459 10460 override int minHeight() { return 10; } 10461 } 10462 10463 version(custom_widgets) 10464 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 10465 thisLabel.reserve(label.length); 10466 bool justSawAmpersand; 10467 foreach(ch; label) { 10468 if(justSawAmpersand) { 10469 justSawAmpersand = false; 10470 if(ch == '&') { 10471 goto plain; 10472 } 10473 thisAccelerator = ch; 10474 } else { 10475 if(ch == '&') { 10476 justSawAmpersand = true; 10477 continue; 10478 } 10479 plain: 10480 thisLabel ~= ch; 10481 } 10482 } 10483 } 10484 10485 /++ 10486 Creates the fieldset (also known as a group box) with the given label. A fieldset is generally used a container for mutually exclusive [Radiobox]s. 10487 10488 10489 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 10490 10491 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10492 10493 History: 10494 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 10495 +/ 10496 class Fieldset : Widget { 10497 // FIXME: on Windows,it doesn't draw the background on the label 10498 // on X, it doesn't fix the clipping rectangle for it 10499 version(win32_widgets) 10500 override int paddingTop() { return defaultLineHeight; } 10501 else version(custom_widgets) 10502 override int paddingTop() { return defaultLineHeight + 2; } 10503 else static assert(false); 10504 override int paddingBottom() { return 6; } 10505 override int paddingLeft() { return 6; } 10506 override int paddingRight() { return 6; } 10507 10508 override int marginLeft() { return 6; } 10509 override int marginRight() { return 6; } 10510 override int marginTop() { return 2; } 10511 override int marginBottom() { return 2; } 10512 10513 string legend; 10514 10515 version(custom_widgets) private dchar accelerator; 10516 10517 this(string legend, Widget parent) { 10518 version(win32_widgets) { 10519 super(parent); 10520 this.legend = legend; 10521 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 10522 tabStop = false; 10523 } else version(custom_widgets) { 10524 super(parent); 10525 tabStop = false; 10526 10527 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 10528 } else static assert(0); 10529 } 10530 10531 version(custom_widgets) 10532 override void paint(WidgetPainter painter) { 10533 painter.fillColor = Color.transparent; 10534 auto cs = getComputedStyle(); 10535 painter.pen = Pen(cs.foregroundColor, 1); 10536 painter.drawRectangle(Point(0, defaultLineHeight / 2), width, height - defaultLineHeight / 2); 10537 10538 auto tx = painter.textSize(legend); 10539 painter.outlineColor = Color.transparent; 10540 10541 static if(UsingSimpledisplayX11) { 10542 painter.fillColor = getComputedStyle().windowBackgroundColor; 10543 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 10544 } else version(Windows) { 10545 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 10546 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 10547 SelectObject(painter.impl.hdc, b); 10548 } else static assert(0); 10549 painter.outlineColor = cs.foregroundColor; 10550 painter.drawText(Point(8, 0), legend); 10551 } 10552 10553 override int maxHeight() { 10554 auto m = paddingTop() + paddingBottom(); 10555 foreach(child; children) { 10556 auto mh = child.maxHeight(); 10557 if(mh == int.max) 10558 return int.max; 10559 m += mh; 10560 m += child.marginBottom(); 10561 m += child.marginTop(); 10562 } 10563 m += 6; 10564 if(m < minHeight) 10565 return minHeight; 10566 return m; 10567 } 10568 10569 override int minHeight() { 10570 auto m = paddingTop() + paddingBottom(); 10571 foreach(child; children) { 10572 m += child.minHeight(); 10573 m += child.marginBottom(); 10574 m += child.marginTop(); 10575 } 10576 return m + 6; 10577 } 10578 } 10579 10580 /++ 10581 $(IMG //arsdnet.net/minigui-screenshots/windows/Fieldset.png, A box saying "baby will" with three round buttons inside it for the options of "eat", "cry", and "sleep") 10582 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 10583 +/ 10584 version(minigui_screenshots) 10585 @Screenshot("Fieldset") 10586 unittest { 10587 auto window = new Window(200, 100); 10588 auto set = new Fieldset("Baby will", window); 10589 auto option1 = new Radiobox("Eat", set); 10590 auto option2 = new Radiobox("Cry", set); 10591 auto option3 = new Radiobox("Sleep", set); 10592 window.loop(); 10593 } 10594 10595 /// Draws a line 10596 class HorizontalRule : Widget { 10597 mixin Margin!q{ 2 }; 10598 override int minHeight() { return 2; } 10599 override int maxHeight() { return 2; } 10600 10601 /// 10602 this(Widget parent) { 10603 super(parent); 10604 } 10605 10606 override void paint(WidgetPainter painter) { 10607 auto cs = getComputedStyle(); 10608 painter.outlineColor = cs.darkAccentColor; 10609 painter.drawLine(Point(0, 0), Point(width, 0)); 10610 painter.outlineColor = cs.lightAccentColor; 10611 painter.drawLine(Point(0, 1), Point(width, 1)); 10612 } 10613 } 10614 10615 version(minigui_screenshots) 10616 @Screenshot("HorizontalRule") 10617 /++ 10618 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 10619 10620 +/ 10621 unittest { 10622 auto window = new Window(200, 100); 10623 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 10624 new HorizontalRule(window); 10625 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 10626 window.loop(); 10627 } 10628 10629 /// ditto 10630 class VerticalRule : Widget { 10631 mixin Margin!q{ 2 }; 10632 override int minWidth() { return 2; } 10633 override int maxWidth() { return 2; } 10634 10635 /// 10636 this(Widget parent) { 10637 super(parent); 10638 } 10639 10640 override void paint(WidgetPainter painter) { 10641 auto cs = getComputedStyle(); 10642 painter.outlineColor = cs.darkAccentColor; 10643 painter.drawLine(Point(0, 0), Point(0, height)); 10644 painter.outlineColor = cs.lightAccentColor; 10645 painter.drawLine(Point(1, 0), Point(1, height)); 10646 } 10647 } 10648 10649 10650 /// 10651 class Menu : Window { 10652 void remove() { 10653 foreach(i, child; parentWindow.children) 10654 if(child is this) { 10655 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 10656 break; 10657 } 10658 parentWindow.redraw(); 10659 10660 parentWindow.releaseMouseCapture(); 10661 } 10662 10663 /// 10664 void addSeparator() { 10665 version(win32_widgets) 10666 AppendMenu(handle, MF_SEPARATOR, 0, null); 10667 else version(custom_widgets) 10668 auto hr = new HorizontalRule(this); 10669 else static assert(0); 10670 } 10671 10672 override int paddingTop() { return 4; } 10673 override int paddingBottom() { return 4; } 10674 override int paddingLeft() { return 2; } 10675 override int paddingRight() { return 2; } 10676 10677 version(win32_widgets) {} 10678 else version(custom_widgets) { 10679 SimpleWindow dropDown; 10680 Widget menuParent; 10681 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 10682 this.menuParent = parent; 10683 10684 int w = 150; 10685 int h = paddingTop + paddingBottom; 10686 if(this.children.length) { 10687 // hacking it to get the ideal height out of recomputeChildLayout 10688 this.width = w; 10689 this.height = h; 10690 this.recomputeChildLayout(); 10691 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 10692 h += paddingBottom; 10693 10694 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 10695 } 10696 10697 if(offsetY == int.min) 10698 offsetY = parent.defaultLineHeight; 10699 10700 auto coord = parent.globalCoordinates(); 10701 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 10702 this.x = 0; 10703 this.y = 0; 10704 this.width = dropDown.width; 10705 this.height = dropDown.height; 10706 this.drawableWindow = dropDown; 10707 this.recomputeChildLayout(); 10708 10709 static if(UsingSimpledisplayX11) 10710 XSync(XDisplayConnection.get, 0); 10711 10712 dropDown.visibilityChanged = (bool visible) { 10713 if(visible) { 10714 this.redraw(); 10715 dropDown.grabInput(); 10716 } else { 10717 dropDown.releaseInputGrab(); 10718 } 10719 }; 10720 10721 dropDown.show(); 10722 10723 clickListener = this.addEventListener((scope ClickEvent ev) { 10724 unpopup(); 10725 // need to unlock asap just in case other user handlers block... 10726 static if(UsingSimpledisplayX11) 10727 flushGui(); 10728 }, true /* again for asap action */); 10729 } 10730 10731 EventListener clickListener; 10732 } 10733 else static assert(false); 10734 10735 version(custom_widgets) 10736 void unpopup() { 10737 mouseLastOver = mouseLastDownOn = null; 10738 dropDown.hide(); 10739 if(!menuParent.parentWindow.win.closed) { 10740 if(auto maw = cast(MouseActivatedWidget) menuParent) { 10741 maw.setDynamicState(DynamicState.depressed, false); 10742 maw.setDynamicState(DynamicState.hover, false); 10743 maw.redraw(); 10744 } 10745 // menuParent.parentWindow.win.focus(); 10746 } 10747 clickListener.disconnect(); 10748 } 10749 10750 MenuItem[] items; 10751 10752 /// 10753 MenuItem addItem(MenuItem item) { 10754 addChild(item); 10755 items ~= item; 10756 version(win32_widgets) { 10757 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10758 } 10759 return item; 10760 } 10761 10762 string label; 10763 10764 version(win32_widgets) { 10765 HMENU handle; 10766 /// 10767 this(string label, Widget parent) { 10768 // not actually passing the parent since it effs up the drawing 10769 super(cast(Widget) null);// parent); 10770 this.label = label; 10771 handle = CreatePopupMenu(); 10772 } 10773 } else version(custom_widgets) { 10774 /// 10775 this(string label, Widget parent) { 10776 10777 if(dropDown) { 10778 dropDown.close(); 10779 } 10780 dropDown = new SimpleWindow( 10781 150, 4, 10782 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 10783 10784 this.label = label; 10785 10786 super(dropDown); 10787 } 10788 } else static assert(false); 10789 10790 override int maxHeight() { return defaultLineHeight; } 10791 override int minHeight() { return defaultLineHeight; } 10792 10793 version(custom_widgets) 10794 override void paint(WidgetPainter painter) { 10795 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 10796 } 10797 } 10798 10799 /++ 10800 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 10801 +/ 10802 class MenuItem : MouseActivatedWidget { 10803 Menu submenu; 10804 10805 Action action; 10806 string label; 10807 10808 override int paddingLeft() { return 4; } 10809 10810 override int maxHeight() { return defaultLineHeight + 4; } 10811 override int minHeight() { return defaultLineHeight + 4; } 10812 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 10813 override int maxWidth() { 10814 if(cast(MenuBar) parent) { 10815 return minWidth(); 10816 } 10817 return int.max; 10818 } 10819 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 10820 this(string lbl, Widget parent = null) { 10821 super(parent); 10822 //label = lbl; // FIXME 10823 foreach(char ch; lbl) // FIXME 10824 if(ch != '&') // FIXME 10825 label ~= ch; // FIXME 10826 tabStop = false; // these are selected some other way 10827 } 10828 10829 /// 10830 this(Action action, Widget parent = null) { 10831 assert(action !is null); 10832 this(action.label, parent); 10833 this.action = action; 10834 tabStop = false; // these are selected some other way 10835 } 10836 10837 version(custom_widgets) 10838 override void paint(WidgetPainter painter) { 10839 auto cs = getComputedStyle(); 10840 if(dynamicState & DynamicState.depressed) 10841 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10842 if(dynamicState & DynamicState.hover) 10843 painter.outlineColor = cs.activeMenuItemColor; 10844 else 10845 painter.outlineColor = cs.foregroundColor; 10846 painter.fillColor = Color.transparent; 10847 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 10848 if(action && action.accelerator !is KeyEvent.init) { 10849 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 10850 10851 } 10852 } 10853 10854 static class Style : Widget.Style { 10855 override bool variesWithState(ulong dynamicStateFlags) { 10856 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 10857 } 10858 } 10859 mixin OverrideStyle!Style; 10860 10861 override void defaultEventHandler_triggered(Event event) { 10862 if(action) 10863 foreach(handler; action.triggered) 10864 handler(); 10865 10866 if(auto pmenu = cast(Menu) this.parent) 10867 pmenu.remove(); 10868 10869 super.defaultEventHandler_triggered(event); 10870 } 10871 } 10872 10873 version(win32_widgets) 10874 /// A "mouse activiated widget" is really just an abstract variant of button. 10875 class MouseActivatedWidget : Widget { 10876 @property bool isChecked() { 10877 assert(hwnd); 10878 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 10879 10880 } 10881 @property void isChecked(bool state) { 10882 assert(hwnd); 10883 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 10884 10885 } 10886 10887 override void handleWmCommand(ushort cmd, ushort id) { 10888 if(cmd == 0) { 10889 auto event = new Event(EventType.triggered, this); 10890 event.dispatch(); 10891 } 10892 } 10893 10894 this(Widget parent) { 10895 super(parent); 10896 } 10897 } 10898 else version(custom_widgets) 10899 /// ditto 10900 class MouseActivatedWidget : Widget { 10901 @property bool isChecked() { return isChecked_; } 10902 @property bool isChecked(bool b) { return isChecked_ = b; } 10903 10904 private bool isChecked_; 10905 10906 this(Widget parent) { 10907 super(parent); 10908 10909 addEventListener((MouseDownEvent ev) { 10910 if(ev.button == MouseButton.left) { 10911 setDynamicState(DynamicState.depressed, true); 10912 setDynamicState(DynamicState.hover, true); 10913 redraw(); 10914 } 10915 }); 10916 10917 addEventListener((MouseUpEvent ev) { 10918 if(ev.button == MouseButton.left) { 10919 setDynamicState(DynamicState.depressed, false); 10920 setDynamicState(DynamicState.hover, false); 10921 redraw(); 10922 } 10923 }); 10924 10925 addEventListener((MouseMoveEvent mme) { 10926 if(!(mme.state & ModifierState.leftButtonDown)) { 10927 if(dynamicState_ & DynamicState.depressed) { 10928 setDynamicState(DynamicState.depressed, false); 10929 redraw(); 10930 } 10931 } 10932 }); 10933 } 10934 10935 override void defaultEventHandler_focus(Event ev) { 10936 super.defaultEventHandler_focus(ev); 10937 this.redraw(); 10938 } 10939 override void defaultEventHandler_blur(Event ev) { 10940 super.defaultEventHandler_blur(ev); 10941 setDynamicState(DynamicState.depressed, false); 10942 this.redraw(); 10943 } 10944 override void defaultEventHandler_keydown(KeyDownEvent ev) { 10945 super.defaultEventHandler_keydown(ev); 10946 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 10947 setDynamicState(DynamicState.depressed, true); 10948 setDynamicState(DynamicState.hover, true); 10949 this.redraw(); 10950 } 10951 } 10952 override void defaultEventHandler_keyup(KeyUpEvent ev) { 10953 super.defaultEventHandler_keyup(ev); 10954 if(!(dynamicState & DynamicState.depressed)) 10955 return; 10956 setDynamicState(DynamicState.depressed, false); 10957 setDynamicState(DynamicState.hover, false); 10958 this.redraw(); 10959 10960 auto event = new Event(EventType.triggered, this); 10961 event.sendDirectly(); 10962 } 10963 override void defaultEventHandler_click(ClickEvent ev) { 10964 super.defaultEventHandler_click(ev); 10965 if(ev.button == MouseButton.left) { 10966 auto event = new Event(EventType.triggered, this); 10967 event.sendDirectly(); 10968 } 10969 } 10970 10971 } 10972 else static assert(false); 10973 10974 /* 10975 /++ 10976 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 10977 10978 Basically the same as a checkbox. 10979 +/ 10980 class OnOffSwitch : MouseActivatedWidget { 10981 10982 } 10983 */ 10984 10985 /++ 10986 History: 10987 Added June 15, 2021 (dub v10.1) 10988 +/ 10989 struct ImageLabel { 10990 /++ 10991 Defines a label+image combo used by some widgets. 10992 10993 If you provide just a text label, that is all the widget will try to 10994 display. Or just an image will display just that. If you provide both, 10995 it may display both text and image side by side or display the image 10996 and offer text on an input event depending on the widget. 10997 10998 History: 10999 The `alignment` parameter was added on September 27, 2021 11000 +/ 11001 this(string label, TextAlignment alignment = TextAlignment.Center) { 11002 this.label = label; 11003 this.displayFlags = DisplayFlags.displayText; 11004 this.alignment = alignment; 11005 } 11006 11007 /// ditto 11008 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11009 this.label = label; 11010 this.image = image; 11011 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11012 this.alignment = alignment; 11013 } 11014 11015 /// ditto 11016 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11017 this.image = image; 11018 this.displayFlags = DisplayFlags.displayImage; 11019 this.alignment = alignment; 11020 } 11021 11022 /// ditto 11023 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 11024 this.label = label; 11025 this.image = image; 11026 this.alignment = alignment; 11027 this.displayFlags = displayFlags; 11028 } 11029 11030 string label; 11031 MemoryImage image; 11032 11033 enum DisplayFlags { 11034 displayText = 1 << 0, 11035 displayImage = 1 << 1, 11036 } 11037 11038 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11039 11040 TextAlignment alignment; 11041 } 11042 11043 /++ 11044 A basic checked or not checked box with an attached label. 11045 11046 11047 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 11048 11049 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11050 11051 History: 11052 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 11053 +/ 11054 class Checkbox : MouseActivatedWidget { 11055 version(win32_widgets) { 11056 override int maxHeight() { return scaleWithDpi(16); } 11057 override int minHeight() { return scaleWithDpi(16); } 11058 } else version(custom_widgets) { 11059 override int maxHeight() { return defaultLineHeight; } 11060 override int minHeight() { return defaultLineHeight; } 11061 } else static assert(0); 11062 11063 override int marginLeft() { return 4; } 11064 11065 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 11066 11067 /++ 11068 Just an alias because I keep typing checked out of web habit. 11069 11070 History: 11071 Added May 31, 2021 11072 +/ 11073 alias checked = isChecked; 11074 11075 private string label; 11076 private dchar accelerator; 11077 11078 /++ 11079 +/ 11080 this(string label, Widget parent) { 11081 this(ImageLabel(label), Appearance.checkbox, parent); 11082 } 11083 11084 /// ditto 11085 this(string label, Appearance appearance, Widget parent) { 11086 this(ImageLabel(label), appearance, parent); 11087 } 11088 11089 /++ 11090 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 11091 11092 History: 11093 Added June 29, 2021 (dub v10.2) 11094 +/ 11095 enum Appearance { 11096 checkbox, /// a normal checkbox 11097 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 11098 //sliderswitch, 11099 } 11100 private Appearance appearance; 11101 11102 /// ditto 11103 private this(ImageLabel label, Appearance appearance, Widget parent) { 11104 super(parent); 11105 version(win32_widgets) { 11106 this.label = label.label; 11107 11108 uint extraStyle; 11109 final switch(appearance) { 11110 case Appearance.checkbox: 11111 break; 11112 case Appearance.pushbutton: 11113 extraStyle |= BS_PUSHLIKE; 11114 break; 11115 } 11116 11117 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 11118 } else version(custom_widgets) { 11119 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 11120 } else static assert(0); 11121 } 11122 11123 version(custom_widgets) 11124 override void paint(WidgetPainter painter) { 11125 auto cs = getComputedStyle(); 11126 if(isFocused()) { 11127 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11128 painter.fillColor = cs.windowBackgroundColor; 11129 painter.drawRectangle(Point(0, 0), width, height); 11130 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11131 } else { 11132 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 11133 painter.fillColor = cs.windowBackgroundColor; 11134 painter.drawRectangle(Point(0, 0), width, height); 11135 } 11136 11137 11138 enum buttonSize = 16; 11139 11140 painter.outlineColor = Color.black; 11141 painter.fillColor = Color.white; 11142 painter.drawRectangle(scaleWithDpi(Point(2, 2)), scaleWithDpi(buttonSize - 2), scaleWithDpi(buttonSize - 2)); 11143 11144 if(isChecked) { 11145 painter.pen = Pen(Color.black, 2); 11146 // I'm using height so the checkbox is square 11147 enum padding = 5; 11148 painter.drawLine(scaleWithDpi(Point(padding, padding)), scaleWithDpi(Point(buttonSize - (padding-2), buttonSize - (padding-2)))); 11149 painter.drawLine(scaleWithDpi(Point(buttonSize-(padding-2), padding)), scaleWithDpi(Point(padding, buttonSize - (padding-2)))); 11150 11151 painter.pen = Pen(Color.black, 1); 11152 } 11153 11154 if(label !is null) { 11155 painter.outlineColor = cs.foregroundColor(); 11156 painter.fillColor = cs.foregroundColor(); 11157 11158 // FIXME: should prolly just align the baseline or something 11159 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 2)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11160 } 11161 } 11162 11163 override void defaultEventHandler_triggered(Event ev) { 11164 isChecked = !isChecked; 11165 11166 this.emit!(ChangeEvent!bool)(&isChecked); 11167 11168 redraw(); 11169 } 11170 11171 /// Emits a change event with the checked state 11172 mixin Emits!(ChangeEvent!bool); 11173 } 11174 11175 /// Adds empty space to a layout. 11176 class VerticalSpacer : Widget { 11177 /// 11178 this(Widget parent) { 11179 super(parent); 11180 } 11181 } 11182 11183 /// ditto 11184 class HorizontalSpacer : Widget { 11185 /// 11186 this(Widget parent) { 11187 super(parent); 11188 this.tabStop = false; 11189 } 11190 } 11191 11192 11193 /++ 11194 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 11195 11196 11197 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 11198 11199 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11200 11201 History: 11202 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 11203 +/ 11204 class Radiobox : MouseActivatedWidget { 11205 11206 version(win32_widgets) { 11207 override int maxHeight() { return scaleWithDpi(16); } 11208 override int minHeight() { return scaleWithDpi(16); } 11209 } else version(custom_widgets) { 11210 override int maxHeight() { return defaultLineHeight; } 11211 override int minHeight() { return defaultLineHeight; } 11212 } else static assert(0); 11213 11214 override int marginLeft() { return 4; } 11215 11216 // FIXME: make a label getter 11217 private string label; 11218 private dchar accelerator; 11219 11220 version(win32_widgets) 11221 this(string label, Widget parent) { 11222 super(parent); 11223 this.label = label; 11224 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 11225 } 11226 else version(custom_widgets) 11227 this(string label, Widget parent) { 11228 super(parent); 11229 label.extractWindowsStyleLabel(this.label, this.accelerator); 11230 height = 16; 11231 width = height + 4 + cast(int) label.length * 16; 11232 } 11233 else static assert(false); 11234 11235 version(custom_widgets) 11236 override void paint(WidgetPainter painter) { 11237 auto cs = getComputedStyle(); 11238 if(isFocused) { 11239 painter.fillColor = cs.windowBackgroundColor; 11240 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11241 } else { 11242 painter.fillColor = cs.windowBackgroundColor; 11243 painter.outlineColor = cs.windowBackgroundColor; 11244 } 11245 painter.drawRectangle(Point(0, 0), width, height); 11246 11247 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11248 11249 enum buttonSize = 16; 11250 11251 painter.outlineColor = Color.black; 11252 painter.fillColor = Color.white; 11253 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 11254 if(isChecked) { 11255 painter.outlineColor = Color.black; 11256 painter.fillColor = Color.black; 11257 // I'm using height so the checkbox is square 11258 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5))); 11259 } 11260 11261 painter.outlineColor = cs.foregroundColor(); 11262 painter.fillColor = cs.foregroundColor(); 11263 11264 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11265 } 11266 11267 11268 override void defaultEventHandler_triggered(Event ev) { 11269 isChecked = true; 11270 11271 if(this.parent) { 11272 foreach(child; this.parent.children) { 11273 if(child is this) continue; 11274 if(auto rb = cast(Radiobox) child) { 11275 rb.isChecked = false; 11276 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 11277 rb.redraw(); 11278 } 11279 } 11280 } 11281 11282 this.emit!(ChangeEvent!bool)(&this.isChecked); 11283 11284 redraw(); 11285 } 11286 11287 /// 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. 11288 mixin Emits!(ChangeEvent!bool); 11289 } 11290 11291 11292 /++ 11293 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 11294 11295 11296 Please note that the ampersand (&) character gets special treatment as described on this page https://docs.microsoft.com/en-us/windows/win32/menurc/common-control-parameters?redirectedfrom=MSDN 11297 11298 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11299 11300 History: 11301 The ampersand behavior was always the case on Windows, but it wasn't until June 15, 2021 when Linux was changed to match it and the documentation updated to reflect it. 11302 +/ 11303 class Button : MouseActivatedWidget { 11304 override int heightStretchiness() { return 3; } 11305 override int widthStretchiness() { return 3; } 11306 11307 /++ 11308 If true, this button will emit trigger events on double (and other quick events, if added) click events as well as on normal single click events. 11309 11310 History: 11311 Added July 2, 2021 11312 +/ 11313 public bool triggersOnMultiClick; 11314 11315 private string label_; 11316 private TextAlignment alignment; 11317 private dchar accelerator; 11318 11319 /// 11320 string label() { return label_; } 11321 /// 11322 void label(string l) { 11323 label_ = l; 11324 version(win32_widgets) { 11325 WCharzBuffer bfr = WCharzBuffer(l); 11326 SetWindowTextW(hwnd, bfr.ptr); 11327 } else version(custom_widgets) { 11328 redraw(); 11329 } 11330 } 11331 11332 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 11333 super.defaultEventHandler_dblclick(ev); 11334 if(triggersOnMultiClick) { 11335 if(ev.button == MouseButton.left) { 11336 auto event = new Event(EventType.triggered, this); 11337 event.sendDirectly(); 11338 } 11339 } 11340 } 11341 11342 private Sprite sprite; 11343 private int displayFlags; 11344 11345 /++ 11346 Creates a push button with the given label, which may be an image or some text. 11347 11348 Bugs: 11349 If the image is bigger than the button, it may not be displayed in the right position on Linux. 11350 11351 History: 11352 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 11353 11354 The button with label and image will respect requests to show both on Windows as 11355 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 11356 +/ 11357 this(ImageLabel label, Widget parent) { 11358 version(win32_widgets) { 11359 // FIXME: use ideal button size instead 11360 width = 50; 11361 height = 30; 11362 super(parent); 11363 11364 // BS_BITMAP is set when we want image only, so checking for exactly that combination 11365 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 11366 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 11367 11368 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 11369 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 11370 11371 if(label.image) { 11372 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 11373 11374 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 11375 } 11376 11377 this.label = label.label; 11378 } else version(custom_widgets) { 11379 width = 50; 11380 height = 30; 11381 super(parent); 11382 11383 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 11384 11385 if(label.image) { 11386 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 11387 this.displayFlags = label.displayFlags; 11388 } 11389 11390 this.alignment = label.alignment; 11391 } 11392 } 11393 11394 /// 11395 this(string label, Widget parent) { 11396 this(ImageLabel(label), parent); 11397 } 11398 11399 override int minHeight() { return defaultLineHeight + 4; } 11400 11401 static class Style : Widget.Style { 11402 override WidgetBackground background() { 11403 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 11404 11405 auto pressed = DynamicState.depressed | DynamicState.hover; 11406 if((widget.dynamicState & pressed) == pressed) { 11407 return WidgetBackground(cs.depressedButtonColor()); 11408 } else if(widget.dynamicState & DynamicState.hover) { 11409 return WidgetBackground(cs.hoveringColor()); 11410 } else { 11411 return WidgetBackground(cs.buttonColor()); 11412 } 11413 } 11414 11415 override FrameStyle borderStyle() { 11416 auto pressed = DynamicState.depressed | DynamicState.hover; 11417 if((widget.dynamicState & pressed) == pressed) { 11418 return FrameStyle.sunk; 11419 } else { 11420 return FrameStyle.risen; 11421 } 11422 11423 } 11424 11425 override bool variesWithState(ulong dynamicStateFlags) { 11426 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11427 } 11428 } 11429 mixin OverrideStyle!Style; 11430 11431 version(custom_widgets) 11432 override void paint(WidgetPainter painter) { 11433 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 11434 if(sprite) { 11435 sprite.drawAt( 11436 painter, 11437 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 11438 Point(0, 0) 11439 ); 11440 } else { 11441 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 11442 } 11443 return bounds; 11444 }); 11445 } 11446 11447 override int flexBasisWidth() { 11448 version(win32_widgets) { 11449 SIZE size; 11450 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11451 if(size.cx == 0) 11452 goto fallback; 11453 return size.cx + scaleWithDpi(16); 11454 } 11455 fallback: 11456 return scaleWithDpi(cast(int) label.length * 8 + 16); 11457 } 11458 11459 override int flexBasisHeight() { 11460 version(win32_widgets) { 11461 SIZE size; 11462 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11463 if(size.cy == 0) 11464 goto fallback; 11465 return size.cy + scaleWithDpi(6); 11466 } 11467 fallback: 11468 return defaultLineHeight + 4; 11469 } 11470 } 11471 11472 /++ 11473 A button with a consistent size, suitable for user commands like OK and cANCEL. 11474 +/ 11475 class CommandButton : Button { 11476 this(string label, Widget parent) { 11477 super(label, parent); 11478 } 11479 11480 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 11481 11482 override int maxHeight() { 11483 return defaultLineHeight + 4; 11484 } 11485 11486 override int maxWidth() { 11487 return defaultLineHeight * 4; 11488 } 11489 11490 override int marginLeft() { return 12; } 11491 override int marginRight() { return 12; } 11492 override int marginTop() { return 12; } 11493 override int marginBottom() { return 12; } 11494 } 11495 11496 /// 11497 enum ArrowDirection { 11498 left, /// 11499 right, /// 11500 up, /// 11501 down /// 11502 } 11503 11504 /// 11505 version(custom_widgets) 11506 class ArrowButton : Button { 11507 /// 11508 this(ArrowDirection direction, Widget parent) { 11509 super("", parent); 11510 this.direction = direction; 11511 triggersOnMultiClick = true; 11512 } 11513 11514 private ArrowDirection direction; 11515 11516 override int minHeight() { return scaleWithDpi(16); } 11517 override int maxHeight() { return scaleWithDpi(16); } 11518 override int minWidth() { return scaleWithDpi(16); } 11519 override int maxWidth() { return scaleWithDpi(16); } 11520 11521 override void paint(WidgetPainter painter) { 11522 super.paint(painter); 11523 11524 auto cs = getComputedStyle(); 11525 11526 painter.outlineColor = cs.foregroundColor; 11527 painter.fillColor = cs.foregroundColor; 11528 11529 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 11530 11531 final switch(direction) { 11532 case ArrowDirection.up: 11533 painter.drawPolygon( 11534 scaleWithDpi(Point(2, 10) + offset), 11535 scaleWithDpi(Point(7, 5) + offset), 11536 scaleWithDpi(Point(12, 10) + offset), 11537 scaleWithDpi(Point(2, 10) + offset) 11538 ); 11539 break; 11540 case ArrowDirection.down: 11541 painter.drawPolygon( 11542 scaleWithDpi(Point(2, 6) + offset), 11543 scaleWithDpi(Point(7, 11) + offset), 11544 scaleWithDpi(Point(12, 6) + offset), 11545 scaleWithDpi(Point(2, 6) + offset) 11546 ); 11547 break; 11548 case ArrowDirection.left: 11549 painter.drawPolygon( 11550 scaleWithDpi(Point(10, 2) + offset), 11551 scaleWithDpi(Point(5, 7) + offset), 11552 scaleWithDpi(Point(10, 12) + offset), 11553 scaleWithDpi(Point(10, 2) + offset) 11554 ); 11555 break; 11556 case ArrowDirection.right: 11557 painter.drawPolygon( 11558 scaleWithDpi(Point(6, 2) + offset), 11559 scaleWithDpi(Point(11, 7) + offset), 11560 scaleWithDpi(Point(6, 12) + offset), 11561 scaleWithDpi(Point(6, 2) + offset) 11562 ); 11563 break; 11564 } 11565 } 11566 } 11567 11568 private 11569 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 11570 int x, y; 11571 Widget par = c; 11572 while(par) { 11573 x += par.x; 11574 y += par.y; 11575 par = par.parent; 11576 } 11577 return [x, y]; 11578 } 11579 11580 version(win32_widgets) 11581 private 11582 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 11583 // MapWindowPoints? 11584 int x, y; 11585 Widget par = c; 11586 while(par) { 11587 x += par.x; 11588 y += par.y; 11589 par = par.parent; 11590 if(par !is null && par.useNativeDrawing()) 11591 break; 11592 } 11593 return [x, y]; 11594 } 11595 11596 /// 11597 class ImageBox : Widget { 11598 private MemoryImage image_; 11599 11600 override int widthStretchiness() { return 1; } 11601 override int heightStretchiness() { return 1; } 11602 override int widthShrinkiness() { return 1; } 11603 override int heightShrinkiness() { return 1; } 11604 11605 override int flexBasisHeight() { 11606 return image_.height; 11607 } 11608 11609 override int flexBasisWidth() { 11610 return image_.width; 11611 } 11612 11613 /// 11614 public void setImage(MemoryImage image){ 11615 this.image_ = image; 11616 if(this.parentWindow && this.parentWindow.win) { 11617 if(sprite) 11618 sprite.dispose(); 11619 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11620 } 11621 redraw(); 11622 } 11623 11624 /// How to fit the image in the box if they aren't an exact match in size? 11625 enum HowToFit { 11626 center, /// centers the image, cropping around all the edges as needed 11627 crop, /// always draws the image in the upper left, cropping the lower right if needed 11628 // stretch, /// not implemented 11629 } 11630 11631 private Sprite sprite; 11632 private HowToFit howToFit_; 11633 11634 private Color backgroundColor_; 11635 11636 /// 11637 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 11638 this.image_ = image; 11639 this.tabStop = false; 11640 this.howToFit_ = howToFit; 11641 this.backgroundColor_ = backgroundColor; 11642 super(parent); 11643 updateSprite(); 11644 } 11645 11646 /// ditto 11647 this(MemoryImage image, HowToFit howToFit, Widget parent) { 11648 this(image, howToFit, Color.transparent, parent); 11649 } 11650 11651 private void updateSprite() { 11652 if(sprite is null && this.parentWindow && this.parentWindow.win) { 11653 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11654 } 11655 } 11656 11657 override void paint(WidgetPainter painter) { 11658 updateSprite(); 11659 if(backgroundColor_.a) { 11660 painter.fillColor = backgroundColor_; 11661 painter.drawRectangle(Point(0, 0), width, height); 11662 } 11663 if(howToFit_ == HowToFit.crop) 11664 sprite.drawAt(painter, Point(0, 0)); 11665 else if(howToFit_ == HowToFit.center) { 11666 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 11667 } 11668 } 11669 } 11670 11671 /// 11672 class TextLabel : Widget { 11673 override int maxHeight() { return defaultLineHeight; } 11674 override int minHeight() { return defaultLineHeight; } 11675 override int minWidth() { return 32; } 11676 11677 override int flexBasisHeight() { return minHeight(); } 11678 override int flexBasisWidth() { return defaultTextWidth(label); } 11679 11680 string label_; 11681 11682 /++ 11683 Indicates which other control this label is here for. Similar to HTML `for` attribute. 11684 11685 In practice this means a click on the label will focus the `labelFor`. In future versions 11686 it will also set screen reader hints but that is not yet implemented. 11687 11688 History: 11689 Added October 3, 2021 (dub v10.4) 11690 +/ 11691 Widget labelFor; 11692 11693 /// 11694 @scriptable 11695 string label() { return label_; } 11696 11697 /// 11698 @scriptable 11699 void label(string l) { 11700 label_ = l; 11701 version(win32_widgets) { 11702 WCharzBuffer bfr = WCharzBuffer(l); 11703 SetWindowTextW(hwnd, bfr.ptr); 11704 } else version(custom_widgets) 11705 redraw(); 11706 } 11707 11708 /// 11709 this(string label, TextAlignment alignment, Widget parent) { 11710 this.label_ = label; 11711 this.alignment = alignment; 11712 this.tabStop = false; 11713 super(parent); 11714 11715 version(win32_widgets) 11716 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 11717 } 11718 11719 override void defaultEventHandler_click(scope ClickEvent ce) { 11720 if(this.labelFor !is null) 11721 this.labelFor.focus(); 11722 } 11723 11724 /++ 11725 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 11726 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 11727 +/ 11728 this(string label, Widget parent) { 11729 this(label, TextAlignment.Right, parent); 11730 } 11731 11732 11733 TextAlignment alignment; 11734 11735 version(custom_widgets) 11736 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 11737 painter.outlineColor = getComputedStyle().foregroundColor; 11738 painter.drawText(Point(0, 0), this.label, Point(width, height), alignment); 11739 return bounds; 11740 } 11741 11742 } 11743 11744 version(custom_widgets) 11745 private struct etc { 11746 mixin ExperimentalTextComponent; 11747 } 11748 11749 version(win32_widgets) 11750 alias EditableTextWidgetParent = Widget; /// 11751 else version(custom_widgets) { 11752 version(trash_text) { 11753 alias EditableTextWidgetParent = ScrollableWidget; /// 11754 } else { 11755 alias EditableTextWidgetParent = Widget; 11756 version=use_new_text_system; 11757 import arsd.textlayouter; 11758 } 11759 } else static assert(0); 11760 11761 version(use_new_text_system) 11762 class TextDisplayHelper : Widget { 11763 protected TextLayouter l; 11764 protected ScrollMessageWidget smw; 11765 11766 private const(TextLayouter.State)*[] undoStack; 11767 private const(TextLayouter.State)*[] redoStack; 11768 11769 bool readonly; 11770 bool caretNavigation; // scroll lock can flip this 11771 bool singleLine; 11772 bool acceptsTabInput; 11773 11774 private Menu ctx; 11775 override Menu contextMenu(int x, int y) { 11776 if(ctx is null) { 11777 ctx = new Menu("Actions", this); 11778 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 11779 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 11780 ctx.addSeparator(); 11781 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 11782 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 11783 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 11784 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 11785 ctx.addSeparator(); 11786 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 11787 } 11788 return ctx; 11789 } 11790 11791 override void defaultEventHandler_blur(Event ev) { 11792 super.defaultEventHandler_blur(ev); 11793 if(l.wasMutated()) { 11794 auto evt = new ChangeEvent!string(this, &this.content); 11795 evt.dispatch(); 11796 l.clearWasMutatedFlag(); 11797 } 11798 } 11799 11800 private string content() { 11801 return l.getTextString(); 11802 } 11803 11804 void undo() { 11805 if(undoStack.length) { 11806 auto state = undoStack[$-1]; 11807 undoStack = undoStack[0 .. $-1]; 11808 undoStack.assumeSafeAppend(); 11809 redoStack ~= l.saveState(); 11810 l.restoreState(state); 11811 adjustScrollbarSizes(); 11812 scrollForCaret(); 11813 redraw(); 11814 stateCheckpoint = true; 11815 } 11816 } 11817 11818 void redo() { 11819 if(redoStack.length) { 11820 doStateCheckpoint(); 11821 auto state = redoStack[$-1]; 11822 redoStack = redoStack[0 .. $-1]; 11823 redoStack.assumeSafeAppend(); 11824 l.restoreState(state); 11825 adjustScrollbarSizes(); 11826 scrollForCaret(); 11827 redraw(); 11828 stateCheckpoint = true; 11829 } 11830 } 11831 11832 void cut() { 11833 with(l.selection()) { 11834 if(!isEmpty()) { 11835 setClipboardText(parentWindow.win, getContentString()); 11836 doStateCheckpoint(); 11837 replaceContent(""); 11838 adjustScrollbarSizes(); 11839 scrollForCaret(); 11840 this.redraw(); 11841 } 11842 } 11843 11844 } 11845 11846 void copy() { 11847 with(l.selection()) { 11848 if(!isEmpty()) { 11849 setClipboardText(parentWindow.win, getContentString()); 11850 this.redraw(); 11851 } 11852 } 11853 } 11854 11855 void paste() { 11856 getClipboardText(parentWindow.win, (txt) { 11857 doStateCheckpoint(); 11858 l.selection.replaceContent(txt); 11859 adjustScrollbarSizes(); 11860 scrollForCaret(); 11861 this.redraw(); 11862 }); 11863 } 11864 11865 void deleteContentOfSelection() { 11866 doStateCheckpoint(); 11867 l.selection.replaceContent(""); 11868 l.selection.setUserXCoordinate(); 11869 adjustScrollbarSizes(); 11870 scrollForCaret(); 11871 redraw(); 11872 } 11873 11874 void selectAll() { 11875 with(l.selection) { 11876 moveToStartOfDocument(); 11877 setAnchor(); 11878 moveToEndOfDocument(); 11879 setFocus(); 11880 } 11881 redraw(); 11882 } 11883 11884 protected bool stateCheckpoint = true; 11885 11886 protected void doStateCheckpoint() { 11887 if(stateCheckpoint) { 11888 undoStack ~= l.saveState(); 11889 stateCheckpoint = false; 11890 } 11891 } 11892 11893 protected void adjustScrollbarSizes() { 11894 // FIXME: will want a content area helper function instead of doing all these subtractions myself 11895 auto borderWidth = 2; 11896 this.smw.setTotalArea(l.width, l.height); 11897 this.smw.setViewableArea( 11898 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 11899 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 11900 } 11901 11902 protected void scrollForCaret() { 11903 // import std.stdio; writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 11904 smw.scrollIntoView(l.selection.focusBoundingBox()); 11905 } 11906 11907 // FIXME: this should be a theme changed event listener instead 11908 private BaseVisualTheme currentTheme; 11909 override void recomputeChildLayout() { 11910 if(currentTheme is null) 11911 currentTheme = WidgetPainter.visualTheme; 11912 if(WidgetPainter.visualTheme !is currentTheme) { 11913 currentTheme = WidgetPainter.visualTheme; 11914 auto ds = this.l.defaultStyle; 11915 if(auto ms = cast(MyTextStyle) ds) { 11916 auto cs = getComputedStyle(); 11917 auto font = cs.font(); 11918 if(font !is null) 11919 ms.font_ = font; 11920 else { 11921 auto osc = new OperatingSystemFont(); 11922 osc.loadDefault; 11923 ms.font_ = osc; 11924 } 11925 } 11926 } 11927 super.recomputeChildLayout(); 11928 } 11929 11930 private Point adjustForSingleLine(Point p) { 11931 if(singleLine) 11932 return Point(p.x, this.height / 2); 11933 else 11934 return p; 11935 } 11936 11937 private bool wordWrapEnabled_; 11938 11939 this(TextLayouter l, ScrollMessageWidget parent) { 11940 this.smw = parent; 11941 11942 smw.addDefaultWheelListeners(16, 16, 8); 11943 smw.movementPerButtonClick(16, 16); 11944 11945 this.defaultPadding = Rectangle(2, 2, 2, 2); 11946 11947 this.l = l; 11948 super(parent); 11949 11950 smw.addEventListener((scope ScrollEvent se) { 11951 this.redraw(); 11952 }); 11953 11954 bool mouseDown; 11955 11956 this.addEventListener((scope ResizeEvent re) { 11957 // FIXME: I should add a method to give this client area width thing 11958 if(wordWrapEnabled_) 11959 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 11960 11961 adjustScrollbarSizes(); 11962 scrollForCaret(); 11963 11964 this.redraw(); 11965 }); 11966 11967 this.addEventListener((scope KeyDownEvent kde) { 11968 switch(kde.key) { 11969 case Key.Up, Key.Down, Key.Left, Key.Right: 11970 case Key.Home, Key.End: 11971 stateCheckpoint = true; 11972 bool setPosition = false; 11973 switch(kde.key) { 11974 case Key.Up: l.selection.moveUp(); break; 11975 case Key.Down: l.selection.moveDown(); break; 11976 case Key.Left: l.selection.moveLeft(); setPosition = true; break; 11977 case Key.Right: l.selection.moveRight(); setPosition = true; break; 11978 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 11979 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 11980 default: assert(0); 11981 } 11982 11983 if(kde.shiftKey) 11984 l.selection.setFocus(); 11985 else 11986 l.selection.setAnchor(); 11987 if(setPosition) 11988 l.selection.setUserXCoordinate(); 11989 scrollForCaret(); 11990 redraw(); 11991 break; 11992 case Key.PageUp, Key.PageDown: 11993 // FIXME 11994 scrollForCaret(); 11995 break; 11996 case Key.Delete: 11997 if(l.selection.isEmpty()) { 11998 l.selection.setAnchor(); 11999 l.selection.moveRight(); 12000 l.selection.setFocus(); 12001 } 12002 deleteContentOfSelection(); 12003 adjustScrollbarSizes(); 12004 scrollForCaret(); 12005 break; 12006 case Key.Insert: 12007 break; 12008 case Key.A: 12009 if(kde.ctrlKey) 12010 selectAll(); 12011 break; 12012 case Key.F: 12013 // find 12014 break; 12015 case Key.Z: 12016 if(kde.ctrlKey) 12017 undo(); 12018 break; 12019 case Key.R: 12020 if(kde.ctrlKey) 12021 redo(); 12022 break; 12023 case Key.X: 12024 if(kde.ctrlKey) 12025 cut(); 12026 break; 12027 case Key.C: 12028 if(kde.ctrlKey) 12029 copy(); 12030 break; 12031 case Key.V: 12032 if(kde.ctrlKey) 12033 paste(); 12034 break; 12035 case Key.F1: 12036 with(l.selection()) { 12037 moveToStartOfLine(); 12038 setAnchor(); 12039 moveToEndOfLine(); 12040 moveToIncludeAdjacentEndOfLineMarker(); 12041 setFocus(); 12042 replaceContent(""); 12043 } 12044 12045 redraw(); 12046 break; 12047 /* 12048 case Key.F2: 12049 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 12050 //(cast(MyTextStyle) old).font, 12051 font2, 12052 Color.red))); 12053 redraw(); 12054 break; 12055 */ 12056 case Key.Tab: 12057 // we process the char event, so don't want to change focus on it 12058 if(acceptsTabInput) 12059 kde.preventDefault(); 12060 break; 12061 default: 12062 } 12063 }); 12064 12065 Point downAt; 12066 12067 static if(UsingSimpledisplayX11) 12068 this.addEventListener((scope ClickEvent ce) { 12069 if(ce.button == MouseButton.middle) { 12070 parentWindow.win.getPrimarySelection((txt) { 12071 l.selection.replaceContent(txt); 12072 redraw(); 12073 }); 12074 } 12075 }); 12076 12077 this.addEventListener((scope MouseDownEvent ce) { 12078 if(ce.button == MouseButton.left) { 12079 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12080 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 12081 l.selection.setAnchor(); 12082 mouseDown = true; 12083 parentWindow.captureMouse(this); 12084 this.redraw(); 12085 } else if(ce.button == MouseButton.right) { 12086 this.showContextMenu(ce.clientX, ce.clientY); 12087 } 12088 //import std.stdio; 12089 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12090 }); 12091 12092 Timer autoscrollTimer; 12093 int autoscrollDirection; 12094 int autoscrollAmount; 12095 12096 void autoscroll() { 12097 switch(autoscrollDirection) { 12098 case 0: smw.scrollUp(autoscrollAmount); break; 12099 case 1: smw.scrollDown(autoscrollAmount); break; 12100 case 2: smw.scrollLeft(autoscrollAmount); break; 12101 case 3: smw.scrollRight(autoscrollAmount); break; 12102 default: assert(0); 12103 } 12104 12105 this.redraw(); 12106 } 12107 12108 void setAutoscrollTimer(int direction, int amount) { 12109 if(autoscrollTimer is null) { 12110 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 12111 } 12112 12113 autoscrollDirection = direction; 12114 autoscrollAmount = amount; 12115 } 12116 12117 void stopAutoscrollTimer() { 12118 if(autoscrollTimer !is null) { 12119 autoscrollTimer.dispose(); 12120 autoscrollTimer = null; 12121 } 12122 autoscrollAmount = 0; 12123 autoscrollDirection = 0; 12124 } 12125 12126 this.addEventListener((scope MouseMoveEvent ce) { 12127 if(mouseDown) { 12128 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12129 12130 // FIXME: when scrolling i actually do want a timer. 12131 // i also want a zone near the sides of the window where i can auto scroll 12132 12133 auto scrollMultiplier = scaleWithDpi(16); 12134 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 12135 12136 if(!singleLine && movedTo.y < 4) { 12137 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 12138 } else 12139 if(!singleLine && (movedTo.y + 6) > this.height) { 12140 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 12141 } else 12142 if(movedTo.x < 4) { 12143 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 12144 } else 12145 if((movedTo.x + 6) > this.width) { 12146 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 12147 } else 12148 stopAutoscrollTimer(); 12149 12150 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 12151 l.selection.setFocus(); 12152 this.redraw(); 12153 } 12154 }); 12155 12156 this.addEventListener((scope MouseUpEvent ce) { 12157 // FIXME: assert primary selection 12158 if(mouseDown && ce.button == MouseButton.left) { 12159 stateCheckpoint = true; 12160 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 12161 //l.selection.setFocus(); 12162 mouseDown = false; 12163 parentWindow.releaseMouseCapture(); 12164 stopAutoscrollTimer(); 12165 this.redraw(); 12166 } 12167 //import std.stdio; 12168 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12169 }); 12170 12171 this.addEventListener((scope CharEvent ce) { 12172 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 12173 return; // skip the ctrl+x characters we don't care about as plain text 12174 12175 if(singleLine && ce.character == '\n') 12176 return; 12177 if(!acceptsTabInput && ce.character == '\t') 12178 return; 12179 12180 doStateCheckpoint(); 12181 12182 char[4] buffer; 12183 import std.utf; 12184 auto stride = encode(buffer, ce.character); 12185 l.selection.replaceContent(buffer[0 .. stride]); 12186 l.selection.setUserXCoordinate(); 12187 adjustScrollbarSizes(); 12188 scrollForCaret(); 12189 redraw(); 12190 }); 12191 } 12192 12193 static class Style : Widget.Style { 12194 override WidgetBackground background() { 12195 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 12196 } 12197 12198 override Color foregroundColor() { 12199 return WidgetPainter.visualTheme.foregroundColor; 12200 } 12201 12202 override FrameStyle borderStyle() { 12203 return FrameStyle.sunk; 12204 } 12205 12206 override MouseCursor cursor() { 12207 return GenericCursor.Text; 12208 } 12209 } 12210 mixin OverrideStyle!Style; 12211 12212 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, Window.lineHeight))).height; } 12213 override int maxHeight() { 12214 if(singleLine) 12215 return minHeight; 12216 else 12217 return super.maxHeight(); 12218 } 12219 12220 void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 12221 painter.drawText(upperLeft, text); 12222 } 12223 12224 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 12225 //painter.setFont(font); 12226 12227 auto cs = getComputedStyle(); 12228 auto defaultColor = cs.foregroundColor; 12229 12230 auto old = painter.setClipRectangle(bounds); 12231 scope(exit) painter.setClipRectangle(old); 12232 12233 l.getDrawableText(delegate bool(txt, style, info, carets...) { 12234 //import std.stdio; writeln("Segment: ", txt); 12235 assert(style !is null); 12236 12237 auto myStyle = cast(MyTextStyle) style; 12238 assert(myStyle !is null); 12239 12240 painter.setFont(myStyle.font); 12241 // defaultColor = myStyle.color; // FIXME: so wrong 12242 12243 if(info.selections && info.boundingBox.width > 0) { 12244 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 12245 painter.fillColor = color; 12246 painter.outlineColor = color; 12247 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 12248 painter.outlineColor = cs.selectionForegroundColor; 12249 //painter.fillColor = Color.white; 12250 } else { 12251 painter.outlineColor = defaultColor; 12252 } 12253 12254 if(this.isFocused) 12255 foreach(idx, caret; carets) { 12256 if(idx == 0) 12257 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 12258 painter.drawLine( 12259 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 12260 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 12261 ); 12262 } 12263 12264 import std.string; 12265 if(txt.strip.length) 12266 drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRight); 12267 12268 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) 12269 return false; 12270 else { 12271 return true; 12272 } 12273 }, Rectangle(smw.position(), bounds.size)); 12274 12275 /+ 12276 int place = 0; 12277 int y = 75; 12278 foreach(width; widths) { 12279 painter.fillColor = Color.red; 12280 painter.drawRectangle(Point(place, y), Size(width, 75)); 12281 //y += 15; 12282 place += width; 12283 } 12284 +/ 12285 12286 return bounds; 12287 } 12288 12289 static class MyTextStyle : TextStyle { 12290 OperatingSystemFont font_; 12291 this(OperatingSystemFont font, bool passwordMode = false) { 12292 this.font_ = font; 12293 } 12294 12295 override OperatingSystemFont font() { 12296 return font_; 12297 } 12298 } 12299 } 12300 12301 /+ 12302 version(use_new_text_system) 12303 class TextWidget : Widget { 12304 TextLayouter l; 12305 ScrollMessageWidget smw; 12306 TextDisplayHelper helper; 12307 this(TextLayouter l, Widget parent) { 12308 this.l = l; 12309 super(parent); 12310 12311 smw = new ScrollMessageWidget(this); 12312 //smw.horizontalScrollBar.hide; 12313 //smw.verticalScrollBar.hide; 12314 smw.addDefaultWheelListeners(16, 16, 8); 12315 smw.movementPerButtonClick(16, 16); 12316 helper = new TextDisplayHelper(l, smw); 12317 12318 // no need to do this here since there's gonna be a resize 12319 // event immediately before any drawing 12320 // smw.setTotalArea(l.width, l.height); 12321 smw.setViewableArea( 12322 this.width - this.paddingLeft - this.paddingRight, 12323 this.height - this.paddingTop - this.paddingBottom); 12324 12325 /+ 12326 import std.stdio; 12327 writeln(l.width, "x", l.height); 12328 +/ 12329 } 12330 } 12331 +/ 12332 12333 12334 12335 12336 /+ 12337 This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too 12338 +/ 12339 12340 /// Contains the implementation of text editing 12341 abstract class EditableTextWidget : EditableTextWidgetParent { 12342 this(Widget parent) { 12343 super(parent); 12344 12345 version(custom_widgets) 12346 setupCustomTextEditing(); 12347 } 12348 12349 private bool wordWrapEnabled_; 12350 void wordWrapEnabled(bool enabled) { 12351 version(win32_widgets) { 12352 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 12353 } else version(custom_widgets) { 12354 wordWrapEnabled_ = enabled; 12355 version(use_new_text_system) 12356 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 12357 } else static assert(false); 12358 } 12359 12360 override int minWidth() { return scaleWithDpi(16); } 12361 override int widthStretchiness() { return 7; } 12362 12363 version(use_new_text_system) 12364 override int maxHeight() { return tdh.maxHeight; } 12365 12366 version(use_new_text_system) 12367 override void focus() { if(tdh) tdh.focus(); } 12368 12369 void selectAll() { 12370 version(win32_widgets) 12371 SendMessage(hwnd, EM_SETSEL, 0, -1); 12372 else version(custom_widgets) { 12373 version(use_new_text_system) 12374 tdh.selectAll(); 12375 else 12376 textLayout.selectAll(); 12377 redraw(); 12378 } 12379 } 12380 12381 version(use_new_text_system) 12382 TextDisplayHelper tdh; 12383 12384 @property string content() { 12385 version(win32_widgets) { 12386 wchar[4096] bufferstack; 12387 wchar[] buffer; 12388 auto len = GetWindowTextLength(hwnd); 12389 if(len < bufferstack.length) 12390 buffer = bufferstack[0 .. len + 1]; 12391 else 12392 buffer = new wchar[](len + 1); 12393 12394 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 12395 if(l >= 0) 12396 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 12397 else 12398 return null; 12399 } else version(custom_widgets) { 12400 version(use_new_text_system) { 12401 return textLayout.getTextString(); 12402 } else 12403 return textLayout.getPlainText(); 12404 } else static assert(false); 12405 } 12406 @property void content(string s) { 12407 version(win32_widgets) { 12408 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 12409 SetWindowTextW(hwnd, bfr.ptr); 12410 } else version(custom_widgets) { 12411 version(use_new_text_system) { 12412 selectAll(); 12413 textLayout.selection.replaceContent(s); 12414 12415 tdh.adjustScrollbarSizes(); 12416 //scrollForCaret(); 12417 12418 redraw(); 12419 } else { 12420 textLayout.clear(); 12421 textLayout.addText(s); 12422 12423 { 12424 // FIXME: it should be able to get this info easier 12425 auto painter = draw(); 12426 textLayout.redoLayout(painter); 12427 } 12428 auto cbb = textLayout.contentBoundingBox(); 12429 setContentSize(cbb.width, cbb.height); 12430 /* 12431 textLayout.addText(ForegroundColor.red, s); 12432 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 12433 textLayout.addText(" is the best!"); 12434 */ 12435 redraw(); 12436 } 12437 } 12438 else static assert(false); 12439 } 12440 12441 void addText(string txt) { 12442 version(custom_widgets) { 12443 version(use_new_text_system) { 12444 textLayout.appendText(txt); 12445 redraw(); 12446 } else { 12447 textLayout.addText(txt); 12448 12449 { 12450 // FIXME: it should be able to get this info easier 12451 auto painter = draw(); 12452 textLayout.redoLayout(painter); 12453 } 12454 auto cbb = textLayout.contentBoundingBox(); 12455 setContentSize(cbb.width, cbb.height); 12456 } 12457 } else version(win32_widgets) { 12458 // get the current selection 12459 DWORD StartPos, EndPos; 12460 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 12461 12462 // move the caret to the end of the text 12463 int outLength = GetWindowTextLengthW(hwnd); 12464 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 12465 12466 // insert the text at the new caret position 12467 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 12468 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 12469 12470 // restore the previous selection 12471 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 12472 } else static assert(0); 12473 } 12474 12475 version(custom_widgets) 12476 version(trash_text) 12477 override void paintFrameAndBackground(WidgetPainter painter) { 12478 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 12479 } 12480 12481 version(use_new_text_system) 12482 TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 12483 return new TextDisplayHelper(textLayout, smw); 12484 } 12485 12486 version(use_new_text_system) 12487 TextStyle defaultTextStyle() { 12488 auto cs = getComputedStyle(); 12489 auto font = cs.font; 12490 if(font is null) { 12491 font = new OperatingSystemFont; 12492 font.loadDefault(); 12493 } 12494 return new TextDisplayHelper.MyTextStyle(font); 12495 } 12496 12497 version(win32_widgets) { /* will do it with Windows calls in the classes */ } 12498 else version(custom_widgets) { 12499 // FIXME 12500 version(use_new_text_system) { 12501 TextLayouter textLayout; 12502 12503 void setupCustomTextEditing() { 12504 textLayout = new TextLayouter(defaultTextStyle()); 12505 auto smw = new ScrollMessageWidget(this); 12506 if(!showingHorizontalScroll) 12507 smw.horizontalScrollBar.hide(); 12508 if(!showingVerticalScroll) 12509 smw.verticalScrollBar.hide(); 12510 this.tabStop = false; 12511 smw.tabStop = false; 12512 tdh = textDisplayHelperFactory(textLayout, smw); 12513 } 12514 12515 } else { 12516 12517 static if(SimpledisplayTimerAvailable) 12518 Timer caretTimer; 12519 etc.TextLayout textLayout; 12520 12521 void setupCustomTextEditing() { 12522 textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4)); 12523 textLayout.selectionXorColor = getComputedStyle().activeListXorColor; 12524 } 12525 12526 override void paint(WidgetPainter painter) { 12527 if(parentWindow.win.closed) return; 12528 12529 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 12530 12531 /* 12532 painter.outlineColor = Color.white; 12533 painter.fillColor = Color.white; 12534 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 12535 */ 12536 12537 painter.outlineColor = Color.black; 12538 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 12539 12540 textLayout.caretShowingOnScreen = false; 12541 12542 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 12543 } 12544 } 12545 12546 static class Style : Widget.Style { 12547 override FrameStyle borderStyle() { 12548 return FrameStyle.sunk; 12549 } 12550 override MouseCursor cursor() { 12551 return GenericCursor.Text; 12552 } 12553 } 12554 mixin OverrideStyle!Style; 12555 } 12556 else static assert(false); 12557 12558 version(trash_text) 12559 version(custom_widgets) 12560 override void defaultEventHandler_mousedown(MouseDownEvent ev) { 12561 super.defaultEventHandler_mousedown(ev); 12562 if(parentWindow.win.closed) return; 12563 if(ev.button == MouseButton.left) { 12564 if(textLayout.selectNone()) 12565 redraw(); 12566 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 12567 this.focus(); 12568 //this.parentWindow.win.grabInput(); 12569 } else if(ev.button == MouseButton.middle) { 12570 static if(UsingSimpledisplayX11) { 12571 getPrimarySelection(parentWindow.win, (in char[] txt) { 12572 textLayout.insert(txt); 12573 redraw(); 12574 12575 auto cbb = textLayout.contentBoundingBox(); 12576 setContentSize(cbb.width, cbb.height); 12577 }); 12578 } 12579 } 12580 } 12581 12582 version(trash_text) 12583 version(custom_widgets) 12584 override void defaultEventHandler_mouseup(MouseUpEvent ev) { 12585 //this.parentWindow.win.releaseInputGrab(); 12586 super.defaultEventHandler_mouseup(ev); 12587 } 12588 12589 version(trash_text) 12590 version(custom_widgets) 12591 override void defaultEventHandler_mousemove(MouseMoveEvent ev) { 12592 super.defaultEventHandler_mousemove(ev); 12593 if(ev.state & ModifierState.leftButtonDown) { 12594 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 12595 redraw(); 12596 } 12597 } 12598 12599 version(trash_text) 12600 version(custom_widgets) 12601 override void defaultEventHandler_focus(Event ev) { 12602 super.defaultEventHandler_focus(ev); 12603 if(parentWindow.win.closed) return; 12604 auto painter = this.draw(); 12605 textLayout.drawCaret(painter); 12606 12607 static if(SimpledisplayTimerAvailable) 12608 if(caretTimer) { 12609 caretTimer.destroy(); 12610 caretTimer = null; 12611 } 12612 12613 bool blinkingCaret = true; 12614 static if(UsingSimpledisplayX11) 12615 if(!Image.impl.xshmAvailable) 12616 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 12617 12618 if(blinkingCaret) 12619 static if(SimpledisplayTimerAvailable) 12620 caretTimer = new Timer(500, { 12621 if(parentWindow.win.closed) { 12622 caretTimer.destroy(); 12623 return; 12624 } 12625 if(isFocused()) { 12626 auto painter = this.draw(); 12627 textLayout.drawCaret(painter); 12628 } else if(textLayout.caretShowingOnScreen) { 12629 auto painter = this.draw(); 12630 textLayout.eraseCaret(painter); 12631 } 12632 }); 12633 } 12634 12635 version(trash_text) { 12636 private string lastContentBlur; 12637 12638 override void defaultEventHandler_blur(Event ev) { 12639 super.defaultEventHandler_blur(ev); 12640 if(parentWindow.win.closed) return; 12641 version(custom_widgets) { 12642 auto painter = this.draw(); 12643 textLayout.eraseCaret(painter); 12644 static if(SimpledisplayTimerAvailable) 12645 if(caretTimer) { 12646 caretTimer.destroy(); 12647 caretTimer = null; 12648 } 12649 } 12650 12651 if(this.content != lastContentBlur) { 12652 auto evt = new ChangeEvent!string(this, &this.content); 12653 evt.dispatch(); 12654 lastContentBlur = this.content; 12655 } 12656 } 12657 } 12658 12659 version(win32_widgets) { 12660 private string lastContentBlur; 12661 12662 override void defaultEventHandler_blur(Event ev) { 12663 super.defaultEventHandler_blur(ev); 12664 12665 if(this.content != lastContentBlur) { 12666 auto evt = new ChangeEvent!string(this, &this.content); 12667 evt.dispatch(); 12668 lastContentBlur = this.content; 12669 } 12670 } 12671 } 12672 12673 12674 version(trash_text) 12675 version(custom_widgets) 12676 override void defaultEventHandler_char(CharEvent ev) { 12677 super.defaultEventHandler_char(ev); 12678 textLayout.insert(ev.character); 12679 redraw(); 12680 12681 // FIXME: too inefficient 12682 auto cbb = textLayout.contentBoundingBox(); 12683 setContentSize(cbb.width, cbb.height); 12684 } 12685 version(trash_text) 12686 version(custom_widgets) 12687 override void defaultEventHandler_keydown(KeyDownEvent ev) { 12688 //super.defaultEventHandler_keydown(ev); 12689 switch(ev.key) { 12690 case Key.Delete: 12691 textLayout.delete_(); 12692 redraw(); 12693 break; 12694 case Key.Left: 12695 textLayout.moveLeft(); 12696 redraw(); 12697 break; 12698 case Key.Right: 12699 textLayout.moveRight(); 12700 redraw(); 12701 break; 12702 case Key.Up: 12703 textLayout.moveUp(); 12704 redraw(); 12705 break; 12706 case Key.Down: 12707 textLayout.moveDown(); 12708 redraw(); 12709 break; 12710 case Key.Home: 12711 textLayout.moveHome(); 12712 redraw(); 12713 break; 12714 case Key.End: 12715 textLayout.moveEnd(); 12716 redraw(); 12717 break; 12718 case Key.PageUp: 12719 foreach(i; 0 .. 32) 12720 textLayout.moveUp(); 12721 redraw(); 12722 break; 12723 case Key.PageDown: 12724 foreach(i; 0 .. 32) 12725 textLayout.moveDown(); 12726 redraw(); 12727 break; 12728 12729 default: 12730 {} // intentionally blank, let "char" handle it 12731 } 12732 /* 12733 if(ev.key == Key.Backspace) { 12734 textLayout.backspace(); 12735 redraw(); 12736 } 12737 */ 12738 ensureVisibleInScroll(textLayout.caretBoundingBox()); 12739 } 12740 12741 version(use_new_text_system) { 12742 bool showingVerticalScroll() { return true; } 12743 bool showingHorizontalScroll() { return true; } 12744 } 12745 } 12746 12747 /// 12748 class LineEdit : EditableTextWidget { 12749 // FIXME: hack 12750 version(custom_widgets) { 12751 override bool showingVerticalScroll() { return false; } 12752 override bool showingHorizontalScroll() { return false; } 12753 } 12754 12755 override int flexBasisWidth() { return 250; } 12756 12757 /// 12758 this(Widget parent) { 12759 super(parent); 12760 version(win32_widgets) { 12761 createWin32Window(this, "edit"w, "", 12762 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 12763 } else version(custom_widgets) { 12764 version(trash_text) { 12765 setupCustomTextEditing(); 12766 addEventListener(delegate(CharEvent ev) { 12767 if(ev.character == '\n') 12768 ev.preventDefault(); 12769 }); 12770 } 12771 } else static assert(false); 12772 } 12773 12774 version(use_new_text_system) 12775 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 12776 auto tdh = new TextDisplayHelper(textLayout, smw); 12777 tdh.singleLine = true; 12778 return tdh; 12779 } 12780 12781 version(win32_widgets) { 12782 mixin Padding!q{2}; 12783 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 12784 override int maxHeight() { return minHeight; } 12785 } 12786 12787 /+ 12788 @property void passwordMode(bool p) { 12789 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 12790 } 12791 +/ 12792 } 12793 12794 /++ 12795 A [LineEdit] that displays `*` in place of the actual characters. 12796 12797 Alas, Windows requires the window to be created differently to use this style, 12798 so it had to be a new class instead of a toggle on and off on an existing object. 12799 12800 FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now. 12801 12802 History: 12803 Added January 24, 2021 12804 +/ 12805 class PasswordEdit : EditableTextWidget { 12806 version(custom_widgets) { 12807 override bool showingVerticalScroll() { return false; } 12808 override bool showingHorizontalScroll() { return false; } 12809 } 12810 12811 override int flexBasisWidth() { return 250; } 12812 12813 version(use_new_text_system) 12814 override TextStyle defaultTextStyle() { 12815 auto cs = getComputedStyle(); 12816 12817 auto osf = new class OperatingSystemFont { 12818 this() { 12819 super(cs.font); 12820 } 12821 override int stringWidth(scope const(char)[] text, SimpleWindow window = null) { 12822 int count = 0; 12823 foreach(dchar ch; text) 12824 count++; 12825 return count * super.stringWidth("*", window); 12826 } 12827 }; 12828 12829 return new TextDisplayHelper.MyTextStyle(osf); 12830 } 12831 12832 version(use_new_text_system) 12833 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 12834 static class TDH : TextDisplayHelper { 12835 this(TextLayouter textLayout, ScrollMessageWidget smw) { 12836 singleLine = true; 12837 super(textLayout, smw); 12838 } 12839 12840 override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 12841 char[256] buffer = void; 12842 int bufferLength = 0; 12843 foreach(dchar ch; text) 12844 buffer[bufferLength++] = '*'; 12845 painter.drawText(upperLeft, buffer[0..bufferLength]); 12846 } 12847 } 12848 12849 return new TDH(textLayout, smw); 12850 } 12851 12852 /// 12853 this(Widget parent) { 12854 super(parent); 12855 version(win32_widgets) { 12856 createWin32Window(this, "edit"w, "", 12857 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 12858 } else version(custom_widgets) { 12859 version(trash_text) 12860 setupCustomTextEditing(); 12861 addEventListener(delegate(CharEvent ev) { 12862 if(ev.character == '\n') 12863 ev.preventDefault(); 12864 }); 12865 } else static assert(false); 12866 } 12867 version(win32_widgets) { 12868 mixin Padding!q{2}; 12869 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 12870 override int maxHeight() { return minHeight; } 12871 } 12872 } 12873 12874 12875 /// 12876 class TextEdit : EditableTextWidget { 12877 /// 12878 this(Widget parent) { 12879 super(parent); 12880 version(win32_widgets) { 12881 createWin32Window(this, "edit"w, "", 12882 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 12883 } else version(custom_widgets) { 12884 version(trash_text) 12885 setupCustomTextEditing(); 12886 } else static assert(false); 12887 } 12888 override int maxHeight() { return int.max; } 12889 override int heightStretchiness() { return 7; } 12890 12891 override int flexBasisWidth() { return 250; } 12892 override int flexBasisHeight() { return 25; } 12893 } 12894 12895 12896 /++ 12897 12898 +/ 12899 version(none) 12900 class RichTextDisplay : Widget { 12901 @property void content(string c) {} 12902 void appendContent(string c) {} 12903 } 12904 12905 /// 12906 class MessageBox : Window { 12907 private string message; 12908 MessageBoxButton buttonPressed = MessageBoxButton.None; 12909 /// 12910 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 12911 super(300, 100); 12912 12913 assert(buttons.length); 12914 assert(buttons.length == buttonIds.length); 12915 12916 this.message = message; 12917 12918 int buttonsWidth = cast(int) buttons.length * 50 + (cast(int) buttons.length - 1) * 16; 12919 buttonsWidth = scaleWithDpi(buttonsWidth); 12920 12921 int x = this.width / 2 - buttonsWidth / 2; 12922 12923 foreach(idx, buttonText; buttons) { 12924 auto button = new Button(buttonText, this); 12925 button.x = x; 12926 button.y = height - (button.height + 10); 12927 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 12928 this.buttonPressed = buttonIds[idx]; 12929 win.close(); 12930 }; })(idx)); 12931 12932 button.registerMovement(); 12933 x += button.width; 12934 x += scaleWithDpi(16); 12935 if(idx == 0) 12936 button.focus(); 12937 } 12938 12939 win.show(); 12940 redraw(); 12941 } 12942 12943 override void paint(WidgetPainter painter) { 12944 super.paint(painter); 12945 12946 auto cs = getComputedStyle(); 12947 12948 painter.outlineColor = cs.foregroundColor(); 12949 painter.fillColor = cs.foregroundColor(); 12950 12951 painter.drawText(Point(0, 0), message, Point(width, height / 2), TextAlignment.Center | TextAlignment.VerticalCenter); 12952 } 12953 12954 // this one is all fixed position 12955 override void recomputeChildLayout() {} 12956 } 12957 12958 /// 12959 enum MessageBoxStyle { 12960 OK, /// 12961 OKCancel, /// 12962 RetryCancel, /// 12963 YesNo, /// 12964 YesNoCancel, /// 12965 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. 12966 } 12967 12968 /// 12969 enum MessageBoxIcon { 12970 None, /// 12971 Info, /// 12972 Warning, /// 12973 Error /// 12974 } 12975 12976 /// Identifies the button the user pressed on a message box. 12977 enum MessageBoxButton { 12978 None, /// The user closed the message box without clicking any of the buttons. 12979 OK, /// 12980 Cancel, /// 12981 Retry, /// 12982 Yes, /// 12983 No, /// 12984 Continue /// 12985 } 12986 12987 12988 /++ 12989 Displays a modal message box, blocking until the user dismisses it. 12990 12991 Returns: the button pressed. 12992 +/ 12993 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 12994 version(win32_widgets) { 12995 WCharzBuffer t = WCharzBuffer(title); 12996 WCharzBuffer m = WCharzBuffer(message); 12997 UINT type; 12998 with(MessageBoxStyle) 12999 final switch(style) { 13000 case OK: type |= MB_OK; break; 13001 case OKCancel: type |= MB_OKCANCEL; break; 13002 case RetryCancel: type |= MB_RETRYCANCEL; break; 13003 case YesNo: type |= MB_YESNO; break; 13004 case YesNoCancel: type |= MB_YESNOCANCEL; break; 13005 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 13006 } 13007 with(MessageBoxIcon) 13008 final switch(icon) { 13009 case None: break; 13010 case Info: type |= MB_ICONINFORMATION; break; 13011 case Warning: type |= MB_ICONWARNING; break; 13012 case Error: type |= MB_ICONERROR; break; 13013 } 13014 switch(MessageBoxW(null, m.ptr, t.ptr, type)) { 13015 case IDOK: return MessageBoxButton.OK; 13016 case IDCANCEL: return MessageBoxButton.Cancel; 13017 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 13018 case IDYES: return MessageBoxButton.Yes; 13019 case IDNO: return MessageBoxButton.No; 13020 case IDCONTINUE: return MessageBoxButton.Continue; 13021 default: return MessageBoxButton.None; 13022 } 13023 } else { 13024 string[] buttons; 13025 MessageBoxButton[] buttonIds; 13026 with(MessageBoxStyle) 13027 final switch(style) { 13028 case OK: 13029 buttons = ["OK"]; 13030 buttonIds = [MessageBoxButton.OK]; 13031 break; 13032 case OKCancel: 13033 buttons = ["OK", "Cancel"]; 13034 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 13035 break; 13036 case RetryCancel: 13037 buttons = ["Retry", "Cancel"]; 13038 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 13039 break; 13040 case YesNo: 13041 buttons = ["Yes", "No"]; 13042 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 13043 break; 13044 case YesNoCancel: 13045 buttons = ["Yes", "No", "Cancel"]; 13046 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 13047 break; 13048 case RetryCancelContinue: 13049 buttons = ["Try Again", "Cancel", "Continue"]; 13050 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 13051 break; 13052 } 13053 auto mb = new MessageBox(message, buttons, buttonIds); 13054 EventLoop el = EventLoop.get; 13055 el.run(() { return !mb.win.closed; }); 13056 return mb.buttonPressed; 13057 } 13058 } 13059 13060 /// ditto 13061 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13062 return messageBox(null, message, style, icon); 13063 } 13064 13065 13066 13067 /// 13068 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 13069 13070 /++ 13071 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 13072 13073 History: 13074 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. 13075 +/ 13076 struct EventListener { 13077 private Widget widget; 13078 private string event; 13079 private EventHandler handler; 13080 private bool useCapture; 13081 13082 /// 13083 void disconnect() { 13084 widget.removeEventListener(this); 13085 } 13086 } 13087 13088 /++ 13089 The purpose of this enum was to give a compile-time checked version of various standard event strings. 13090 13091 Now, I recommend you use a statically typed event object instead. 13092 13093 See_Also: [Event] 13094 +/ 13095 enum EventType : string { 13096 click = "click", /// 13097 13098 mouseenter = "mouseenter", /// 13099 mouseleave = "mouseleave", /// 13100 mousein = "mousein", /// 13101 mouseout = "mouseout", /// 13102 mouseup = "mouseup", /// 13103 mousedown = "mousedown", /// 13104 mousemove = "mousemove", /// 13105 13106 keydown = "keydown", /// 13107 keyup = "keyup", /// 13108 char_ = "char", /// 13109 13110 focus = "focus", /// 13111 blur = "blur", /// 13112 13113 triggered = "triggered", /// 13114 13115 change = "change", /// 13116 } 13117 13118 /++ 13119 Represents an event that is currently being processed. 13120 13121 13122 Minigui's event model is based on the web browser. An event has a name, a target, 13123 and an associated data object. It starts from the window and works its way down through 13124 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 13125 then goes back up again all the way back to the window, triggering bubble phase handlers. At 13126 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 13127 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 13128 was called; that just stops it from calling other handlers in the widget tree, but the default happens 13129 whenever propagation is done, not only if it gets to the end of the chain). 13130 13131 This model has several nice points: 13132 13133 $(LIST 13134 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 13135 with event handlers set, then add/remove children as much as you want without needing 13136 to manage the event handlers on them - the parent alone can manage everything. 13137 13138 * It is easy to create new custom events in your application. 13139 13140 * It is familiar to many web developers. 13141 ) 13142 13143 There's a few downsides though: 13144 13145 $(LIST 13146 * There's not a lot of type safety. 13147 13148 * You don't get a static list of what events a widget can emit. 13149 13150 * Tracing where an event got cancelled along the chain can get difficult; the downside of 13151 the central delegation benefit is it can be lead to debugging of action at a distance. 13152 ) 13153 13154 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 13155 while keeping the benefits - and most compatibility with - the existing model. The main idea is 13156 to simply use a D object type which provides a static interface as well as a built-in event name. 13157 Then, a new static interface allows you to see what an event can emit and attach handlers to it 13158 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 13159 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 13160 to having a little more help from the D compiler and documentation generator. 13161 13162 Your code would change like this: 13163 13164 --- 13165 // old 13166 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 13167 13168 // new 13169 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 13170 --- 13171 13172 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. 13173 13174 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 13175 13176 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 13177 13178 Thus the family of functions are: 13179 13180 [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. 13181 13182 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 13183 13184 [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. 13185 13186 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 13187 13188 --- 13189 class MyCheckbox : Widget { 13190 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 13191 /// It is NOT actually required but should be used whenever possible. 13192 mixin Emits!(ChangeEvent!bool); 13193 13194 this(Widget parent) { 13195 super(parent); 13196 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 13197 } 13198 13199 private bool _checked; 13200 @property bool checked() { return _checked; } 13201 @property void checked(bool set) { 13202 _checked = set; 13203 emit!(ChangeEvent!bool)(&checked); 13204 } 13205 } 13206 --- 13207 13208 ## Creating Your Own Events 13209 13210 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. 13211 13212 --- 13213 class MyEvent : Event { 13214 this(Widget target) { super(EventString, target); } 13215 mixin Register; // adds EventString and other reflection information 13216 } 13217 --- 13218 13219 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 13220 13221 History: 13222 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. 13223 13224 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. 13225 +/ 13226 /+ 13227 13228 ## General Conventions 13229 13230 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. 13231 13232 13233 ## Qt-style signals and slots 13234 13235 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. 13236 13237 The intention is for events to be used when 13238 13239 --- 13240 class Demo : Widget { 13241 this() { 13242 myPropertyChanged = Signal!int(this); 13243 } 13244 @property myProperty(int v) { 13245 myPropertyChanged.emit(v); 13246 } 13247 13248 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 13249 // but it can just genuinely not care about `this` since that's not really passed. 13250 } 13251 13252 class Foo : Widget { 13253 // the slot uda is not necessary, but it helps the script and ui builder find it. 13254 @slot void setValue(int v) { ... } 13255 } 13256 13257 demo.myPropertyChanged.connect(&foo.setValue); 13258 --- 13259 13260 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 13261 13262 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 13263 13264 class StringChangeEvent : ChangeEvent, Signal!string { 13265 mixin SignalImpl 13266 } 13267 13268 +/ 13269 class Event : ReflectableProperties { 13270 /// Creates an event without populating any members and without sending it. See [dispatch] 13271 this(string eventName, Widget emittedBy) { 13272 this.eventName = eventName; 13273 this.srcElement = emittedBy; 13274 } 13275 13276 13277 /// Implementations for the [ReflectableProperties] interface/ 13278 void getPropertiesList(scope void delegate(string name) sink) const {} 13279 /// ditto 13280 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 13281 /// ditto 13282 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 13283 return SetPropertyResult.notPermitted; 13284 } 13285 13286 13287 /+ 13288 /++ 13289 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 13290 13291 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. 13292 +/ 13293 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 13294 if(value.length == 0) { 13295 finalSink(memberName, `""`); 13296 return; 13297 } 13298 13299 char[1024] bufferBacking; 13300 char[] buffer = bufferBacking; 13301 int bufferPosition; 13302 13303 void sink(char ch) { 13304 if(bufferPosition >= buffer.length) 13305 buffer.length = buffer.length + 1024; 13306 buffer[bufferPosition++] = ch; 13307 } 13308 13309 sink('"'); 13310 13311 foreach(ch; value) { 13312 switch(ch) { 13313 case '\\': 13314 sink('\\'); sink('\\'); 13315 break; 13316 case '"': 13317 sink('\\'); sink('"'); 13318 break; 13319 case '\n': 13320 sink('\\'); sink('n'); 13321 break; 13322 case '\r': 13323 sink('\\'); sink('r'); 13324 break; 13325 case '\t': 13326 sink('\\'); sink('t'); 13327 break; 13328 default: 13329 sink(ch); 13330 } 13331 } 13332 13333 sink('"'); 13334 13335 finalSink(memberName, buffer[0 .. bufferPosition]); 13336 } 13337 +/ 13338 13339 /+ 13340 enum EventInitiator { 13341 system, 13342 minigui, 13343 user 13344 } 13345 13346 immutable EventInitiator; initiatedBy; 13347 +/ 13348 13349 /++ 13350 Events should generally follow the propagation model, but there's some exceptions 13351 to that rule. If so, they should override this to return false. In that case, only 13352 bubbling event handlers on the target itself and capturing event handlers on the containing 13353 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 13354 capture -> target -> bubble process.) 13355 13356 History: 13357 Added May 12, 2021 13358 +/ 13359 bool propagates() const pure nothrow @nogc @safe { 13360 return true; 13361 } 13362 13363 /++ 13364 hints as to whether preventDefault will actually do anything. not entirely reliable. 13365 13366 History: 13367 Added May 14, 2021 13368 +/ 13369 bool cancelable() const pure nothrow @nogc @safe { 13370 return true; 13371 } 13372 13373 /++ 13374 You can mix this into child class to register some boilerplate. It includes the `EventString` 13375 member, a constructor, and implementations of the dynamic get data interfaces. 13376 13377 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 13378 13379 13380 You can override the default EventString by simply providing your own in the form of 13381 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 13382 which provides some namespace protection against conflicts in other libraries while still being fairly 13383 easy to use. 13384 13385 If you provide your own constructor, it will override the default constructor provided here. A constructor 13386 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 13387 first argument to your constructor. 13388 13389 History: 13390 Added May 13, 2021. 13391 +/ 13392 protected static mixin template Register() { 13393 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 13394 this(Widget target) { super(EventString, target); } 13395 13396 mixin ReflectableProperties.RegisterGetters; 13397 } 13398 13399 /++ 13400 This is the widget that emitted the event. 13401 13402 13403 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 13404 13405 History: 13406 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 13407 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 13408 so I don't intend to remove these aliases. 13409 +/ 13410 Widget source; 13411 /// ditto 13412 alias source target; 13413 /// ditto 13414 alias source srcElement; 13415 13416 Widget relatedTarget; /// Note: likely to be deprecated at some point. 13417 13418 /// Prevents the default event handler (if there is one) from being called 13419 void preventDefault() { 13420 lastDefaultPrevented = true; 13421 defaultPrevented = true; 13422 } 13423 13424 /// Stops the event propagation immediately. 13425 void stopPropagation() { 13426 propagationStopped = true; 13427 } 13428 13429 private bool defaultPrevented; 13430 private bool propagationStopped; 13431 private string eventName; 13432 13433 private bool isBubbling; 13434 13435 /// 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. 13436 protected void adjustScrolling() { } 13437 /// ditto 13438 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 13439 13440 /++ 13441 this sends it only to the target. If you want propagation, use dispatch() instead. 13442 13443 This should be made private!!! 13444 13445 +/ 13446 void sendDirectly() { 13447 if(srcElement is null) 13448 return; 13449 13450 // 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. 13451 13452 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 13453 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 13454 13455 adjustScrolling(); 13456 13457 if(auto e = target.parentWindow) { 13458 if(auto handlers = "*" in e.capturingEventHandlers) 13459 foreach(handler; *handlers) 13460 if(handler) handler(e, this); 13461 if(auto handlers = eventName in e.capturingEventHandlers) 13462 foreach(handler; *handlers) 13463 if(handler) handler(e, this); 13464 } 13465 13466 auto e = srcElement; 13467 13468 if(auto handlers = eventName in e.bubblingEventHandlers) 13469 foreach(handler; *handlers) 13470 if(handler) handler(e, this); 13471 13472 if(auto handlers = "*" in e.bubblingEventHandlers) 13473 foreach(handler; *handlers) 13474 if(handler) handler(e, this); 13475 13476 // there's never a default for a catch-all event 13477 if(!defaultPrevented) 13478 if(eventName in e.defaultEventHandlers) 13479 e.defaultEventHandlers[eventName](e, this); 13480 } 13481 13482 /// this dispatches the element using the capture -> target -> bubble process 13483 void dispatch() { 13484 if(srcElement is null) 13485 return; 13486 13487 if(!propagates) { 13488 sendDirectly; 13489 return; 13490 } 13491 13492 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 13493 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 13494 13495 adjustScrolling(); 13496 // first capture, then bubble 13497 13498 Widget[] chain; 13499 Widget curr = srcElement; 13500 while(curr) { 13501 auto l = curr; 13502 chain ~= l; 13503 curr = curr.parent; 13504 } 13505 13506 isBubbling = false; 13507 13508 foreach_reverse(e; chain) { 13509 if(auto handlers = "*" in e.capturingEventHandlers) 13510 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13511 13512 if(propagationStopped) 13513 break; 13514 13515 if(auto handlers = eventName in e.capturingEventHandlers) 13516 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13517 13518 // the default on capture should really be to always do nothing 13519 13520 //if(!defaultPrevented) 13521 // if(eventName in e.defaultEventHandlers) 13522 // e.defaultEventHandlers[eventName](e.element, this); 13523 13524 if(propagationStopped) 13525 break; 13526 } 13527 13528 int adjustX; 13529 int adjustY; 13530 13531 isBubbling = true; 13532 if(!propagationStopped) 13533 foreach(e; chain) { 13534 if(auto handlers = eventName in e.bubblingEventHandlers) 13535 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13536 13537 if(propagationStopped) 13538 break; 13539 13540 if(auto handlers = "*" in e.bubblingEventHandlers) 13541 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13542 13543 if(propagationStopped) 13544 break; 13545 13546 if(e.encapsulatedChildren()) { 13547 adjustClientCoordinates(adjustX, adjustY); 13548 target = e; 13549 } else { 13550 adjustX += e.x; 13551 adjustY += e.y; 13552 } 13553 } 13554 13555 if(!defaultPrevented) 13556 foreach(e; chain) { 13557 if(eventName in e.defaultEventHandlers) 13558 e.defaultEventHandlers[eventName](e, this); 13559 } 13560 } 13561 13562 13563 /* old compatibility things */ 13564 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 13565 final @property { 13566 Key key() { return (cast(KeyEventBase) this).key; } 13567 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 13568 13569 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 13570 bool altKey() { return (cast(KeyEventBase) this).altKey; } 13571 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 13572 } 13573 13574 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 13575 final @property { 13576 int clientX() { return (cast(MouseEventBase) this).clientX; } 13577 int clientY() { return (cast(MouseEventBase) this).clientY; } 13578 13579 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 13580 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 13581 13582 int button() { return (cast(MouseEventBase) this).button; } 13583 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 13584 } 13585 13586 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 13587 final @property { 13588 int state() { 13589 if(auto meb = cast(MouseEventBase) this) 13590 return meb.state; 13591 if(auto keb = cast(KeyEventBase) this) 13592 return keb.state; 13593 assert(0); 13594 } 13595 } 13596 13597 deprecated("Use a CharEvent instead of Event in your handler going forward") 13598 final @property { 13599 dchar character() { 13600 if(auto ce = cast(CharEvent) this) 13601 return ce.character; 13602 return dchar.init; 13603 } 13604 } 13605 13606 // for change events 13607 @property { 13608 /// 13609 int intValue() { return 0; } 13610 /// 13611 string stringValue() { return null; } 13612 } 13613 } 13614 13615 /++ 13616 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 13617 13618 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 13619 dynamic and custom events, but the static list helps ensure you get them right. 13620 13621 If this is declared, you can use [Widget.emit] to send the event. 13622 13623 All events work the same way though, following the capture->widget->bubble model described under [Event]. 13624 13625 History: 13626 Added May 4, 2021 13627 +/ 13628 mixin template Emits(EventType) { 13629 import arsd.minigui : EventString; 13630 static if(is(EventType : Event) && !is(EventType == Event)) 13631 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 13632 else 13633 static assert(0, "You can only emit subclasses of Event"); 13634 } 13635 13636 /// ditto 13637 mixin template Emits(string eventString) { 13638 mixin("private Event[0] emits_" ~ eventString ~";"); 13639 } 13640 13641 /* 13642 class SignalEvent(string name) : Event { 13643 13644 } 13645 */ 13646 13647 /++ 13648 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". 13649 13650 13651 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. 13652 13653 History: 13654 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 13655 +/ 13656 class CommandEvent : Event { 13657 enum EventString = "command"; 13658 this(Widget source, string CommandString = EventString) { 13659 super(CommandString, source); 13660 } 13661 } 13662 13663 /++ 13664 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 13665 +/ 13666 class CommandEventWithArgs(Args...) : CommandEvent { 13667 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 13668 Args args; 13669 } 13670 13671 /++ 13672 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. 13673 13674 See [CommandEvent] for more information. 13675 13676 Returns: 13677 The [EventListener] you can use to remove the handler. 13678 +/ 13679 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 13680 return w.addEventListener(CommandString, (Event ev) { 13681 if(ev.target is w) 13682 return; // it does not consume its own commands! 13683 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 13684 handler(cev.args); 13685 ev.stopPropagation(); 13686 } 13687 }); 13688 } 13689 13690 /++ 13691 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. 13692 +/ 13693 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 13694 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 13695 event.dispatch(); 13696 } 13697 13698 class ResizeEvent : Event { 13699 enum EventString = "resize"; 13700 13701 this(Widget target) { super(EventString, target); } 13702 13703 override bool propagates() const { return false; } 13704 } 13705 13706 /++ 13707 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 13708 13709 ClosedEvent happens when the window has been closed. It is already gone by the time this event fires, meaning you cannot prevent the close. Use [ClosingEvent] if you want to cancel, use [ClosedEvent] if you simply want to be notified. 13710 13711 History: 13712 Added June 21, 2021 (dub v10.1) 13713 +/ 13714 class ClosingEvent : Event { 13715 enum EventString = "closing"; 13716 13717 this(Widget target) { super(EventString, target); } 13718 13719 override bool propagates() const { return false; } 13720 override bool cancelable() const { return true; } 13721 } 13722 13723 /// ditto 13724 class ClosedEvent : Event { 13725 enum EventString = "closed"; 13726 13727 this(Widget target) { super(EventString, target); } 13728 13729 override bool propagates() const { return false; } 13730 override bool cancelable() const { return false; } 13731 } 13732 13733 /// 13734 class BlurEvent : Event { 13735 enum EventString = "blur"; 13736 13737 // FIXME: related target? 13738 this(Widget target) { super(EventString, target); } 13739 13740 override bool propagates() const { return false; } 13741 } 13742 13743 /// 13744 class FocusEvent : Event { 13745 enum EventString = "focus"; 13746 13747 // FIXME: related target? 13748 this(Widget target) { super(EventString, target); } 13749 13750 override bool propagates() const { return false; } 13751 } 13752 13753 /++ 13754 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 13755 13756 History: 13757 Added July 3, 2021 13758 +/ 13759 class FocusInEvent : Event { 13760 enum EventString = "focusin"; 13761 13762 // FIXME: related target? 13763 this(Widget target) { super(EventString, target); } 13764 13765 override bool cancelable() const { return false; } 13766 } 13767 13768 /// ditto 13769 class FocusOutEvent : Event { 13770 enum EventString = "focusout"; 13771 13772 // FIXME: related target? 13773 this(Widget target) { super(EventString, target); } 13774 13775 override bool cancelable() const { return false; } 13776 } 13777 13778 /// 13779 class ScrollEvent : Event { 13780 enum EventString = "scroll"; 13781 this(Widget target) { super(EventString, target); } 13782 13783 override bool cancelable() const { return false; } 13784 } 13785 13786 /++ 13787 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 13788 13789 History: 13790 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 13791 +/ 13792 class CharEvent : Event { 13793 enum EventString = "char"; 13794 this(Widget target, dchar ch) { 13795 character = ch; 13796 super(EventString, target); 13797 } 13798 13799 immutable dchar character; 13800 } 13801 13802 /++ 13803 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 13804 +/ 13805 abstract class ChangeEventBase : Event { 13806 enum EventString = "change"; 13807 this(Widget target) { 13808 super(EventString, target); 13809 } 13810 13811 /+ 13812 // idk where or how exactly i want to do this. 13813 // i might come back to it later. 13814 13815 // If a widget itself broadcasts one of theses itself, it stops propagation going down 13816 // this way the source doesn't get too confused (think of a nested scroll widget) 13817 // 13818 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 13819 // then you consume that command and change you scroll x position to whatever. then you do 13820 // some kind of change event that is broadcast back to the children and any horizontal scroll 13821 // listeners are now able to update, without having an explicit connection between them. 13822 void broadcastToChildren(string fieldName) { 13823 13824 } 13825 +/ 13826 } 13827 13828 /++ 13829 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. 13830 13831 13832 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). 13833 13834 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);` 13835 13836 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 13837 13838 History: 13839 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. 13840 +/ 13841 class ChangeEvent(T) : ChangeEventBase { 13842 this(Widget target, T delegate() getNewValue) { 13843 assert(getNewValue !is null); 13844 this.getNewValue = getNewValue; 13845 super(target); 13846 } 13847 13848 private T delegate() getNewValue; 13849 13850 /++ 13851 Gets the new value that just changed. 13852 +/ 13853 @property T value() { 13854 return getNewValue(); 13855 } 13856 13857 /// compatibility method for old generic Events 13858 static if(is(immutable T == immutable int)) 13859 override int intValue() { return value; } 13860 /// ditto 13861 static if(is(immutable T == immutable string)) 13862 override string stringValue() { return value; } 13863 } 13864 13865 /++ 13866 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 13867 13868 13869 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 13870 13871 History: 13872 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 13873 +/ 13874 abstract class KeyEventBase : Event { 13875 this(string name, Widget target) { 13876 super(name, target); 13877 } 13878 13879 // for key events 13880 Key key; /// 13881 13882 KeyEvent originalKeyEvent; 13883 13884 /++ 13885 Indicates the current state of the given keyboard modifier keys. 13886 13887 History: 13888 Added to events on April 15, 2020. 13889 +/ 13890 bool ctrlKey; 13891 13892 /// ditto 13893 bool altKey; 13894 13895 /// ditto 13896 bool shiftKey; 13897 13898 /++ 13899 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 13900 13901 See [arsd.simpledisplay.ModifierState] for other possible flags. 13902 +/ 13903 int state; 13904 13905 mixin Register; 13906 } 13907 13908 /++ 13909 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]. 13910 13911 13912 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 13913 13914 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. 13915 13916 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. 13917 13918 See_Also: [KeyUpEvent], [CharEvent] 13919 13920 History: 13921 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 13922 +/ 13923 class KeyDownEvent : KeyEventBase { 13924 enum EventString = "keydown"; 13925 this(Widget target) { super(EventString, target); } 13926 } 13927 13928 /++ 13929 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 13930 13931 13932 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 13933 13934 See_Also: [KeyDownEvent], [CharEvent] 13935 13936 History: 13937 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 13938 +/ 13939 class KeyUpEvent : KeyEventBase { 13940 enum EventString = "keyup"; 13941 this(Widget target) { super(EventString, target); } 13942 } 13943 13944 /++ 13945 Contains shared properties for various mouse events; 13946 13947 13948 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 13949 13950 History: 13951 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 13952 +/ 13953 abstract class MouseEventBase : Event { 13954 this(string name, Widget target) { 13955 super(name, target); 13956 } 13957 13958 // for mouse events 13959 int clientX; /// The mouse event location relative to the target widget 13960 int clientY; /// ditto 13961 13962 int viewportX; /// The mouse event location relative to the window origin 13963 int viewportY; /// ditto 13964 13965 int button; /// See: [MouseEvent.button] 13966 int buttonLinear; /// See: [MouseEvent.buttonLinear] 13967 13968 /++ 13969 Indicates the current state of the given keyboard modifier keys. 13970 13971 History: 13972 Added to mouse events on September 28, 2010. 13973 +/ 13974 bool ctrlKey; 13975 13976 /// ditto 13977 bool altKey; 13978 13979 /// ditto 13980 bool shiftKey; 13981 13982 13983 13984 int state; /// 13985 13986 /++ 13987 for consistent names with key event. 13988 13989 History: 13990 Added September 28, 2021 (dub v10.3) 13991 +/ 13992 alias modifierState = state; 13993 13994 /++ 13995 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 13996 13997 History: 13998 Added May 15, 2021 13999 +/ 14000 bool isMouseWheel() { 14001 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 14002 } 14003 14004 // private 14005 override void adjustClientCoordinates(int deltaX, int deltaY) { 14006 clientX += deltaX; 14007 clientY += deltaY; 14008 } 14009 14010 override void adjustScrolling() { 14011 version(custom_widgets) { // TEMP 14012 viewportX = clientX; 14013 viewportY = clientY; 14014 if(auto se = cast(ScrollableWidget) srcElement) { 14015 clientX += se.scrollOrigin.x; 14016 clientY += se.scrollOrigin.y; 14017 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 14018 //clientX += se.scrollX_; 14019 //clientY += se.scrollY_; 14020 } 14021 } 14022 } 14023 14024 mixin Register; 14025 } 14026 14027 /++ 14028 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 14029 14030 14031 $(WARNING 14032 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 14033 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 14034 behavior. 14035 ) 14036 14037 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 14038 14039 [MouseUpEvent] is sent when the user releases a mouse button. 14040 14041 [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.) 14042 14043 [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. 14044 14045 [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. 14046 14047 [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. 14048 14049 [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. 14050 14051 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 14052 14053 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 14054 14055 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14056 14057 Rationale: 14058 14059 If you only want to do drag, mousedown/up works just fine being consistently sent. 14060 14061 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). 14062 14063 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. 14064 14065 History: 14066 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. 14067 +/ 14068 class MouseUpEvent : MouseEventBase { 14069 enum EventString = "mouseup"; /// 14070 this(Widget target) { super(EventString, target); } 14071 } 14072 /// ditto 14073 class MouseDownEvent : MouseEventBase { 14074 enum EventString = "mousedown"; /// 14075 this(Widget target) { super(EventString, target); } 14076 } 14077 /// ditto 14078 class MouseMoveEvent : MouseEventBase { 14079 enum EventString = "mousemove"; /// 14080 this(Widget target) { super(EventString, target); } 14081 } 14082 /// ditto 14083 class ClickEvent : MouseEventBase { 14084 enum EventString = "click"; /// 14085 this(Widget target) { super(EventString, target); } 14086 } 14087 /// ditto 14088 class DoubleClickEvent : MouseEventBase { 14089 enum EventString = "dblclick"; /// 14090 this(Widget target) { super(EventString, target); } 14091 } 14092 /// ditto 14093 class MouseOverEvent : Event { 14094 enum EventString = "mouseover"; /// 14095 this(Widget target) { super(EventString, target); } 14096 } 14097 /// ditto 14098 class MouseOutEvent : Event { 14099 enum EventString = "mouseout"; /// 14100 this(Widget target) { super(EventString, target); } 14101 } 14102 /// ditto 14103 class MouseEnterEvent : Event { 14104 enum EventString = "mouseenter"; /// 14105 this(Widget target) { super(EventString, target); } 14106 14107 override bool propagates() const { return false; } 14108 } 14109 /// ditto 14110 class MouseLeaveEvent : Event { 14111 enum EventString = "mouseleave"; /// 14112 this(Widget target) { super(EventString, target); } 14113 14114 override bool propagates() const { return false; } 14115 } 14116 14117 private bool isAParentOf(Widget a, Widget b) { 14118 if(a is null || b is null) 14119 return false; 14120 14121 while(b !is null) { 14122 if(a is b) 14123 return true; 14124 b = b.parent; 14125 } 14126 14127 return false; 14128 } 14129 14130 private struct WidgetAtPointResponse { 14131 Widget widget; 14132 14133 // x, y relative to the widget in the response. 14134 int x; 14135 int y; 14136 } 14137 14138 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 14139 assert(starting !is null); 14140 14141 starting.addScrollPosition(x, y); 14142 14143 auto child = starting.getChildAtPosition(x, y); 14144 while(child) { 14145 if(child.hidden) 14146 continue; 14147 starting = child; 14148 x -= child.x; 14149 y -= child.y; 14150 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 14151 child = r.widget; 14152 if(child is starting) 14153 break; 14154 } 14155 return WidgetAtPointResponse(starting, x, y); 14156 } 14157 14158 version(win32_widgets) { 14159 private: 14160 import core.sys.windows.commctrl; 14161 14162 pragma(lib, "comctl32"); 14163 shared static this() { 14164 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 14165 INITCOMMONCONTROLSEX ic; 14166 ic.dwSize = cast(DWORD) ic.sizeof; 14167 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 14168 if(!InitCommonControlsEx(&ic)) { 14169 //import std.stdio; writeln("ICC failed"); 14170 } 14171 } 14172 14173 14174 // everything from here is just win32 headers copy pasta 14175 private: 14176 extern(Windows): 14177 14178 alias HANDLE HMENU; 14179 HMENU CreateMenu(); 14180 bool SetMenu(HWND, HMENU); 14181 HMENU CreatePopupMenu(); 14182 enum MF_POPUP = 0x10; 14183 enum MF_STRING = 0; 14184 14185 14186 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 14187 struct INITCOMMONCONTROLSEX { 14188 DWORD dwSize; 14189 DWORD dwICC; 14190 } 14191 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 14192 enum { 14193 IDB_STD_SMALL_COLOR, 14194 IDB_STD_LARGE_COLOR, 14195 IDB_VIEW_SMALL_COLOR = 4, 14196 IDB_VIEW_LARGE_COLOR = 5 14197 } 14198 enum { 14199 STD_CUT, 14200 STD_COPY, 14201 STD_PASTE, 14202 STD_UNDO, 14203 STD_REDOW, 14204 STD_DELETE, 14205 STD_FILENEW, 14206 STD_FILEOPEN, 14207 STD_FILESAVE, 14208 STD_PRINTPRE, 14209 STD_PROPERTIES, 14210 STD_HELP, 14211 STD_FIND, 14212 STD_REPLACE, 14213 STD_PRINT // = 14 14214 } 14215 14216 alias HANDLE HIMAGELIST; 14217 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 14218 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 14219 BOOL ImageList_Destroy(HIMAGELIST); 14220 14221 uint MAKELONG(ushort a, ushort b) { 14222 return cast(uint) ((b << 16) | a); 14223 } 14224 14225 14226 struct TBBUTTON { 14227 int iBitmap; 14228 int idCommand; 14229 BYTE fsState; 14230 BYTE fsStyle; 14231 version(Win64) 14232 BYTE[6] bReserved; 14233 else 14234 BYTE[2] bReserved; 14235 DWORD dwData; 14236 INT_PTR iString; 14237 } 14238 14239 enum { 14240 TB_ADDBUTTONSA = WM_USER + 20, 14241 TB_INSERTBUTTONA = WM_USER + 21, 14242 TB_GETIDEALSIZE = WM_USER + 99, 14243 } 14244 14245 struct SIZE { 14246 LONG cx; 14247 LONG cy; 14248 } 14249 14250 14251 enum { 14252 TBSTATE_CHECKED = 1, 14253 TBSTATE_PRESSED = 2, 14254 TBSTATE_ENABLED = 4, 14255 TBSTATE_HIDDEN = 8, 14256 TBSTATE_INDETERMINATE = 16, 14257 TBSTATE_WRAP = 32 14258 } 14259 14260 14261 14262 enum { 14263 ILC_COLOR = 0, 14264 ILC_COLOR4 = 4, 14265 ILC_COLOR8 = 8, 14266 ILC_COLOR16 = 16, 14267 ILC_COLOR24 = 24, 14268 ILC_COLOR32 = 32, 14269 ILC_COLORDDB = 254, 14270 ILC_MASK = 1, 14271 ILC_PALETTE = 2048 14272 } 14273 14274 14275 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 14276 14277 14278 enum { 14279 TB_ENABLEBUTTON = WM_USER + 1, 14280 TB_CHECKBUTTON, 14281 TB_PRESSBUTTON, 14282 TB_HIDEBUTTON, 14283 TB_INDETERMINATE, // = WM_USER + 5, 14284 TB_ISBUTTONENABLED = WM_USER + 9, 14285 TB_ISBUTTONCHECKED, 14286 TB_ISBUTTONPRESSED, 14287 TB_ISBUTTONHIDDEN, 14288 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 14289 TB_SETSTATE = WM_USER + 17, 14290 TB_GETSTATE = WM_USER + 18, 14291 TB_ADDBITMAP = WM_USER + 19, 14292 TB_DELETEBUTTON = WM_USER + 22, 14293 TB_GETBUTTON, 14294 TB_BUTTONCOUNT, 14295 TB_COMMANDTOINDEX, 14296 TB_SAVERESTOREA, 14297 TB_CUSTOMIZE, 14298 TB_ADDSTRINGA, 14299 TB_GETITEMRECT, 14300 TB_BUTTONSTRUCTSIZE, 14301 TB_SETBUTTONSIZE, 14302 TB_SETBITMAPSIZE, 14303 TB_AUTOSIZE, // = WM_USER + 33, 14304 TB_GETTOOLTIPS = WM_USER + 35, 14305 TB_SETTOOLTIPS = WM_USER + 36, 14306 TB_SETPARENT = WM_USER + 37, 14307 TB_SETROWS = WM_USER + 39, 14308 TB_GETROWS, 14309 TB_GETBITMAPFLAGS, 14310 TB_SETCMDID, 14311 TB_CHANGEBITMAP, 14312 TB_GETBITMAP, 14313 TB_GETBUTTONTEXTA, 14314 TB_REPLACEBITMAP, // = WM_USER + 46, 14315 TB_GETBUTTONSIZE = WM_USER + 58, 14316 TB_SETBUTTONWIDTH = WM_USER + 59, 14317 TB_GETBUTTONTEXTW = WM_USER + 75, 14318 TB_SAVERESTOREW = WM_USER + 76, 14319 TB_ADDSTRINGW = WM_USER + 77, 14320 } 14321 14322 extern(Windows) 14323 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 14324 14325 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 14326 14327 14328 enum { 14329 TB_SETINDENT = WM_USER + 47, 14330 TB_SETIMAGELIST, 14331 TB_GETIMAGELIST, 14332 TB_LOADIMAGES, 14333 TB_GETRECT, 14334 TB_SETHOTIMAGELIST, 14335 TB_GETHOTIMAGELIST, 14336 TB_SETDISABLEDIMAGELIST, 14337 TB_GETDISABLEDIMAGELIST, 14338 TB_SETSTYLE, 14339 TB_GETSTYLE, 14340 //TB_GETBUTTONSIZE, 14341 //TB_SETBUTTONWIDTH, 14342 TB_SETMAXTEXTROWS, 14343 TB_GETTEXTROWS // = WM_USER + 61 14344 } 14345 14346 enum { 14347 CCM_FIRST = 0x2000, 14348 CCM_LAST = CCM_FIRST + 0x200, 14349 CCM_SETBKCOLOR = 8193, 14350 CCM_SETCOLORSCHEME = 8194, 14351 CCM_GETCOLORSCHEME = 8195, 14352 CCM_GETDROPTARGET = 8196, 14353 CCM_SETUNICODEFORMAT = 8197, 14354 CCM_GETUNICODEFORMAT = 8198, 14355 CCM_SETVERSION = 0x2007, 14356 CCM_GETVERSION = 0x2008, 14357 CCM_SETNOTIFYWINDOW = 0x2009 14358 } 14359 14360 14361 enum { 14362 PBM_SETRANGE = WM_USER + 1, 14363 PBM_SETPOS, 14364 PBM_DELTAPOS, 14365 PBM_SETSTEP, 14366 PBM_STEPIT, // = WM_USER + 5 14367 PBM_SETRANGE32 = 1030, 14368 PBM_GETRANGE, 14369 PBM_GETPOS, 14370 PBM_SETBARCOLOR, // = 1033 14371 PBM_SETBKCOLOR = CCM_SETBKCOLOR 14372 } 14373 14374 enum { 14375 PBS_SMOOTH = 1, 14376 PBS_VERTICAL = 4 14377 } 14378 14379 enum { 14380 ICC_LISTVIEW_CLASSES = 1, 14381 ICC_TREEVIEW_CLASSES = 2, 14382 ICC_BAR_CLASSES = 4, 14383 ICC_TAB_CLASSES = 8, 14384 ICC_UPDOWN_CLASS = 16, 14385 ICC_PROGRESS_CLASS = 32, 14386 ICC_HOTKEY_CLASS = 64, 14387 ICC_ANIMATE_CLASS = 128, 14388 ICC_WIN95_CLASSES = 255, 14389 ICC_DATE_CLASSES = 256, 14390 ICC_USEREX_CLASSES = 512, 14391 ICC_COOL_CLASSES = 1024, 14392 ICC_STANDARD_CLASSES = 0x00004000, 14393 } 14394 14395 enum WM_USER = 1024; 14396 } 14397 14398 version(win32_widgets) 14399 pragma(lib, "comdlg32"); 14400 14401 14402 /// 14403 enum GenericIcons : ushort { 14404 None, /// 14405 // these happen to match the win32 std icons numerically if you just subtract one from the value 14406 Cut, /// 14407 Copy, /// 14408 Paste, /// 14409 Undo, /// 14410 Redo, /// 14411 Delete, /// 14412 New, /// 14413 Open, /// 14414 Save, /// 14415 PrintPreview, /// 14416 Properties, /// 14417 Help, /// 14418 Find, /// 14419 Replace, /// 14420 Print, /// 14421 } 14422 14423 enum FileDialogType { 14424 Automatic, 14425 Open, 14426 Save 14427 } 14428 string previousFileReferenced; 14429 14430 /++ 14431 Used in automatic menu functions to indicate that the user should be able to browse for a file. 14432 14433 Params: 14434 storage = an alias to a `static string` variable that stores the last file referenced. It will 14435 use this to pre-fill the dialog with a suggestion. 14436 14437 Please note that it MUST be `static` or you will get compile errors. 14438 14439 filters = the filters param to [getFileName] 14440 14441 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 14442 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 14443 a save dialog box. Otherwise, it will show an open dialog box. 14444 +/ 14445 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 14446 string name; 14447 alias name this; 14448 } 14449 14450 /++ 14451 History: 14452 onCancel was added November 6, 2021. 14453 14454 The dialog itself on Linux was modified on December 2, 2021 to include 14455 a directory picker in addition to the command line completion view. 14456 14457 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 14458 Future_directions: 14459 I want to add some kind of custom preview and maybe thumbnail thing in the future, 14460 at least on Linux, maybe on Windows too. 14461 +/ 14462 void getOpenFileName( 14463 void delegate(string) onOK, 14464 string prefilledName = null, 14465 string[] filters = null, 14466 void delegate() onCancel = null, 14467 string initialDirectory = null, 14468 ) 14469 { 14470 return getFileName(true, onOK, prefilledName, filters, onCancel, initialDirectory); 14471 } 14472 14473 /// ditto 14474 void getSaveFileName( 14475 void delegate(string) onOK, 14476 string prefilledName = null, 14477 string[] filters = null, 14478 void delegate() onCancel = null, 14479 string initialDirectory = null, 14480 ) 14481 { 14482 return getFileName(false, onOK, prefilledName, filters, onCancel, initialDirectory); 14483 } 14484 14485 void getFileName( 14486 bool openOrSave, 14487 void delegate(string) onOK, 14488 string prefilledName = null, 14489 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 14490 void delegate() onCancel = null, 14491 string initialDirectory = null, 14492 ) 14493 { 14494 14495 version(win32_widgets) { 14496 import core.sys.windows.commdlg; 14497 /* 14498 Ofn.lStructSize = sizeof(OPENFILENAME); 14499 Ofn.hwndOwner = hWnd; 14500 Ofn.lpstrFilter = szFilter; 14501 Ofn.lpstrFile= szFile; 14502 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 14503 Ofn.lpstrFileTitle = szFileTitle; 14504 Ofn.nMaxFileTitle = sizeof(szFileTitle); 14505 Ofn.lpstrInitialDir = (LPSTR)NULL; 14506 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 14507 Ofn.lpstrTitle = szTitle; 14508 */ 14509 14510 14511 wchar[1024] file = 0; 14512 wchar[1024] filterBuffer = 0; 14513 makeWindowsString(prefilledName, file[]); 14514 OPENFILENAME ofn; 14515 ofn.lStructSize = ofn.sizeof; 14516 if(filters.length) { 14517 string filter; 14518 foreach(i, f; filters) { 14519 filter ~= f; 14520 filter ~= "\0"; 14521 } 14522 filter ~= "\0"; 14523 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 14524 } 14525 ofn.lpstrFile = file.ptr; 14526 ofn.nMaxFile = file.length; 14527 14528 wchar[1024] initialDir = 0; 14529 if(initialDirectory !is null) { 14530 makeWindowsString(initialDirectory, initialDir[]); 14531 ofn.lpstrInitialDir = file.ptr; 14532 } 14533 14534 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 14535 { 14536 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 14537 if(okString.length && okString[$-1] == '\0') 14538 okString = okString[0..$-1]; 14539 onOK(okString); 14540 } else { 14541 if(onCancel) 14542 onCancel(); 14543 } 14544 } else version(custom_widgets) { 14545 if(filters.length == 0) 14546 filters = ["All Files\0*.*"]; 14547 auto picker = new FilePicker(prefilledName, filters, initialDirectory); 14548 picker.onOK = onOK; 14549 picker.onCancel = onCancel; 14550 picker.show(); 14551 } 14552 } 14553 14554 version(custom_widgets) 14555 private 14556 class FilePicker : Dialog { 14557 void delegate(string) onOK; 14558 void delegate() onCancel; 14559 LineEdit lineEdit; 14560 14561 enum GetFilesResult { 14562 success, 14563 fileNotFound 14564 } 14565 static GetFilesResult getFiles(string cwd, scope void delegate(string name, bool isDirectory) dg) { 14566 version(Windows) { 14567 WIN32_FIND_DATA data; 14568 WCharzBuffer search = WCharzBuffer(cwd ~ "/*"); 14569 auto handle = FindFirstFileW(search.ptr, &data); 14570 scope(exit) if(handle !is INVALID_HANDLE_VALUE) FindClose(handle); 14571 if(handle is INVALID_HANDLE_VALUE) { 14572 if(GetLastError() == ERROR_FILE_NOT_FOUND) 14573 return GetFilesResult.fileNotFound; 14574 throw new WindowsApiException("FindFirstFileW", GetLastError()); 14575 } 14576 14577 try_more: 14578 14579 string name = makeUtf8StringFromWindowsString(data.cFileName[0 .. findIndexOfZero(data.cFileName[])]); 14580 14581 dg(name, (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? true : false); 14582 14583 auto ret = FindNextFileW(handle, &data); 14584 if(ret == 0) { 14585 if(GetLastError() == ERROR_NO_MORE_FILES) 14586 return GetFilesResult.success; 14587 throw new WindowsApiException("FindNextFileW", GetLastError()); 14588 } 14589 14590 goto try_more; 14591 14592 } else version(Posix) { 14593 import core.sys.posix.dirent; 14594 import core.stdc.errno; 14595 auto dir = opendir((cwd ~ "\0").ptr); 14596 scope(exit) 14597 if(dir) closedir(dir); 14598 if(dir is null) 14599 throw new ErrnoApiException("opendir [" ~ cwd ~ "]", errno); 14600 14601 auto dirent = readdir(dir); 14602 if(dirent is null) 14603 return GetFilesResult.fileNotFound; 14604 14605 try_more: 14606 14607 string name = dirent.d_name[0 .. findIndexOfZero(dirent.d_name[])].idup; 14608 14609 dg(name, dirent.d_type == DT_DIR); 14610 14611 dirent = readdir(dir); 14612 if(dirent is null) 14613 return GetFilesResult.success; 14614 14615 goto try_more; 14616 } else static assert(0); 14617 } 14618 14619 // returns common prefix 14620 string loadFiles(string cwd, string[] filters...) { 14621 string[] files; 14622 string[] dirs; 14623 14624 string commonPrefix; 14625 14626 getFiles(cwd, (string name, bool isDirectory) { 14627 if(name == ".") 14628 return; // skip this as unnecessary 14629 if(isDirectory) 14630 dirs ~= name; 14631 else { 14632 foreach(filter; filters) 14633 if( 14634 filter.length <= 1 || 14635 filter == "*.*" || 14636 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 14637 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 14638 ) 14639 { 14640 files ~= name; 14641 14642 if(filter.length > 0 && filter[$-1] == '*') { 14643 if(commonPrefix is null) { 14644 commonPrefix = name; 14645 } else { 14646 foreach(idx, char i; name) { 14647 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 14648 commonPrefix = commonPrefix[0 .. idx]; 14649 break; 14650 } 14651 } 14652 } 14653 } 14654 14655 break; 14656 } 14657 } 14658 }); 14659 14660 extern(C) static int comparator(scope const void* a, scope const void* b) { 14661 auto sa = *cast(string*) a; 14662 auto sb = *cast(string*) b; 14663 14664 for(int i = 0; i < sa.length; i++) { 14665 if(i == sb.length) 14666 return 1; 14667 return sa[i] - sb[i]; 14668 } 14669 14670 return 0; 14671 } 14672 14673 nonPhobosSort(files, &comparator); 14674 nonPhobosSort(dirs, &comparator); 14675 14676 listWidget.clear(); 14677 dirWidget.clear(); 14678 foreach(name; dirs) 14679 dirWidget.addOption(name); 14680 foreach(name; files) 14681 listWidget.addOption(name); 14682 14683 return commonPrefix; 14684 } 14685 14686 ListWidget listWidget; 14687 ListWidget dirWidget; 14688 14689 string currentDirectory; 14690 string[] processedFilters; 14691 14692 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"] 14693 this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) { 14694 super(300, 200, "Choose File..."); // owner); 14695 14696 foreach(filter; filters) { 14697 while(filter.length && filter[0] != 0) { 14698 filter = filter[1 .. $]; 14699 } 14700 if(filter.length) 14701 filter = filter[1 .. $]; // trim off the 0 14702 14703 while(filter.length) { 14704 int idx = 0; 14705 while(idx < filter.length && filter[idx] != ';') { 14706 idx++; 14707 } 14708 14709 processedFilters ~= filter[0 .. idx]; 14710 if(idx < filter.length) 14711 idx++; // skip the ; 14712 filter = filter[idx .. $]; 14713 } 14714 } 14715 14716 currentDirectory = initialDirectory is null ? "." : initialDirectory; 14717 14718 { 14719 auto hl = new HorizontalLayout(this); 14720 dirWidget = new ListWidget(hl); 14721 listWidget = new ListWidget(hl); 14722 14723 // double click events normally trigger something else but 14724 // here user might be clicking kinda fast and we'd rather just 14725 // keep it 14726 dirWidget.addEventListener((scope DoubleClickEvent dev) { 14727 auto ce = new ChangeEvent!void(dirWidget, () {}); 14728 ce.dispatch(); 14729 }); 14730 14731 dirWidget.addEventListener((scope ChangeEvent!void sce) { 14732 string v; 14733 foreach(o; dirWidget.options) 14734 if(o.selected) { 14735 v = o.label; 14736 break; 14737 } 14738 if(v.length) { 14739 currentDirectory ~= "/" ~ v; 14740 loadFiles(currentDirectory, processedFilters); 14741 } 14742 }); 14743 14744 // double click here, on the other hand, selects the file 14745 // and moves on 14746 listWidget.addEventListener((scope DoubleClickEvent dev) { 14747 OK(); 14748 }); 14749 } 14750 14751 lineEdit = new LineEdit(this); 14752 lineEdit.focus(); 14753 lineEdit.addEventListener(delegate(CharEvent event) { 14754 if(event.character == '\t' || event.character == '\n') 14755 event.preventDefault(); 14756 }); 14757 14758 listWidget.addEventListener(EventType.change, () { 14759 foreach(o; listWidget.options) 14760 if(o.selected) 14761 lineEdit.content = o.label; 14762 }); 14763 14764 loadFiles(currentDirectory, processedFilters); 14765 14766 lineEdit.addEventListener((KeyDownEvent event) { 14767 if(event.key == Key.Tab) { 14768 14769 auto current = lineEdit.content; 14770 if(current.length >= 2 && current[0 ..2] == "./") 14771 current = current[2 .. $]; 14772 14773 auto commonPrefix = loadFiles(".", current ~ "*"); 14774 14775 if(commonPrefix.length) 14776 lineEdit.content = commonPrefix; 14777 14778 // FIXME: if that is a directory, add the slash? or even go inside? 14779 14780 event.preventDefault(); 14781 } 14782 }); 14783 14784 lineEdit.content = prefilledName; 14785 14786 auto hl = new HorizontalLayout(60, this); 14787 auto cancelButton = new Button("Cancel", hl); 14788 auto okButton = new Button("OK", hl); 14789 14790 cancelButton.addEventListener(EventType.triggered, &Cancel); 14791 okButton.addEventListener(EventType.triggered, &OK); 14792 14793 this.addEventListener((KeyDownEvent event) { 14794 if(event.key == Key.Enter || event.key == Key.PadEnter) { 14795 event.preventDefault(); 14796 OK(); 14797 } 14798 if(event.key == Key.Escape) 14799 Cancel(); 14800 }); 14801 14802 } 14803 14804 override void OK() { 14805 if(lineEdit.content.length) { 14806 string accepted; 14807 auto c = lineEdit.content; 14808 if(c.length && c[0] == '/') 14809 accepted = c; 14810 else 14811 accepted = currentDirectory ~ "/" ~ lineEdit.content; 14812 14813 if(isDir(accepted)) { 14814 // FIXME: would be kinda nice to support ~ and collapse these paths too 14815 // FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later. 14816 currentDirectory = accepted; 14817 loadFiles(currentDirectory, processedFilters); 14818 lineEdit.content = ""; 14819 return; 14820 } 14821 14822 if(onOK) 14823 onOK(accepted); 14824 } 14825 close(); 14826 } 14827 14828 override void Cancel() { 14829 if(onCancel) 14830 onCancel(); 14831 close(); 14832 } 14833 } 14834 14835 private bool isDir(string name) { 14836 version(Windows) { 14837 auto ws = WCharzBuffer(name); 14838 auto ret = GetFileAttributesW(ws.ptr); 14839 if(ret == INVALID_FILE_ATTRIBUTES) 14840 return false; 14841 return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0; 14842 } else version(Posix) { 14843 import core.sys.posix.sys.stat; 14844 stat_t buf; 14845 auto ret = stat((name ~ '\0').ptr, &buf); 14846 if(ret == -1) 14847 return false; // I could probably check more specific errors tbh 14848 return (buf.st_mode & S_IFMT) == S_IFDIR; 14849 } else return false; 14850 } 14851 14852 /* 14853 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 14854 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 14855 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 14856 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 14857 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 14858 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 14859 http://www.sbin.org/doc/Xlib/chapt_03.html 14860 14861 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 14862 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 14863 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 14864 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 14865 */ 14866 14867 14868 // These are all for setMenuAndToolbarFromAnnotatedCode 14869 /// This item in the menu will be preceded by a separator line 14870 /// Group: generating_from_code 14871 struct separator {} 14872 deprecated("It was misspelled, use separator instead") alias seperator = separator; 14873 /// Program-wide keyboard shortcut to trigger the action 14874 /// Group: generating_from_code 14875 struct accelerator { string keyString; } 14876 /// tells which menu the action will be on 14877 /// Group: generating_from_code 14878 struct menu { string name; } 14879 /// Describes which toolbar section the action appears on 14880 /// Group: generating_from_code 14881 struct toolbar { string groupName; } 14882 /// 14883 /// Group: generating_from_code 14884 struct icon { ushort id; } 14885 /// 14886 /// Group: generating_from_code 14887 struct label { string label; } 14888 /// 14889 /// Group: generating_from_code 14890 struct hotkey { dchar ch; } 14891 /// 14892 /// Group: generating_from_code 14893 struct tip { string tip; } 14894 14895 14896 /++ 14897 Observes and allows inspection of an object via automatic gui 14898 +/ 14899 /// Group: generating_from_code 14900 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 14901 return new ObjectInspectionWindowImpl!(T)(t); 14902 } 14903 14904 class ObjectInspectionWindow : Window { 14905 this(int a, int b, string c) { 14906 super(a, b, c); 14907 } 14908 14909 abstract void readUpdatesFromObject(); 14910 } 14911 14912 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 14913 T t; 14914 this(T t) { 14915 this.t = t; 14916 14917 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 14918 14919 foreach(memberName; __traits(derivedMembers, T)) {{ 14920 alias member = I!(__traits(getMember, t, memberName))[0]; 14921 alias type = typeof(member); 14922 static if(is(type == int)) { 14923 auto le = new LabeledLineEdit(memberName ~ ": ", this); 14924 //le.addEventListener("char", (Event ev) { 14925 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 14926 //ev.preventDefault(); 14927 //}); 14928 le.addEventListener(EventType.change, (Event ev) { 14929 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 14930 }); 14931 14932 updateMemberDelegates[memberName] = () { 14933 le.content = toInternal!string(__traits(getMember, t, memberName)); 14934 }; 14935 } 14936 }} 14937 } 14938 14939 void delegate()[string] updateMemberDelegates; 14940 14941 override void readUpdatesFromObject() { 14942 foreach(k, v; updateMemberDelegates) 14943 v(); 14944 } 14945 } 14946 14947 /++ 14948 Creates a dialog based on a data structure. 14949 14950 --- 14951 dialog((YourStructure value) { 14952 // the user filled in the struct and clicked OK, 14953 // you can check the members now 14954 }); 14955 --- 14956 14957 Params: 14958 initialData = the initial value to show in the dialog. It will not modify this unless 14959 it is a class then it might, no promises. 14960 14961 History: 14962 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 14963 +/ 14964 /// Group: generating_from_code 14965 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 14966 dialog(T.init, onOK, onCancel, title); 14967 } 14968 /// ditto 14969 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 14970 auto dg = new AutomaticDialog!T(initialData, onOK, onCancel, title); 14971 dg.show(); 14972 } 14973 14974 private static template I(T...) { alias I = T; } 14975 14976 14977 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 14978 if(name == "id") 14979 return allLowerCase ? name : "ID"; 14980 14981 char[160] buffer; 14982 int bufferIndex = 0; 14983 bool shouldCap = true; 14984 bool shouldSpace; 14985 bool lastWasCap; 14986 foreach(idx, char ch; name) { 14987 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 14988 14989 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 14990 if(lastWasCap) { 14991 // two caps in a row, don't change. Prolly acronym. 14992 } else { 14993 if(idx) 14994 shouldSpace = true; // new word, add space 14995 } 14996 14997 lastWasCap = true; 14998 } else { 14999 lastWasCap = false; 15000 } 15001 15002 if(shouldSpace) { 15003 buffer[bufferIndex++] = space; 15004 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15005 shouldSpace = false; 15006 } 15007 if(shouldCap) { 15008 if(ch >= 'a' && ch <= 'z') 15009 ch -= 32; 15010 shouldCap = false; 15011 } 15012 if(allLowerCase && ch >= 'A' && ch <= 'Z') 15013 ch += 32; 15014 buffer[bufferIndex++] = ch; 15015 } 15016 return buffer[0 .. bufferIndex].idup; 15017 } 15018 15019 /++ 15020 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. 15021 +/ 15022 class AutomaticDialog(T) : Dialog { 15023 T t; 15024 15025 void delegate(T) onOK; 15026 void delegate() onCancel; 15027 15028 override int paddingTop() { return defaultLineHeight; } 15029 override int paddingBottom() { return defaultLineHeight; } 15030 override int paddingRight() { return defaultLineHeight; } 15031 override int paddingLeft() { return defaultLineHeight; } 15032 15033 this(T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 15034 assert(onOK !is null); 15035 15036 t = initialData; 15037 15038 static if(is(T == class)) { 15039 if(t is null) 15040 t = new T(); 15041 } 15042 this.onOK = onOK; 15043 this.onCancel = onCancel; 15044 super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + 4 + 2) + Window.lineHeight + 56, title); 15045 15046 static if(is(T == class)) 15047 this.addDataControllerWidget(t); 15048 else 15049 this.addDataControllerWidget(&t); 15050 15051 auto hl = new HorizontalLayout(this); 15052 auto stretch = new HorizontalSpacer(hl); // to right align 15053 auto ok = new CommandButton("OK", hl); 15054 auto cancel = new CommandButton("Cancel", hl); 15055 ok.addEventListener(EventType.triggered, &OK); 15056 cancel.addEventListener(EventType.triggered, &Cancel); 15057 15058 this.addEventListener((KeyDownEvent ev) { 15059 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 15060 ok.focus(); 15061 OK(); 15062 ev.preventDefault(); 15063 } 15064 if(ev.key == Key.Escape) { 15065 Cancel(); 15066 ev.preventDefault(); 15067 } 15068 }); 15069 15070 this.addEventListener((scope ClosedEvent ce) { 15071 if(onCancel) 15072 onCancel(); 15073 }); 15074 15075 //this.children[0].focus(); 15076 } 15077 15078 override void OK() { 15079 onOK(t); 15080 close(); 15081 } 15082 15083 override void Cancel() { 15084 if(onCancel) 15085 onCancel(); 15086 close(); 15087 } 15088 } 15089 15090 private template baseClassCount(Class) { 15091 private int helper() { 15092 int count = 0; 15093 static if(is(Class bases == super)) { 15094 foreach(base; bases) 15095 static if(is(base == class)) 15096 count += 1 + baseClassCount!base; 15097 } 15098 return count; 15099 } 15100 15101 enum int baseClassCount = helper(); 15102 } 15103 15104 private long stringToLong(string s) { 15105 long ret; 15106 if(s.length == 0) 15107 return ret; 15108 bool negative = s[0] == '-'; 15109 if(negative) 15110 s = s[1 .. $]; 15111 foreach(ch; s) { 15112 if(ch >= '0' && ch <= '9') { 15113 ret *= 10; 15114 ret += ch - '0'; 15115 } 15116 } 15117 if(negative) 15118 ret = -ret; 15119 return ret; 15120 } 15121 15122 15123 interface ReflectableProperties { 15124 /++ 15125 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 15126 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 15127 json in the current implementation. 15128 15129 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 15130 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 15131 as of the June 2, 2021 release. 15132 15133 History: 15134 Added June 2, 2021. 15135 15136 See_Also: [getPropertyAsString], [setPropertyFromString] 15137 +/ 15138 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 15139 /++ 15140 Requests a property to be delivered to you as a string, through your `sink` delegate. 15141 15142 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 15143 be interpreted as json, otherwise, it is just a plain string. 15144 15145 The sink should always be called exactly once for each call (it is basically a return value, but it might 15146 use a local buffer it maintains instead of allocating a return value). 15147 15148 History: 15149 Added June 2, 2021. 15150 15151 See_Also: [getPropertiesList], [setPropertyFromString] 15152 +/ 15153 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 15154 /++ 15155 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. 15156 15157 History: 15158 Added June 2, 2021. 15159 15160 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 15161 +/ 15162 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 15163 15164 /// [setPropertyFromString] possible return values 15165 enum SetPropertyResult { 15166 success = 0, /// the property has been successfully set to the request value 15167 notPermitted = -1, /// the property exists but it cannot be changed at this time 15168 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 15169 noSuchProperty = -3, /// there is no property by that name 15170 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 15171 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) 15172 } 15173 15174 /++ 15175 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 15176 15177 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15178 15179 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15180 rarely need to use these building blocks directly. 15181 +/ 15182 mixin template RegisterSetters() { 15183 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 15184 switch(name) { 15185 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15186 case memberName: 15187 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15188 if(value != "true" && value != "false") 15189 return SetPropertyResult.wrongFormat; 15190 __traits(getMember, this, memberName) = value == "true" ? true : false; 15191 return SetPropertyResult.success; 15192 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15193 import core.stdc.stdlib; 15194 char[128] zero = 0; 15195 if(buffer.length + 1 >= zero.length) 15196 return SetPropertyResult.wrongFormat; 15197 zero[0 .. buffer.length] = buffer[]; 15198 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 15199 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15200 import core.stdc.stdlib; 15201 char[128] zero = 0; 15202 if(buffer.length + 1 >= zero.length) 15203 return SetPropertyResult.wrongFormat; 15204 zero[0 .. buffer.length] = buffer[]; 15205 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 15206 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15207 __traits(getMember, this, memberName) = value.idup; 15208 } else { 15209 return SetPropertyResult.notImplemented; 15210 } 15211 15212 } 15213 default: 15214 return super.setPropertyFromString(name, value, valueIsJson); 15215 } 15216 } 15217 } 15218 15219 /++ 15220 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 15221 15222 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15223 15224 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15225 rarely need to use these building blocks directly. 15226 +/ 15227 mixin template RegisterGetters() { 15228 override void getPropertiesList(scope void delegate(string name) sink) const { 15229 super.getPropertiesList(sink); 15230 15231 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15232 sink(memberName); 15233 } 15234 } 15235 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 15236 switch(name) { 15237 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15238 case memberName: 15239 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15240 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 15241 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15242 import core.stdc.stdio; 15243 char[32] buffer; 15244 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 15245 sink(name, buffer[0 .. len], true); 15246 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15247 import core.stdc.stdio; 15248 char[32] buffer; 15249 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 15250 sink(name, buffer[0 .. len], true); 15251 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15252 sink(name, __traits(getMember, this, memberName), false); 15253 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 15254 } else { 15255 sink(name, null, true); 15256 } 15257 15258 return; 15259 } 15260 default: 15261 return super.getPropertyAsString(name, sink); 15262 } 15263 } 15264 } 15265 } 15266 15267 private struct Stack(T) { 15268 this(int maxSize) { 15269 internalLength = 0; 15270 arr = initialBuffer[]; 15271 } 15272 15273 ///. 15274 void push(T t) { 15275 if(internalLength >= arr.length) { 15276 auto oldarr = arr; 15277 if(arr.length < 4096) 15278 arr = new T[arr.length * 2]; 15279 else 15280 arr = new T[arr.length + 4096]; 15281 arr[0 .. oldarr.length] = oldarr[]; 15282 } 15283 15284 arr[internalLength] = t; 15285 internalLength++; 15286 } 15287 15288 ///. 15289 T pop() { 15290 assert(internalLength); 15291 internalLength--; 15292 return arr[internalLength]; 15293 } 15294 15295 ///. 15296 T peek() { 15297 assert(internalLength); 15298 return arr[internalLength - 1]; 15299 } 15300 15301 ///. 15302 @property bool empty() { 15303 return internalLength ? false : true; 15304 } 15305 15306 ///. 15307 private T[] arr; 15308 private size_t internalLength; 15309 private T[64] initialBuffer; 15310 // the static array is allocated with this object, so if we have a small stack (which we prolly do; dom trees usually aren't insanely deep), 15311 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 15312 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 15313 } 15314 15315 /// This is the lazy range that walks the tree for you. It tries to go in the lexical order of the source: node, then children from first to last, each recursively. 15316 private struct WidgetStream { 15317 15318 ///. 15319 @property Widget front() { 15320 return current.widget; 15321 } 15322 15323 /// Use Widget.tree instead. 15324 this(Widget start) { 15325 current.widget = start; 15326 current.childPosition = -1; 15327 isEmpty = false; 15328 stack = typeof(stack)(0); 15329 } 15330 15331 /* 15332 Handle it 15333 handle its children 15334 15335 */ 15336 15337 ///. 15338 void popFront() { 15339 more: 15340 if(isEmpty) return; 15341 15342 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 15343 15344 current.childPosition++; 15345 if(current.childPosition >= current.widget.children.length) { 15346 if(stack.empty()) 15347 isEmpty = true; 15348 else { 15349 current = stack.pop(); 15350 goto more; 15351 } 15352 } else { 15353 stack.push(current); 15354 current.widget = current.widget.children[current.childPosition]; 15355 current.childPosition = -1; 15356 } 15357 } 15358 15359 ///. 15360 @property bool empty() { 15361 return isEmpty; 15362 } 15363 15364 private: 15365 15366 struct Current { 15367 Widget widget; 15368 int childPosition; 15369 } 15370 15371 Current current; 15372 15373 Stack!(Current) stack; 15374 15375 bool isEmpty; 15376 } 15377 15378 15379 /+ 15380 15381 I could fix up the hierarchy kinda like this 15382 15383 class Widget { 15384 Widget[] children() { return null; } 15385 } 15386 interface WidgetContainer { 15387 Widget asWidget(); 15388 void addChild(Widget w); 15389 15390 // alias asWidget this; // but meh 15391 } 15392 15393 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 15394 15395 class Layout : Widget, WidgetContainer {} 15396 15397 class Window : WidgetContainer {} 15398 15399 15400 All constructors that previously took Widgets should now take WidgetContainers instead 15401 15402 15403 15404 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". 15405 +/ 15406 15407 /+ 15408 LAYOUTS 2.0 15409 15410 can just be assigned as a function. assigning a new one will cause it to be immediately called. 15411 15412 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 15413 15414 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 15415 15416 and even Paint can just use computedStyle... 15417 15418 background color 15419 font 15420 border color and style 15421 15422 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 15423 please note that many widgets and in some modes will completely ignore properties as they will. 15424 they are just hints you set, not promises. 15425 15426 15427 15428 15429 15430 So generally the existing virtual functions are just the default for the class. But individual objects 15431 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 15432 +/ 15433 15434 /++ 15435 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. 15436 15437 History: 15438 Added May 24, 2021. 15439 +/ 15440 struct WidgetBackground { 15441 /++ 15442 A background with the given solid color. 15443 +/ 15444 this(Color color) { 15445 this.color = color; 15446 } 15447 15448 this(WidgetBackground bg) { 15449 this = bg; 15450 } 15451 15452 /++ 15453 Creates a widget from the string. 15454 15455 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 15456 +/ 15457 static WidgetBackground fromString(string s) { 15458 return WidgetBackground(Color.fromString(s)); 15459 } 15460 15461 /++ 15462 The background is not necessarily a solid color, but you can always specify a color as a fallback. 15463 15464 History: 15465 Made `public` on December 18, 2022 (dub v10.10). 15466 +/ 15467 Color color; 15468 } 15469 15470 /++ 15471 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!) 15472 15473 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. 15474 15475 You should not inherit from this directly, but instead use [VisualTheme]. 15476 15477 History: 15478 Added May 8, 2021 15479 +/ 15480 abstract class BaseVisualTheme { 15481 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 15482 abstract void doPaint(Widget widget, WidgetPainter painter); 15483 15484 /+ 15485 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 15486 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 15487 +/ 15488 15489 /++ 15490 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 15491 where the interpretation of the string varies for each property and may include things like measurement units. 15492 +/ 15493 abstract string getPropertyString(Widget widget, string propertyName); 15494 15495 /++ 15496 Default background color of the window. Widgets also use this to simulate transparency. 15497 15498 Probably some shade of grey. 15499 +/ 15500 abstract Color windowBackgroundColor(); 15501 abstract Color widgetBackgroundColor(); 15502 abstract Color foregroundColor(); 15503 abstract Color lightAccentColor(); 15504 abstract Color darkAccentColor(); 15505 15506 /++ 15507 Colors used to indicate active selections in lists and text boxes, etc. 15508 +/ 15509 abstract Color selectionForegroundColor(); 15510 /// ditto 15511 abstract Color selectionBackgroundColor(); 15512 15513 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 15514 15515 abstract OperatingSystemFont defaultFont(); 15516 15517 private OperatingSystemFont defaultFontCache_; 15518 private bool defaultFontCachePopulated; 15519 private OperatingSystemFont defaultFontCached() { 15520 if(!defaultFontCachePopulated) { 15521 // FIXME: set this to false if X disconnect or if visual theme changes 15522 defaultFontCache_ = defaultFont(); 15523 defaultFontCachePopulated = true; 15524 } 15525 return defaultFontCache_; 15526 } 15527 } 15528 15529 /+ 15530 A widget should have: 15531 classList 15532 dataset 15533 attributes 15534 computedStyles 15535 state (persistent) 15536 dynamic state (focused, hover, etc) 15537 +/ 15538 15539 // visualTheme.computedStyle(this).paddingLeft 15540 15541 15542 /++ 15543 This is your entry point to create your own visual theme for custom widgets. 15544 +/ 15545 abstract class VisualTheme(CRTP) : BaseVisualTheme { 15546 override string getPropertyString(Widget widget, string propertyName) { 15547 return null; 15548 } 15549 15550 /+ 15551 mixin StyleOverride!Widget 15552 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 15553 w.useStyleProperties(dg); 15554 } 15555 +/ 15556 15557 final override void doPaint(Widget widget, WidgetPainter painter) { 15558 auto derived = cast(CRTP) cast(void*) this; 15559 15560 scope void delegate(Widget, WidgetPainter) bestMatch; 15561 int bestMatchScore; 15562 15563 static if(__traits(hasMember, CRTP, "paint")) 15564 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 15565 static if(is(typeof(overload) Params == __parameters)) { 15566 static assert(Params.length == 2); 15567 static assert(is(Params[0] : Widget)); 15568 static assert(is(Params[1] == WidgetPainter)); 15569 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); 15570 15571 alias type = Params[0]; 15572 if(cast(type) widget) { 15573 auto score = baseClassCount!type; 15574 15575 if(score > bestMatchScore) { 15576 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 15577 bestMatchScore = score; 15578 } 15579 } 15580 } else static assert(0, "paint should be a method."); 15581 } 15582 15583 if(bestMatch) 15584 bestMatch(widget, painter); 15585 else 15586 widget.paint(painter); 15587 } 15588 15589 // 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 15590 override Color windowBackgroundColor() { return Color(212, 212, 212); } 15591 override Color widgetBackgroundColor() { return Color.white; } 15592 override Color foregroundColor() { return Color.black; } 15593 override Color darkAccentColor() { return Color(172, 172, 172); } 15594 override Color lightAccentColor() { return Color(223, 223, 223); } 15595 override Color selectionForegroundColor() { return Color.white; } 15596 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 15597 override OperatingSystemFont defaultFont() { return null; } // will just use the default out of simpledisplay's xfontstr 15598 15599 private static struct Cached { 15600 // i prolly want to do this 15601 } 15602 } 15603 15604 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 15605 /+ 15606 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 15607 Color windowBackgroundColor() { return Color(242, 242, 242); } 15608 Color darkAccentColor() { return windowBackgroundColor; } 15609 Color lightAccentColor() { return windowBackgroundColor; } 15610 +/ 15611 } 15612 15613 /++ 15614 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 15615 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 15616 15617 History: 15618 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 15619 +/ 15620 class StateChanged(alias field) : Event { 15621 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 15622 override bool cancelable() const { return false; } 15623 this(Widget target, typeof(field) newValue) { 15624 this.newValue = newValue; 15625 super(EventString, target); 15626 } 15627 15628 typeof(field) newValue; 15629 } 15630 15631 /++ 15632 Convenience function to add a `triggered` event listener. 15633 15634 Its implementation is simply `w.addEventListener("triggered", dg);` 15635 15636 History: 15637 Added November 27, 2021 (dub v10.4) 15638 +/ 15639 void addWhenTriggered(Widget w, void delegate() dg) { 15640 w.addEventListener("triggered", dg); 15641 } 15642 15643 /++ 15644 Observable varables can be added to widgets and when they are changed, it fires 15645 off a [StateChanged] event so you can react to it. 15646 15647 It is implemented as a getter and setter property, along with another helper you 15648 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 15649 event through the usual means. Just give the name of the variable. See [StateChanged] for an 15650 example. 15651 15652 History: 15653 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 15654 +/ 15655 mixin template Observable(T, string name) { 15656 private T backing; 15657 15658 mixin(q{ 15659 void } ~ name ~ q{_changed (void delegate(T) dg) { 15660 this.addEventListener((StateChanged!this_thing ev) { 15661 dg(ev.newValue); 15662 }); 15663 } 15664 15665 @property T } ~ name ~ q{ () { 15666 return backing; 15667 } 15668 15669 @property void } ~ name ~ q{ (T t) { 15670 backing = t; 15671 auto event = new StateChanged!this_thing(this, t); 15672 event.dispatch(); 15673 } 15674 }); 15675 15676 mixin("private alias this_thing = " ~ name ~ ";"); 15677 } 15678 15679 15680 private bool startsWith(string test, string thing) { 15681 if(test.length < thing.length) 15682 return false; 15683 return test[0 .. thing.length] == thing; 15684 } 15685 15686 private bool endsWith(string test, string thing) { 15687 if(test.length < thing.length) 15688 return false; 15689 return test[$ - thing.length .. $] == thing; 15690 } 15691 15692 // still do layout delegation 15693 // and... split off Window from Widget. 15694 15695 version(minigui_screenshots) 15696 struct Screenshot { 15697 string name; 15698 } 15699 15700 version(minigui_screenshots) 15701 static if(__VERSION__ > 2092) 15702 mixin(q{ 15703 shared static this() { 15704 import core.runtime; 15705 15706 static UnitTestResult screenshotMagic() { 15707 string name; 15708 15709 import arsd.png; 15710 15711 auto results = new Window(); 15712 auto button = new Button("do it", results); 15713 15714 Window.newWindowCreated = delegate(Window w) { 15715 Timer timer; 15716 timer = new Timer(250, { 15717 auto img = w.win.takeScreenshot(); 15718 timer.destroy(); 15719 15720 version(Windows) 15721 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 15722 else 15723 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 15724 15725 w.close(); 15726 }); 15727 }; 15728 15729 button.addWhenTriggered( { 15730 15731 foreach(test; __traits(getUnitTests, mixin(__MODULE__))) { 15732 name = null; 15733 static foreach(attr; __traits(getAttributes, test)) { 15734 static if(is(typeof(attr) == Screenshot)) 15735 name = attr.name; 15736 } 15737 if(name.length) { 15738 test(); 15739 } 15740 } 15741 15742 }); 15743 15744 results.loop(); 15745 15746 return UnitTestResult(0, 0, false, false); 15747 } 15748 15749 15750 Runtime.extendedModuleUnitTester = &screenshotMagic; 15751 } 15752 }); 15753 version(minigui_screenshots) { 15754 version(unittest) 15755 void main() {} 15756 else static assert(0, "dont forget the -unittest flag to dmd"); 15757 } 15758 15759 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 15760 // FIXME: make multiple accelerators disambiguate based ona rgs 15761 // FIXME: MainWindow ctor should have same arg order as Window 15762 // FIXME: mainwindow ctor w/ client area size instead of total size. 15763 // Push on/off button (basically an alternate display of a checkbox) -- BS_PUSHLIKE and maybe BS_TEXT (BS_TOP moves it). see also BS_FLAT. 15764 // FIXME: tri-state checkbox 15765 // FIXME: subordinate controls grouping...