1 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx 2 3 // if doing nested menus, make sure the straight line from where it pops up to any destination on the new popup is not going to disappear the menu until at least a delay 4 5 // me@arsd:~/.kde/share/config$ vim kdeglobals 6 7 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too. 8 9 // https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/ 10 11 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever. 12 13 // responsive minigui, menu search, and file open with a preview hook on the side. 14 15 // FIXME: add menu checkbox and menu icon eventually 16 17 /* 18 19 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 20 21 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 22 */ 23 24 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places 25 26 // FIXME: text label must be copyable to the clipboard, at least as a full chunk. 27 28 // FIXME: opt-in file picker widget with image support 29 30 // FIXME: number widget 31 32 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm 33 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles 34 35 // osx style menu search. 36 37 // would be cool for a scroll bar to have marking capabilities 38 // kinda like vim's marks just on clicks etc and visual representation 39 // generically. may be cool to add an up arrow to the bottom too 40 // 41 // leave a shadow of where you last were for going back easily 42 43 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 44 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 45 // the window. 46 47 // so what about context menus? 48 49 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 50 51 // FIXME: make the scroll thing go to bottom when the content changes. 52 53 // 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 54 55 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 56 57 58 // FIXME: add a command search thingy built in and implement tip. 59 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 60 61 // On Windows: 62 // FIXME: various labels look broken in high contrast mode 63 // FIXME: changing themes while the program is upen doesn't trigger a redraw 64 65 // add note about manifest to documentation. also icons. 66 67 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 68 // FIXME: clear the corner of scrollbars if they pop up 69 70 // minigui needs to have a stdout redirection for gui mode on windows writeln 71 72 // I kinda wanna do state reacting. sort of. idk tho 73 74 // need a viewer widget that works like a web page - arrows scroll down consistently 75 76 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 77 78 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 79 // and help info about menu items. 80 // and search in menus? 81 82 // FIXME: a scroll area event signaling when a thing comes into view might be good 83 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 84 85 // FIXME: unify Windows style line endings 86 87 /* 88 TODO: 89 90 pie menu 91 92 class Form with submit behavior -- see AutomaticDialog 93 94 disabled widgets and menu items 95 96 event cleanup 97 tooltips. 98 api improvements 99 100 margins are kinda broken, they don't collapse like they should. at least. 101 102 a table form btw would be a horizontal layout of vertical layouts holding each column 103 that would give the same width things 104 */ 105 106 /* 107 108 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 109 */ 110 111 /++ 112 minigui is a smallish GUI widget library, aiming to be on par with at least 113 HTML4 forms and a few other expected gui components. It uses native controls 114 on Windows and does its own thing on Linux (Mac is not currently supported but 115 may be later, and should use native controls) to keep size down. The Linux 116 appearance is similar to Windows 95 and avoids using images to maintain network 117 efficiency on remote X connections, though you can customize that. 118 119 120 minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color], 121 on which it is built. simpledisplay provides the low-level interfaces and minigui 122 builds the concept of widgets inside the windows on top of it. 123 124 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 125 It isn't hugely concerned with appearance - on Windows, it just uses the native 126 controls and native theme, and on Linux, it keeps it simple and I may change that 127 at any time, though after May 2021, you can customize some things with css-inspired 128 [Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`, 129 you can use the custom implementation there too, but... you shouldn't.) 130 131 The event model is similar to what you use in the browser with Javascript and the 132 layout engine tries to automatically fit things in, similar to a css flexbox. 133 134 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 135 `-L/SUBSYSTEM:WINDOWS:5.0`, for example, because otherwise you'll get a 136 console and other visual bugs. 137 138 HTML_To_Classes: 139 $(SMALL_TABLE 140 HTML Code | Minigui Class 141 142 `<input type="text">` | [LineEdit] 143 `<textarea>` | [TextEdit] 144 `<select>` | [DropDownSelection] 145 `<input type="checkbox">` | [Checkbox] 146 `<input type="radio">` | [Radiobox] 147 `<button>` | [Button] 148 ) 149 150 151 Stretchiness: 152 The default is 4. You can use larger numbers for things that should 153 consume a lot of space, and lower numbers for ones that are better at 154 smaller sizes. 155 156 Overlapped_input: 157 COMING EVENTUALLY: 158 minigui will include a little bit of I/O functionality that just works 159 with the event loop. If you want to get fancy, I suggest spinning up 160 another thread and posting events back and forth. 161 162 $(H2 Add ons) 163 See the `minigui_addons` directory in the arsd repo for some add on widgets 164 you can import separately too. 165 166 $(H3 XML definitions) 167 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 168 169 $(H3 Scriptability) 170 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 171 in this documentation, it means you can call it from the script language. 172 173 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 174 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 175 176 --- 177 import arsd.minigui_xml; 178 import arsd.script; 179 180 var globals = var.emptyObject; 181 globals.makeWidgetFromString = &makeWidgetFromString; 182 183 // this now works 184 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 185 --- 186 187 More to come. 188 189 History: 190 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 191 192 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 193 tag this as version 2.0. 194 195 Among the changes: 196 $(LIST 197 * 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. 198 199 See [Event] for details. 200 201 * 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. 202 203 See [DoubleClickEvent] for details. 204 205 * 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. 206 207 See [Widget.Style] for details. 208 209 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 210 211 * 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. 212 213 * 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. 214 215 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 216 217 * 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. 218 219 * Various non-breaking additions. 220 ) 221 +/ 222 module arsd.minigui; 223 224 import arsd.core; 225 226 /++ 227 This hello world sample will have an oversized button, but that's ok, you see your first window! 228 +/ 229 version(Demo) 230 unittest { 231 import arsd.minigui; 232 233 void main() { 234 auto window = new MainWindow(); 235 236 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 237 auto button = new Button("Close", window); 238 button.addWhenTriggered({ 239 window.close(); 240 }); 241 242 window.loop(); 243 } 244 245 main(); // exclude from docs 246 } 247 248 /++ 249 This example shows one way you can partition your window into a header 250 and sidebar. Here, the header and sidebar have a fixed width, while the 251 rest of the content sizes with the window. 252 253 It might be a new way of thinking about window layout to do things this 254 way - perhaps [GridLayout] more matches your style of thought - but the 255 concept here is to partition the window into sub-boxes with a particular 256 size, then partition those boxes into further boxes. 257 258 $(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.) 259 260 So to make the header, start with a child layout that has a max height. 261 It will use that space from the top, then the remaining children will 262 split the remaining area, meaning you can think of is as just being another 263 box you can split again. Keep splitting until you have the look you desire. 264 +/ 265 // https://github.com/adamdruppe/arsd/issues/310 266 version(minigui_screenshots) 267 @Screenshot("layout") 268 unittest { 269 import arsd.minigui; 270 271 // This helper class is just to help make the layout boxes visible. 272 // think of it like a <div style="background-color: whatever;"></div> in HTML. 273 class ColorWidget : Widget { 274 this(Color color, Widget parent) { 275 this.color = color; 276 super(parent); 277 } 278 Color color; 279 class Style : Widget.Style { 280 override WidgetBackground background() { return WidgetBackground(color); } 281 } 282 mixin OverrideStyle!Style; 283 } 284 285 void main() { 286 auto window = new Window; 287 288 // the key is to give it a max height. This is one way to do it: 289 auto header = new class HorizontalLayout { 290 this() { super(window); } 291 override int maxHeight() { return 50; } 292 }; 293 // this next line is a shortcut way of doing it too, but it only works 294 // for HorizontalLayout and VerticalLayout, and is less explicit, so it 295 // is good to know how to make a new class like above anyway. 296 // auto header = new HorizontalLayout(50, window); 297 298 auto bar = new HorizontalLayout(window); 299 300 // or since this is so common, VerticalLayout and HorizontalLayout both 301 // can just take an argument in their constructor for max width/height respectively 302 303 // (could have tone this above too, but I wanted to demo both techniques) 304 auto left = new VerticalLayout(100, bar); 305 306 // and this is the main section's container. A plain Widget instance is good enough here. 307 auto container = new Widget(bar); 308 309 // and these just add color to the containers we made above for the screenshot. 310 // in a real application, you can just add your actual controls instead of these. 311 auto headerColorBox = new ColorWidget(Color.teal, header); 312 auto leftColorBox = new ColorWidget(Color.green, left); 313 auto rightColorBox = new ColorWidget(Color.purple, container); 314 315 window.loop(); 316 } 317 318 main(); // exclude from docs 319 } 320 321 322 public import arsd.simpledisplay; 323 /++ 324 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 325 326 History: 327 Was private until May 15, 2021. 328 +/ 329 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 330 331 version(Windows) { 332 import core.sys.windows.winnls; 333 import core.sys.windows.windef; 334 import core.sys.windows.basetyps; 335 import core.sys.windows.winbase; 336 import core.sys.windows.winuser; 337 import core.sys.windows.wingdi; 338 static import gdi = core.sys.windows.wingdi; 339 } 340 341 version(Windows) { 342 version(minigui_manifest) {} else version=minigui_no_manifest; 343 344 version(minigui_no_manifest) {} else 345 static if(__VERSION__ >= 2_083) 346 version(CRuntime_Microsoft) { // FIXME: mingw? 347 // assume we want commctrl6 whenever possible since there's really no reason not to 348 // and this avoids some of the manifest hassle 349 pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""); 350 } 351 } 352 353 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 354 private bool lastDefaultPrevented; 355 356 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 357 alias scriptable = arsd_jsvar_compatible; 358 359 version(Windows) { 360 // use native widgets when available unless specifically asked otherwise 361 version(custom_widgets) { 362 enum bool UsingCustomWidgets = true; 363 enum bool UsingWin32Widgets = false; 364 } else { 365 version = win32_widgets; 366 enum bool UsingCustomWidgets = false; 367 enum bool UsingWin32Widgets = true; 368 } 369 // and native theming when needed 370 //version = win32_theming; 371 } else { 372 enum bool UsingCustomWidgets = true; 373 enum bool UsingWin32Widgets = false; 374 version=custom_widgets; 375 } 376 377 378 379 /* 380 381 The main goals of minigui.d are to: 382 1) Provide basic widgets that just work in a lightweight lib. 383 I basically want things comparable to a plain HTML form, 384 plus the easy and obvious things you expect from Windows 385 apps like a menu. 386 2) Use native things when possible for best functionality with 387 least library weight. 388 3) Give building blocks to provide easy extension for your 389 custom widgets, or hooking into additional native widgets 390 I didn't wrap. 391 4) Provide interfaces for easy interaction between third 392 party minigui extensions. (event model, perhaps 393 signals/slots, drop-in ease of use bits.) 394 5) Zero non-system dependencies, including Phobos as much as 395 I reasonably can. It must only import arsd.color and 396 my simpledisplay.d. If you need more, it will have to be 397 an extension module. 398 6) An easy layout system that generally works. 399 400 A stretch goal is to make it easy to make gui forms with code, 401 some kind of resource file (xml?) and even a wysiwyg designer. 402 403 Another stretch goal is to make it easy to hook data into the gui, 404 including from reflection. So like auto-generate a form from a 405 function signature or struct definition, or show a list from an 406 array that automatically updates as the array is changed. Then, 407 your program focuses on the data more than the gui interaction. 408 409 410 411 STILL NEEDED: 412 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 413 * slider 414 * listbox 415 * spinner 416 * label? 417 * rich text 418 */ 419 420 421 /+ 422 enum LayoutMethods { 423 verticalFlex, 424 horizontalFlex, 425 inlineBlock, // left to right, no stretch, goes to next line as needed 426 static, // just set to x, y 427 verticalNoStretch, // browser style default 428 429 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 430 431 grid, // magic 432 } 433 +/ 434 435 /++ 436 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. 437 438 439 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. 440 441 --- 442 class MinimalWidget : Widget { 443 this(Widget parent) { 444 super(parent); 445 } 446 } 447 --- 448 449 $(SIDEBAR 450 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. 451 ) 452 453 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. 454 455 Among the things you'll most likely want to change in your custom widget: 456 457 $(LIST 458 * 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.) 459 460 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. 461 462 Do this $(I after) calling the `super` constructor. 463 464 * 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. 465 466 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 467 468 * 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. 469 470 * 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. 471 ) 472 473 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. 474 475 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 476 477 Your own custom-drawn and native system controls can exist side-by-side. 478 479 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. 480 +/ 481 class Widget : ReflectableProperties { 482 483 private bool willDraw() { 484 return true; 485 } 486 487 /+ 488 /++ 489 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 490 491 History: 492 Added September 15, 2021 493 implemented.... ??? 494 +/ 495 void prepareReflection(this This)() { 496 497 } 498 +/ 499 500 private bool _enabled = true; 501 502 /++ 503 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. 504 505 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. 506 507 History: 508 Added November 23, 2021 (dub v10.4) 509 510 Warning: the specific behavior of disabling with parents may change in the future. 511 Bugs: 512 Currently only implemented for widgets backed by native Windows controls. 513 514 See_Also: [disabledReason], [disabledBy] 515 +/ 516 @property bool enabled() { 517 return disabledBy() is null; 518 } 519 520 /// ditto 521 @property void enabled(bool yes) { 522 _enabled = yes; 523 version(win32_widgets) { 524 if(hwnd) 525 EnableWindow(hwnd, yes); 526 } 527 setDynamicState(DynamicState.disabled, yes); 528 } 529 530 private string disabledReason_; 531 532 /++ 533 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. 534 535 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 536 537 History: 538 Added November 23, 2021 (dub v10.4) 539 See_Also: [enabled], [disabledBy] 540 +/ 541 @property string disabledReason() { 542 auto w = disabledBy(); 543 return (w is null) ? null : w.disabledReason_; 544 } 545 546 /// ditto 547 @property void disabledReason(string reason) { 548 disabledReason_ = reason; 549 } 550 551 /++ 552 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. 553 554 History: 555 Added November 25, 2021 (dub v10.4) 556 See_Also: [enabled], [disabledReason] 557 +/ 558 Widget disabledBy() { 559 Widget p = this; 560 while(p) { 561 if(!p._enabled) 562 return p; 563 p = p.parent; 564 } 565 return null; 566 } 567 568 /// Implementations of [ReflectableProperties] interface. See the interface for details. 569 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 570 if(valueIsJson) 571 return SetPropertyResult.wrongFormat; 572 switch(name) { 573 case "name": 574 this.name = value.idup; 575 return SetPropertyResult.success; 576 case "statusTip": 577 this.statusTip = value.idup; 578 return SetPropertyResult.success; 579 default: 580 return SetPropertyResult.noSuchProperty; 581 } 582 } 583 /// ditto 584 void getPropertiesList(scope void delegate(string name) sink) const { 585 sink("name"); 586 sink("statusTip"); 587 } 588 /// ditto 589 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 590 switch(name) { 591 case "name": 592 sink(name, this.name, false); 593 return; 594 case "statusTip": 595 sink(name, this.statusTip, false); 596 return; 597 default: 598 sink(name, null, true); 599 } 600 } 601 602 /++ 603 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 604 605 History: 606 Added November 25, 2021 (dub v10.5) 607 `Point` overload added January 12, 2022 (dub v10.6) 608 +/ 609 int scaleWithDpi(int value, int assumedDpi = 96) { 610 // avoid potential overflow with common special values 611 if(value == int.max) 612 return int.max; 613 if(value == int.min) 614 return int.min; 615 if(value == 0) 616 return 0; 617 return value * currentDpi(assumedDpi) / assumedDpi; 618 } 619 620 /// ditto 621 Point scaleWithDpi(Point value, int assumedDpi = 96) { 622 return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi)); 623 } 624 625 /++ 626 Returns the current scaling factor as a logical dpi value for this widget. Generally speaking, this divided by 96 gives you the user scaling factor. 627 628 Not entirely stable. 629 630 History: 631 Added August 25, 2023 (dub v11.1) 632 +/ 633 final int currentDpi(int assumedDpi = 96) { 634 // assert(parentWindow !is null); 635 // assert(parentWindow.win !is null); 636 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 637 //divide = 138; // to test 1.5x 638 // 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. 639 // this also covers the case when actualDpi returns 0. 640 if(divide < 96) 641 divide = 96; 642 return divide; 643 } 644 645 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 646 // I'll think up something better eventually 647 648 // FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight. 649 protected final int defaultLineHeight() { 650 auto cs = getComputedStyle(); 651 if(cs.font && !cs.font.isNull) 652 return cs.font.height() * 5 / 4; 653 else 654 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4); 655 } 656 657 /++ 658 659 History: 660 Added August 25, 2023 (dub v11.1) 661 +/ 662 protected final int defaultTextHeight(int numberOfLines = 1) { 663 auto cs = getComputedStyle(); 664 if(cs.font && !cs.font.isNull) 665 return cs.font.height() * numberOfLines; 666 else 667 return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines; 668 } 669 670 protected final int defaultTextWidth(const(char)[] text) { 671 auto cs = getComputedStyle(); 672 if(cs.font && !cs.font.isNull) 673 return cs.font.stringWidth(text); 674 else 675 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2); 676 } 677 678 /++ 679 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. 680 681 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. 682 683 History: 684 Added May 22, 2021 685 +/ 686 protected bool encapsulatedChildren() { 687 return false; 688 } 689 690 private void privateDpiChanged() { 691 dpiChanged(); 692 foreach(child; children) 693 child.privateDpiChanged(); 694 } 695 696 /++ 697 Virtual hook to update any caches or fonts you need on the event of a dpi scaling change. 698 699 History: 700 Added January 12, 2022 (dub v10.6) 701 +/ 702 protected void dpiChanged() { 703 704 } 705 706 // Default layout properties { 707 708 int minWidth() { return 0; } 709 int minHeight() { 710 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 711 int sum = this.paddingTop + this.paddingBottom; 712 foreach(child; children) { 713 if(child.hidden) 714 continue; 715 sum += child.minHeight(); 716 sum += child.marginTop(); 717 sum += child.marginBottom(); 718 } 719 720 return sum; 721 } 722 int maxWidth() { return int.max; } 723 int maxHeight() { return int.max; } 724 int widthStretchiness() { return 4; } 725 int heightStretchiness() { return 4; } 726 727 /++ 728 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 729 730 History: 731 Added June 15, 2021 (dub v10.1) 732 +/ 733 int widthShrinkiness() { return 0; } 734 /// ditto 735 int heightShrinkiness() { return 0; } 736 737 /++ 738 The initial size of the widget for layout calculations. Default is 0. 739 740 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 741 742 History: 743 Added June 15, 2021 (dub v10.1) 744 +/ 745 int flexBasisWidth() { return 0; } 746 /// ditto 747 int flexBasisHeight() { return 0; } 748 749 /++ 750 Not stable. 751 752 Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored. 753 754 So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6. 755 756 History: 757 Added January 5, 2023 758 +/ 759 Rectangle defaultMargin; 760 /// ditto 761 Rectangle defaultPadding; 762 763 int marginLeft() { return scaleWithDpi(defaultMargin.left); } 764 int marginRight() { return scaleWithDpi(defaultMargin.right); } 765 int marginTop() { return scaleWithDpi(defaultMargin.top); } 766 int marginBottom() { return scaleWithDpi(defaultMargin.bottom); } 767 int paddingLeft() { return scaleWithDpi(defaultPadding.left); } 768 int paddingRight() { return scaleWithDpi(defaultPadding.right); } 769 int paddingTop() { return scaleWithDpi(defaultPadding.top); } 770 int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); } 771 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 772 773 private bool recomputeChildLayoutRequired = true; 774 private static class RecomputeEvent {} 775 private __gshared rce = new RecomputeEvent(); 776 protected final void queueRecomputeChildLayout() { 777 recomputeChildLayoutRequired = true; 778 779 if(this.parentWindow) { 780 auto sw = this.parentWindow.win; 781 assert(sw !is null); 782 if(!sw.eventQueued!RecomputeEvent) { 783 sw.postEvent(rce); 784 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 785 } 786 } 787 788 } 789 790 protected final void recomputeChildLayoutEntry() { 791 if(recomputeChildLayoutRequired) { 792 recomputeChildLayout(); 793 recomputeChildLayoutRequired = false; 794 redraw(); 795 } else { 796 // I still need to check the tree just in case one of them was queued up 797 // and the event came up here instead of there. 798 foreach(child; children) 799 child.recomputeChildLayoutEntry(); 800 } 801 } 802 803 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 804 void recomputeChildLayout() { 805 .recomputeChildLayout!"height"(this); 806 } 807 808 // } 809 810 811 /++ 812 Returns the style's tag name string this object uses. 813 814 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. 815 816 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 817 818 History: 819 Added May 10, 2021 820 +/ 821 string styleTagName() const { 822 string n = typeid(this).name; 823 foreach_reverse(idx, ch; n) 824 if(ch == '.') { 825 n = n[idx + 1 .. $]; 826 break; 827 } 828 return n; 829 } 830 831 /// API for the [styleClassList] 832 static struct ClassList { 833 private Widget widget; 834 835 /// 836 void add(string s) { 837 widget.styleClassList_ ~= s; 838 } 839 840 /// 841 void remove(string s) { 842 foreach(idx, s1; widget.styleClassList_) 843 if(s1 == s) { 844 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 845 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 846 widget.styleClassList_.assumeSafeAppend(); 847 return; 848 } 849 } 850 851 /// Returns true if it was added, false if it was removed. 852 bool toggle(string s) { 853 if(contains(s)) { 854 remove(s); 855 return false; 856 } else { 857 add(s); 858 return true; 859 } 860 } 861 862 /// 863 bool contains(string s) const { 864 foreach(s1; widget.styleClassList_) 865 if(s1 == s) 866 return true; 867 return false; 868 869 } 870 } 871 872 private string[] styleClassList_; 873 874 /++ 875 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. 876 877 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 878 879 History: 880 Added May 10, 2021 881 +/ 882 inout(ClassList) styleClassList() inout { 883 return cast(inout(ClassList)) ClassList(cast() this); 884 } 885 886 /++ 887 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. 888 889 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. 890 891 The upper 32 bits are available for your own extensions. 892 893 History: 894 Added May 10, 2021 895 +/ 896 enum DynamicState : ulong { 897 focus = (1 << 0), /// the widget currently has the keyboard focus 898 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 899 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!) 900 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!) 901 checked = (1 << 4), /// the widget is toggleable and currently toggled on 902 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 903 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 904 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 905 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. 906 907 USER_BEGIN = (1UL << 32), 908 } 909 910 // I want to add the primary and cancel styles to buttons at least at some point somehow. 911 912 /// ditto 913 @property ulong dynamicState() { return dynamicState_; } 914 /// ditto 915 @property ulong dynamicState(ulong newValue) { 916 if(dynamicState != newValue) { 917 auto old = dynamicState_; 918 dynamicState_ = newValue; 919 920 useStyleProperties((scope Widget.Style s) { 921 if(s.variesWithState(old ^ newValue)) 922 redraw(); 923 }); 924 } 925 return dynamicState_; 926 } 927 928 /// ditto 929 void setDynamicState(ulong flags, bool state) { 930 auto ds = dynamicState_; 931 if(state) 932 ds |= flags; 933 else 934 ds &= ~flags; 935 936 dynamicState = ds; 937 } 938 939 private ulong dynamicState_; 940 941 deprecated("Use dynamic styles instead now") { 942 Color backgroundColor() { return backgroundColor_; } 943 void backgroundColor(Color c){ this.backgroundColor_ = c; } 944 945 MouseCursor cursor() { return GenericCursor.Default; } 946 } private Color backgroundColor_ = Color.transparent; 947 948 949 /++ 950 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). 951 952 It is here so there can be a specificity switch. 953 954 See [OverrideStyle] for a helper function to use your own. 955 956 History: 957 Added May 11, 2021 958 +/ 959 static class Style/* : StyleProperties*/ { 960 public Widget widget; // public because the mixin template needs access to it 961 962 /++ 963 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 964 965 History: 966 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. 967 +/ 968 bool variesWithState(ulong dynamicStateFlags) { 969 version(win32_widgets) { 970 if(widget.hwnd) 971 return false; 972 } 973 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 974 } 975 976 /// 977 Color foregroundColor() { 978 return WidgetPainter.visualTheme.foregroundColor; 979 } 980 981 /// 982 WidgetBackground background() { 983 // the default is a "transparent" background, which means 984 // it goes as far up as it can to get the color 985 if (widget.backgroundColor_ != Color.transparent) 986 return WidgetBackground(widget.backgroundColor_); 987 if (widget.parent) 988 return widget.parent.getComputedStyle.background; 989 return WidgetBackground(widget.backgroundColor_); 990 } 991 992 private static OperatingSystemFont fontCached_; 993 private OperatingSystemFont fontCached() { 994 if(fontCached_ is null) 995 fontCached_ = font(); 996 return fontCached_; 997 } 998 999 /++ 1000 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. 1001 +/ 1002 OperatingSystemFont font() { 1003 return null; 1004 } 1005 1006 /++ 1007 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. 1008 1009 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 1010 1011 History: 1012 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 1013 +/ 1014 MouseCursor cursor() { 1015 return GenericCursor.Default; 1016 } 1017 1018 FrameStyle borderStyle() { 1019 return FrameStyle.none; 1020 } 1021 1022 /++ 1023 +/ 1024 Color borderColor() { 1025 return Color.transparent; 1026 } 1027 1028 FrameStyle outlineStyle() { 1029 if(widget.dynamicState & DynamicState.focus) 1030 return FrameStyle.dotted; 1031 else 1032 return FrameStyle.none; 1033 } 1034 1035 Color outlineColor() { 1036 return foregroundColor; 1037 } 1038 } 1039 1040 /++ 1041 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 1042 The basic usage is simple: 1043 1044 --- 1045 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 1046 // override style hints as-needed here 1047 } 1048 OverrideStyle!Style; // add the method 1049 --- 1050 1051 $(TIP 1052 While the class is not forced to be `static`, for best results, it should be. A non-static class 1053 can not be inherited by other objects whereas the static one can. A property on the base class, 1054 called [Widget.Style.widget|widget], is available for you to access its properties. 1055 ) 1056 1057 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 1058 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 1059 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 1060 1061 1062 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 1063 You may also just override `variesWithState` when you use this flag. 1064 1065 --- 1066 mixin OverrideStyle!( 1067 DynamicState.focus, YourFocusedStyle, 1068 DynamicState.hover, YourHoverStyle, 1069 YourDefaultStyle 1070 ) 1071 --- 1072 1073 It checks if `dynamicState` matches the state and if so, returns the object given. 1074 1075 If there is no state mask given, the next one matches everything. The first match given is used. 1076 1077 However, since in most cases you'll want check state inside your individual methods, you probably won't 1078 find much use for this whole-class swap out. 1079 1080 History: 1081 Added May 16, 2021 1082 +/ 1083 static protected mixin template OverrideStyle(S...) { 1084 static import amg = arsd.minigui; 1085 override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) { 1086 ulong mask = 0; 1087 foreach(idx, thing; S) { 1088 static if(is(typeof(thing) : ulong)) { 1089 mask = thing; 1090 } else { 1091 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 1092 //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."); 1093 scope amg.Widget.Style s = new thing(); 1094 s.widget = this; 1095 dg(s); 1096 return; 1097 } 1098 } 1099 } 1100 } 1101 } 1102 /++ 1103 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 1104 +/ 1105 void useStyleProperties(scope void delegate(scope Style props) dg) { 1106 scope Style s = new Style(); 1107 s.widget = this; 1108 dg(s); 1109 } 1110 1111 1112 protected void sendResizeEvent() { 1113 this.emit!ResizeEvent(); 1114 } 1115 1116 Menu contextMenu(int x, int y) { return null; } 1117 1118 final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) { 1119 if(parentWindow is null || parentWindow.win is null) return false; 1120 1121 auto menu = this.contextMenu(x, y); 1122 if(menu is null) 1123 return false; 1124 1125 version(win32_widgets) { 1126 // FIXME: if it is -1, -1, do it at the current selection location instead 1127 // tho the corner of the window, whcih it does now, isn't the literal worst. 1128 1129 if(screenX < 0 && screenY < 0) { 1130 auto p = this.globalCoordinates(); 1131 if(screenX == -2) 1132 p.x += x; 1133 if(screenY == -2) 1134 p.y += y; 1135 1136 screenX = p.x; 1137 screenY = p.y; 1138 } 1139 1140 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1141 throw new Exception("TrackContextMenuEx"); 1142 } else version(custom_widgets) { 1143 menu.popup(this, x, y); 1144 } 1145 1146 return true; 1147 } 1148 1149 /++ 1150 Removes this widget from its parent. 1151 1152 History: 1153 `removeWidget` was made `final` on May 11, 2021. 1154 +/ 1155 @scriptable 1156 final void removeWidget() { 1157 auto p = this.parent; 1158 if(p) { 1159 int item; 1160 for(item = 0; item < p._children.length; item++) 1161 if(p._children[item] is this) 1162 break; 1163 auto idx = item; 1164 for(; item < p._children.length - 1; item++) 1165 p._children[item] = p._children[item + 1]; 1166 p._children = p._children[0 .. $-1]; 1167 1168 this.parent.widgetRemoved(idx, this); 1169 //this.parent = null; 1170 1171 p.queueRecomputeChildLayout(); 1172 } 1173 version(win32_widgets) { 1174 removeAllChildren(); 1175 if(hwnd) { 1176 DestroyWindow(hwnd); 1177 hwnd = null; 1178 } 1179 } 1180 } 1181 1182 /++ 1183 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. 1184 1185 History: 1186 Added September 19, 2021 1187 +/ 1188 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 1189 1190 /++ 1191 Removes all child widgets from `this`. You should not use the removed widgets again. 1192 1193 Note that on Windows, it also destroys the native handles for the removed children recursively. 1194 1195 History: 1196 Added July 1, 2021 (dub v10.2) 1197 +/ 1198 void removeAllChildren() { 1199 version(win32_widgets) 1200 foreach(child; _children) { 1201 child.removeAllChildren(); 1202 if(child.hwnd) { 1203 DestroyWindow(child.hwnd); 1204 child.hwnd = null; 1205 } 1206 } 1207 auto orig = this._children; 1208 this._children = null; 1209 foreach(idx, w; orig) 1210 this.widgetRemoved(idx, w); 1211 1212 queueRecomputeChildLayout(); 1213 } 1214 1215 /++ 1216 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1217 +/ 1218 @scriptable 1219 Widget getChildByName(string name) { 1220 return getByName(name); 1221 } 1222 /++ 1223 Finds the nearest descendant with the requested type and [name]. May return `this`. 1224 +/ 1225 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1226 if(this.name == name) 1227 if(auto c = cast(WidgetClass) this) 1228 return c; 1229 foreach(child; children) { 1230 auto w = child.getByName(name); 1231 if(auto c = cast(WidgetClass) w) 1232 return c; 1233 } 1234 return null; 1235 } 1236 1237 /++ 1238 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. 1239 Names should be unique in a window. 1240 1241 See_Also: [getByName], [getChildByName] 1242 +/ 1243 @scriptable string name; 1244 1245 private EventHandler[][string] bubblingEventHandlers; 1246 private EventHandler[][string] capturingEventHandlers; 1247 1248 /++ 1249 Default event handlers. These are called on the appropriate 1250 event unless [Event.preventDefault] is called on the event at 1251 some point through the bubbling process. 1252 1253 1254 If you are implementing your own widget and want to add custom 1255 events, you should follow the same pattern here: create a virtual 1256 function named `defaultEventHandler_eventname` with the implementation, 1257 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1258 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1259 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1260 This ensures virtual dispatch based on the correct subclass. 1261 1262 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1263 overridden version. 1264 1265 You only need to do that on parent classes adding NEW event types. If you 1266 just want to change the default behavior of an existing event type in a subclass, 1267 you override the function (and optionally call `super.method_name`) like normal. 1268 1269 +/ 1270 protected EventHandler[string] defaultEventHandlers; 1271 1272 /// ditto 1273 void setupDefaultEventHandlers() { 1274 defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); }; 1275 defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); }; 1276 defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); }; 1277 defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); }; 1278 defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); }; 1279 defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); }; 1280 defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); }; 1281 defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); }; 1282 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); }; 1283 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); }; 1284 defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); }; 1285 defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); }; 1286 defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); }; 1287 defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); }; 1288 defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); }; 1289 defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); }; 1290 defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); }; 1291 defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); }; 1292 } 1293 1294 /// ditto 1295 void defaultEventHandler_click(ClickEvent event) {} 1296 /// ditto 1297 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1298 /// ditto 1299 void defaultEventHandler_keydown(KeyDownEvent event) {} 1300 /// ditto 1301 void defaultEventHandler_keyup(KeyUpEvent event) {} 1302 /// ditto 1303 void defaultEventHandler_mousedown(MouseDownEvent event) { 1304 if(event.button == MouseButton.left) { 1305 if(this.tabStop) 1306 this.focus(); 1307 } 1308 } 1309 /// ditto 1310 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1311 /// ditto 1312 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1313 /// ditto 1314 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1315 /// ditto 1316 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1317 /// ditto 1318 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1319 /// ditto 1320 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1321 /// ditto 1322 void defaultEventHandler_char(CharEvent event) {} 1323 /// ditto 1324 void defaultEventHandler_triggered(Event event) {} 1325 /// ditto 1326 void defaultEventHandler_change(Event event) {} 1327 /// ditto 1328 void defaultEventHandler_focus(Event event) {} 1329 /// ditto 1330 void defaultEventHandler_blur(Event event) {} 1331 /// ditto 1332 void defaultEventHandler_focusin(Event event) {} 1333 /// ditto 1334 void defaultEventHandler_focusout(Event event) {} 1335 1336 /++ 1337 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1338 1339 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1340 1341 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1342 of participating in handler delegation. 1343 1344 $(TIP 1345 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. 1346 ) 1347 +/ 1348 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1349 return addEventListener(event, (Widget, scope Event e) { 1350 if(e.srcElement is this) 1351 handler(); 1352 }, useCapture); 1353 } 1354 1355 /// ditto 1356 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1357 return addEventListener(event, (Widget, Event e) { 1358 if(e.srcElement is this) 1359 handler(e); 1360 }, useCapture); 1361 } 1362 1363 /// ditto 1364 EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) { 1365 static if(is(Handler Fn == delegate)) { 1366 static if(is(Fn Params == __parameters)) { 1367 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1368 if(e.srcElement !is this) 1369 return; 1370 auto ty = cast(Params[0]) e; 1371 if(ty !is null) 1372 handler(ty); 1373 }, useCapture); 1374 } else static assert(0); 1375 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1376 } 1377 1378 /// ditto 1379 @scriptable 1380 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1381 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1382 } 1383 1384 /// ditto 1385 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1386 static if(is(Handler Fn == delegate)) { 1387 static if(is(Fn Params == __parameters)) { 1388 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1389 auto ty = cast(Params[0]) e; 1390 if(ty !is null) 1391 handler(ty); 1392 }, useCapture); 1393 } else static assert(0); 1394 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1395 } 1396 1397 /// ditto 1398 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1399 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1400 } 1401 1402 /// ditto 1403 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1404 if(event.length > 2 && event[0..2] == "on") 1405 event = event[2 .. $]; 1406 1407 if(useCapture) 1408 capturingEventHandlers[event] ~= handler; 1409 else 1410 bubblingEventHandlers[event] ~= handler; 1411 1412 return EventListener(this, event, handler, useCapture); 1413 } 1414 1415 /// ditto 1416 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1417 if(event.length > 2 && event[0..2] == "on") 1418 event = event[2 .. $]; 1419 1420 if(useCapture) { 1421 if(event in capturingEventHandlers) 1422 foreach(ref evt; capturingEventHandlers[event]) 1423 if(evt is handler) evt = null; 1424 } else { 1425 if(event in bubblingEventHandlers) 1426 foreach(ref evt; bubblingEventHandlers[event]) 1427 if(evt is handler) evt = null; 1428 } 1429 } 1430 1431 /// ditto 1432 void removeEventListener(EventListener listener) { 1433 removeEventListener(listener.event, listener.handler, listener.useCapture); 1434 } 1435 1436 static if(UsingSimpledisplayX11) { 1437 void discardXConnectionState() { 1438 foreach(child; children) 1439 child.discardXConnectionState(); 1440 } 1441 1442 void recreateXConnectionState() { 1443 foreach(child; children) 1444 child.recreateXConnectionState(); 1445 redraw(); 1446 } 1447 } 1448 1449 /++ 1450 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1451 1452 History: 1453 `globalCoordinates` was made `final` on May 11, 2021. 1454 +/ 1455 Point globalCoordinates() { 1456 int x = this.x; 1457 int y = this.y; 1458 auto p = this.parent; 1459 while(p) { 1460 x += p.x; 1461 y += p.y; 1462 p = p.parent; 1463 } 1464 1465 static if(UsingSimpledisplayX11) { 1466 auto dpy = XDisplayConnection.get; 1467 arsd.simpledisplay.Window dummyw; 1468 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1469 } else { 1470 POINT pt; 1471 pt.x = x; 1472 pt.y = y; 1473 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1474 x = pt.x; 1475 y = pt.y; 1476 } 1477 1478 return Point(x, y); 1479 } 1480 1481 version(win32_widgets) 1482 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1483 1484 version(win32_widgets) 1485 /// Called when a WM_COMMAND is sent to the associated hwnd. 1486 void handleWmCommand(ushort cmd, ushort id) {} 1487 1488 version(win32_widgets) 1489 /++ 1490 Called when a WM_NOTIFY is sent to the associated hwnd. 1491 1492 History: 1493 +/ 1494 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1495 1496 version(win32_widgets) 1497 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); } 1498 1499 /++ 1500 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1501 1502 Updates to this variable will only be made visible on the next mouse enter event. 1503 +/ 1504 @scriptable string statusTip; 1505 // string toolTip; 1506 // string helpText; 1507 1508 /++ 1509 If true, this widget can be focused via keyboard control with the tab key. 1510 1511 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1512 +/ 1513 bool tabStop = true; 1514 /++ 1515 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.) 1516 +/ 1517 int tabOrder; 1518 1519 version(win32_widgets) { 1520 static Widget[HWND] nativeMapping; 1521 /// The native handle, if there is one. 1522 HWND hwnd; 1523 WNDPROC originalWindowProcedure; 1524 1525 SimpleWindow simpleWindowWrappingHwnd; 1526 1527 // please note it IGNORES your return value and does NOT forward it to Windows! 1528 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1529 return 0; 1530 } 1531 } 1532 private bool implicitlyCreated; 1533 1534 /// 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. 1535 int x; 1536 /// ditto 1537 int y; 1538 private int _width; 1539 private int _height; 1540 private Widget[] _children; 1541 private Widget _parent; 1542 private Window _parentWindow; 1543 1544 /++ 1545 Returns the window to which this widget is attached. 1546 1547 History: 1548 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. 1549 +/ 1550 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1551 private @property void parentWindow(Window parent) { 1552 _parentWindow = parent; 1553 foreach(child; children) 1554 child.parentWindow = parent; // please note that this is recursive 1555 } 1556 1557 /++ 1558 Returns the list of the widget's children. 1559 1560 History: 1561 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1562 1563 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1564 +/ 1565 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1566 1567 /++ 1568 Returns the widget's parent. 1569 1570 History: 1571 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1572 1573 The parent should only be managed by the [addChild] and [removeWidget] method. 1574 +/ 1575 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1576 1577 /// The widget's current size. 1578 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1579 /// ditto 1580 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1581 1582 /// Only the layout manager should be calling these. 1583 final protected @property int width(int a) @safe { return _width = a; } 1584 /// ditto 1585 final protected @property int height(int a) @safe { return _height = a; } 1586 1587 /++ 1588 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. 1589 1590 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 1591 +/ 1592 protected void registerMovement() { 1593 version(win32_widgets) { 1594 if(hwnd) { 1595 auto pos = getChildPositionRelativeToParentHwnd(this); 1596 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 1597 } 1598 } 1599 sendResizeEvent(); 1600 } 1601 1602 /// Creates the widget and adds it to the parent. 1603 this(Widget parent) { 1604 if(parent !is null) 1605 parent.addChild(this); 1606 setupDefaultEventHandlers(); 1607 } 1608 1609 /// 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. 1610 @scriptable 1611 bool isFocused() { 1612 return parentWindow && parentWindow.focusedWidget is this; 1613 } 1614 1615 private bool showing_ = true; 1616 /// 1617 bool showing() { return showing_; } 1618 /// 1619 bool hidden() { return !showing_; } 1620 /++ 1621 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. 1622 +/ 1623 void showing(bool s, bool recalculate = true) { 1624 auto so = showing_; 1625 showing_ = s; 1626 if(s != so) { 1627 version(win32_widgets) 1628 if(hwnd) 1629 ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE); 1630 1631 if(parent && recalculate) { 1632 parent.queueRecomputeChildLayout(); 1633 parent.redraw(); 1634 } 1635 1636 foreach(child; children) 1637 child.showing(s, false); 1638 1639 } 1640 queueRecomputeChildLayout(); 1641 redraw(); 1642 } 1643 /// Convenience method for `showing = true` 1644 @scriptable 1645 void show() { 1646 showing = true; 1647 } 1648 /// Convenience method for `showing = false` 1649 @scriptable 1650 void hide() { 1651 showing = false; 1652 } 1653 1654 /// 1655 @scriptable 1656 void focus() { 1657 assert(parentWindow !is null); 1658 if(isFocused()) 1659 return; 1660 1661 if(parentWindow.focusedWidget) { 1662 // FIXME: more details here? like from and to 1663 auto from = parentWindow.focusedWidget; 1664 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 1665 parentWindow.focusedWidget = null; 1666 from.emit!BlurEvent(); 1667 this.emit!FocusOutEvent(); 1668 } 1669 1670 1671 version(win32_widgets) { 1672 if(this.hwnd !is null) 1673 SetFocus(this.hwnd); 1674 } 1675 //else static if(UsingSimpledisplayX11) 1676 //this.parentWindow.win.focus(); 1677 1678 parentWindow.focusedWidget = this; 1679 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 1680 this.emit!FocusEvent(); 1681 this.emit!FocusInEvent(); 1682 } 1683 1684 /+ 1685 /++ 1686 Unfocuses the widget. This may reset 1687 +/ 1688 @scriptable 1689 void blur() { 1690 1691 } 1692 +/ 1693 1694 1695 /++ 1696 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 1697 1698 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 1699 +/ 1700 void attachedToWindow(Window w) {} 1701 /++ 1702 Callback when the widget is added to another widget. 1703 1704 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 1705 +/ 1706 void addedTo(Widget w) {} 1707 1708 /++ 1709 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. 1710 1711 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 1712 +/ 1713 protected void addChild(Widget w, int position = int.max) { 1714 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 1715 assert(w !is this, "Child cannot be its own parent!"); 1716 w._parent = this; 1717 if(position == int.max || position == children.length) { 1718 _children ~= w; 1719 } else { 1720 assert(position < _children.length); 1721 _children.length = _children.length + 1; 1722 for(int i = cast(int) _children.length - 1; i > position; i--) 1723 _children[i] = _children[i - 1]; 1724 _children[position] = w; 1725 } 1726 1727 this.parentWindow = this._parentWindow; 1728 1729 w.addedTo(this); 1730 1731 if(this.hidden) 1732 w.showing = false; 1733 1734 if(parentWindow !is null) { 1735 w.attachedToWindow(parentWindow); 1736 parentWindow.queueRecomputeChildLayout(); 1737 parentWindow.redraw(); 1738 } 1739 } 1740 1741 /++ 1742 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. 1743 +/ 1744 Widget getChildAtPosition(int x, int y) { 1745 // it goes backward so the last one to show gets picked first 1746 // might use z-index later 1747 foreach_reverse(child; children) { 1748 if(child.hidden) 1749 continue; 1750 if(child.x <= x && child.y <= y 1751 && ((x - child.x) < child.width) 1752 && ((y - child.y) < child.height)) 1753 { 1754 return child; 1755 } 1756 } 1757 1758 return null; 1759 } 1760 1761 /++ 1762 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. 1763 1764 History: 1765 Added July 2, 2021 (v10.2) 1766 +/ 1767 protected void addScrollPosition(ref int x, ref int y) {}; 1768 1769 /++ 1770 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. 1771 1772 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. 1773 1774 [paint] is not called for system widgets as the OS library draws them instead. 1775 1776 1777 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. 1778 1779 You should also look at [WidgetPainter.visualTheme] to be theme aware. 1780 1781 History: 1782 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. 1783 +/ 1784 void paint(WidgetPainter painter) { 1785 version(win32_widgets) 1786 if(hwnd) { 1787 return; 1788 } 1789 painter.drawThemed(&paintContent); // note this refers to the following overload 1790 } 1791 1792 /++ 1793 Responsible for drawing the content as the theme engine is responsible for other elements. 1794 1795 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 1796 1797 Params: 1798 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. 1799 1800 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. 1801 1802 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. 1803 1804 Returns: 1805 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. 1806 1807 History: 1808 Added May 15, 2021 1809 +/ 1810 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 1811 return bounds; 1812 } 1813 1814 deprecated("Change ScreenPainter to WidgetPainter") 1815 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 1816 1817 /// I don't actually like the name of this 1818 /// this draws a background on it 1819 void erase(WidgetPainter painter) { 1820 version(win32_widgets) 1821 if(hwnd) return; // Windows will do it. I think. 1822 1823 auto c = getComputedStyle().background.color; 1824 painter.fillColor = c; 1825 painter.outlineColor = c; 1826 1827 version(win32_widgets) { 1828 HANDLE b, p; 1829 if(c.a == 0 && parent is parentWindow) { 1830 // I don't remember why I had this really... 1831 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 1832 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 1833 } 1834 } 1835 painter.drawRectangle(Point(0, 0), width, height); 1836 version(win32_widgets) { 1837 if(c.a == 0 && parent is parentWindow) { 1838 SelectObject(painter.impl.hdc, p); 1839 SelectObject(painter.impl.hdc, b); 1840 } 1841 } 1842 } 1843 1844 /// 1845 WidgetPainter draw() { 1846 int x = this.x, y = this.y; 1847 auto parent = this.parent; 1848 while(parent) { 1849 x += parent.x; 1850 y += parent.y; 1851 parent = parent.parent; 1852 } 1853 1854 auto painter = parentWindow.win.draw(true); 1855 painter.originX = x; 1856 painter.originY = y; 1857 painter.setClipRectangle(Point(0, 0), width, height); 1858 return WidgetPainter(painter, this); 1859 } 1860 1861 /// 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. 1862 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 1863 if(hidden) 1864 return; 1865 1866 int paintX = x; 1867 int paintY = y; 1868 if(this.useNativeDrawing()) { 1869 paintX = 0; 1870 paintY = 0; 1871 lox = 0; 1872 loy = 0; 1873 containment = Rectangle(0, 0, int.max, int.max); 1874 } 1875 1876 painter.originX = lox + paintX; 1877 painter.originY = loy + paintY; 1878 1879 bool actuallyPainted = false; 1880 1881 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 1882 if(clip == Rectangle.init) { 1883 // writeln(this, " clipped out"); 1884 return; 1885 } 1886 1887 bool invalidateChildren = invalidate; 1888 1889 if(redrawRequested || force) { 1890 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 1891 1892 painter.drawingUpon = this; 1893 1894 erase(painter); 1895 if(painter.visualTheme) 1896 painter.visualTheme.doPaint(this, painter); 1897 else 1898 paint(painter); 1899 1900 if(invalidate) { 1901 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 1902 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 1903 painter.invalidateRect(region); 1904 // children are contained inside this, so no need to do extra work 1905 invalidateChildren = false; 1906 } 1907 1908 redrawRequested = false; 1909 actuallyPainted = true; 1910 } 1911 1912 foreach(child; children) { 1913 version(win32_widgets) 1914 if(child.useNativeDrawing()) continue; 1915 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 1916 } 1917 1918 version(win32_widgets) 1919 foreach(child; children) { 1920 if(child.useNativeDrawing) { 1921 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 1922 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 1923 } 1924 } 1925 } 1926 1927 protected bool useNativeDrawing() nothrow { 1928 version(win32_widgets) 1929 return hwnd !is null; 1930 else 1931 return false; 1932 } 1933 1934 private static class RedrawEvent {} 1935 private __gshared re = new RedrawEvent(); 1936 1937 private bool redrawRequested; 1938 /// 1939 final void redraw(string file = __FILE__, size_t line = __LINE__) { 1940 redrawRequested = true; 1941 1942 if(this.parentWindow) { 1943 auto sw = this.parentWindow.win; 1944 assert(sw !is null); 1945 if(!sw.eventQueued!RedrawEvent) { 1946 sw.postEvent(re); 1947 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1948 } 1949 } 1950 } 1951 1952 private SimpleWindow drawableWindow; 1953 1954 /++ 1955 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. 1956 1957 Returns: 1958 `true` if you should do your default behavior. 1959 1960 History: 1961 Added May 5, 2021 1962 1963 Bugs: 1964 It does not do the static checks on gdc right now. 1965 +/ 1966 final protected bool emit(EventType, this This, Args...)(Args args) { 1967 version(GNU) {} else 1968 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1969 auto e = new EventType(this, args); 1970 e.dispatch(); 1971 return !e.defaultPrevented; 1972 } 1973 /// ditto 1974 final protected bool emit(string eventString, this This)() { 1975 auto e = new Event(eventString, this); 1976 e.dispatch(); 1977 return !e.defaultPrevented; 1978 } 1979 1980 /++ 1981 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. 1982 1983 History: 1984 Added May 5, 2021 1985 +/ 1986 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 1987 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1988 return addEventListener(handler); 1989 } 1990 1991 /++ 1992 Gets the computed style properties from the visual theme. 1993 1994 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].) 1995 1996 History: 1997 Added May 8, 2021 1998 +/ 1999 final StyleInformation getComputedStyle() { 2000 return StyleInformation(this); 2001 } 2002 2003 int focusableWidgets(scope int delegate(Widget) dg) { 2004 foreach(widget; WidgetStream(this)) { 2005 if(widget.tabStop && !widget.hidden) { 2006 int result = dg(widget); 2007 if (result) 2008 return result; 2009 } 2010 } 2011 return 0; 2012 } 2013 2014 /++ 2015 Calculates the border box (that is, the full width/height of the widget, from border edge to border edge) 2016 for the given content box (the area between the padding) 2017 2018 History: 2019 Added January 4, 2023 (dub v11.0) 2020 +/ 2021 Rectangle borderBoxForContentBox(Rectangle contentBox) { 2022 auto cs = getComputedStyle(); 2023 2024 auto borderWidth = getBorderWidth(cs.borderStyle); 2025 2026 auto rect = contentBox; 2027 2028 rect.left -= borderWidth; 2029 rect.right += borderWidth; 2030 rect.top -= borderWidth; 2031 rect.bottom += borderWidth; 2032 2033 auto insideBorderRect = rect; 2034 2035 rect.left -= cs.paddingLeft; 2036 rect.right += cs.paddingRight; 2037 rect.top -= cs.paddingTop; 2038 rect.bottom += cs.paddingBottom; 2039 2040 return rect; 2041 } 2042 2043 2044 // FIXME: I kinda want to hide events from implementation widgets 2045 // so it just catches them all and stops propagation... 2046 // i guess i can do it with a event listener on star. 2047 2048 mixin Emits!KeyDownEvent; /// 2049 mixin Emits!KeyUpEvent; /// 2050 mixin Emits!CharEvent; /// 2051 2052 mixin Emits!MouseDownEvent; /// 2053 mixin Emits!MouseUpEvent; /// 2054 mixin Emits!ClickEvent; /// 2055 mixin Emits!DoubleClickEvent; /// 2056 mixin Emits!MouseMoveEvent; /// 2057 mixin Emits!MouseOverEvent; /// 2058 mixin Emits!MouseOutEvent; /// 2059 mixin Emits!MouseEnterEvent; /// 2060 mixin Emits!MouseLeaveEvent; /// 2061 2062 mixin Emits!ResizeEvent; /// 2063 2064 mixin Emits!BlurEvent; /// 2065 mixin Emits!FocusEvent; /// 2066 2067 mixin Emits!FocusInEvent; /// 2068 mixin Emits!FocusOutEvent; /// 2069 } 2070 2071 /+ 2072 /++ 2073 Interface to indicate that the widget has a simple value property. 2074 2075 History: 2076 Added August 26, 2021 2077 +/ 2078 interface HasValue!T { 2079 /// Getter 2080 @property T value(); 2081 /// Setter 2082 @property void value(T); 2083 } 2084 2085 /++ 2086 Interface to indicate that the widget has a range of possible values for its simple value property. 2087 This would be present on something like a slider or possibly a number picker. 2088 2089 History: 2090 Added September 11, 2021 2091 +/ 2092 interface HasRangeOfValues!T : HasValue!T { 2093 /// The minimum and maximum values in the range, inclusive. 2094 @property T minValue(); 2095 @property void minValue(T); /// ditto 2096 @property T maxValue(); /// ditto 2097 @property void maxValue(T); /// ditto 2098 2099 /// The smallest step the user interface allows. User may still type in values without this limitation. 2100 @property void step(T); 2101 @property T step(); /// ditto 2102 } 2103 2104 /++ 2105 Interface to indicate that the widget has a list of possible values the user can choose from. 2106 This would be present on something like a drop-down selector. 2107 2108 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2109 combobox. 2110 2111 History: 2112 Added September 11, 2021 2113 +/ 2114 interface HasListOfValues!T : HasValue!T { 2115 @property T[] values; 2116 @property void values(T[]); 2117 2118 @property int selectedIndex(); // note it may return -1! 2119 @property void selectedIndex(int); 2120 } 2121 +/ 2122 2123 /++ 2124 History: 2125 Added September 2021 (dub v10.4) 2126 +/ 2127 class GridLayout : Layout { 2128 2129 // 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. 2130 2131 /++ 2132 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2133 +/ 2134 enum Gravity { 2135 Center = 0, 2136 NorthWest = North | West, 2137 North = 0b10_00, 2138 NorthEast = North | East, 2139 West = 0b00_10, 2140 East = 0b00_01, 2141 SouthWest = South | West, 2142 South = 0b01_00, 2143 SouthEast = South | East, 2144 } 2145 2146 /++ 2147 The width and height are in some proportional units and can often just be 12. 2148 +/ 2149 this(int width, int height, Widget parent) { 2150 this.gridWidth = width; 2151 this.gridHeight = height; 2152 super(parent); 2153 } 2154 2155 /++ 2156 Sets the position of the given child. 2157 2158 The units of these arguments are in the proportional grid units you set in the constructor. 2159 +/ 2160 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2161 // ensure it is in bounds 2162 // then ensure no overlaps 2163 2164 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2165 2166 foreach(ref position; positions) { 2167 if(position.widget is child) { 2168 position = p; 2169 goto set; 2170 } 2171 } 2172 2173 positions ~= p; 2174 2175 set: 2176 2177 // FIXME: should this batch? 2178 queueRecomputeChildLayout(); 2179 2180 return child; 2181 } 2182 2183 override void addChild(Widget w, int position = int.max) { 2184 super.addChild(w, position); 2185 //positions ~= ChildPosition(w); 2186 if(position != int.max) { 2187 // FIXME: align it so they actually match. 2188 } 2189 } 2190 2191 override void widgetRemoved(size_t idx, Widget w) { 2192 // FIXME: keep the positions array aligned 2193 // positions[idx].widget = null; 2194 } 2195 2196 override void recomputeChildLayout() { 2197 registerMovement(); 2198 int onGrid = cast(int) positions.length; 2199 c: foreach(child; children) { 2200 // just snap it to the grid 2201 if(onGrid) 2202 foreach(position; positions) 2203 if(position.widget is child) { 2204 child.x = this.width * position.x / this.gridWidth; 2205 child.y = this.height * position.y / this.gridHeight; 2206 child.width = this.width * position.width / this.gridWidth; 2207 child.height = this.height * position.height / this.gridHeight; 2208 2209 auto diff = child.width - child.maxWidth(); 2210 // FIXME: gravity? 2211 if(diff > 0) { 2212 child.width = child.width - diff; 2213 2214 if(position.gravity & Gravity.West) { 2215 // nothing needed, already aligned 2216 } else if(position.gravity & Gravity.East) { 2217 child.x += diff; 2218 } else { 2219 child.x += diff / 2; 2220 } 2221 } 2222 2223 diff = child.height - child.maxHeight(); 2224 // FIXME: gravity? 2225 if(diff > 0) { 2226 child.height = child.height - diff; 2227 2228 if(position.gravity & Gravity.North) { 2229 // nothing needed, already aligned 2230 } else if(position.gravity & Gravity.South) { 2231 child.y += diff; 2232 } else { 2233 child.y += diff / 2; 2234 } 2235 } 2236 2237 2238 child.recomputeChildLayout(); 2239 onGrid--; 2240 continue c; 2241 } 2242 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2243 } 2244 } 2245 2246 private struct ChildPosition { 2247 Widget widget; 2248 int x; 2249 int y; 2250 int width; 2251 int height; 2252 Gravity gravity; 2253 } 2254 private ChildPosition[] positions; 2255 2256 int gridWidth = 12; 2257 int gridHeight = 12; 2258 } 2259 2260 /// 2261 abstract class ComboboxBase : Widget { 2262 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2263 // or to always show the list, we want CBS_SIMPLE == 1 2264 version(win32_widgets) 2265 this(uint style, Widget parent) { 2266 super(parent); 2267 createWin32Window(this, "ComboBox"w, null, style); 2268 } 2269 else version(custom_widgets) 2270 this(Widget parent) { 2271 super(parent); 2272 2273 addEventListener((KeyDownEvent event) { 2274 if(event.key == Key.Up) { 2275 if(selection_ > -1) { // -1 means select blank 2276 selection_--; 2277 fireChangeEvent(); 2278 } 2279 event.preventDefault(); 2280 } 2281 if(event.key == Key.Down) { 2282 if(selection_ + 1 < options.length) { 2283 selection_++; 2284 fireChangeEvent(); 2285 } 2286 event.preventDefault(); 2287 } 2288 2289 }); 2290 2291 } 2292 else static assert(false); 2293 2294 /++ 2295 Returns the current list of options in the selection. 2296 2297 History: 2298 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2299 +/ 2300 final @property string[] options() const { 2301 return cast(string[]) options_; 2302 } 2303 2304 private string[] options_; 2305 private int selection_ = -1; 2306 2307 /++ 2308 Adds an option to the end of options array. 2309 +/ 2310 void addOption(string s) { 2311 options_ ~= s; 2312 version(win32_widgets) 2313 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2314 } 2315 2316 /++ 2317 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2318 +/ 2319 int getSelection() { 2320 return selection_; 2321 } 2322 2323 /++ 2324 Returns the current selection as a string. 2325 2326 History: 2327 Added November 17, 2021 2328 +/ 2329 string getSelectionString() { 2330 return selection_ == -1 ? null : options[selection_]; 2331 } 2332 2333 /++ 2334 Sets the current selection to an index in the options array, or to the given option if present. 2335 Please note that the string version may do a linear lookup. 2336 2337 Returns: 2338 the index you passed in 2339 2340 History: 2341 The `string` based overload was added on March 1, 2022 (dub v10.7). 2342 2343 The return value was `void` prior to March 1, 2022. 2344 +/ 2345 int setSelection(int idx) { 2346 selection_ = idx; 2347 version(win32_widgets) 2348 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2349 2350 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2351 t.dispatch(); 2352 2353 return idx; 2354 } 2355 2356 /// ditto 2357 int setSelection(string s) { 2358 if(s !is null) 2359 foreach(idx, item; options) 2360 if(item == s) { 2361 return setSelection(cast(int) idx); 2362 } 2363 return setSelection(-1); 2364 } 2365 2366 /++ 2367 This event is fired when the selection changes. Note it inherits 2368 from ChangeEvent!string, meaning you can use that as well, and it also 2369 fills in [Event.intValue]. 2370 +/ 2371 static class SelectionChangedEvent : ChangeEvent!string { 2372 this(Widget target, int iv, string sv) { 2373 super(target, &stringValue); 2374 this.iv = iv; 2375 this.sv = sv; 2376 } 2377 immutable int iv; 2378 immutable string sv; 2379 2380 override @property string stringValue() { return sv; } 2381 override @property int intValue() { return iv; } 2382 } 2383 2384 version(win32_widgets) 2385 override void handleWmCommand(ushort cmd, ushort id) { 2386 if(cmd == CBN_SELCHANGE) { 2387 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2388 fireChangeEvent(); 2389 } 2390 } 2391 2392 private void fireChangeEvent() { 2393 if(selection_ >= options.length) 2394 selection_ = -1; 2395 2396 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2397 t.dispatch(); 2398 } 2399 2400 version(win32_widgets) { 2401 override int minHeight() { return defaultLineHeight + 6; } 2402 override int maxHeight() { return defaultLineHeight + 6; } 2403 } else { 2404 override int minHeight() { return defaultLineHeight + 4; } 2405 override int maxHeight() { return defaultLineHeight + 4; } 2406 } 2407 2408 version(custom_widgets) { 2409 2410 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2411 2412 SimpleWindow dropDown; 2413 void popup() { 2414 auto w = width; 2415 // FIXME: suggestedDropdownHeight see below 2416 auto h = cast(int) this.options.length * defaultLineHeight + 8; 2417 2418 auto coord = this.globalCoordinates(); 2419 auto dropDown = new SimpleWindow( 2420 w, h, 2421 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null); 2422 2423 dropDown.move(coord.x, coord.y + this.height); 2424 2425 { 2426 auto cs = getComputedStyle(); 2427 auto painter = dropDown.draw(); 2428 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2429 auto p = Point(4, 4); 2430 painter.outlineColor = cs.foregroundColor; 2431 foreach(option; options) { 2432 painter.drawText(p, option); 2433 p.y += defaultLineHeight; 2434 } 2435 } 2436 2437 dropDown.setEventHandlers( 2438 (MouseEvent event) { 2439 if(event.type == MouseEventType.buttonReleased) { 2440 dropDown.close(); 2441 auto element = (event.y - 4) / defaultLineHeight; 2442 if(element >= 0 && element <= options.length) { 2443 selection_ = element; 2444 2445 fireChangeEvent(); 2446 } 2447 } 2448 } 2449 ); 2450 2451 dropDown.visibilityChanged = (bool visible) { 2452 if(visible) { 2453 this.redraw(); 2454 dropDown.grabInput(); 2455 } else { 2456 dropDown.releaseInputGrab(); 2457 } 2458 }; 2459 2460 dropDown.show(); 2461 } 2462 2463 } 2464 } 2465 2466 /++ 2467 A drop-down list where the user must select one of the 2468 given options. Like `<select>` in HTML. 2469 +/ 2470 class DropDownSelection : ComboboxBase { 2471 this(Widget parent) { 2472 version(win32_widgets) 2473 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 2474 else version(custom_widgets) { 2475 super(parent); 2476 2477 addEventListener("focus", () { this.redraw; }); 2478 addEventListener("blur", () { this.redraw; }); 2479 addEventListener(EventType.change, () { this.redraw; }); 2480 addEventListener("mousedown", () { this.focus(); this.popup(); }); 2481 addEventListener((KeyDownEvent event) { 2482 if(event.key == Key.Space) 2483 popup(); 2484 }); 2485 } else static assert(false); 2486 } 2487 2488 mixin Padding!q{2}; 2489 static class Style : Widget.Style { 2490 override FrameStyle borderStyle() { return FrameStyle.risen; } 2491 } 2492 mixin OverrideStyle!Style; 2493 2494 version(custom_widgets) 2495 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2496 auto cs = getComputedStyle(); 2497 2498 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 2499 2500 painter.outlineColor = cs.foregroundColor; 2501 painter.fillColor = cs.foregroundColor; 2502 2503 /+ 2504 Point[4] triangle; 2505 enum padding = 6; 2506 enum paddingV = 7; 2507 enum triangleWidth = 10; 2508 triangle[0] = Point(width - padding - triangleWidth, paddingV); 2509 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 2510 triangle[2] = Point(width - padding - 0, paddingV); 2511 triangle[3] = triangle[0]; 2512 painter.drawPolygon(triangle[]); 2513 +/ 2514 2515 auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2); 2516 2517 painter.drawPolygon( 2518 scaleWithDpi(Point(2, 6) + offset), 2519 scaleWithDpi(Point(7, 11) + offset), 2520 scaleWithDpi(Point(12, 6) + offset), 2521 scaleWithDpi(Point(2, 6) + offset) 2522 ); 2523 2524 2525 return bounds; 2526 } 2527 2528 version(win32_widgets) 2529 override void registerMovement() { 2530 version(win32_widgets) { 2531 if(hwnd) { 2532 auto pos = getChildPositionRelativeToParentHwnd(this); 2533 // the height given to this from Windows' perspective is supposed 2534 // to include the drop down's height. so I add to it to give some 2535 // room for that. 2536 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 2537 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 2538 } 2539 } 2540 sendResizeEvent(); 2541 } 2542 } 2543 2544 /++ 2545 A text box with a drop down arrow listing selections. 2546 The user can choose from the list, or type their own. 2547 +/ 2548 class FreeEntrySelection : ComboboxBase { 2549 this(Widget parent) { 2550 version(win32_widgets) 2551 super(2 /* CBS_DROPDOWN */, parent); 2552 else version(custom_widgets) { 2553 super(parent); 2554 auto hl = new HorizontalLayout(this); 2555 lineEdit = new LineEdit(hl); 2556 2557 tabStop = false; 2558 2559 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2560 2561 auto btn = new class ArrowButton { 2562 this() { 2563 super(ArrowDirection.down, hl); 2564 } 2565 override int maxHeight() { 2566 return lineEdit.maxHeight; 2567 } 2568 }; 2569 //btn.addDirectEventListener("focus", &lineEdit.focus); 2570 btn.addEventListener("triggered", &this.popup); 2571 addEventListener(EventType.change, (Event event) { 2572 lineEdit.content = event.stringValue; 2573 lineEdit.focus(); 2574 redraw(); 2575 }); 2576 } 2577 else static assert(false); 2578 } 2579 2580 version(custom_widgets) { 2581 LineEdit lineEdit; 2582 } 2583 } 2584 2585 /++ 2586 A combination of free entry with a list below it. 2587 +/ 2588 class ComboBox : ComboboxBase { 2589 this(Widget parent) { 2590 version(win32_widgets) 2591 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 2592 else version(custom_widgets) { 2593 super(parent); 2594 lineEdit = new LineEdit(this); 2595 listWidget = new ListWidget(this); 2596 listWidget.multiSelect = false; 2597 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 2598 string c = null; 2599 foreach(option; listWidget.options) 2600 if(option.selected) { 2601 c = option.label; 2602 break; 2603 } 2604 lineEdit.content = c; 2605 }); 2606 2607 listWidget.tabStop = false; 2608 this.tabStop = false; 2609 listWidget.addEventListener("focus", &lineEdit.focus); 2610 this.addEventListener("focus", &lineEdit.focus); 2611 2612 addDirectEventListener(EventType.change, { 2613 listWidget.setSelection(selection_); 2614 if(selection_ != -1) 2615 lineEdit.content = options[selection_]; 2616 lineEdit.focus(); 2617 redraw(); 2618 }); 2619 2620 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2621 2622 listWidget.addDirectEventListener(EventType.change, { 2623 int set = -1; 2624 foreach(idx, opt; listWidget.options) 2625 if(opt.selected) { 2626 set = cast(int) idx; 2627 break; 2628 } 2629 if(set != selection_) 2630 this.setSelection(set); 2631 }); 2632 } else static assert(false); 2633 } 2634 2635 override int minHeight() { return defaultLineHeight * 3; } 2636 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 2637 override int heightStretchiness() { return 5; } 2638 2639 version(custom_widgets) { 2640 LineEdit lineEdit; 2641 ListWidget listWidget; 2642 2643 override void addOption(string s) { 2644 listWidget.options ~= ListWidget.Option(s); 2645 ComboboxBase.addOption(s); 2646 } 2647 } 2648 } 2649 2650 /+ 2651 class Spinner : Widget { 2652 version(win32_widgets) 2653 this(Widget parent) { 2654 super(parent); 2655 parentWindow = parent.parentWindow; 2656 auto hlayout = new HorizontalLayout(this); 2657 lineEdit = new LineEdit(hlayout); 2658 upDownControl = new UpDownControl(hlayout); 2659 } 2660 2661 LineEdit lineEdit; 2662 UpDownControl upDownControl; 2663 } 2664 2665 class UpDownControl : Widget { 2666 version(win32_widgets) 2667 this(Widget parent) { 2668 super(parent); 2669 parentWindow = parent.parentWindow; 2670 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 2671 } 2672 2673 override int minHeight() { return defaultLineHeight; } 2674 override int maxHeight() { return defaultLineHeight * 3/2; } 2675 2676 override int minWidth() { return defaultLineHeight * 3/2; } 2677 override int maxWidth() { return defaultLineHeight * 3/2; } 2678 } 2679 +/ 2680 2681 /+ 2682 class DataView : Widget { 2683 // this is the omnibus data viewer 2684 // the internal data layout is something like: 2685 // string[string][] but also each node can have parents 2686 } 2687 +/ 2688 2689 2690 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 2691 2692 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 2693 2694 // FIXME: menus should prolly capture the mouse. ugh i kno. 2695 /* 2696 TextEdit needs: 2697 2698 * caret manipulation 2699 * selection control 2700 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 2701 2702 For example: 2703 2704 connect(paste, &textEdit.insertTextAtCaret); 2705 2706 would be nice. 2707 2708 2709 2710 I kinda want an omnibus dataview that combines list, tree, 2711 and table - it can be switched dynamically between them. 2712 2713 Flattening policy: only show top level, show recursive, show grouped 2714 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 2715 2716 Single select, multi select, organization, drag+drop 2717 */ 2718 2719 //static if(UsingSimpledisplayX11) 2720 version(win32_widgets) {} 2721 else version(custom_widgets) { 2722 enum scrollClickRepeatInterval = 50; 2723 2724 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 2725 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 2726 enum activeTabColor = lightAccentColor; 2727 enum hoveringColor = Color(228, 228, 228); 2728 enum buttonColor = windowBackgroundColor; 2729 enum depressedButtonColor = darkAccentColor; 2730 enum activeListXorColor = Color(255, 255, 127); 2731 enum progressBarColor = Color(0, 0, 128); 2732 enum activeMenuItemColor = Color(0, 0, 128); 2733 2734 }} 2735 else static assert(false); 2736 deprecated("Get these properties off the `visualTheme` instead.") { 2737 // these are used by horizontal rule so not just custom_widgets. for now at least. 2738 enum darkAccentColor = Color(172, 172, 172); 2739 enum lightAccentColor = Color(223, 223, 223); // used to be 223 2740 } 2741 2742 private const(wchar)* toWstringzInternal(in char[] s) { 2743 wchar[] str; 2744 str.reserve(s.length + 1); 2745 foreach(dchar ch; s) 2746 str ~= ch; 2747 str ~= '\0'; 2748 return str.ptr; 2749 } 2750 2751 static if(SimpledisplayTimerAvailable) 2752 void setClickRepeat(Widget w, int interval, int delay = 250) { 2753 Timer timer; 2754 int delayRemaining = delay / interval; 2755 if(delayRemaining <= 1) 2756 delayRemaining = 2; 2757 2758 immutable originalDelayRemaining = delayRemaining; 2759 2760 w.addDirectEventListener((scope MouseDownEvent ev) { 2761 if(ev.srcElement !is w) 2762 return; 2763 if(timer !is null) { 2764 timer.destroy(); 2765 timer = null; 2766 } 2767 delayRemaining = originalDelayRemaining; 2768 timer = new Timer(interval, () { 2769 if(delayRemaining > 0) 2770 delayRemaining--; 2771 else { 2772 auto ev = new Event("triggered", w); 2773 ev.sendDirectly(); 2774 } 2775 }); 2776 }); 2777 2778 w.addDirectEventListener((scope MouseUpEvent ev) { 2779 if(ev.srcElement !is w) 2780 return; 2781 if(timer !is null) { 2782 timer.destroy(); 2783 timer = null; 2784 } 2785 }); 2786 2787 w.addDirectEventListener((scope MouseLeaveEvent ev) { 2788 if(ev.srcElement !is w) 2789 return; 2790 if(timer !is null) { 2791 timer.destroy(); 2792 timer = null; 2793 } 2794 }); 2795 2796 } 2797 else 2798 void setClickRepeat(Widget w, int interval, int delay = 250) {} 2799 2800 enum FrameStyle { 2801 none, /// 2802 risen, /// a 3d pop-out effect (think Windows 95 button) 2803 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 2804 solid, /// 2805 dotted, /// 2806 fantasy, /// a style based on a popular fantasy video game 2807 } 2808 2809 version(custom_widgets) 2810 deprecated 2811 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 2812 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2813 } 2814 2815 version(custom_widgets) 2816 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 2817 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 2818 } 2819 2820 version(custom_widgets) 2821 deprecated 2822 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 2823 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2824 } 2825 2826 int getBorderWidth(FrameStyle style) { 2827 final switch(style) { 2828 case FrameStyle.sunk, FrameStyle.risen: 2829 return 2; 2830 case FrameStyle.none: 2831 return 0; 2832 case FrameStyle.solid: 2833 return 1; 2834 case FrameStyle.dotted: 2835 return 1; 2836 case FrameStyle.fantasy: 2837 return 3; 2838 } 2839 } 2840 2841 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 2842 int borderWidth = getBorderWidth(style); 2843 final switch(style) { 2844 case FrameStyle.sunk, FrameStyle.risen: 2845 // outer layer 2846 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 2847 break; 2848 case FrameStyle.none: 2849 painter.outlineColor = background; 2850 break; 2851 case FrameStyle.solid: 2852 painter.pen = Pen(border, 1); 2853 break; 2854 case FrameStyle.dotted: 2855 painter.pen = Pen(border, 1, Pen.Style.Dotted); 2856 break; 2857 case FrameStyle.fantasy: 2858 painter.pen = Pen(border, 3); 2859 break; 2860 } 2861 2862 painter.fillColor = background; 2863 painter.drawRectangle(Point(x + 0, y + 0), width, height); 2864 2865 2866 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 2867 // 3d effect 2868 auto vt = WidgetPainter.visualTheme; 2869 2870 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 2871 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 2872 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 2873 2874 // inner layer 2875 //right, bottom 2876 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 2877 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 2878 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 2879 // left, top 2880 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 2881 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 2882 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 2883 } else if(style == FrameStyle.fantasy) { 2884 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 2885 painter.fillColor = Color.transparent; 2886 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 2887 } 2888 2889 return borderWidth; 2890 } 2891 2892 /++ 2893 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. 2894 2895 See_Also: 2896 [MenuItem] 2897 [ToolButton] 2898 [Menu.addItem] 2899 +/ 2900 class Action { 2901 version(win32_widgets) { 2902 private int id; 2903 private static int lastId = 9000; 2904 private static Action[int] mapping; 2905 } 2906 2907 KeyEvent accelerator; 2908 2909 // FIXME: disable message 2910 // and toggle thing? 2911 // ??? and trigger arguments too ??? 2912 2913 /++ 2914 Params: 2915 label = the textual label 2916 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 2917 triggered = initial handler, more can be added via the [triggered] member. 2918 +/ 2919 this(string label, ushort icon = 0, void delegate() triggered = null) { 2920 this.label = label; 2921 this.iconId = icon; 2922 if(triggered !is null) 2923 this.triggered ~= triggered; 2924 version(win32_widgets) { 2925 id = ++lastId; 2926 mapping[id] = this; 2927 } 2928 } 2929 2930 private string label; 2931 private ushort iconId; 2932 // icon 2933 2934 // when it is triggered, the triggered event is fired on the window 2935 /// The list of handlers when it is triggered. 2936 void delegate()[] triggered; 2937 } 2938 2939 /* 2940 plan: 2941 keyboard accelerators 2942 2943 * menus (and popups and tooltips) 2944 * status bar 2945 * toolbars and buttons 2946 2947 sortable table view 2948 2949 maybe notification area icons 2950 basic clipboard 2951 2952 * radio box 2953 splitter 2954 toggle buttons (optionally mutually exclusive, like in Paint) 2955 label, rich text display, multi line plain text (selectable) 2956 * fieldset 2957 * nestable grid layout 2958 single line text input 2959 * multi line text input 2960 slider 2961 spinner 2962 list box 2963 drop down 2964 combo box 2965 auto complete box 2966 * progress bar 2967 2968 terminal window/widget (on unix it might even be a pty but really idk) 2969 2970 ok button 2971 cancel button 2972 2973 keyboard hotkeys 2974 2975 scroll widget 2976 2977 event redirections and network transparency 2978 script integration 2979 */ 2980 2981 2982 /* 2983 MENUS 2984 2985 auto bar = new MenuBar(window); 2986 window.menuBar = bar; 2987 2988 auto fileMenu = bar.addItem(new Menu("&File")); 2989 fileMenu.addItem(new MenuItem("&Exit")); 2990 2991 2992 EVENTS 2993 2994 For controls, you should usually use "triggered" rather than "click", etc., because 2995 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 2996 This is the case on menus and pushbuttons. 2997 2998 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 2999 */ 3000 3001 3002 /* 3003 enum LinePreference { 3004 AlwaysOnOwnLine, // always on its own line 3005 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 3006 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 3007 } 3008 */ 3009 3010 /++ 3011 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. 3012 3013 --- 3014 class MyWidget : Widget { 3015 this(Widget parent) { super(parent); } 3016 3017 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 3018 mixin Padding!q{4}; 3019 3020 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 3021 mixin Margin!q{8}; 3022 3023 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 3024 // while Top/Bottom/Right remain 8 from the mixin above. 3025 override int marginLeft() { return 2; } 3026 } 3027 --- 3028 3029 3030 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]). 3031 3032 Padding is the area inside a widget where its background is drawn, but the content avoids. 3033 3034 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!). 3035 3036 * 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. 3037 +/ 3038 mixin template Padding(string code) { 3039 override int paddingLeft() { return mixin(code);} 3040 override int paddingRight() { return mixin(code);} 3041 override int paddingTop() { return mixin(code);} 3042 override int paddingBottom() { return mixin(code);} 3043 } 3044 3045 /// ditto 3046 mixin template Margin(string code) { 3047 override int marginLeft() { return mixin(code);} 3048 override int marginRight() { return mixin(code);} 3049 override int marginTop() { return mixin(code);} 3050 override int marginBottom() { return mixin(code);} 3051 } 3052 3053 private 3054 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3055 enum calcingV = relevantMeasure == "height"; 3056 3057 parent.registerMovement(); 3058 3059 if(parent.children.length == 0) 3060 return; 3061 3062 auto parentStyle = parent.getComputedStyle(); 3063 3064 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3065 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3066 3067 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3068 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3069 3070 // my own width and height should already be set by the caller of this function... 3071 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3072 mixin("parentStyle.padding"~firstThingy~"()") - 3073 mixin("parentStyle.padding"~secondThingy~"()"); 3074 3075 int stretchinessSum; 3076 int stretchyChildSum; 3077 int lastMargin = 0; 3078 3079 int shrinkinessSum; 3080 int shrinkyChildSum; 3081 3082 // set initial size 3083 foreach(child; parent.children) { 3084 3085 auto childStyle = child.getComputedStyle(); 3086 3087 if(cast(StaticPosition) child) 3088 continue; 3089 if(child.hidden) 3090 continue; 3091 3092 const iw = child.flexBasisWidth(); 3093 const ih = child.flexBasisHeight(); 3094 3095 static if(calcingV) { 3096 child.width = parent.width - 3097 mixin("childStyle.margin"~otherFirstThingy~"()") - 3098 mixin("childStyle.margin"~otherSecondThingy~"()") - 3099 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3100 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3101 3102 if(child.width < 0) 3103 child.width = 0; 3104 if(child.width > childStyle.maxWidth()) 3105 child.width = childStyle.maxWidth(); 3106 3107 if(iw > 0) { 3108 auto totalPossible = child.width; 3109 if(child.width > iw && child.widthStretchiness() == 0) 3110 child.width = iw; 3111 } 3112 3113 child.height = mymax(childStyle.minHeight(), ih); 3114 } else { 3115 // set to take all the space 3116 child.height = parent.height - 3117 mixin("childStyle.margin"~firstThingy~"()") - 3118 mixin("childStyle.margin"~secondThingy~"()") - 3119 mixin("parentStyle.padding"~firstThingy~"()") - 3120 mixin("parentStyle.padding"~secondThingy~"()"); 3121 3122 // then clamp it 3123 if(child.height < 0) 3124 child.height = 0; 3125 if(child.height > childStyle.maxHeight()) 3126 child.height = childStyle.maxHeight(); 3127 3128 // and if possible, respect the ideal target 3129 if(ih > 0) { 3130 auto totalPossible = child.height; 3131 if(child.height > ih && child.heightStretchiness() == 0) 3132 child.height = ih; 3133 } 3134 3135 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3136 child.width = mymax(childStyle.minWidth(), iw); 3137 } 3138 3139 spaceRemaining -= mixin("child." ~ relevantMeasure); 3140 3141 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3142 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3143 lastMargin = margin; 3144 spaceRemaining -= thisMargin + margin; 3145 3146 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3147 stretchinessSum += s; 3148 if(s > 0) 3149 stretchyChildSum++; 3150 3151 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3152 shrinkinessSum += s2; 3153 if(s2 > 0) 3154 shrinkyChildSum++; 3155 } 3156 3157 if(spaceRemaining < 0 && shrinkyChildSum) { 3158 // shrink to get into the space if it is possible 3159 auto toRemove = -spaceRemaining; 3160 auto removalPerItem = toRemove * shrinkinessSum / shrinkyChildSum; 3161 auto remainder = toRemove * shrinkinessSum % shrinkyChildSum; 3162 3163 // FIXME: wtf why am i shrinking things with no shrinkiness? 3164 3165 foreach(child; parent.children) { 3166 auto childStyle = child.getComputedStyle(); 3167 if(cast(StaticPosition) child) 3168 continue; 3169 if(child.hidden) 3170 continue; 3171 static if(calcingV) { 3172 auto maximum = childStyle.maxHeight(); 3173 } else { 3174 auto maximum = childStyle.maxWidth(); 3175 } 3176 3177 if(mixin("child._" ~ relevantMeasure) >= maximum) 3178 continue; 3179 3180 mixin("child._" ~ relevantMeasure) -= removalPerItem + remainder; // this is removing more than needed to trigger the next thing. ugh. 3181 3182 spaceRemaining += removalPerItem + remainder; 3183 } 3184 } 3185 3186 // stretch to fill space 3187 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3188 auto spacePerChild = spaceRemaining / stretchinessSum; 3189 bool spreadEvenly; 3190 bool giveToBiggest; 3191 if(spacePerChild <= 0) { 3192 spacePerChild = spaceRemaining / stretchyChildSum; 3193 spreadEvenly = true; 3194 } 3195 if(spacePerChild <= 0) { 3196 giveToBiggest = true; 3197 } 3198 int previousSpaceRemaining = spaceRemaining; 3199 stretchinessSum = 0; 3200 Widget mostStretchy; 3201 int mostStretchyS; 3202 foreach(child; parent.children) { 3203 auto childStyle = child.getComputedStyle(); 3204 if(cast(StaticPosition) child) 3205 continue; 3206 if(child.hidden) 3207 continue; 3208 static if(calcingV) { 3209 auto maximum = childStyle.maxHeight(); 3210 } else { 3211 auto maximum = childStyle.maxWidth(); 3212 } 3213 3214 if(mixin("child." ~ relevantMeasure) >= maximum) { 3215 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3216 mixin("child._" ~ relevantMeasure) -= adj; 3217 spaceRemaining += adj; 3218 continue; 3219 } 3220 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3221 if(s <= 0) 3222 continue; 3223 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3224 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3225 spaceRemaining -= spaceAdjustment; 3226 if(mixin("child." ~ relevantMeasure) > maximum) { 3227 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3228 mixin("child._" ~ relevantMeasure) -= diff; 3229 spaceRemaining += diff; 3230 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3231 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3232 if(mostStretchy is null || s >= mostStretchyS) { 3233 mostStretchy = child; 3234 mostStretchyS = s; 3235 } 3236 } 3237 } 3238 3239 if(giveToBiggest && mostStretchy !is null) { 3240 auto child = mostStretchy; 3241 auto childStyle = child.getComputedStyle(); 3242 int spaceAdjustment = spaceRemaining; 3243 3244 static if(calcingV) 3245 auto maximum = childStyle.maxHeight(); 3246 else 3247 auto maximum = childStyle.maxWidth(); 3248 3249 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3250 spaceRemaining -= spaceAdjustment; 3251 if(mixin("child._" ~ relevantMeasure) > maximum) { 3252 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3253 mixin("child._" ~ relevantMeasure) -= diff; 3254 spaceRemaining += diff; 3255 } 3256 } 3257 3258 if(spaceRemaining == previousSpaceRemaining) { 3259 if(mostStretchy !is null) { 3260 static if(calcingV) 3261 auto maximum = mostStretchy.maxHeight(); 3262 else 3263 auto maximum = mostStretchy.maxWidth(); 3264 3265 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3266 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3267 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3268 } 3269 break; // apparently nothing more we can do 3270 } 3271 } 3272 3273 foreach(child; parent.children) { 3274 auto childStyle = child.getComputedStyle(); 3275 if(cast(StaticPosition) child) 3276 continue; 3277 if(child.hidden) 3278 continue; 3279 3280 static if(calcingV) 3281 auto maximum = childStyle.maxHeight(); 3282 else 3283 auto maximum = childStyle.maxWidth(); 3284 if(mixin("child._" ~ relevantMeasure) > maximum) 3285 mixin("child._" ~ relevantMeasure) = maximum; 3286 } 3287 3288 // position 3289 lastMargin = 0; 3290 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3291 foreach(child; parent.children) { 3292 auto childStyle = child.getComputedStyle(); 3293 if(cast(StaticPosition) child) { 3294 child.recomputeChildLayout(); 3295 continue; 3296 } 3297 if(child.hidden) 3298 continue; 3299 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3300 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3301 currentPos += thisMargin; 3302 static if(calcingV) { 3303 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3304 child.y = currentPos; 3305 } else { 3306 child.x = currentPos; 3307 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3308 3309 } 3310 currentPos += mixin("child." ~ relevantMeasure); 3311 currentPos += margin; 3312 lastMargin = margin; 3313 3314 child.recomputeChildLayout(); 3315 } 3316 } 3317 3318 int mymax(int a, int b) { return a > b ? a : b; } 3319 int mymax(int a, int b, int c) { 3320 auto d = mymax(a, b); 3321 return c > d ? c : d; 3322 } 3323 3324 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3325 // and here, it must be integrable with the layout, the event system, and not be painted over. 3326 version(win32_widgets) { 3327 3328 // this function just does stuff that a parent window needs for redirection 3329 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3330 this_.hookedWndProc(msg, wParam, lParam); 3331 3332 switch(msg) { 3333 3334 case WM_VSCROLL, WM_HSCROLL: 3335 auto pos = HIWORD(wParam); 3336 auto m = LOWORD(wParam); 3337 3338 auto scrollbarHwnd = cast(HWND) lParam; 3339 3340 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3341 3342 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3343 3344 switch(m) { 3345 /+ 3346 // I don't think those messages are ever actually sent normally by the widget itself, 3347 // they are more used for the keyboard interface. methinks. 3348 case SB_BOTTOM: 3349 // writeln("end"); 3350 auto event = new Event("scrolltoend", *widgetp); 3351 event.dispatch(); 3352 //if(!event.defaultPrevented) 3353 break; 3354 case SB_TOP: 3355 // writeln("top"); 3356 auto event = new Event("scrolltobeginning", *widgetp); 3357 event.dispatch(); 3358 break; 3359 case SB_ENDSCROLL: 3360 // idk 3361 break; 3362 +/ 3363 case SB_LINEDOWN: 3364 (*widgetp).emitCommand!"scrolltonextline"(); 3365 return 0; 3366 case SB_LINEUP: 3367 (*widgetp).emitCommand!"scrolltopreviousline"(); 3368 return 0; 3369 case SB_PAGEDOWN: 3370 (*widgetp).emitCommand!"scrolltonextpage"(); 3371 return 0; 3372 case SB_PAGEUP: 3373 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3374 return 0; 3375 case SB_THUMBPOSITION: 3376 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3377 ev.dispatch(); 3378 return 0; 3379 case SB_THUMBTRACK: 3380 // eh kinda lying but i like the real time update display 3381 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3382 ev.dispatch(); 3383 3384 // the event loop doesn't seem to carry on with a requested redraw.. 3385 // so we request it to get our dirty bit set... 3386 // then we need to immediately actually redraw it too for instant feedback to user 3387 SimpleWindow.processAllCustomEvents(); 3388 SimpleWindow.processAllCustomEvents(); 3389 //if(this_.parentWindow) 3390 //this_.parentWindow.actualRedraw(); 3391 3392 // and this ensures the WM_PAINT message is sent fairly quickly 3393 // still seems to lag a little in large windows but meh it basically works. 3394 if(this_.parentWindow) { 3395 // FIXME: if painting is slow, this does still lag 3396 // we probably will want to expose some user hook to ScrollWindowEx 3397 // or something. 3398 UpdateWindow(this_.parentWindow.hwnd); 3399 } 3400 return 0; 3401 default: 3402 } 3403 } 3404 break; 3405 3406 case WM_CONTEXTMENU: 3407 auto hwndFrom = cast(HWND) wParam; 3408 3409 auto xPos = cast(short) LOWORD(lParam); 3410 auto yPos = cast(short) HIWORD(lParam); 3411 3412 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3413 POINT p; 3414 p.x = xPos; 3415 p.y = yPos; 3416 ScreenToClient(hwnd, &p); 3417 auto clientX = cast(ushort) p.x; 3418 auto clientY = cast(ushort) p.y; 3419 3420 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 3421 3422 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 3423 return 0; 3424 } 3425 } 3426 break; 3427 3428 case WM_DRAWITEM: 3429 auto dis = cast(DRAWITEMSTRUCT*) lParam; 3430 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 3431 return (*widgetp).handleWmDrawItem(dis); 3432 } 3433 break; 3434 3435 case WM_NOTIFY: 3436 auto hdr = cast(NMHDR*) lParam; 3437 auto hwndFrom = hdr.hwndFrom; 3438 auto code = hdr.code; 3439 3440 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3441 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 3442 } 3443 break; 3444 case WM_COMMAND: 3445 auto handle = cast(HWND) lParam; 3446 auto cmd = HIWORD(wParam); 3447 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 3448 3449 default: 3450 // pass it on 3451 } 3452 return 0; 3453 } 3454 3455 3456 3457 extern(Windows) 3458 private 3459 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 3460 // but can i merge them?! 3461 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3462 // try { writeln(iMessage); } catch(Exception e) {}; 3463 3464 if(auto te = hWnd in Widget.nativeMapping) { 3465 try { 3466 3467 te.hookedWndProc(iMessage, wParam, lParam); 3468 3469 int mustReturn; 3470 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 3471 if(mustReturn) 3472 return ret; 3473 3474 if(iMessage == WM_SETFOCUS) { 3475 auto lol = *te; 3476 while(lol !is null && lol.implicitlyCreated) 3477 lol = lol.parent; 3478 lol.focus(); 3479 //(*te).parentWindow.focusedWidget = lol; 3480 } 3481 3482 3483 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 3484 SetBkMode(cast(HDC) wParam, TRANSPARENT); 3485 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 3486 //GetStockObject(NULL_BRUSH); 3487 } 3488 3489 auto pos = getChildPositionRelativeToParentOrigin(*te); 3490 lastDefaultPrevented = false; 3491 // try { writeln(typeid(*te)); } catch(Exception e) {} 3492 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 3493 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 3494 else { 3495 // it was something we recognized, should only call the window procedure if the default was not prevented 3496 } 3497 } catch(Exception e) { 3498 assert(0, e.toString()); 3499 } 3500 return 0; 3501 } 3502 assert(0, "shouldn't be receiving messages for this window...."); 3503 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 3504 } 3505 3506 extern(Windows) 3507 private 3508 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 3509 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3510 if(iMessage == WM_ERASEBKGND) { 3511 auto dc = GetDC(hWnd); 3512 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 3513 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 3514 RECT r; 3515 GetWindowRect(hWnd, &r); 3516 // since the pen is null, to fill the whole space, we need the +1 on both. 3517 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 3518 SelectObject(dc, p); 3519 SelectObject(dc, b); 3520 ReleaseDC(hWnd, dc); 3521 InvalidateRect(hWnd, null, false); // redraw the border 3522 return 1; 3523 } 3524 return HookedWndProc(hWnd, iMessage, wParam, lParam); 3525 } 3526 3527 /++ 3528 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 3529 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 3530 of minigui's expectations. 3531 3532 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 3533 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 3534 3535 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 3536 3537 To check if you can use this, use `static if(UsingWin32Widgets)`. 3538 +/ 3539 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 3540 assert(p.parentWindow !is null); 3541 assert(p.parentWindow.win.impl.hwnd !is null); 3542 3543 auto bsgroupbox = style == BS_GROUPBOX; 3544 3545 HWND phwnd; 3546 3547 auto wtf = p.parent; 3548 while(wtf) { 3549 if(wtf.hwnd !is null) { 3550 phwnd = wtf.hwnd; 3551 break; 3552 } 3553 wtf = wtf.parent; 3554 } 3555 3556 if(phwnd is null) 3557 phwnd = p.parentWindow.win.impl.hwnd; 3558 3559 assert(phwnd !is null); 3560 3561 WCharzBuffer wt = WCharzBuffer(windowText); 3562 3563 style |= WS_VISIBLE | WS_CHILD; 3564 //if(className != WC_TABCONTROL) 3565 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 3566 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 3567 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 3568 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 3569 3570 assert(p.hwnd !is null); 3571 3572 3573 static HFONT font; 3574 if(font is null) { 3575 NONCLIENTMETRICS params; 3576 params.cbSize = params.sizeof; 3577 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 3578 font = CreateFontIndirect(¶ms.lfMessageFont); 3579 } 3580 } 3581 3582 if(font) 3583 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 3584 3585 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 3586 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 3587 Widget.nativeMapping[p.hwnd] = p; 3588 3589 if(bsgroupbox) 3590 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 3591 else 3592 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3593 3594 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 3595 3596 p.registerMovement(); 3597 } 3598 } 3599 3600 version(win32_widgets) 3601 private 3602 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 3603 if(hwnd is null || hwnd in Widget.nativeMapping) 3604 return true; 3605 auto parent = cast(Widget) cast(void*) lparam; 3606 Widget p = new Widget(null); 3607 p._parent = parent; 3608 p.parentWindow = parent.parentWindow; 3609 p.hwnd = hwnd; 3610 p.implicitlyCreated = true; 3611 Widget.nativeMapping[p.hwnd] = p; 3612 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3613 return true; 3614 } 3615 3616 /++ 3617 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 3618 +/ 3619 struct WidgetPainter { 3620 this(ScreenPainter screenPainter, Widget drawingUpon) { 3621 this.drawingUpon = drawingUpon; 3622 this.screenPainter = screenPainter; 3623 if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 3624 this.screenPainter.setFont(font); 3625 } 3626 3627 /++ 3628 EXPERIMENTAL. subject to change. 3629 3630 When you draw a cursor, you can draw this to notify your window of where it is, 3631 for IME systems to use. 3632 +/ 3633 void notifyCursorPosition(int x, int y, int width, int height) { 3634 if(auto a = drawingUpon.parentWindow) 3635 if(auto w = a.inputProxy) { 3636 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 3637 } 3638 } 3639 3640 3641 /// 3642 ScreenPainter screenPainter; 3643 /// Forward to the screen painter for other methods 3644 alias screenPainter this; 3645 3646 private Widget drawingUpon; 3647 3648 /++ 3649 This is the list of rectangles that actually need to be redrawn. 3650 3651 Not actually implemented yet. 3652 +/ 3653 Rectangle[] invalidatedRectangles; 3654 3655 private static BaseVisualTheme _visualTheme; 3656 3657 /++ 3658 Functions to access the visual theme and helpers to easily use it. 3659 3660 These are aware of the current widget's computed style out of the theme. 3661 +/ 3662 static @property BaseVisualTheme visualTheme() { 3663 if(_visualTheme is null) 3664 _visualTheme = new DefaultVisualTheme(); 3665 return _visualTheme; 3666 } 3667 3668 /// ditto 3669 static @property void visualTheme(BaseVisualTheme theme) { 3670 _visualTheme = theme; 3671 3672 // FIXME: notify all windows about the new theme 3673 } 3674 3675 /// ditto 3676 Color themeForeground() { 3677 return drawingUpon.getComputedStyle().foregroundColor(); 3678 } 3679 3680 /// ditto 3681 Color themeBackground() { 3682 return drawingUpon.getComputedStyle().background.color; 3683 } 3684 3685 int isDarkTheme() { 3686 return 0; // unspecified, yes, no as enum. FIXME 3687 } 3688 3689 /++ 3690 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. 3691 3692 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 3693 3694 If you change teh clip rectangle, you should change it back before you return. 3695 3696 3697 The sequence it uses is: 3698 background 3699 content (delegated to you) 3700 border 3701 focused outline 3702 selected overlay 3703 3704 Example code: 3705 3706 --- 3707 void paint(WidgetPainter painter) { 3708 painter.drawThemed((bounds) { 3709 return bounds; // if the selection overlay should be contained, you can return it here. 3710 }); 3711 } 3712 --- 3713 +/ 3714 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 3715 drawThemed((WidgetPainter painter, const Rectangle bounds) { 3716 return drawBody(bounds); 3717 }); 3718 } 3719 // this overload is actually mroe for setting the delegate to a virtual function 3720 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 3721 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 3722 3723 auto cs = drawingUpon.getComputedStyle(); 3724 3725 auto bg = cs.background.color; 3726 3727 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 3728 3729 rect.left += borderWidth; 3730 rect.right -= borderWidth; 3731 rect.top += borderWidth; 3732 rect.bottom -= borderWidth; 3733 3734 auto insideBorderRect = rect; 3735 3736 rect.left += cs.paddingLeft; 3737 rect.right -= cs.paddingRight; 3738 rect.top += cs.paddingTop; 3739 rect.bottom -= cs.paddingBottom; 3740 3741 this.outlineColor = this.themeForeground; 3742 this.fillColor = bg; 3743 3744 auto widgetFont = cs.fontCached; 3745 if(widgetFont !is null) 3746 this.setFont(widgetFont); 3747 3748 rect = drawBody(this, rect); 3749 3750 if(widgetFont !is null) { 3751 if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 3752 this.setFont(vtFont); 3753 else 3754 this.setFont(null); 3755 } 3756 3757 if(auto os = cs.outlineStyle()) { 3758 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 3759 this.fillColor = Color.transparent; 3760 this.drawRectangle(insideBorderRect); 3761 } 3762 } 3763 3764 /++ 3765 First, draw the background. 3766 Then draw your content. 3767 Next, draw the border. 3768 And the focused indicator. 3769 And the is-selected box. 3770 3771 If it is focused i can draw the outline too... 3772 3773 If selected i can even do the xor action but that's at the end. 3774 +/ 3775 void drawThemeBackground() { 3776 3777 } 3778 3779 void drawThemeBorder() { 3780 3781 } 3782 3783 // all this stuff is a dangerous experiment.... 3784 static class ScriptableVersion { 3785 ScreenPainterImplementation* p; 3786 int originX, originY; 3787 3788 @scriptable: 3789 void drawRectangle(int x, int y, int width, int height) { 3790 p.drawRectangle(x + originX, y + originY, width, height); 3791 } 3792 void drawLine(int x1, int y1, int x2, int y2) { 3793 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 3794 } 3795 void drawText(int x, int y, string text) { 3796 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 3797 } 3798 void setOutlineColor(int r, int g, int b) { 3799 p.pen = Pen(Color(r,g,b), 1); 3800 } 3801 void setFillColor(int r, int g, int b) { 3802 p.fillColor = Color(r,g,b); 3803 } 3804 } 3805 3806 ScriptableVersion toArsdJsvar() { 3807 auto sv = new ScriptableVersion; 3808 sv.p = this.screenPainter.impl; 3809 sv.originX = this.screenPainter.originX; 3810 sv.originY = this.screenPainter.originY; 3811 return sv; 3812 } 3813 3814 static WidgetPainter fromJsVar(T)(T t) { 3815 return WidgetPainter.init; 3816 } 3817 // done.......... 3818 } 3819 3820 3821 struct Style { 3822 static struct helper(string m, T) { 3823 enum method = m; 3824 T v; 3825 3826 mixin template MethodOverride(typeof(this) v) { 3827 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 3828 } 3829 } 3830 3831 static auto opDispatch(string method, T)(T value) { 3832 return helper!(method, T)(value); 3833 } 3834 } 3835 3836 /++ 3837 Implementation detail of the [ControlledBy] UDA. 3838 3839 History: 3840 Added Oct 28, 2020 3841 +/ 3842 struct ControlledBy_(T, Args...) { 3843 Args args; 3844 3845 static if(Args.length) 3846 this(Args args) { 3847 this.args = args; 3848 } 3849 3850 private T construct(Widget parent) { 3851 return new T(args, parent); 3852 } 3853 } 3854 3855 /++ 3856 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 3857 3858 History: 3859 Added Oct 28, 2020 3860 +/ 3861 auto ControlledBy(T, Args...)(Args args) { 3862 return ControlledBy_!(T, Args)(args); 3863 } 3864 3865 struct ContainerMeta { 3866 string name; 3867 ContainerMeta[] children; 3868 Widget function(Widget parent) factory; 3869 3870 Widget instantiate(Widget parent) { 3871 auto n = factory(parent); 3872 n.name = name; 3873 foreach(child; children) 3874 child.instantiate(n); 3875 return n; 3876 } 3877 } 3878 3879 /++ 3880 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 3881 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 3882 3883 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 3884 structures. It works fine on structs declared inside functions though. 3885 3886 See: https://issues.dlang.org/show_bug.cgi?id=21984 3887 +/ 3888 template Container(CArgs...) { 3889 static if(CArgs.length && is(CArgs[0] : Widget)) { 3890 private alias Super = CArgs[0]; 3891 private alias CArgs2 = CArgs[1 .. $]; 3892 } else { 3893 private alias Super = Layout; 3894 private alias CArgs2 = CArgs; 3895 } 3896 3897 class Container : Super { 3898 this(Widget parent) { super(parent); } 3899 3900 // just to partially support old gdc versions 3901 version(GNU) { 3902 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 3903 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 3904 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 3905 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 3906 } else mixin(q{ 3907 static foreach(Arg; CArgs2) { 3908 mixin Arg.MethodOverride!(Arg); 3909 } 3910 }); 3911 3912 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 3913 return ContainerMeta( 3914 name, 3915 children.dup, 3916 function (Widget parent) { return new typeof(this)(parent); } 3917 ); 3918 } 3919 3920 static ContainerMeta opCall(ContainerMeta[] children...) { 3921 return opCall(null, children); 3922 } 3923 } 3924 } 3925 3926 /++ 3927 The data controller widget is created by reflecting over the given 3928 data type. You can use [ControlledBy] as a UDA on a struct or 3929 just let it create things automatically. 3930 3931 Unlike [dialog], this uses real-time updating of the data and 3932 you add it to another window yourself. 3933 3934 --- 3935 struct Test { 3936 int x; 3937 int y; 3938 } 3939 3940 auto window = new Window(); 3941 auto dcw = new DataControllerWidget!Test(new Test, window); 3942 --- 3943 3944 The way it works is any public members are given a widget based 3945 on their data type, and public methods trigger an action button 3946 if no relevant parameters or a dialog action if it does have 3947 parameters, similar to the [menu] facility. 3948 3949 If you change data programmatically, without going through the 3950 DataControllerWidget methods, you will have to tell it something 3951 has changed and it needs to redraw. This is done with the `invalidate` 3952 method. 3953 3954 History: 3955 Added Oct 28, 2020 3956 +/ 3957 /// Group: generating_from_code 3958 class DataControllerWidget(T) : WidgetContainer { 3959 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 3960 private alias Tref = T; 3961 else 3962 private alias Tref = T*; 3963 3964 Tref datum; 3965 3966 /++ 3967 See_also: [addDataControllerWidget] 3968 +/ 3969 this(Tref datum, Widget parent) { 3970 this.datum = datum; 3971 3972 Widget cp = this; 3973 3974 super(parent); 3975 3976 foreach(attr; __traits(getAttributes, T)) 3977 static if(is(typeof(attr) == ContainerMeta)) { 3978 cp = attr.instantiate(this); 3979 } 3980 3981 auto def = this.getByName("default"); 3982 if(def !is null) 3983 cp = def; 3984 3985 Widget helper(string name) { 3986 auto maybe = this.getByName(name); 3987 if(maybe is null) 3988 return cp; 3989 return maybe; 3990 3991 } 3992 3993 foreach(member; __traits(allMembers, T)) 3994 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 3995 static if(is(typeof(__traits(getMember, this.datum, member)))) 3996 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 3997 void delegate() update; 3998 3999 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 4000 4001 if(update) 4002 updaters ~= update; 4003 4004 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 4005 w.addEventListener("triggered", delegate() { 4006 makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))(); 4007 notifyDataUpdated(); 4008 }); 4009 } else static if(is(typeof(w.isChecked) == bool)) { 4010 w.addEventListener(EventType.change, (Event ev) { 4011 __traits(getMember, this.datum, member) = w.isChecked; 4012 }); 4013 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 4014 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 4015 } else static if(is(typeof(w.value) == int)) { 4016 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4017 } else static if(is(typeof(w) == DropDownSelection)) { 4018 // special case for this to kinda support enums and such. coudl be better though 4019 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4020 } else { 4021 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 4022 } 4023 } 4024 } 4025 4026 /++ 4027 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 4028 4029 History: 4030 Added May 28, 2021 4031 +/ 4032 void notifyDataUpdated() { 4033 foreach(updater; updaters) 4034 updater(); 4035 4036 this.emit!(ChangeEvent!void)(delegate{}); 4037 } 4038 4039 private Widget[string] memberWidgets; 4040 private void delegate()[] updaters; 4041 4042 mixin Emits!(ChangeEvent!void); 4043 } 4044 4045 private int saturatedSum(int[] values...) { 4046 int sum; 4047 foreach(value; values) { 4048 if(value == int.max) 4049 return int.max; 4050 sum += value; 4051 } 4052 return sum; 4053 } 4054 4055 void genericSetValue(T, W)(T* where, W what) { 4056 import std.conv; 4057 *where = to!T(what); 4058 //*where = cast(T) stringToLong(what); 4059 } 4060 4061 /++ 4062 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4063 4064 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4065 4066 Note that this creates the widget but does not attach any event handlers to it. 4067 +/ 4068 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 4069 4070 string displayName = __traits(identifier, tt).beautify; 4071 4072 static if(controlledByCount!tt == 1) { 4073 foreach(i, attr; __traits(getAttributes, tt)) { 4074 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4075 auto w = attr.construct(parent); 4076 static if(__traits(compiles, w.setPosition(*valptr))) 4077 update = () { w.setPosition(*valptr); }; 4078 else static if(__traits(compiles, w.setValue(*valptr))) 4079 update = () { w.setValue(*valptr); }; 4080 4081 if(update) 4082 update(); 4083 return w; 4084 } 4085 } 4086 } else static if(controlledByCount!tt == 0) { 4087 static if(is(typeof(tt) == enum)) { 4088 // FIXME: update 4089 auto dds = new DropDownSelection(parent); 4090 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4091 dds.addOption(option); 4092 if(__traits(getMember, typeof(tt), option) == *valptr) 4093 dds.setSelection(cast(int) idx); 4094 } 4095 return dds; 4096 } else static if(is(typeof(tt) == bool)) { 4097 auto box = new Checkbox(displayName, parent); 4098 update = () { box.isChecked = *valptr; }; 4099 update(); 4100 return box; 4101 } else static if(is(typeof(tt) : const long)) { 4102 auto le = new LabeledLineEdit(displayName, parent); 4103 update = () { le.content = toInternal!string(*valptr); }; 4104 update(); 4105 return le; 4106 } else static if(is(typeof(tt) : const double)) { 4107 auto le = new LabeledLineEdit(displayName, parent); 4108 import std.conv; 4109 update = () { le.content = to!string(*valptr); }; 4110 update(); 4111 return le; 4112 } else static if(is(typeof(tt) : const string)) { 4113 auto le = new LabeledLineEdit(displayName, parent); 4114 update = () { le.content = *valptr; }; 4115 update(); 4116 return le; 4117 } else static if(is(typeof(tt) == function)) { 4118 auto w = new Button(displayName, parent); 4119 return w; 4120 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4121 return parent.addDataControllerWidget(tt); 4122 } else static assert(0, typeof(tt).stringof); 4123 } else static assert(0, "multiple controllers not yet supported"); 4124 } 4125 4126 private template controlledByCount(alias tt) { 4127 static int helper() { 4128 int count; 4129 foreach(i, attr; __traits(getAttributes, tt)) 4130 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 4131 count++; 4132 return count; 4133 } 4134 4135 enum controlledByCount = helper; 4136 } 4137 4138 /++ 4139 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 4140 4141 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 4142 4143 History: 4144 The `redrawOnChange` parameter was added on May 28, 2021. 4145 +/ 4146 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 4147 auto dcw = new DataControllerWidget!T(t, parent); 4148 initializeDataControllerWidget(dcw, redrawOnChange); 4149 return dcw; 4150 } 4151 4152 /// ditto 4153 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 4154 auto dcw = new DataControllerWidget!T(t, parent); 4155 initializeDataControllerWidget(dcw, redrawOnChange); 4156 return dcw; 4157 } 4158 4159 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 4160 if(redrawOnChange !is null) 4161 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 4162 } 4163 4164 /++ 4165 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. 4166 4167 History: 4168 Finalized on June 3, 2021 for the dub v10.0 release 4169 +/ 4170 struct StyleInformation { 4171 private Widget w; 4172 private BaseVisualTheme visualTheme; 4173 4174 private this(Widget w) { 4175 this.w = w; 4176 this.visualTheme = WidgetPainter.visualTheme; 4177 } 4178 4179 /++ 4180 Forwards to [Widget.Style] 4181 4182 Bugs: 4183 It is supposed to fall back to the [VisualTheme] if 4184 the style doesn't override the default, but that is 4185 not generally implemented. Many of them may end up 4186 being explicit overloads instead of the generic 4187 opDispatch fallback, like [font] is now. 4188 +/ 4189 public @property opDispatch(string name)() { 4190 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4191 w.useStyleProperties((scope Widget.Style props) { 4192 //visualTheme.useStyleProperties(w, (props) { 4193 prop = __traits(getMember, props, name); 4194 }); 4195 return prop; 4196 } 4197 4198 /++ 4199 Returns the cached font object associated with the widget, 4200 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 4201 4202 History: 4203 Prior to March 21, 2022 (dub v10.7), `font` went through 4204 [opDispatch], which did not use the cache. You can now call it 4205 repeatedly without guilt. 4206 +/ 4207 public @property OperatingSystemFont font() { 4208 OperatingSystemFont prop; 4209 w.useStyleProperties((scope Widget.Style props) { 4210 prop = props.fontCached; 4211 }); 4212 if(prop is null) { 4213 prop = visualTheme.defaultFontCached(w.currentDpi); 4214 } 4215 return prop; 4216 } 4217 4218 @property { 4219 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 4220 /** */ int paddingLeft() { return w.paddingLeft(); } 4221 /** */ int paddingRight() { return w.paddingRight(); } 4222 /** */ int paddingTop() { return w.paddingTop(); } 4223 /** */ int paddingBottom() { return w.paddingBottom(); } 4224 4225 /** */ int marginLeft() { return w.marginLeft(); } 4226 /** */ int marginRight() { return w.marginRight(); } 4227 /** */ int marginTop() { return w.marginTop(); } 4228 /** */ int marginBottom() { return w.marginBottom(); } 4229 4230 /** */ int maxHeight() { return w.maxHeight(); } 4231 /** */ int minHeight() { return w.minHeight(); } 4232 4233 /** */ int maxWidth() { return w.maxWidth(); } 4234 /** */ int minWidth() { return w.minWidth(); } 4235 4236 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 4237 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 4238 4239 /** */ int heightStretchiness() { return w.heightStretchiness(); } 4240 /** */ int widthStretchiness() { return w.widthStretchiness(); } 4241 4242 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 4243 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 4244 4245 // Global helpers some of these are unstable. 4246 static: 4247 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4248 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4249 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4250 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4251 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 4252 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4253 4254 /** */ Color activeTabColor() { return lightAccentColor; } 4255 /** */ Color buttonColor() { return windowBackgroundColor; } 4256 /** */ Color depressedButtonColor() { return darkAccentColor; } 4257 /** */ Color hoveringColor() { return lightAccentColor; } 4258 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 4259 auto c = WidgetPainter.visualTheme.selectionColor(); 4260 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4261 } 4262 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4263 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4264 } 4265 4266 4267 4268 /+ 4269 4270 private static auto extractStyleProperty(string name)(Widget w) { 4271 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4272 w.useStyleProperties((props) { 4273 prop = __traits(getMember, props, name); 4274 }); 4275 return prop; 4276 } 4277 4278 // FIXME: clear this upon a X server disconnect 4279 private static OperatingSystemFont[string] fontCache; 4280 4281 T getProperty(T)(string name, lazy T default_) { 4282 if(visualTheme !is null) { 4283 auto str = visualTheme.getPropertyString(w, name); 4284 if(str is null) 4285 return default_; 4286 static if(is(T == Color)) 4287 return Color.fromString(str); 4288 else static if(is(T == Measurement)) 4289 return Measurement(cast(int) toInternal!int(str)); 4290 else static if(is(T == WidgetBackground)) 4291 return WidgetBackground.fromString(str); 4292 else static if(is(T == OperatingSystemFont)) { 4293 if(auto f = str in fontCache) 4294 return *f; 4295 else 4296 return fontCache[str] = new OperatingSystemFont(str); 4297 } else static if(is(T == FrameStyle)) { 4298 switch(str) { 4299 default: 4300 return FrameStyle.none; 4301 foreach(style; __traits(allMembers, FrameStyle)) 4302 case style: 4303 return __traits(getMember, FrameStyle, style); 4304 } 4305 } else static assert(0); 4306 } else 4307 return default_; 4308 } 4309 4310 static struct Measurement { 4311 int value; 4312 alias value this; 4313 } 4314 4315 @property: 4316 4317 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 4318 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 4319 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 4320 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 4321 4322 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 4323 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 4324 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 4325 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 4326 4327 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 4328 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 4329 4330 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 4331 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 4332 4333 4334 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 4335 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 4336 4337 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 4338 4339 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 4340 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 4341 4342 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 4343 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 4344 4345 4346 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4347 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4348 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4349 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4350 4351 Color activeTabColor() { return lightAccentColor; } 4352 Color buttonColor() { return windowBackgroundColor; } 4353 Color depressedButtonColor() { return darkAccentColor; } 4354 Color hoveringColor() { return Color(228, 228, 228); } 4355 Color activeListXorColor() { 4356 auto c = WidgetPainter.visualTheme.selectionColor(); 4357 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4358 } 4359 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4360 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4361 +/ 4362 } 4363 4364 4365 4366 // pragma(msg, __traits(classInstanceSize, Widget)); 4367 4368 /*private*/ template EventString(E) { 4369 static if(is(typeof(E.EventString))) 4370 enum EventString = E.EventString; 4371 else 4372 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 4373 } 4374 4375 /*private*/ template EventStringIdentifier(E) { 4376 string helper() { 4377 auto es = EventString!E; 4378 char[] id = new char[](es.length * 2); 4379 size_t idx; 4380 foreach(char ch; es) { 4381 id[idx++] = cast(char)('a' + (ch >> 4)); 4382 id[idx++] = cast(char)('a' + (ch & 0x0f)); 4383 } 4384 return cast(string) id; 4385 } 4386 4387 enum EventStringIdentifier = helper(); 4388 } 4389 4390 4391 template classStaticallyEmits(This, EventType) { 4392 static if(is(This Base == super)) 4393 static if(is(Base : Widget)) 4394 enum baseEmits = classStaticallyEmits!(Base, EventType); 4395 else 4396 enum baseEmits = false; 4397 else 4398 enum baseEmits = false; 4399 4400 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 4401 4402 enum classStaticallyEmits = thisEmits || baseEmits; 4403 } 4404 4405 /++ 4406 A helper to make widgets out of other native windows. 4407 4408 History: 4409 Factored out of OpenGlWidget on November 5, 2021 4410 +/ 4411 class NestedChildWindowWidget : Widget { 4412 SimpleWindow win; 4413 4414 /++ 4415 Used on X to send focus to the appropriate child window when requested by the window manager. 4416 4417 Normally returns its own nested window. Can also return another child or null to revert to the parent 4418 if you override it in a child class. 4419 4420 History: 4421 Added April 2, 2022 (dub v10.8) 4422 +/ 4423 SimpleWindow focusableWindow() { 4424 return win; 4425 } 4426 4427 /// 4428 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4429 this(SimpleWindow win, Widget parent) { 4430 this.parentWindow = parent.parentWindow; 4431 this.win = win; 4432 4433 super(parent); 4434 windowsetup(win); 4435 } 4436 4437 static protected SimpleWindow getParentWindow(Widget parent) { 4438 assert(parent !is null); 4439 SimpleWindow pwin = parent.parentWindow.win; 4440 4441 version(win32_widgets) { 4442 HWND phwnd; 4443 auto wtf = parent; 4444 while(wtf) { 4445 if(wtf.hwnd) { 4446 phwnd = wtf.hwnd; 4447 break; 4448 } 4449 wtf = wtf.parent; 4450 } 4451 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 4452 if(phwnd) 4453 pwin = new SimpleWindow(phwnd); 4454 } 4455 4456 return pwin; 4457 } 4458 4459 /++ 4460 Called upon the nested window being destroyed. 4461 Remember the window has already been destroyed at 4462 this point, so don't use the native handle for anything. 4463 4464 History: 4465 Added April 3, 2022 (dub v10.8) 4466 +/ 4467 protected void dispose() { 4468 4469 } 4470 4471 protected void windowsetup(SimpleWindow w) { 4472 /* 4473 win.onFocusChange = (bool getting) { 4474 if(getting) 4475 this.focus(); 4476 }; 4477 */ 4478 4479 /+ 4480 win.onFocusChange = (bool getting) { 4481 if(getting) { 4482 this.parentWindow.focusedWidget = this; 4483 this.emit!FocusEvent(); 4484 this.emit!FocusInEvent(); 4485 } else { 4486 this.emit!BlurEvent(); 4487 this.emit!FocusOutEvent(); 4488 } 4489 }; 4490 +/ 4491 4492 win.onDestroyed = () { 4493 this.dispose(); 4494 }; 4495 4496 version(win32_widgets) { 4497 Widget.nativeMapping[win.hwnd] = this; 4498 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4499 } else { 4500 win.setEventHandlers( 4501 (MouseEvent e) { 4502 Widget p = this; 4503 while(p ! is parentWindow) { 4504 e.x += p.x; 4505 e.y += p.y; 4506 p = p.parent; 4507 } 4508 parentWindow.dispatchMouseEvent(e); 4509 }, 4510 (KeyEvent e) { 4511 //writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 4512 parentWindow.dispatchKeyEvent(e); 4513 }, 4514 (dchar e) { 4515 parentWindow.dispatchCharEvent(e); 4516 }, 4517 ); 4518 } 4519 4520 } 4521 4522 override void showing(bool s, bool recalc) { 4523 auto cur = hidden; 4524 win.hidden = !s; 4525 if(cur != s && s) 4526 redraw(); 4527 } 4528 4529 /// OpenGL widgets cannot have child widgets. Do not call this. 4530 /* @disable */ final override void addChild(Widget, int) { 4531 throw new Error("cannot add children to OpenGL widgets"); 4532 } 4533 4534 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 4535 /// Keep in mind that events like mouse coordinates are still relative to your size. 4536 override void registerMovement() { 4537 // writefln("%d %d %d %d", x,y,width,height); 4538 version(win32_widgets) 4539 auto pos = getChildPositionRelativeToParentHwnd(this); 4540 else 4541 auto pos = getChildPositionRelativeToParentOrigin(this); 4542 win.moveResize(pos[0], pos[1], width, height); 4543 4544 registerMovementAdditionalWork(); 4545 sendResizeEvent(); 4546 } 4547 4548 abstract void registerMovementAdditionalWork(); 4549 } 4550 4551 /++ 4552 Nests an opengl capable window inside this window as a widget. 4553 4554 You may also just want to create an additional [SimpleWindow] with 4555 [OpenGlOptions.yes] yourself. 4556 4557 An OpenGL widget cannot have child widgets. It will throw if you try. 4558 +/ 4559 static if(OpenGlEnabled) 4560 class OpenGlWidget : NestedChildWindowWidget { 4561 4562 override void registerMovementAdditionalWork() { 4563 win.setAsCurrentOpenGlContext(); 4564 } 4565 4566 /// 4567 this(Widget parent) { 4568 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4569 super(win, parent); 4570 } 4571 4572 override void paint(WidgetPainter painter) { 4573 win.setAsCurrentOpenGlContext(); 4574 glViewport(0, 0, this.width, this.height); 4575 win.redrawOpenGlSceneNow(); 4576 } 4577 4578 void redrawOpenGlScene(void delegate() dg) { 4579 win.redrawOpenGlScene = dg; 4580 } 4581 } 4582 4583 /++ 4584 This demo shows how to draw text in an opengl scene. 4585 +/ 4586 unittest { 4587 import arsd.minigui; 4588 import arsd.ttf; 4589 4590 void main() { 4591 auto window = new Window(); 4592 4593 auto widget = new OpenGlWidget(window); 4594 4595 // old means non-shader code so compatible with glBegin etc. 4596 // tbh I haven't implemented new one in font yet... 4597 // anyway, declaring here, will construct soon. 4598 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 4599 4600 // this is a little bit awkward, calling some methods through 4601 // the underlying SimpleWindow `win` method, and you can't do this 4602 // on a nanovega widget due to conflicts so I should probably fix 4603 // the api to be a bit easier. But here it will work. 4604 // 4605 // Alternatively, you could load the font on the first draw, inside 4606 // the redrawOpenGlScene, and keep a flag so you don't do it every 4607 // time. That'd be a bit easier since the lib sets up the context 4608 // by then guaranteed. 4609 // 4610 // But still, I wanna show this. 4611 widget.win.visibleForTheFirstTime = delegate { 4612 // must set the opengl context 4613 widget.win.setAsCurrentOpenGlContext(); 4614 4615 // if you were doing a OpenGL 3+ shader, this 4616 // gets especially important to do in order. With 4617 // old-style opengl, I think you can even do it 4618 // in main(), but meh, let's show it more correctly. 4619 4620 // Anyway, now it is time to load the font from the 4621 // OS (you can alternatively load one from a .ttf file 4622 // you bundle with the application), then load the 4623 // font into texture for drawing. 4624 4625 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 4626 4627 assert(!osfont.isNull()); // make sure it actually loaded 4628 4629 // using typeof to avoid repeating the long name lol 4630 glfont = new typeof(glfont)( 4631 // get the raw data from the font for loading in here 4632 // since it doesn't use the OS function to draw the 4633 // text, we gotta treat it more as a file than as 4634 // a drawing api. 4635 osfont.getTtfBytes(), 4636 18, // need to respecify size since opengl world is different coordinate system 4637 4638 // these last two numbers are why it is called 4639 // "Limited" font. It only loads the characters 4640 // in the given range, since the texture atlas 4641 // it references is all a big image generated ahead 4642 // of time. You could maybe do the whole thing but 4643 // idk how much memory that is. 4644 // 4645 // But here, 0-128 represents the ASCII range, so 4646 // good enough for most English things, numeric labels, 4647 // etc. 4648 0, 4649 128 4650 ); 4651 }; 4652 4653 widget.redrawOpenGlScene = () { 4654 // now we can use the glfont's drawString function 4655 4656 // first some opengl setup. You can do this in one place 4657 // on window first visible too in many cases, just showing 4658 // here cuz it is easier for me. 4659 4660 // gonna need some alpha blending or it just looks awful 4661 glEnable(GL_BLEND); 4662 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 4663 glClearColor(0,0,0,0); 4664 glDepthFunc(GL_LEQUAL); 4665 4666 // Also need to enable 2d textures, since it draws the 4667 // font characters as images baked in 4668 glMatrixMode(GL_MODELVIEW); 4669 glLoadIdentity(); 4670 glDisable(GL_DEPTH_TEST); 4671 glEnable(GL_TEXTURE_2D); 4672 4673 // the orthographic matrix is best for 2d things like text 4674 // so let's set that up. This matrix makes the coordinates 4675 // in the opengl scene be one-to-one with the actual pixels 4676 // on screen. (Not necessarily best, you may wish to scale 4677 // things, but it does help keep fonts looking normal.) 4678 glMatrixMode(GL_PROJECTION); 4679 glLoadIdentity(); 4680 glOrtho(0, widget.width, widget.height, 0, 0, 1); 4681 4682 // you can do other glScale, glRotate, glTranslate, etc 4683 // to the matrix here of course if you want. 4684 4685 // note the x,y coordinates here are for the text baseline 4686 // NOT the upper-left corner. The baseline is like the line 4687 // in the notebook you write on. Most the letters are actually 4688 // above it, but some, like p and q, dip a bit below it. 4689 // 4690 // So if you're used to the upper left coordinate like the 4691 // rest of simpledisplay/minigui usually do, do the 4692 // y + glfont.ascent to bring it down a little. So this 4693 // example puts the string in the upper left of the window. 4694 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 4695 4696 // re color btw: the function sets a solid color internally, 4697 // but you actually COULD do your own thing for rainbow effects 4698 // and the sort if you wanted too, by pulling its guts out. 4699 // Just view its source for an idea of how it actually draws: 4700 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 4701 4702 // it gets a bit complicated with the character positioning, 4703 // but the opengl parts are fairly simple: bind a texture, 4704 // set the color, draw a quad for each letter. 4705 4706 4707 // the last optional argument there btw is a bounding box 4708 // it will/ use to word wrap and return an object you can 4709 // use to implement scrolling or pagination; it tells how 4710 // much of the string didn't fit in the box. But for simple 4711 // labels we can just ignore that. 4712 4713 4714 // I'd suggest drawing text as the last step, after you 4715 // do your other drawing. You might use the push/pop matrix 4716 // stuff to keep your place. You, in theory, should be able 4717 // to do text in a 3d space but I've never actually tried 4718 // that.... 4719 }; 4720 4721 window.loop(); 4722 } 4723 } 4724 4725 version(custom_widgets) 4726 private alias ListWidgetBase = ScrollableWidget; 4727 else 4728 private alias ListWidgetBase = Widget; 4729 4730 /++ 4731 A list widget contains a list of strings that the user can examine and select. 4732 4733 4734 In the future, items in the list may be possible to be more than just strings. 4735 4736 See_Also: 4737 [TableView] 4738 +/ 4739 class ListWidget : ListWidgetBase { 4740 /// 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. 4741 mixin Emits!(ChangeEvent!void); 4742 4743 static struct Option { 4744 string label; 4745 bool selected; 4746 void* tag; 4747 } 4748 4749 /++ 4750 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 4751 +/ 4752 void setSelection(int y) { 4753 if(!multiSelect) 4754 foreach(ref opt; options) 4755 opt.selected = false; 4756 if(y >= 0 && y < options.length) 4757 options[y].selected = !options[y].selected; 4758 4759 this.emit!(ChangeEvent!void)(delegate {}); 4760 4761 version(custom_widgets) 4762 redraw(); 4763 } 4764 4765 /++ 4766 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 4767 Returns -1 if nothing is selected. 4768 +/ 4769 int getSelection() 4770 { 4771 foreach(i, opt; options) { 4772 if (opt.selected) 4773 return cast(int) i; 4774 } 4775 return -1; 4776 } 4777 4778 version(custom_widgets) 4779 override void defaultEventHandler_click(ClickEvent event) { 4780 this.focus(); 4781 if(event.button == MouseButton.left) { 4782 auto y = (event.clientY - 4) / defaultLineHeight; 4783 if(y >= 0 && y < options.length) { 4784 setSelection(y); 4785 } 4786 } 4787 super.defaultEventHandler_click(event); 4788 } 4789 4790 this(Widget parent) { 4791 tabStop = false; 4792 super(parent); 4793 version(win32_widgets) 4794 createWin32Window(this, WC_LISTBOX, "", 4795 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 4796 } 4797 4798 version(win32_widgets) 4799 override void handleWmCommand(ushort code, ushort id) { 4800 switch(code) { 4801 case LBN_SELCHANGE: 4802 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 4803 setSelection(cast(int) sel); 4804 break; 4805 default: 4806 } 4807 } 4808 4809 4810 version(custom_widgets) 4811 override void paintFrameAndBackground(WidgetPainter painter) { 4812 draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor); 4813 } 4814 4815 version(custom_widgets) 4816 override void paint(WidgetPainter painter) { 4817 auto cs = getComputedStyle(); 4818 auto pos = Point(4, 4); 4819 foreach(idx, option; options) { 4820 painter.fillColor = painter.visualTheme.widgetBackgroundColor; 4821 painter.outlineColor = painter.visualTheme.widgetBackgroundColor; 4822 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4823 if(option.selected) { 4824 //painter.rasterOp = RasterOp.xor; 4825 painter.outlineColor = cs.selectionForegroundColor; 4826 painter.fillColor = cs.selectionBackgroundColor; 4827 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4828 //painter.rasterOp = RasterOp.normal; 4829 } 4830 painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor; 4831 painter.drawText(pos, option.label); 4832 pos.y += defaultLineHeight; 4833 } 4834 } 4835 4836 static class Style : Widget.Style { 4837 override WidgetBackground background() { 4838 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 4839 } 4840 } 4841 mixin OverrideStyle!Style; 4842 //mixin Padding!q{2}; 4843 4844 void addOption(string text, void* tag = null) { 4845 options ~= Option(text, false, tag); 4846 version(win32_widgets) { 4847 WCharzBuffer buffer = WCharzBuffer(text); 4848 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 4849 } 4850 version(custom_widgets) { 4851 setContentSize(width, cast(int) (options.length * defaultLineHeight)); 4852 redraw(); 4853 } 4854 } 4855 4856 void clear() { 4857 options = null; 4858 version(win32_widgets) { 4859 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 4860 {} 4861 4862 } else version(custom_widgets) { 4863 scrollTo(Point(0, 0)); 4864 redraw(); 4865 } 4866 } 4867 4868 Option[] options; 4869 version(win32_widgets) 4870 enum multiSelect = false; /// not implemented yet 4871 else 4872 bool multiSelect; 4873 4874 override int heightStretchiness() { return 6; } 4875 } 4876 4877 4878 4879 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 4880 enum ScrollBarShowPolicy { 4881 automatic, /// automatically show the scroll bar if it is necessary 4882 never, /// never show the scroll bar (scrolling must be done programmatically) 4883 always /// always show the scroll bar, even if it is disabled 4884 } 4885 4886 /++ 4887 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 4888 4889 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 4890 +/ 4891 // FIXME ScrollBarShowPolicy 4892 // FIXME: use the ScrollMessageWidget in here now that it exists 4893 class ScrollableWidget : Widget { 4894 // FIXME: make line size configurable 4895 // FIXME: add keyboard controls 4896 version(win32_widgets) { 4897 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 4898 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 4899 auto pos = HIWORD(wParam); 4900 auto m = LOWORD(wParam); 4901 4902 // FIXME: I can reintroduce the 4903 // scroll bars now by using this 4904 // in the top-level window handler 4905 // to forward comamnds 4906 auto scrollbarHwnd = lParam; 4907 switch(m) { 4908 case SB_BOTTOM: 4909 if(msg == WM_HSCROLL) 4910 horizontalScrollTo(contentWidth_); 4911 else 4912 verticalScrollTo(contentHeight_); 4913 break; 4914 case SB_TOP: 4915 if(msg == WM_HSCROLL) 4916 horizontalScrollTo(0); 4917 else 4918 verticalScrollTo(0); 4919 break; 4920 case SB_ENDSCROLL: 4921 // idk 4922 break; 4923 case SB_LINEDOWN: 4924 if(msg == WM_HSCROLL) 4925 horizontalScroll(scaleWithDpi(16)); 4926 else 4927 verticalScroll(scaleWithDpi(16)); 4928 break; 4929 case SB_LINEUP: 4930 if(msg == WM_HSCROLL) 4931 horizontalScroll(scaleWithDpi(-16)); 4932 else 4933 verticalScroll(scaleWithDpi(-16)); 4934 break; 4935 case SB_PAGEDOWN: 4936 if(msg == WM_HSCROLL) 4937 horizontalScroll(scaleWithDpi(100)); 4938 else 4939 verticalScroll(scaleWithDpi(100)); 4940 break; 4941 case SB_PAGEUP: 4942 if(msg == WM_HSCROLL) 4943 horizontalScroll(scaleWithDpi(-100)); 4944 else 4945 verticalScroll(scaleWithDpi(-100)); 4946 break; 4947 case SB_THUMBPOSITION: 4948 case SB_THUMBTRACK: 4949 if(msg == WM_HSCROLL) 4950 horizontalScrollTo(pos); 4951 else 4952 verticalScrollTo(pos); 4953 4954 if(m == SB_THUMBTRACK) { 4955 // the event loop doesn't seem to carry on with a requested redraw.. 4956 // so we request it to get our dirty bit set... 4957 redraw(); 4958 4959 // then we need to immediately actually redraw it too for instant feedback to user 4960 4961 SimpleWindow.processAllCustomEvents(); 4962 //if(parentWindow) 4963 //parentWindow.actualRedraw(); 4964 } 4965 break; 4966 default: 4967 } 4968 } 4969 return super.hookedWndProc(msg, wParam, lParam); 4970 } 4971 } 4972 /// 4973 this(Widget parent) { 4974 this.parentWindow = parent.parentWindow; 4975 4976 version(win32_widgets) { 4977 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 4978 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 4979 super(parent); 4980 } else version(custom_widgets) { 4981 outerContainer = new InternalScrollableContainerWidget(this, parent); 4982 super(outerContainer); 4983 } else static assert(0); 4984 } 4985 4986 version(custom_widgets) 4987 InternalScrollableContainerWidget outerContainer; 4988 4989 override void defaultEventHandler_click(ClickEvent event) { 4990 if(event.button == MouseButton.wheelUp) 4991 verticalScroll(scaleWithDpi(-16)); 4992 if(event.button == MouseButton.wheelDown) 4993 verticalScroll(scaleWithDpi(16)); 4994 super.defaultEventHandler_click(event); 4995 } 4996 4997 override void defaultEventHandler_keydown(KeyDownEvent event) { 4998 switch(event.key) { 4999 case Key.Left: 5000 horizontalScroll(scaleWithDpi(-16)); 5001 break; 5002 case Key.Right: 5003 horizontalScroll(scaleWithDpi(16)); 5004 break; 5005 case Key.Up: 5006 verticalScroll(scaleWithDpi(-16)); 5007 break; 5008 case Key.Down: 5009 verticalScroll(scaleWithDpi(16)); 5010 break; 5011 case Key.Home: 5012 verticalScrollTo(0); 5013 break; 5014 case Key.End: 5015 verticalScrollTo(contentHeight); 5016 break; 5017 case Key.PageUp: 5018 verticalScroll(scaleWithDpi(-160)); 5019 break; 5020 case Key.PageDown: 5021 verticalScroll(scaleWithDpi(160)); 5022 break; 5023 default: 5024 } 5025 super.defaultEventHandler_keydown(event); 5026 } 5027 5028 5029 version(win32_widgets) 5030 override void recomputeChildLayout() { 5031 super.recomputeChildLayout(); 5032 SCROLLINFO info; 5033 info.cbSize = info.sizeof; 5034 info.nPage = viewportHeight; 5035 info.fMask = SIF_PAGE | SIF_RANGE; 5036 info.nMin = 0; 5037 info.nMax = contentHeight_; 5038 SetScrollInfo(hwnd, SB_VERT, &info, true); 5039 5040 info.cbSize = info.sizeof; 5041 info.nPage = viewportWidth; 5042 info.fMask = SIF_PAGE | SIF_RANGE; 5043 info.nMin = 0; 5044 info.nMax = contentWidth_; 5045 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5046 } 5047 5048 /* 5049 Scrolling 5050 ------------ 5051 5052 You are assigned a width and a height by the layout engine, which 5053 is your viewport box. However, you may draw more than that by setting 5054 a contentWidth and contentHeight. 5055 5056 If these can be contained by the viewport, no scrollbar is displayed. 5057 If they cannot fit though, it will automatically show scroll as necessary. 5058 5059 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 5060 is zero, no vertical scrolling is performed. 5061 5062 If scrolling is necessary, the lib will automatically work with the bars. 5063 When you redraw, the origin and clipping info in the painter is set so if 5064 you just draw everything, it will work, but you can be more efficient by checking 5065 the viewportWidth, viewportHeight, and scrollOrigin members. 5066 */ 5067 5068 /// 5069 final @property int viewportWidth() { 5070 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 5071 } 5072 /// 5073 final @property int viewportHeight() { 5074 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 5075 } 5076 5077 // FIXME property 5078 Point scrollOrigin_; 5079 5080 /// 5081 final const(Point) scrollOrigin() { 5082 return scrollOrigin_; 5083 } 5084 5085 // the user sets these two 5086 private int contentWidth_ = 0; 5087 private int contentHeight_ = 0; 5088 5089 /// 5090 int contentWidth() { return contentWidth_; } 5091 /// 5092 int contentHeight() { return contentHeight_; } 5093 5094 /// 5095 void setContentSize(int width, int height) { 5096 contentWidth_ = width; 5097 contentHeight_ = height; 5098 5099 version(custom_widgets) { 5100 if(showingVerticalScroll || showingHorizontalScroll) { 5101 outerContainer.recomputeChildLayout(); 5102 } 5103 5104 if(showingVerticalScroll()) 5105 outerContainer.verticalScrollBar.redraw(); 5106 if(showingHorizontalScroll()) 5107 outerContainer.horizontalScrollBar.redraw(); 5108 } else version(win32_widgets) { 5109 recomputeChildLayout(); 5110 } else static assert(0); 5111 } 5112 5113 /// 5114 void verticalScroll(int delta) { 5115 verticalScrollTo(scrollOrigin.y + delta); 5116 } 5117 /// 5118 void verticalScrollTo(int pos) { 5119 scrollOrigin_.y = pos; 5120 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 5121 scrollOrigin_.y = contentHeight - viewportHeight; 5122 5123 if(scrollOrigin_.y < 0) 5124 scrollOrigin_.y = 0; 5125 5126 version(win32_widgets) { 5127 SCROLLINFO info; 5128 info.cbSize = info.sizeof; 5129 info.fMask = SIF_POS; 5130 info.nPos = scrollOrigin_.y; 5131 SetScrollInfo(hwnd, SB_VERT, &info, true); 5132 } else version(custom_widgets) { 5133 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 5134 } else static assert(0); 5135 5136 redraw(); 5137 } 5138 5139 /// 5140 void horizontalScroll(int delta) { 5141 horizontalScrollTo(scrollOrigin.x + delta); 5142 } 5143 /// 5144 void horizontalScrollTo(int pos) { 5145 scrollOrigin_.x = pos; 5146 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 5147 scrollOrigin_.x = contentWidth - viewportWidth; 5148 5149 if(scrollOrigin_.x < 0) 5150 scrollOrigin_.x = 0; 5151 5152 version(win32_widgets) { 5153 SCROLLINFO info; 5154 info.cbSize = info.sizeof; 5155 info.fMask = SIF_POS; 5156 info.nPos = scrollOrigin_.x; 5157 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5158 } else version(custom_widgets) { 5159 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 5160 } else static assert(0); 5161 5162 redraw(); 5163 } 5164 /// 5165 void scrollTo(Point p) { 5166 verticalScrollTo(p.y); 5167 horizontalScrollTo(p.x); 5168 } 5169 5170 /// 5171 void ensureVisibleInScroll(Point p) { 5172 auto rect = viewportRectangle(); 5173 if(rect.contains(p)) 5174 return; 5175 if(p.x < rect.left) 5176 horizontalScroll(p.x - rect.left); 5177 else if(p.x > rect.right) 5178 horizontalScroll(p.x - rect.right); 5179 5180 if(p.y < rect.top) 5181 verticalScroll(p.y - rect.top); 5182 else if(p.y > rect.bottom) 5183 verticalScroll(p.y - rect.bottom); 5184 } 5185 5186 /// 5187 void ensureVisibleInScroll(Rectangle rect) { 5188 ensureVisibleInScroll(rect.upperLeft); 5189 ensureVisibleInScroll(rect.lowerRight); 5190 } 5191 5192 /// 5193 Rectangle viewportRectangle() { 5194 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 5195 } 5196 5197 /// 5198 bool showingHorizontalScroll() { 5199 return contentWidth > width; 5200 } 5201 /// 5202 bool showingVerticalScroll() { 5203 return contentHeight > height; 5204 } 5205 5206 /// This is called before the ordinary paint delegate, 5207 /// giving you a chance to draw the window frame, etc, 5208 /// before the scroll clip takes effect 5209 void paintFrameAndBackground(WidgetPainter painter) { 5210 version(win32_widgets) { 5211 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5212 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5213 // since the pen is null, to fill the whole space, we need the +1 on both. 5214 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5215 SelectObject(painter.impl.hdc, p); 5216 SelectObject(painter.impl.hdc, b); 5217 } 5218 5219 } 5220 5221 // make space for the scroll bar, and that's it. 5222 final override int paddingRight() { return scaleWithDpi(16); } 5223 final override int paddingBottom() { return scaleWithDpi(16); } 5224 5225 /* 5226 END SCROLLING 5227 */ 5228 5229 override WidgetPainter draw() { 5230 int x = this.x, y = this.y; 5231 auto parent = this.parent; 5232 while(parent) { 5233 x += parent.x; 5234 y += parent.y; 5235 parent = parent.parent; 5236 } 5237 5238 //version(win32_widgets) { 5239 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5240 //} else { 5241 auto painter = parentWindow.win.draw(true); 5242 //} 5243 painter.originX = x; 5244 painter.originY = y; 5245 5246 painter.originX = painter.originX - scrollOrigin.x; 5247 painter.originY = painter.originY - scrollOrigin.y; 5248 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 5249 5250 return WidgetPainter(painter, this); 5251 } 5252 5253 mixin ScrollableChildren; 5254 } 5255 5256 // you need to have a Point scrollOrigin in the class somewhere 5257 // and a paintFrameAndBackground 5258 private mixin template ScrollableChildren() { 5259 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5260 if(hidden) 5261 return; 5262 5263 //version(win32_widgets) 5264 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5265 5266 painter.originX = lox + x; 5267 painter.originY = loy + y; 5268 5269 bool actuallyPainted = false; 5270 5271 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 5272 if(clip == Rectangle.init) 5273 return; 5274 5275 if(force || redrawRequested) { 5276 //painter.setClipRectangle(scrollOrigin, width, height); 5277 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5278 paintFrameAndBackground(painter); 5279 } 5280 5281 painter.originX = painter.originX - scrollOrigin.x; 5282 painter.originY = painter.originY - scrollOrigin.y; 5283 if(force || redrawRequested) { 5284 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 5285 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 5286 5287 //erase(painter); // we paintFrameAndBackground above so no need 5288 if(painter.visualTheme) 5289 painter.visualTheme.doPaint(this, painter); 5290 else 5291 paint(painter); 5292 5293 if(invalidate) { 5294 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5295 // children are contained inside this, so no need to do extra work 5296 invalidate = false; 5297 } 5298 5299 5300 actuallyPainted = true; 5301 redrawRequested = false; 5302 } 5303 foreach(child; children) { 5304 if(cast(FixedPosition) child) 5305 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5306 else 5307 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5308 } 5309 } 5310 } 5311 5312 private class InternalScrollableContainerInsideWidget : ContainerWidget { 5313 ScrollableContainerWidget scw; 5314 5315 this(ScrollableContainerWidget parent) { 5316 scw = parent; 5317 super(parent); 5318 } 5319 5320 version(custom_widgets) 5321 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5322 if(hidden) 5323 return; 5324 5325 bool actuallyPainted = false; 5326 5327 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 5328 5329 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 5330 if(clip == Rectangle.init) 5331 return; 5332 5333 painter.originX = lox + x - scrollOrigin.x; 5334 painter.originY = loy + y - scrollOrigin.y; 5335 if(force || redrawRequested) { 5336 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5337 5338 erase(painter); 5339 if(painter.visualTheme) 5340 painter.visualTheme.doPaint(this, painter); 5341 else 5342 paint(painter); 5343 5344 if(invalidate) { 5345 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5346 // children are contained inside this, so no need to do extra work 5347 invalidate = false; 5348 } 5349 5350 actuallyPainted = true; 5351 redrawRequested = false; 5352 } 5353 foreach(child; children) { 5354 if(cast(FixedPosition) child) 5355 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5356 else 5357 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5358 } 5359 } 5360 5361 version(custom_widgets) 5362 override protected void addScrollPosition(ref int x, ref int y) { 5363 x += scw.scrollX_; 5364 y += scw.scrollY_; 5365 } 5366 } 5367 5368 /++ 5369 A widget meant to contain other widgets that may need to scroll. 5370 5371 Currently buggy. 5372 5373 History: 5374 Added July 1, 2021 (dub v10.2) 5375 5376 On January 3, 2022, I tried to use it in a few other cases 5377 and found it only worked well in the original test case. Since 5378 it still sucks, I think I'm going to rewrite it again. 5379 +/ 5380 class ScrollableContainerWidget : ContainerWidget { 5381 /// 5382 this(Widget parent) { 5383 super(parent); 5384 5385 container = new InternalScrollableContainerInsideWidget(this); 5386 hsb = new HorizontalScrollbar(this); 5387 vsb = new VerticalScrollbar(this); 5388 5389 tabStop = false; 5390 container.tabStop = false; 5391 magic = true; 5392 5393 5394 vsb.addEventListener("scrolltonextline", () { 5395 scrollBy(0, scaleWithDpi(16)); 5396 }); 5397 vsb.addEventListener("scrolltopreviousline", () { 5398 scrollBy(0,scaleWithDpi( -16)); 5399 }); 5400 vsb.addEventListener("scrolltonextpage", () { 5401 scrollBy(0, container.height); 5402 }); 5403 vsb.addEventListener("scrolltopreviouspage", () { 5404 scrollBy(0, -container.height); 5405 }); 5406 vsb.addEventListener((scope ScrollToPositionEvent spe) { 5407 scrollTo(scrollX_, spe.value); 5408 }); 5409 5410 this.addEventListener(delegate (scope ClickEvent e) { 5411 if(e.button == MouseButton.wheelUp) { 5412 if(!e.defaultPrevented) 5413 scrollBy(0, scaleWithDpi(-16)); 5414 e.stopPropagation(); 5415 } else if(e.button == MouseButton.wheelDown) { 5416 if(!e.defaultPrevented) 5417 scrollBy(0, scaleWithDpi(16)); 5418 e.stopPropagation(); 5419 } 5420 }); 5421 } 5422 5423 /+ 5424 override void defaultEventHandler_click(ClickEvent e) { 5425 } 5426 +/ 5427 5428 override void removeAllChildren() { 5429 container.removeAllChildren(); 5430 } 5431 5432 void scrollTo(int x, int y) { 5433 scrollBy(x - scrollX_, y - scrollY_); 5434 } 5435 5436 void scrollBy(int x, int y) { 5437 auto ox = scrollX_; 5438 auto oy = scrollY_; 5439 5440 auto nx = ox + x; 5441 auto ny = oy + y; 5442 5443 if(nx < 0) 5444 nx = 0; 5445 if(ny < 0) 5446 ny = 0; 5447 5448 auto maxX = hsb.max - container.width; 5449 if(maxX < 0) maxX = 0; 5450 auto maxY = vsb.max - container.height; 5451 if(maxY < 0) maxY = 0; 5452 5453 if(nx > maxX) 5454 nx = maxX; 5455 if(ny > maxY) 5456 ny = maxY; 5457 5458 auto dx = nx - ox; 5459 auto dy = ny - oy; 5460 5461 if(dx || dy) { 5462 version(win32_widgets) 5463 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 5464 else { 5465 redraw(); 5466 } 5467 5468 hsb.setPosition = nx; 5469 vsb.setPosition = ny; 5470 5471 scrollX_ = nx; 5472 scrollY_ = ny; 5473 } 5474 } 5475 5476 private int scrollX_; 5477 private int scrollY_; 5478 5479 void setTotalArea(int width, int height) { 5480 hsb.setMax(width); 5481 vsb.setMax(height); 5482 } 5483 5484 /// 5485 void setViewableArea(int width, int height) { 5486 hsb.setViewableArea(width); 5487 vsb.setViewableArea(height); 5488 } 5489 5490 private bool magic; 5491 override void addChild(Widget w, int position = int.max) { 5492 if(magic) 5493 container.addChild(w, position); 5494 else 5495 super.addChild(w, position); 5496 } 5497 5498 override void recomputeChildLayout() { 5499 if(hsb is null || vsb is null || container is null) return; 5500 5501 /+ 5502 writeln(x, " ", y , " ", width, " ", height); 5503 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 5504 +/ 5505 5506 registerMovement(); 5507 5508 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 5509 hsb.x = 0; 5510 hsb.y = this.height - hsb.height; 5511 hsb.width = this.width - scaleWithDpi(16); 5512 hsb.recomputeChildLayout(); 5513 5514 vsb.width = scaleWithDpi(16); // FIXME? 5515 vsb.x = this.width - vsb.width; 5516 vsb.y = 0; 5517 vsb.height = this.height - scaleWithDpi(16); 5518 vsb.recomputeChildLayout(); 5519 5520 container.x = 0; 5521 container.y = 0; 5522 container.width = this.width - vsb.width; 5523 container.height = this.height - hsb.height; 5524 container.recomputeChildLayout(); 5525 5526 scrollX_ = 0; 5527 scrollY_ = 0; 5528 5529 hsb.setPosition(0); 5530 vsb.setPosition(0); 5531 5532 int mw, mh; 5533 Widget c = container; 5534 // FIXME: hack here to handle a layout inside... 5535 if(c.children.length == 1 && cast(Layout) c.children[0]) 5536 c = c.children[0]; 5537 foreach(child; c.children) { 5538 auto w = child.x + child.width; 5539 auto h = child.y + child.height; 5540 5541 if(w > mw) mw = w; 5542 if(h > mh) mh = h; 5543 } 5544 5545 setTotalArea(mw, mh); 5546 setViewableArea(width, height); 5547 } 5548 5549 override int minHeight() { return scaleWithDpi(64); } 5550 5551 HorizontalScrollbar hsb; 5552 VerticalScrollbar vsb; 5553 ContainerWidget container; 5554 } 5555 5556 5557 version(custom_widgets) 5558 private class InternalScrollableContainerWidget : Widget { 5559 5560 ScrollableWidget sw; 5561 5562 VerticalScrollbar verticalScrollBar; 5563 HorizontalScrollbar horizontalScrollBar; 5564 5565 this(ScrollableWidget sw, Widget parent) { 5566 this.sw = sw; 5567 5568 this.tabStop = false; 5569 5570 super(parent); 5571 5572 horizontalScrollBar = new HorizontalScrollbar(this); 5573 verticalScrollBar = new VerticalScrollbar(this); 5574 5575 horizontalScrollBar.showing_ = false; 5576 verticalScrollBar.showing_ = false; 5577 5578 horizontalScrollBar.addEventListener("scrolltonextline", { 5579 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 5580 sw.horizontalScrollTo(horizontalScrollBar.position); 5581 }); 5582 horizontalScrollBar.addEventListener("scrolltopreviousline", { 5583 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 5584 sw.horizontalScrollTo(horizontalScrollBar.position); 5585 }); 5586 verticalScrollBar.addEventListener("scrolltonextline", { 5587 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 5588 sw.verticalScrollTo(verticalScrollBar.position); 5589 }); 5590 verticalScrollBar.addEventListener("scrolltopreviousline", { 5591 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 5592 sw.verticalScrollTo(verticalScrollBar.position); 5593 }); 5594 horizontalScrollBar.addEventListener("scrolltonextpage", { 5595 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 5596 sw.horizontalScrollTo(horizontalScrollBar.position); 5597 }); 5598 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 5599 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 5600 sw.horizontalScrollTo(horizontalScrollBar.position); 5601 }); 5602 verticalScrollBar.addEventListener("scrolltonextpage", { 5603 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 5604 sw.verticalScrollTo(verticalScrollBar.position); 5605 }); 5606 verticalScrollBar.addEventListener("scrolltopreviouspage", { 5607 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 5608 sw.verticalScrollTo(verticalScrollBar.position); 5609 }); 5610 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 5611 horizontalScrollBar.setPosition(event.intValue); 5612 sw.horizontalScrollTo(horizontalScrollBar.position); 5613 }); 5614 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 5615 verticalScrollBar.setPosition(event.intValue); 5616 sw.verticalScrollTo(verticalScrollBar.position); 5617 }); 5618 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 5619 horizontalScrollBar.setPosition(event.intValue); 5620 sw.horizontalScrollTo(horizontalScrollBar.position); 5621 }); 5622 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 5623 verticalScrollBar.setPosition(event.intValue); 5624 }); 5625 } 5626 5627 // this is supposed to be basically invisible... 5628 override int minWidth() { return sw.minWidth; } 5629 override int minHeight() { return sw.minHeight; } 5630 override int maxWidth() { return sw.maxWidth; } 5631 override int maxHeight() { return sw.maxHeight; } 5632 override int widthStretchiness() { return sw.widthStretchiness; } 5633 override int heightStretchiness() { return sw.heightStretchiness; } 5634 override int marginLeft() { return sw.marginLeft; } 5635 override int marginRight() { return sw.marginRight; } 5636 override int marginTop() { return sw.marginTop; } 5637 override int marginBottom() { return sw.marginBottom; } 5638 override int paddingLeft() { return sw.paddingLeft; } 5639 override int paddingRight() { return sw.paddingRight; } 5640 override int paddingTop() { return sw.paddingTop; } 5641 override int paddingBottom() { return sw.paddingBottom; } 5642 override void focus() { sw.focus(); } 5643 5644 5645 override void recomputeChildLayout() { 5646 // The stupid thing needs to calculate if a scroll bar is needed... 5647 recomputeChildLayoutHelper(); 5648 // then running it again will position things correctly if the bar is NOT needed 5649 recomputeChildLayoutHelper(); 5650 5651 // this sucks but meh it barely works 5652 } 5653 5654 private void recomputeChildLayoutHelper() { 5655 if(sw is null) return; 5656 5657 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 5658 if(horizontalScrollBar && verticalScrollBar) { 5659 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 5660 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 5661 horizontalScrollBar.x = 0; 5662 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 5663 5664 verticalScrollBar.width = verticalScrollBar.minWidth(); 5665 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 5666 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 5667 verticalScrollBar.y = 0 + 2; 5668 5669 sw.x = 0; 5670 sw.y = 0; 5671 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 5672 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 5673 5674 if(sw.contentWidth_ <= this.width) 5675 sw.scrollOrigin_.x = 0; 5676 if(sw.contentHeight_ <= this.height) 5677 sw.scrollOrigin_.y = 0; 5678 5679 horizontalScrollBar.recomputeChildLayout(); 5680 verticalScrollBar.recomputeChildLayout(); 5681 sw.recomputeChildLayout(); 5682 } 5683 5684 if(sw.contentWidth_ <= this.width) 5685 sw.scrollOrigin_.x = 0; 5686 if(sw.contentHeight_ <= this.height) 5687 sw.scrollOrigin_.y = 0; 5688 5689 if(sw.showingHorizontalScroll()) 5690 horizontalScrollBar.showing(true, false); 5691 else 5692 horizontalScrollBar.showing(false, false); 5693 if(sw.showingVerticalScroll()) 5694 verticalScrollBar.showing(true, false); 5695 else 5696 verticalScrollBar.showing(false, false); 5697 5698 verticalScrollBar.setViewableArea(sw.viewportHeight()); 5699 verticalScrollBar.setMax(sw.contentHeight); 5700 verticalScrollBar.setPosition(sw.scrollOrigin.y); 5701 5702 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 5703 horizontalScrollBar.setMax(sw.contentWidth); 5704 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 5705 } 5706 } 5707 5708 /* 5709 class ScrollableClientWidget : Widget { 5710 this(Widget parent) { 5711 super(parent); 5712 } 5713 override void paint(WidgetPainter p) { 5714 parent.paint(p); 5715 } 5716 } 5717 */ 5718 5719 /++ 5720 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. 5721 +/ 5722 abstract class Slider : Widget { 5723 this(int min, int max, int step, Widget parent) { 5724 min_ = min; 5725 max_ = max; 5726 step_ = step; 5727 page_ = step; 5728 super(parent); 5729 } 5730 5731 private int min_; 5732 private int max_; 5733 private int step_; 5734 private int position_; 5735 private int page_; 5736 5737 // selection start and selection end 5738 // tics 5739 // tooltip? 5740 // some way to see and just type the value 5741 // win32 buddy controls are labels 5742 5743 /// 5744 void setMin(int a) { 5745 min_ = a; 5746 version(custom_widgets) 5747 redraw(); 5748 version(win32_widgets) 5749 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 5750 } 5751 /// 5752 int min() { 5753 return min_; 5754 } 5755 /// 5756 void setMax(int a) { 5757 max_ = a; 5758 version(custom_widgets) 5759 redraw(); 5760 version(win32_widgets) 5761 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 5762 } 5763 /// 5764 int max() { 5765 return max_; 5766 } 5767 /// 5768 void setPosition(int a) { 5769 if(a > max) 5770 a = max; 5771 if(a < min) 5772 a = min; 5773 position_ = a; 5774 version(custom_widgets) 5775 setPositionCustom(a); 5776 5777 version(win32_widgets) 5778 setPositionWindows(a); 5779 } 5780 version(win32_widgets) { 5781 protected abstract void setPositionWindows(int a); 5782 } 5783 5784 protected abstract int win32direction(); 5785 5786 /++ 5787 Alias for [position] for better compatibility with generic code. 5788 5789 History: 5790 Added October 5, 2021 5791 +/ 5792 @property int value() { 5793 return position; 5794 } 5795 5796 /// 5797 int position() { 5798 return position_; 5799 } 5800 /// 5801 void setStep(int a) { 5802 step_ = a; 5803 version(win32_widgets) 5804 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 5805 } 5806 /// 5807 int step() { 5808 return step_; 5809 } 5810 /// 5811 void setPageSize(int a) { 5812 page_ = a; 5813 version(win32_widgets) 5814 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 5815 } 5816 /// 5817 int pageSize() { 5818 return page_; 5819 } 5820 5821 private void notify() { 5822 auto event = new ChangeEvent!int(this, &this.position); 5823 event.dispatch(); 5824 } 5825 5826 version(win32_widgets) 5827 void win32Setup(int style) { 5828 createWin32Window(this, TRACKBAR_CLASS, "", 5829 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 5830 5831 // the trackbar sends the same messages as scroll, which 5832 // our other layer sends as these... just gonna translate 5833 // here 5834 this.addDirectEventListener("scrolltoposition", (Event event) { 5835 event.stopPropagation(); 5836 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 5837 notify(); 5838 }); 5839 this.addDirectEventListener("scrolltonextline", (Event event) { 5840 event.stopPropagation(); 5841 this.setPosition(this.position + this.step_ * this.win32direction); 5842 notify(); 5843 }); 5844 this.addDirectEventListener("scrolltopreviousline", (Event event) { 5845 event.stopPropagation(); 5846 this.setPosition(this.position - this.step_ * this.win32direction); 5847 notify(); 5848 }); 5849 this.addDirectEventListener("scrolltonextpage", (Event event) { 5850 event.stopPropagation(); 5851 this.setPosition(this.position + this.page_ * this.win32direction); 5852 notify(); 5853 }); 5854 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 5855 event.stopPropagation(); 5856 this.setPosition(this.position - this.page_ * this.win32direction); 5857 notify(); 5858 }); 5859 5860 setMin(min_); 5861 setMax(max_); 5862 setStep(step_); 5863 setPageSize(page_); 5864 } 5865 5866 version(custom_widgets) { 5867 protected MouseTrackingWidget thumb; 5868 5869 protected abstract void setPositionCustom(int a); 5870 5871 override void defaultEventHandler_keydown(KeyDownEvent event) { 5872 switch(event.key) { 5873 case Key.Up: 5874 case Key.Right: 5875 setPosition(position() - step() * win32direction); 5876 changed(); 5877 break; 5878 case Key.Down: 5879 case Key.Left: 5880 setPosition(position() + step() * win32direction); 5881 changed(); 5882 break; 5883 case Key.Home: 5884 setPosition(win32direction > 0 ? min() : max()); 5885 changed(); 5886 break; 5887 case Key.End: 5888 setPosition(win32direction > 0 ? max() : min()); 5889 changed(); 5890 break; 5891 case Key.PageUp: 5892 setPosition(position() - pageSize() * win32direction); 5893 changed(); 5894 break; 5895 case Key.PageDown: 5896 setPosition(position() + pageSize() * win32direction); 5897 changed(); 5898 break; 5899 default: 5900 } 5901 super.defaultEventHandler_keydown(event); 5902 } 5903 5904 protected void changed() { 5905 auto ev = new ChangeEvent!int(this, &position); 5906 ev.dispatch(); 5907 } 5908 } 5909 } 5910 5911 /++ 5912 5913 +/ 5914 class VerticalSlider : Slider { 5915 this(int min, int max, int step, Widget parent) { 5916 version(custom_widgets) 5917 initialize(); 5918 5919 super(min, max, step, parent); 5920 5921 version(win32_widgets) 5922 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 5923 } 5924 5925 protected override int win32direction() { 5926 return -1; 5927 } 5928 5929 version(win32_widgets) 5930 protected override void setPositionWindows(int a) { 5931 // the windows thing makes the top 0 and i don't like that. 5932 SendMessage(hwnd, TBM_SETPOS, true, max - a); 5933 } 5934 5935 version(custom_widgets) 5936 private void initialize() { 5937 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 5938 5939 thumb.tabStop = false; 5940 5941 thumb.thumbWidth = width; 5942 thumb.thumbHeight = scaleWithDpi(16); 5943 5944 thumb.addEventListener(EventType.change, () { 5945 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 5946 sx = max - sx; 5947 //informProgramThatUserChangedPosition(sx); 5948 5949 position_ = sx; 5950 5951 changed(); 5952 }); 5953 } 5954 5955 version(custom_widgets) 5956 override void recomputeChildLayout() { 5957 thumb.thumbWidth = this.width; 5958 super.recomputeChildLayout(); 5959 setPositionCustom(position_); 5960 } 5961 5962 version(custom_widgets) 5963 protected override void setPositionCustom(int a) { 5964 if(max()) 5965 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 5966 redraw(); 5967 } 5968 } 5969 5970 /++ 5971 5972 +/ 5973 class HorizontalSlider : Slider { 5974 this(int min, int max, int step, Widget parent) { 5975 version(custom_widgets) 5976 initialize(); 5977 5978 super(min, max, step, parent); 5979 5980 version(win32_widgets) 5981 win32Setup(TBS_HORZ); 5982 } 5983 5984 version(win32_widgets) 5985 protected override void setPositionWindows(int a) { 5986 SendMessage(hwnd, TBM_SETPOS, true, a); 5987 } 5988 5989 protected override int win32direction() { 5990 return 1; 5991 } 5992 5993 version(custom_widgets) 5994 private void initialize() { 5995 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 5996 5997 thumb.tabStop = false; 5998 5999 thumb.thumbWidth = scaleWithDpi(16); 6000 thumb.thumbHeight = height; 6001 6002 thumb.addEventListener(EventType.change, () { 6003 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 6004 //informProgramThatUserChangedPosition(sx); 6005 6006 position_ = sx; 6007 6008 changed(); 6009 }); 6010 } 6011 6012 version(custom_widgets) 6013 override void recomputeChildLayout() { 6014 thumb.thumbHeight = this.height; 6015 super.recomputeChildLayout(); 6016 setPositionCustom(position_); 6017 } 6018 6019 version(custom_widgets) 6020 protected override void setPositionCustom(int a) { 6021 if(max()) 6022 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 6023 redraw(); 6024 } 6025 } 6026 6027 6028 /// 6029 abstract class ScrollbarBase : Widget { 6030 /// 6031 this(Widget parent) { 6032 super(parent); 6033 tabStop = false; 6034 step_ = scaleWithDpi(16); 6035 } 6036 6037 private int viewableArea_; 6038 private int max_; 6039 private int step_;// = 16; 6040 private int position_; 6041 6042 /// 6043 bool atEnd() { 6044 return position_ + viewableArea_ >= max_; 6045 } 6046 6047 /// 6048 bool atStart() { 6049 return position_ == 0; 6050 } 6051 6052 /// 6053 void setViewableArea(int a) { 6054 viewableArea_ = a; 6055 version(custom_widgets) 6056 redraw(); 6057 } 6058 /// 6059 void setMax(int a) { 6060 max_ = a; 6061 version(custom_widgets) 6062 redraw(); 6063 } 6064 /// 6065 int max() { 6066 return max_; 6067 } 6068 /// 6069 void setPosition(int a) { 6070 auto logicalMax = max_ - viewableArea_; 6071 if(a == int.max) 6072 a = logicalMax; 6073 6074 if(a > logicalMax) 6075 a = logicalMax; 6076 if(a < 0) 6077 a = 0; 6078 6079 position_ = a; 6080 6081 version(custom_widgets) 6082 redraw(); 6083 } 6084 /// 6085 int position() { 6086 return position_; 6087 } 6088 /// 6089 void setStep(int a) { 6090 step_ = a; 6091 } 6092 /// 6093 int step() { 6094 return step_; 6095 } 6096 6097 // FIXME: remove this.... maybe 6098 /+ 6099 protected void informProgramThatUserChangedPosition(int n) { 6100 position_ = n; 6101 auto evt = new Event(EventType.change, this); 6102 evt.intValue = n; 6103 evt.dispatch(); 6104 } 6105 +/ 6106 6107 version(custom_widgets) { 6108 enum MIN_THUMB_SIZE = 8; 6109 6110 abstract protected int getBarDim(); 6111 int thumbSize() { 6112 if(viewableArea_ >= max_ || max_ == 0) 6113 return getBarDim(); 6114 6115 int res = viewableArea_ * getBarDim() / max_; 6116 6117 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 6118 res = scaleWithDpi(MIN_THUMB_SIZE); 6119 6120 return res; 6121 } 6122 6123 int thumbPosition() { 6124 /* 6125 viewableArea_ is the viewport height/width 6126 position_ is where we are 6127 */ 6128 //if(position_ + viewableArea_ >= max_) 6129 //return getBarDim - thumbSize; 6130 6131 auto maximumPossibleValue = getBarDim() - thumbSize; 6132 auto maximiumLogicalValue = max_ - viewableArea_; 6133 6134 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 6135 6136 return p; 6137 } 6138 } 6139 } 6140 6141 //public import mgt; 6142 6143 /++ 6144 A mouse tracking widget is one that follows the mouse when dragged inside it. 6145 6146 Concrete subclasses may include a scrollbar thumb and a volume control. 6147 +/ 6148 //version(custom_widgets) 6149 class MouseTrackingWidget : Widget { 6150 6151 /// 6152 int positionX() { return positionX_; } 6153 /// 6154 int positionY() { return positionY_; } 6155 6156 /// 6157 void positionX(int p) { positionX_ = p; } 6158 /// 6159 void positionY(int p) { positionY_ = p; } 6160 6161 private int positionX_; 6162 private int positionY_; 6163 6164 /// 6165 enum Orientation { 6166 horizontal, /// 6167 vertical, /// 6168 twoDimensional, /// 6169 } 6170 6171 private int thumbWidth_; 6172 private int thumbHeight_; 6173 6174 /// 6175 int thumbWidth() { return thumbWidth_; } 6176 /// 6177 int thumbHeight() { return thumbHeight_; } 6178 /// 6179 int thumbWidth(int a) { return thumbWidth_ = a; } 6180 /// 6181 int thumbHeight(int a) { return thumbHeight_ = a; } 6182 6183 private bool dragging; 6184 private bool hovering; 6185 private int startMouseX, startMouseY; 6186 6187 /// 6188 this(Orientation orientation, Widget parent) { 6189 super(parent); 6190 6191 //assert(parentWindow !is null); 6192 6193 addEventListener((MouseDownEvent event) { 6194 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6195 dragging = true; 6196 startMouseX = event.clientX - positionX; 6197 startMouseY = event.clientY - positionY; 6198 parentWindow.captureMouse(this); 6199 } else { 6200 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6201 positionX = event.clientX - thumbWidth / 2; 6202 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6203 positionY = event.clientY - thumbHeight / 2; 6204 6205 if(positionX + thumbWidth > this.width) 6206 positionX = this.width - thumbWidth; 6207 if(positionY + thumbHeight > this.height) 6208 positionY = this.height - thumbHeight; 6209 6210 if(positionX < 0) 6211 positionX = 0; 6212 if(positionY < 0) 6213 positionY = 0; 6214 6215 6216 // this.emit!(ChangeEvent!void)(); 6217 auto evt = new Event(EventType.change, this); 6218 evt.sendDirectly(); 6219 6220 redraw(); 6221 6222 } 6223 }); 6224 6225 addEventListener(EventType.mouseup, (Event event) { 6226 dragging = false; 6227 parentWindow.releaseMouseCapture(); 6228 }); 6229 6230 addEventListener(EventType.mouseout, (Event event) { 6231 if(!hovering) 6232 return; 6233 hovering = false; 6234 redraw(); 6235 }); 6236 6237 int lpx, lpy; 6238 6239 addEventListener((MouseMoveEvent event) { 6240 auto oh = hovering; 6241 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6242 hovering = true; 6243 } else { 6244 hovering = false; 6245 } 6246 if(!dragging) { 6247 if(hovering != oh) 6248 redraw(); 6249 return; 6250 } 6251 6252 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6253 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 6254 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6255 positionY = event.clientY - startMouseY; 6256 6257 if(positionX + thumbWidth > this.width) 6258 positionX = this.width - thumbWidth; 6259 if(positionY + thumbHeight > this.height) 6260 positionY = this.height - thumbHeight; 6261 6262 if(positionX < 0) 6263 positionX = 0; 6264 if(positionY < 0) 6265 positionY = 0; 6266 6267 if(positionX != lpx || positionY != lpy) { 6268 lpx = positionX; 6269 lpy = positionY; 6270 6271 auto evt = new Event(EventType.change, this); 6272 evt.sendDirectly(); 6273 } 6274 6275 redraw(); 6276 }); 6277 } 6278 6279 version(custom_widgets) 6280 override void paint(WidgetPainter painter) { 6281 auto cs = getComputedStyle(); 6282 auto c = darken(cs.windowBackgroundColor, 0.2); 6283 painter.outlineColor = c; 6284 painter.fillColor = c; 6285 painter.drawRectangle(Point(0, 0), this.width, this.height); 6286 6287 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 6288 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 6289 } 6290 } 6291 6292 //version(custom_widgets) 6293 //private 6294 class HorizontalScrollbar : ScrollbarBase { 6295 6296 version(custom_widgets) { 6297 private MouseTrackingWidget thumb; 6298 6299 override int getBarDim() { 6300 return thumb.width; 6301 } 6302 } 6303 6304 override void setViewableArea(int a) { 6305 super.setViewableArea(a); 6306 6307 version(win32_widgets) { 6308 SCROLLINFO info; 6309 info.cbSize = info.sizeof; 6310 info.nPage = a + 1; 6311 info.fMask = SIF_PAGE; 6312 SetScrollInfo(hwnd, SB_CTL, &info, true); 6313 } else version(custom_widgets) { 6314 thumb.positionX = thumbPosition; 6315 thumb.thumbWidth = thumbSize; 6316 thumb.redraw(); 6317 } else static assert(0); 6318 6319 } 6320 6321 override void setMax(int a) { 6322 super.setMax(a); 6323 version(win32_widgets) { 6324 SCROLLINFO info; 6325 info.cbSize = info.sizeof; 6326 info.nMin = 0; 6327 info.nMax = max; 6328 info.fMask = SIF_RANGE; 6329 SetScrollInfo(hwnd, SB_CTL, &info, true); 6330 } else version(custom_widgets) { 6331 thumb.positionX = thumbPosition; 6332 thumb.thumbWidth = thumbSize; 6333 thumb.redraw(); 6334 } 6335 } 6336 6337 override void setPosition(int a) { 6338 super.setPosition(a); 6339 version(win32_widgets) { 6340 SCROLLINFO info; 6341 info.cbSize = info.sizeof; 6342 info.fMask = SIF_POS; 6343 info.nPos = position; 6344 SetScrollInfo(hwnd, SB_CTL, &info, true); 6345 } else version(custom_widgets) { 6346 thumb.positionX = thumbPosition(); 6347 thumb.thumbWidth = thumbSize; 6348 thumb.redraw(); 6349 } else static assert(0); 6350 } 6351 6352 this(Widget parent) { 6353 super(parent); 6354 6355 version(win32_widgets) { 6356 createWin32Window(this, "Scrollbar"w, "", 6357 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 6358 } else version(custom_widgets) { 6359 auto vl = new HorizontalLayout(this); 6360 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 6361 leftButton.setClickRepeat(scrollClickRepeatInterval); 6362 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 6363 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 6364 rightButton.setClickRepeat(scrollClickRepeatInterval); 6365 6366 leftButton.tabStop = false; 6367 rightButton.tabStop = false; 6368 thumb.tabStop = false; 6369 6370 leftButton.addEventListener(EventType.triggered, () { 6371 this.emitCommand!"scrolltopreviousline"(); 6372 //informProgramThatUserChangedPosition(position - step()); 6373 }); 6374 rightButton.addEventListener(EventType.triggered, () { 6375 this.emitCommand!"scrolltonextline"(); 6376 //informProgramThatUserChangedPosition(position + step()); 6377 }); 6378 6379 thumb.thumbWidth = this.minWidth; 6380 thumb.thumbHeight = scaleWithDpi(16); 6381 6382 thumb.addEventListener(EventType.change, () { 6383 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 6384 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 6385 6386 //informProgramThatUserChangedPosition(sx); 6387 6388 auto ev = new ScrollToPositionEvent(this, sx); 6389 ev.dispatch(); 6390 }); 6391 } 6392 } 6393 6394 override int minHeight() { return scaleWithDpi(16); } 6395 override int maxHeight() { return scaleWithDpi(16); } 6396 override int minWidth() { return scaleWithDpi(48); } 6397 } 6398 6399 class ScrollToPositionEvent : Event { 6400 enum EventString = "scrolltoposition"; 6401 6402 this(Widget target, int value) { 6403 this.value = value; 6404 super(EventString, target); 6405 } 6406 6407 immutable int value; 6408 6409 override @property int intValue() { 6410 return value; 6411 } 6412 } 6413 6414 //version(custom_widgets) 6415 //private 6416 class VerticalScrollbar : ScrollbarBase { 6417 6418 version(custom_widgets) { 6419 override int getBarDim() { 6420 return thumb.height; 6421 } 6422 6423 private MouseTrackingWidget thumb; 6424 } 6425 6426 override void setViewableArea(int a) { 6427 super.setViewableArea(a); 6428 6429 version(win32_widgets) { 6430 SCROLLINFO info; 6431 info.cbSize = info.sizeof; 6432 info.nPage = a + 1; 6433 info.fMask = SIF_PAGE; 6434 SetScrollInfo(hwnd, SB_CTL, &info, true); 6435 } else version(custom_widgets) { 6436 thumb.positionY = thumbPosition; 6437 thumb.thumbHeight = thumbSize; 6438 thumb.redraw(); 6439 } else static assert(0); 6440 6441 } 6442 6443 override void setMax(int a) { 6444 super.setMax(a); 6445 version(win32_widgets) { 6446 SCROLLINFO info; 6447 info.cbSize = info.sizeof; 6448 info.nMin = 0; 6449 info.nMax = max; 6450 info.fMask = SIF_RANGE; 6451 SetScrollInfo(hwnd, SB_CTL, &info, true); 6452 } else version(custom_widgets) { 6453 thumb.positionY = thumbPosition; 6454 thumb.thumbHeight = thumbSize; 6455 thumb.redraw(); 6456 } 6457 } 6458 6459 override void setPosition(int a) { 6460 super.setPosition(a); 6461 version(win32_widgets) { 6462 SCROLLINFO info; 6463 info.cbSize = info.sizeof; 6464 info.fMask = SIF_POS; 6465 info.nPos = position; 6466 SetScrollInfo(hwnd, SB_CTL, &info, true); 6467 } else version(custom_widgets) { 6468 thumb.positionY = thumbPosition; 6469 thumb.thumbHeight = thumbSize; 6470 thumb.redraw(); 6471 } else static assert(0); 6472 } 6473 6474 this(Widget parent) { 6475 super(parent); 6476 6477 version(win32_widgets) { 6478 createWin32Window(this, "Scrollbar"w, "", 6479 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 6480 } else version(custom_widgets) { 6481 auto vl = new VerticalLayout(this); 6482 auto upButton = new ArrowButton(ArrowDirection.up, vl); 6483 upButton.setClickRepeat(scrollClickRepeatInterval); 6484 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 6485 auto downButton = new ArrowButton(ArrowDirection.down, vl); 6486 downButton.setClickRepeat(scrollClickRepeatInterval); 6487 6488 upButton.addEventListener(EventType.triggered, () { 6489 this.emitCommand!"scrolltopreviousline"(); 6490 //informProgramThatUserChangedPosition(position - step()); 6491 }); 6492 downButton.addEventListener(EventType.triggered, () { 6493 this.emitCommand!"scrolltonextline"(); 6494 //informProgramThatUserChangedPosition(position + step()); 6495 }); 6496 6497 thumb.thumbWidth = this.minWidth; 6498 thumb.thumbHeight = scaleWithDpi(16); 6499 6500 thumb.addEventListener(EventType.change, () { 6501 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 6502 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 6503 6504 auto ev = new ScrollToPositionEvent(this, sy); 6505 ev.dispatch(); 6506 6507 //informProgramThatUserChangedPosition(sy); 6508 }); 6509 6510 upButton.tabStop = false; 6511 downButton.tabStop = false; 6512 thumb.tabStop = false; 6513 } 6514 } 6515 6516 override int minWidth() { return scaleWithDpi(16); } 6517 override int maxWidth() { return scaleWithDpi(16); } 6518 override int minHeight() { return scaleWithDpi(48); } 6519 } 6520 6521 6522 /++ 6523 EXPERIMENTAL 6524 6525 A widget specialized for being a container for other widgets. 6526 6527 History: 6528 Added May 29, 2021. Not stabilized at this time. 6529 +/ 6530 class WidgetContainer : Widget { 6531 this(Widget parent) { 6532 tabStop = false; 6533 super(parent); 6534 } 6535 6536 override int maxHeight() { 6537 if(this.children.length == 1) { 6538 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 6539 } else { 6540 return int.max; 6541 } 6542 } 6543 6544 override int maxWidth() { 6545 if(this.children.length == 1) { 6546 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 6547 } else { 6548 return int.max; 6549 } 6550 } 6551 6552 /+ 6553 6554 override int minHeight() { 6555 int largest = 0; 6556 int margins = 0; 6557 int lastMargin = 0; 6558 foreach(child; children) { 6559 auto mh = child.minHeight(); 6560 if(mh > largest) 6561 largest = mh; 6562 margins += mymax(lastMargin, child.marginTop()); 6563 lastMargin = child.marginBottom(); 6564 } 6565 return largest + margins; 6566 } 6567 6568 override int maxHeight() { 6569 int largest = 0; 6570 int margins = 0; 6571 int lastMargin = 0; 6572 foreach(child; children) { 6573 auto mh = child.maxHeight(); 6574 if(mh == int.max) 6575 return int.max; 6576 if(mh > largest) 6577 largest = mh; 6578 margins += mymax(lastMargin, child.marginTop()); 6579 lastMargin = child.marginBottom(); 6580 } 6581 return largest + margins; 6582 } 6583 6584 override int minWidth() { 6585 int min; 6586 foreach(child; children) { 6587 auto cm = child.minWidth; 6588 if(cm > min) 6589 min = cm; 6590 } 6591 return min + paddingLeft + paddingRight; 6592 } 6593 6594 override int minHeight() { 6595 int min; 6596 foreach(child; children) { 6597 auto cm = child.minHeight; 6598 if(cm > min) 6599 min = cm; 6600 } 6601 return min + paddingTop + paddingBottom; 6602 } 6603 6604 override int maxHeight() { 6605 int largest = 0; 6606 int margins = 0; 6607 int lastMargin = 0; 6608 foreach(child; children) { 6609 auto mh = child.maxHeight(); 6610 if(mh == int.max) 6611 return int.max; 6612 if(mh > largest) 6613 largest = mh; 6614 margins += mymax(lastMargin, child.marginTop()); 6615 lastMargin = child.marginBottom(); 6616 } 6617 return largest + margins; 6618 } 6619 6620 override int heightStretchiness() { 6621 int max; 6622 foreach(child; children) { 6623 auto c = child.heightStretchiness; 6624 if(c > max) 6625 max = c; 6626 } 6627 return max; 6628 } 6629 6630 override int marginTop() { 6631 if(this.children.length) 6632 return this.children[0].marginTop; 6633 return 0; 6634 } 6635 +/ 6636 } 6637 6638 /// 6639 abstract class Layout : Widget { 6640 this(Widget parent) { 6641 tabStop = false; 6642 super(parent); 6643 } 6644 } 6645 6646 /++ 6647 Makes all children minimum width and height, placing them down 6648 left to right, top to bottom. 6649 6650 Useful if you want to make a list of buttons that automatically 6651 wrap to a new line when necessary. 6652 +/ 6653 class InlineBlockLayout : Layout { 6654 /// 6655 this(Widget parent) { super(parent); } 6656 6657 override void recomputeChildLayout() { 6658 registerMovement(); 6659 6660 int x = this.paddingLeft, y = this.paddingTop; 6661 6662 int lineHeight; 6663 int previousMargin = 0; 6664 int previousMarginBottom = 0; 6665 6666 foreach(child; children) { 6667 if(child.hidden) 6668 continue; 6669 if(cast(FixedPosition) child) { 6670 child.recomputeChildLayout(); 6671 continue; 6672 } 6673 child.width = child.flexBasisWidth(); 6674 if(child.width == 0) 6675 child.width = child.minWidth(); 6676 if(child.width == 0) 6677 child.width = 32; 6678 6679 child.height = child.flexBasisHeight(); 6680 if(child.height == 0) 6681 child.height = child.minHeight(); 6682 if(child.height == 0) 6683 child.height = 32; 6684 6685 if(x + child.width + paddingRight > this.width) { 6686 x = this.paddingLeft; 6687 y += lineHeight; 6688 lineHeight = 0; 6689 previousMargin = 0; 6690 previousMarginBottom = 0; 6691 } 6692 6693 auto margin = child.marginLeft; 6694 if(previousMargin > margin) 6695 margin = previousMargin; 6696 6697 x += margin; 6698 6699 child.x = x; 6700 child.y = y; 6701 6702 int marginTopApplied; 6703 if(child.marginTop > previousMarginBottom) { 6704 child.y += child.marginTop; 6705 marginTopApplied = child.marginTop; 6706 } 6707 6708 x += child.width; 6709 previousMargin = child.marginRight; 6710 6711 if(child.marginBottom > previousMarginBottom) 6712 previousMarginBottom = child.marginBottom; 6713 6714 auto h = child.height + previousMarginBottom + marginTopApplied; 6715 if(h > lineHeight) 6716 lineHeight = h; 6717 6718 child.recomputeChildLayout(); 6719 } 6720 6721 } 6722 6723 override int minWidth() { 6724 int min; 6725 foreach(child; children) { 6726 auto cm = child.minWidth; 6727 if(cm > min) 6728 min = cm; 6729 } 6730 return min + paddingLeft + paddingRight; 6731 } 6732 6733 override int minHeight() { 6734 int min; 6735 foreach(child; children) { 6736 auto cm = child.minHeight; 6737 if(cm > min) 6738 min = cm; 6739 } 6740 return min + paddingTop + paddingBottom; 6741 } 6742 } 6743 6744 /++ 6745 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 6746 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 6747 the [TabWidget] will automatically change pages of child widgets. 6748 6749 This allows you to react to it however you see fit rather than having to 6750 be tied to just the new sets of child widgets. 6751 6752 It sends the message in the form of `this.emitCommand!"changetab"();`. 6753 6754 History: 6755 Added December 24, 2021 (dub v10.5) 6756 +/ 6757 class TabMessageWidget : Widget { 6758 6759 protected void tabIndexClicked(int item) { 6760 this.emitCommand!"changetab"(); 6761 } 6762 6763 /++ 6764 Adds the a new tab to the control with the given title. 6765 6766 Returns: 6767 The index of the newly added tab. You will need to know 6768 this index to refer to it later and to know which tab to 6769 change to when you get a changetab message. 6770 +/ 6771 int addTab(string title, int pos = int.max) { 6772 version(win32_widgets) { 6773 TCITEM item; 6774 item.mask = TCIF_TEXT; 6775 WCharzBuffer buf = WCharzBuffer(title); 6776 item.pszText = buf.ptr; 6777 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 6778 } else version(custom_widgets) { 6779 if(pos >= tabs.length) { 6780 tabs ~= title; 6781 redraw(); 6782 return cast(int) tabs.length - 1; 6783 } else if(pos <= 0) { 6784 tabs = title ~ tabs; 6785 redraw(); 6786 return 0; 6787 } else { 6788 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 6789 redraw(); 6790 return pos; 6791 } 6792 } 6793 } 6794 6795 override void addChild(Widget child, int pos = int.max) { 6796 if(container) 6797 container.addChild(child, pos); 6798 else 6799 super.addChild(child, pos); 6800 } 6801 6802 protected Widget makeContainer() { 6803 return new Widget(this); 6804 } 6805 6806 private Widget container; 6807 6808 override void recomputeChildLayout() { 6809 version(win32_widgets) { 6810 this.registerMovement(); 6811 6812 RECT rect; 6813 GetWindowRect(hwnd, &rect); 6814 6815 auto left = rect.left; 6816 auto top = rect.top; 6817 6818 TabCtrl_AdjustRect(hwnd, false, &rect); 6819 foreach(child; children) { 6820 if(!child.showing) continue; 6821 child.x = rect.left - left; 6822 child.y = rect.top - top; 6823 child.width = rect.right - rect.left; 6824 child.height = rect.bottom - rect.top; 6825 child.recomputeChildLayout(); 6826 } 6827 } else version(custom_widgets) { 6828 this.registerMovement(); 6829 foreach(child; children) { 6830 if(!child.showing) continue; 6831 child.x = 2; 6832 child.y = tabBarHeight + 2; // for the border 6833 child.width = width - 4; // for the border 6834 child.height = height - tabBarHeight - 2 - 2; // for the border 6835 child.recomputeChildLayout(); 6836 } 6837 } else static assert(0); 6838 } 6839 6840 version(custom_widgets) 6841 string[] tabs; 6842 6843 this(Widget parent) { 6844 super(parent); 6845 6846 tabStop = false; 6847 6848 version(win32_widgets) { 6849 createWin32Window(this, WC_TABCONTROL, "", 0); 6850 } else version(custom_widgets) { 6851 addEventListener((ClickEvent event) { 6852 if(event.target !is this && this.container !is null && event.target !is this.container) return; 6853 if(event.clientY < tabBarHeight) { 6854 auto t = (event.clientX / tabWidth); 6855 if(t >= 0 && t < tabs.length) { 6856 currentTab_ = t; 6857 tabIndexClicked(t); 6858 redraw(); 6859 } 6860 } 6861 }); 6862 } else static assert(0); 6863 6864 this.container = makeContainer(); 6865 } 6866 6867 override int marginTop() { return 4; } 6868 override int paddingBottom() { return 4; } 6869 6870 override int minHeight() { 6871 int max = 0; 6872 foreach(child; children) 6873 max = mymax(child.minHeight, max); 6874 6875 6876 version(win32_widgets) { 6877 RECT rect; 6878 rect.right = this.width; 6879 rect.bottom = max; 6880 TabCtrl_AdjustRect(hwnd, true, &rect); 6881 6882 max = rect.bottom; 6883 } else { 6884 max += defaultLineHeight + 4; 6885 } 6886 6887 6888 return max; 6889 } 6890 6891 version(win32_widgets) 6892 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 6893 switch(code) { 6894 case TCN_SELCHANGE: 6895 auto sel = TabCtrl_GetCurSel(hwnd); 6896 tabIndexClicked(sel); 6897 break; 6898 default: 6899 } 6900 return 0; 6901 } 6902 6903 version(custom_widgets) { 6904 private int currentTab_; 6905 private int tabBarHeight() { return defaultLineHeight; } 6906 int tabWidth() { return scaleWithDpi(80); } 6907 } 6908 6909 version(win32_widgets) 6910 override void paint(WidgetPainter painter) {} 6911 6912 version(custom_widgets) 6913 override void paint(WidgetPainter painter) { 6914 auto cs = getComputedStyle(); 6915 6916 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 6917 6918 int posX = 0; 6919 foreach(idx, title; tabs) { 6920 auto isCurrent = idx == getCurrentTab(); 6921 6922 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 6923 6924 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 6925 painter.outlineColor = cs.foregroundColor; 6926 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 6927 6928 if(isCurrent) { 6929 painter.outlineColor = cs.windowBackgroundColor; 6930 painter.fillColor = Color.transparent; 6931 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 6932 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 6933 6934 painter.outlineColor = Color.white; 6935 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 6936 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 6937 painter.outlineColor = cs.activeTabColor; 6938 painter.drawPixel(Point(posX, tabBarHeight - 1)); 6939 } 6940 6941 posX += tabWidth - 2; 6942 } 6943 } 6944 6945 /// 6946 @scriptable 6947 void setCurrentTab(int item) { 6948 version(win32_widgets) 6949 TabCtrl_SetCurSel(hwnd, item); 6950 else version(custom_widgets) 6951 currentTab_ = item; 6952 else static assert(0); 6953 6954 tabIndexClicked(item); 6955 } 6956 6957 /// 6958 @scriptable 6959 int getCurrentTab() { 6960 version(win32_widgets) 6961 return TabCtrl_GetCurSel(hwnd); 6962 else version(custom_widgets) 6963 return currentTab_; // FIXME 6964 else static assert(0); 6965 } 6966 6967 /// 6968 @scriptable 6969 void removeTab(int item) { 6970 if(item && item == getCurrentTab()) 6971 setCurrentTab(item - 1); 6972 6973 version(win32_widgets) { 6974 TabCtrl_DeleteItem(hwnd, item); 6975 } 6976 6977 for(int a = item; a < children.length - 1; a++) 6978 this._children[a] = this._children[a + 1]; 6979 this._children = this._children[0 .. $-1]; 6980 } 6981 6982 } 6983 6984 6985 /++ 6986 A tab widget is a set of clickable tab buttons followed by a content area. 6987 6988 6989 Tabs can change existing content or can be new pages. 6990 6991 When the user picks a different tab, a `change` message is generated. 6992 +/ 6993 class TabWidget : TabMessageWidget { 6994 this(Widget parent) { 6995 super(parent); 6996 } 6997 6998 override protected Widget makeContainer() { 6999 return null; 7000 } 7001 7002 override void addChild(Widget child, int pos = int.max) { 7003 if(auto twp = cast(TabWidgetPage) child) { 7004 Widget.addChild(child, pos); 7005 if(pos == int.max) 7006 pos = cast(int) this.children.length - 1; 7007 7008 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 7009 7010 if(pos != getCurrentTab) { 7011 child.showing = false; 7012 } 7013 } else { 7014 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 7015 } 7016 } 7017 7018 // FIXME: add tab icons at some point, Windows supports them 7019 /++ 7020 Adds a page and its associated tab with the given label to the widget. 7021 7022 Returns: 7023 The added page object, to which you can add other widgets. 7024 +/ 7025 @scriptable 7026 TabWidgetPage addPage(string title) { 7027 return new TabWidgetPage(title, this); 7028 } 7029 7030 /++ 7031 Gets the page at the given tab index, or `null` if the index is bad. 7032 7033 History: 7034 Added December 24, 2021. 7035 +/ 7036 TabWidgetPage getPage(int index) { 7037 if(index < this.children.length) 7038 return null; 7039 return cast(TabWidgetPage) this.children[index]; 7040 } 7041 7042 /++ 7043 While you can still use the addTab from the parent class, 7044 *strongly* recommend you use [addPage] insteaad. 7045 7046 History: 7047 Added December 24, 2021 to fulful the interface 7048 requirement that came from adding [TabMessageWidget]. 7049 7050 You should not use it though since the [addPage] function 7051 is much easier to use here. 7052 +/ 7053 override int addTab(string title, int pos = int.max) { 7054 auto p = addPage(title); 7055 foreach(idx, child; this.children) 7056 if(child is p) 7057 return cast(int) idx; 7058 return -1; 7059 } 7060 7061 protected override void tabIndexClicked(int item) { 7062 foreach(idx, child; children) { 7063 child.showing(false, false); // batch the recalculates for the end 7064 } 7065 7066 foreach(idx, child; children) { 7067 if(idx == item) { 7068 child.showing(true, false); 7069 if(parentWindow) { 7070 auto f = parentWindow.getFirstFocusable(child); 7071 if(f) 7072 f.focus(); 7073 } 7074 recomputeChildLayout(); 7075 } 7076 } 7077 7078 version(win32_widgets) { 7079 InvalidateRect(hwnd, null, true); 7080 } else version(custom_widgets) { 7081 this.redraw(); 7082 } 7083 } 7084 7085 } 7086 7087 /++ 7088 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 7089 7090 You add [TabWidgetPage]s to it. 7091 +/ 7092 class PageWidget : Widget { 7093 this(Widget parent) { 7094 super(parent); 7095 } 7096 7097 override int minHeight() { 7098 int max = 0; 7099 foreach(child; children) 7100 max = mymax(child.minHeight, max); 7101 7102 return max; 7103 } 7104 7105 7106 override void addChild(Widget child, int pos = int.max) { 7107 if(auto twp = cast(TabWidgetPage) child) { 7108 super.addChild(child, pos); 7109 if(pos == int.max) 7110 pos = cast(int) this.children.length - 1; 7111 7112 if(pos != getCurrentTab) { 7113 child.showing = false; 7114 } 7115 } else { 7116 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 7117 } 7118 } 7119 7120 override void recomputeChildLayout() { 7121 this.registerMovement(); 7122 foreach(child; children) { 7123 child.x = 0; 7124 child.y = 0; 7125 child.width = width; 7126 child.height = height; 7127 child.recomputeChildLayout(); 7128 } 7129 } 7130 7131 private int currentTab_; 7132 7133 /// 7134 @scriptable 7135 void setCurrentTab(int item) { 7136 currentTab_ = item; 7137 7138 showOnly(item); 7139 } 7140 7141 /// 7142 @scriptable 7143 int getCurrentTab() { 7144 return currentTab_; 7145 } 7146 7147 /// 7148 @scriptable 7149 void removeTab(int item) { 7150 if(item && item == getCurrentTab()) 7151 setCurrentTab(item - 1); 7152 7153 for(int a = item; a < children.length - 1; a++) 7154 this._children[a] = this._children[a + 1]; 7155 this._children = this._children[0 .. $-1]; 7156 } 7157 7158 /// 7159 @scriptable 7160 TabWidgetPage addPage(string title) { 7161 return new TabWidgetPage(title, this); 7162 } 7163 7164 private void showOnly(int item) { 7165 foreach(idx, child; children) 7166 if(idx == item) { 7167 child.show(); 7168 child.recomputeChildLayout(); 7169 } else { 7170 child.hide(); 7171 } 7172 } 7173 7174 } 7175 7176 /++ 7177 7178 +/ 7179 class TabWidgetPage : Widget { 7180 string title; 7181 this(string title, Widget parent) { 7182 this.title = title; 7183 this.tabStop = false; 7184 super(parent); 7185 7186 ///* 7187 version(win32_widgets) { 7188 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 7189 } 7190 //*/ 7191 } 7192 7193 override int minHeight() { 7194 int sum = 0; 7195 foreach(child; children) 7196 sum += child.minHeight(); 7197 return sum; 7198 } 7199 } 7200 7201 version(none) 7202 /++ 7203 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 7204 7205 I think I need to modify the layout algorithms to support this. 7206 +/ 7207 class CollapsableSidebar : Widget { 7208 7209 } 7210 7211 /// Stacks the widgets vertically, taking all the available width for each child. 7212 class VerticalLayout : Layout { 7213 // most of this is intentionally blank - widget's default is vertical layout right now 7214 /// 7215 this(Widget parent) { super(parent); } 7216 7217 /++ 7218 Sets a max width for the layout so you don't have to subclass. The max width 7219 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7220 7221 History: 7222 Added November 29, 2021 (dub v10.5) 7223 +/ 7224 this(int maxWidth, Widget parent) { 7225 this.mw = maxWidth; 7226 super(parent); 7227 } 7228 7229 private int mw = int.max; 7230 7231 override int maxWidth() { return scaleWithDpi(mw); } 7232 } 7233 7234 /// Stacks the widgets horizontally, taking all the available height for each child. 7235 class HorizontalLayout : Layout { 7236 /// 7237 this(Widget parent) { super(parent); } 7238 7239 /++ 7240 Sets a max height for the layout so you don't have to subclass. The max height 7241 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7242 7243 History: 7244 Added November 29, 2021 (dub v10.5) 7245 +/ 7246 this(int maxHeight, Widget parent) { 7247 this.mh = maxHeight; 7248 super(parent); 7249 } 7250 7251 private int mh = 0; 7252 7253 7254 7255 override void recomputeChildLayout() { 7256 .recomputeChildLayout!"width"(this); 7257 } 7258 7259 override int minHeight() { 7260 int largest = 0; 7261 int margins = 0; 7262 int lastMargin = 0; 7263 foreach(child; children) { 7264 auto mh = child.minHeight(); 7265 if(mh > largest) 7266 largest = mh; 7267 margins += mymax(lastMargin, child.marginTop()); 7268 lastMargin = child.marginBottom(); 7269 } 7270 return largest + margins; 7271 } 7272 7273 override int maxHeight() { 7274 if(mh != 0) 7275 return mymax(minHeight, scaleWithDpi(mh)); 7276 7277 int largest = 0; 7278 int margins = 0; 7279 int lastMargin = 0; 7280 foreach(child; children) { 7281 auto mh = child.maxHeight(); 7282 if(mh == int.max) 7283 return int.max; 7284 if(mh > largest) 7285 largest = mh; 7286 margins += mymax(lastMargin, child.marginTop()); 7287 lastMargin = child.marginBottom(); 7288 } 7289 return largest + margins; 7290 } 7291 7292 override int heightStretchiness() { 7293 int max; 7294 foreach(child; children) { 7295 auto c = child.heightStretchiness; 7296 if(c > max) 7297 max = c; 7298 } 7299 return max; 7300 } 7301 7302 } 7303 7304 version(win32_widgets) 7305 private 7306 extern(Windows) 7307 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 7308 Widget* pwin = hwnd in Widget.nativeMapping; 7309 if(pwin is null) 7310 return DefWindowProc(hwnd, message, wparam, lparam); 7311 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 7312 if(win is null) 7313 return DefWindowProc(hwnd, message, wparam, lparam); 7314 7315 switch(message) { 7316 case WM_SIZE: 7317 auto width = LOWORD(lparam); 7318 auto height = HIWORD(lparam); 7319 7320 auto hdc = GetDC(hwnd); 7321 auto hdcBmp = CreateCompatibleDC(hdc); 7322 7323 // FIXME: could this be more efficient? it never relinquishes a large bitmap 7324 if(width > win.bmpWidth || height > win.bmpHeight) { 7325 auto oldBuffer = win.buffer; 7326 win.buffer = CreateCompatibleBitmap(hdc, width, height); 7327 7328 if(oldBuffer) 7329 DeleteObject(oldBuffer); 7330 7331 win.bmpWidth = width; 7332 win.bmpHeight = height; 7333 } 7334 7335 // just always erase it upon resizing so minigui can draw over with a clean slate 7336 auto oldBmp = SelectObject(hdcBmp, win.buffer); 7337 7338 auto brush = GetSysColorBrush(COLOR_3DFACE); 7339 RECT r; 7340 r.left = 0; 7341 r.top = 0; 7342 r.right = width; 7343 r.bottom = height; 7344 FillRect(hdcBmp, &r, brush); 7345 7346 SelectObject(hdcBmp, oldBmp); 7347 DeleteDC(hdcBmp); 7348 ReleaseDC(hwnd, hdc); 7349 break; 7350 case WM_PAINT: 7351 if(win.buffer is null) 7352 goto default; 7353 7354 BITMAP bm; 7355 PAINTSTRUCT ps; 7356 7357 HDC hdc = BeginPaint(hwnd, &ps); 7358 7359 HDC hdcMem = CreateCompatibleDC(hdc); 7360 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 7361 7362 GetObject(win.buffer, bm.sizeof, &bm); 7363 7364 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 7365 7366 SelectObject(hdcMem, hbmOld); 7367 DeleteDC(hdcMem); 7368 EndPaint(hwnd, &ps); 7369 break; 7370 default: 7371 return DefWindowProc(hwnd, message, wparam, lparam); 7372 } 7373 7374 return 0; 7375 } 7376 7377 private wstring Win32Class(wstring name)() { 7378 static bool classRegistered; 7379 if(!classRegistered) { 7380 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 7381 WNDCLASSEX wc; 7382 wc.cbSize = wc.sizeof; 7383 wc.hInstance = hInstance; 7384 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 7385 wc.lpfnWndProc = &DoubleBufferWndProc; 7386 wc.lpszClassName = name.ptr; 7387 if(!RegisterClassExW(&wc)) 7388 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 7389 classRegistered = true; 7390 } 7391 7392 return name; 7393 } 7394 7395 /+ 7396 version(win32_widgets) 7397 extern(Windows) 7398 private 7399 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 7400 switch(iMessage) { 7401 case WM_PAINT: 7402 if(auto te = hWnd in Widget.nativeMapping) { 7403 try { 7404 //te.redraw(); 7405 writeln(te, " drawing"); 7406 } catch(Exception) {} 7407 } 7408 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7409 default: 7410 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7411 } 7412 } 7413 +/ 7414 7415 7416 /++ 7417 A widget specifically designed to hold other widgets. 7418 7419 History: 7420 Added July 1, 2021 7421 +/ 7422 class ContainerWidget : Widget { 7423 this(Widget parent) { 7424 super(parent); 7425 this.tabStop = false; 7426 7427 version(win32_widgets) { 7428 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 7429 } 7430 } 7431 } 7432 7433 /++ 7434 A widget that takes your widget, puts scroll bars around it, and sends 7435 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 7436 no effort to automatically scroll or clip its child widgets - it just sends 7437 the messages. 7438 7439 7440 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 7441 The scroll coordinates are all given in a unit you interpret as you wish. One 7442 of these units is moved on each press of the arrow buttons and represents the 7443 smallest amount the user can scroll. The intention is for this to be one line, 7444 one item in a list, one row in a table, etc. Whatever makes sense for your widget 7445 in each direction that the user might be interested in. 7446 7447 You can set a "page size" with the [step] property. (Yes, I regret the name...) 7448 This is the amount it jumps when the user pressed page up and page down, or clicks 7449 in the exposed part of the scroll bar. 7450 7451 You should add child content to the ScrollMessageWidget. However, it is important to 7452 note that the coordinates are always independent of the scroll position! It is YOUR 7453 responsibility to do any necessary transforms, clipping, etc., while drawing the 7454 content and interpreting mouse events if they are supposed to change with the scroll. 7455 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 7456 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 7457 you more control (which can be considerably more efficient and adapted to your actual data) 7458 at the expense of you also needing to be aware of its reality. 7459 7460 Please note that it does NOT react to mouse wheel events or various keyboard events as of 7461 version 10.3. Maybe this will change in the future.... but for now you must call 7462 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 7463 +/ 7464 class ScrollMessageWidget : Widget { 7465 this(Widget parent) { 7466 super(parent); 7467 7468 container = new Widget(this); 7469 hsb = new HorizontalScrollbar(this); 7470 vsb = new VerticalScrollbar(this); 7471 7472 hsb.addEventListener("scrolltonextline", { 7473 hsb.setPosition(hsb.position + movementPerButtonClickH_); 7474 notify(); 7475 }); 7476 hsb.addEventListener("scrolltopreviousline", { 7477 hsb.setPosition(hsb.position - movementPerButtonClickH_); 7478 notify(); 7479 }); 7480 vsb.addEventListener("scrolltonextline", { 7481 vsb.setPosition(vsb.position + movementPerButtonClickV_); 7482 notify(); 7483 }); 7484 vsb.addEventListener("scrolltopreviousline", { 7485 vsb.setPosition(vsb.position - movementPerButtonClickV_); 7486 notify(); 7487 }); 7488 hsb.addEventListener("scrolltonextpage", { 7489 hsb.setPosition(hsb.position + hsb.step_); 7490 notify(); 7491 }); 7492 hsb.addEventListener("scrolltopreviouspage", { 7493 hsb.setPosition(hsb.position - hsb.step_); 7494 notify(); 7495 }); 7496 vsb.addEventListener("scrolltonextpage", { 7497 vsb.setPosition(vsb.position + vsb.step_); 7498 notify(); 7499 }); 7500 vsb.addEventListener("scrolltopreviouspage", { 7501 vsb.setPosition(vsb.position - vsb.step_); 7502 notify(); 7503 }); 7504 hsb.addEventListener("scrolltoposition", (Event event) { 7505 hsb.setPosition(event.intValue); 7506 notify(); 7507 }); 7508 vsb.addEventListener("scrolltoposition", (Event event) { 7509 vsb.setPosition(event.intValue); 7510 notify(); 7511 }); 7512 7513 7514 tabStop = false; 7515 container.tabStop = false; 7516 magic = true; 7517 } 7518 7519 private int movementPerButtonClickH_ = 1; 7520 private int movementPerButtonClickV_ = 1; 7521 public void movementPerButtonClick(int h, int v) { 7522 movementPerButtonClickH_ = h; 7523 movementPerButtonClickV_ = v; 7524 } 7525 7526 /++ 7527 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 7528 7529 7530 The defaults for [addDefaultWheelListeners] are: 7531 7532 $(LIST 7533 * Mouse wheel scrolls vertically 7534 * Alt key + mouse wheel scrolls horiontally 7535 * Shift + mouse wheel scrolls faster. 7536 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 7537 ) 7538 7539 The defaults for [addDefaultKeyboardListeners] are: 7540 7541 $(LIST 7542 * Arrow keys scroll by the given amounts 7543 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 7544 * Page up and down scroll by the vertical viewable area 7545 * Home and end scroll to the start and end of the verticle viewable area. 7546 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 7547 ) 7548 7549 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 7550 7551 Params: 7552 horizontalArrowScrollAmount = 7553 verticalArrowScrollAmount = 7554 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 7555 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 7556 shiftMultiplier = multiplies the scroll amount by this when shift is held 7557 +/ 7558 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 7559 auto _this = this; 7560 7561 container.addEventListener((scope KeyDownEvent ke) { 7562 switch(ke.key) { 7563 case Key.Left: 7564 _this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7565 break; 7566 case Key.Right: 7567 _this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7568 break; 7569 case Key.Up: 7570 _this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7571 break; 7572 case Key.Down: 7573 _this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7574 break; 7575 case Key.PageUp: 7576 if(ke.altKey) 7577 _this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7578 else 7579 _this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7580 break; 7581 case Key.PageDown: 7582 if(ke.altKey) 7583 _this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7584 else 7585 _this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7586 break; 7587 case Key.Home: 7588 if(ke.altKey) 7589 _this.scrollLeft(short.max * 16); 7590 else 7591 _this.scrollUp(short.max * 16); 7592 break; 7593 case Key.End: 7594 if(ke.altKey) 7595 _this.scrollRight(short.max * 16); 7596 else 7597 _this.scrollDown(short.max * 16); 7598 break; 7599 7600 default: 7601 // ignore, not for us. 7602 } 7603 7604 }); 7605 } 7606 7607 /// ditto 7608 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 7609 auto _this = this; 7610 container.addEventListener((scope ClickEvent ce) { 7611 7612 if(ce.target && ce.target.tabStop) 7613 ce.target.focus(); 7614 7615 // ctrl is reserved for the application 7616 if(ce.ctrlKey) 7617 return; 7618 7619 if(horizontalWheelScrollAmount == 0 && ce.altKey) 7620 return; 7621 7622 if(shiftMultiplier == 0 && ce.shiftKey) 7623 return; 7624 7625 if(ce.button == MouseButton.wheelDown) { 7626 if(ce.altKey) 7627 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7628 else 7629 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7630 } else if(ce.button == MouseButton.wheelUp) { 7631 if(ce.altKey) 7632 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7633 else 7634 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7635 } 7636 }); 7637 } 7638 7639 /++ 7640 Scrolls the given amount. 7641 7642 History: 7643 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. 7644 +/ 7645 void scrollUp(int amount = 1) { 7646 vsb.setPosition(vsb.position - amount); 7647 notify(); 7648 } 7649 /// ditto 7650 void scrollDown(int amount = 1) { 7651 vsb.setPosition(vsb.position + amount); 7652 notify(); 7653 } 7654 /// ditto 7655 void scrollLeft(int amount = 1) { 7656 hsb.setPosition(hsb.position - amount); 7657 notify(); 7658 } 7659 /// ditto 7660 void scrollRight(int amount = 1) { 7661 hsb.setPosition(hsb.position + amount); 7662 notify(); 7663 } 7664 7665 /// 7666 VerticalScrollbar verticalScrollBar() { return vsb; } 7667 /// 7668 HorizontalScrollbar horizontalScrollBar() { return hsb; } 7669 7670 void notify() { 7671 static bool insideNotify; 7672 7673 if(insideNotify) 7674 return; // avoid the recursive call, even if it isn't strictly correct 7675 7676 insideNotify = true; 7677 scope(exit) insideNotify = false; 7678 7679 this.emit!ScrollEvent(); 7680 } 7681 7682 mixin Emits!ScrollEvent; 7683 7684 /// 7685 Point position() { 7686 return Point(hsb.position, vsb.position); 7687 } 7688 7689 /// 7690 void setPosition(int x, int y) { 7691 hsb.setPosition(x); 7692 vsb.setPosition(y); 7693 } 7694 7695 /// 7696 void setPageSize(int unitsX, int unitsY) { 7697 hsb.setStep(unitsX); 7698 vsb.setStep(unitsY); 7699 } 7700 7701 /// Always call this BEFORE setViewableArea 7702 void setTotalArea(int width, int height) { 7703 hsb.setMax(width); 7704 vsb.setMax(height); 7705 } 7706 7707 /++ 7708 Always set the viewable area AFTER setitng the total area if you are going to change both. 7709 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 7710 If you need to do that, use [queueRecomputeChildLayout]. 7711 +/ 7712 void setViewableArea(int width, int height) { 7713 7714 // actually there IS A need to dothis cuz the max might have changed since then 7715 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 7716 //return; // no need to do what is already done 7717 hsb.setViewableArea(width); 7718 vsb.setViewableArea(height); 7719 7720 bool needsNotify = false; 7721 7722 // FIXME: if at any point the rhs is outside the scrollbar, we need 7723 // to reset to 0. but it should remember the old position in case the 7724 // window resizes again, so it can kinda return ot where it was. 7725 // 7726 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 7727 if(width >= hsb.max) { 7728 // there's plenty of room to display it all so we need to reset to zero 7729 // FIXME: adjust so it matches the note above 7730 hsb.setPosition(0); 7731 needsNotify = true; 7732 } 7733 if(height >= vsb.max) { 7734 // there's plenty of room to display it all so we need to reset to zero 7735 // FIXME: adjust so it matches the note above 7736 vsb.setPosition(0); 7737 needsNotify = true; 7738 } 7739 if(needsNotify) 7740 notify(); 7741 } 7742 7743 private bool magic; 7744 override void addChild(Widget w, int position = int.max) { 7745 if(magic) 7746 container.addChild(w, position); 7747 else 7748 super.addChild(w, position); 7749 } 7750 7751 override void recomputeChildLayout() { 7752 if(hsb is null || vsb is null || container is null) return; 7753 7754 registerMovement(); 7755 7756 enum BUTTON_SIZE = 16; 7757 7758 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 7759 hsb.x = 0; 7760 hsb.y = this.height - hsb.height; 7761 7762 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 7763 vsb.x = this.width - vsb.width; 7764 vsb.y = 0; 7765 7766 auto vsb_width = vsb.showing ? vsb.width : 0; 7767 auto hsb_height = hsb.showing ? hsb.height : 0; 7768 7769 hsb.width = this.width - vsb_width; 7770 vsb.height = this.height - hsb_height; 7771 7772 hsb.recomputeChildLayout(); 7773 vsb.recomputeChildLayout(); 7774 7775 if(this.header is null) { 7776 container.x = 0; 7777 container.y = 0; 7778 container.width = this.width - vsb_width; 7779 container.height = this.height - hsb_height; 7780 container.recomputeChildLayout(); 7781 } else { 7782 header.x = 0; 7783 header.y = 0; 7784 header.width = this.width - vsb_width; 7785 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 7786 header.recomputeChildLayout(); 7787 7788 container.x = 0; 7789 container.y = scaleWithDpi(BUTTON_SIZE); 7790 container.width = this.width - vsb_width; 7791 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 7792 container.recomputeChildLayout(); 7793 } 7794 } 7795 7796 private HorizontalScrollbar hsb; 7797 private VerticalScrollbar vsb; 7798 Widget container; 7799 private Widget header; 7800 7801 /++ 7802 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 7803 7804 History: 7805 Added September 27, 2021 (dub v10.3) 7806 +/ 7807 Widget getHeader() { 7808 if(this.header is null) { 7809 magic = false; 7810 scope(exit) magic = true; 7811 this.header = new Widget(this); 7812 recomputeChildLayout(); 7813 } 7814 return this.header; 7815 } 7816 7817 /++ 7818 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 7819 7820 History: 7821 Added January 3, 2023 (dub v11.0) 7822 +/ 7823 void scrollIntoView(Rectangle rect) { 7824 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 7825 7826 // import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1))); 7827 7828 // the lower right is exclusive normally 7829 auto test = rect.lowerRight; 7830 if(test.x > 0) test.x--; 7831 if(test.y > 0) test.y--; 7832 7833 if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) { 7834 // try to scroll only one dimension at a time if we can 7835 if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y))) 7836 setPosition(rect.upperLeft.x, position.y); 7837 if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y))) 7838 setPosition(position.x, rect.upperLeft.y); 7839 } 7840 7841 } 7842 7843 override int minHeight() { 7844 int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0)); 7845 if(header !is null) 7846 min += header.minHeight; 7847 if(horizontalScrollBar.showing) 7848 min += horizontalScrollBar.minHeight; 7849 return min; 7850 } 7851 7852 override int maxHeight() { 7853 int max = container ? container.maxHeight : int.max; 7854 if(max == int.max) 7855 return max; 7856 if(horizontalScrollBar.showing) 7857 max += horizontalScrollBar.minHeight; 7858 return max; 7859 } 7860 } 7861 7862 /++ 7863 $(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") 7864 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 7865 +/ 7866 version(minigui_screenshots) 7867 @Screenshot("ScrollMessageWidget") 7868 unittest { 7869 auto window = new Window("ScrollMessageWidget"); 7870 7871 auto smw = new ScrollMessageWidget(window); 7872 smw.addDefaultKeyboardListeners(); 7873 smw.addDefaultWheelListeners(); 7874 7875 window.loop(); 7876 } 7877 7878 /++ 7879 Bypasses automatic layout for its children, using manual positioning and sizing only. 7880 While you need to manually position them, you must ensure they are inside the StaticLayout's 7881 bounding box to avoid undefined behavior. 7882 7883 You should almost never use this. 7884 +/ 7885 class StaticLayout : Layout { 7886 /// 7887 this(Widget parent) { super(parent); } 7888 override void recomputeChildLayout() { 7889 registerMovement(); 7890 foreach(child; children) 7891 child.recomputeChildLayout(); 7892 } 7893 } 7894 7895 /++ 7896 Bypasses automatic positioning when being laid out. It is your responsibility to make 7897 room for this widget in the parent layout. 7898 7899 Its children are laid out normally, unless there is exactly one, in which case it takes 7900 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 7901 can do that with `padding`). 7902 +/ 7903 class StaticPosition : Layout { 7904 /// 7905 this(Widget parent) { super(parent); } 7906 7907 override void recomputeChildLayout() { 7908 registerMovement(); 7909 if(this.children.length == 1) { 7910 auto child = children[0]; 7911 child.x = 0; 7912 child.y = 0; 7913 child.width = this.width; 7914 child.height = this.height; 7915 child.recomputeChildLayout(); 7916 } else 7917 foreach(child; children) 7918 child.recomputeChildLayout(); 7919 } 7920 7921 alias width = typeof(super).width; 7922 alias height = typeof(super).height; 7923 7924 @property int width(int w) @nogc pure @safe nothrow { 7925 return this._width = w; 7926 } 7927 7928 @property int height(int w) @nogc pure @safe nothrow { 7929 return this._height = w; 7930 } 7931 7932 } 7933 7934 /++ 7935 FixedPosition is like [StaticPosition], but its coordinates 7936 are always relative to the viewport, meaning they do not scroll with 7937 the parent content. 7938 +/ 7939 class FixedPosition : StaticPosition { 7940 /// 7941 this(Widget parent) { super(parent); } 7942 } 7943 7944 version(win32_widgets) 7945 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 7946 if(true) { 7947 // cmd == 0 = menu, cmd == 1 = accelerator 7948 if(auto item = idm in Action.mapping) { 7949 foreach(handler; (*item).triggered) 7950 handler(); 7951 /* 7952 auto event = new Event("triggered", *item); 7953 event.button = idm; 7954 event.dispatch(); 7955 */ 7956 return 0; 7957 } 7958 } 7959 if(handle) 7960 if(auto widgetp = handle in Widget.nativeMapping) { 7961 (*widgetp).handleWmCommand(cmd, idm); 7962 return 0; 7963 } 7964 return 1; 7965 } 7966 7967 7968 /// 7969 class Window : Widget { 7970 int mouseCaptureCount = 0; 7971 Widget mouseCapturedBy; 7972 void captureMouse(Widget byWhom) { 7973 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 7974 mouseCaptureCount++; 7975 mouseCapturedBy = byWhom; 7976 win.grabInput(); 7977 } 7978 void releaseMouseCapture() { 7979 mouseCaptureCount--; 7980 mouseCapturedBy = null; 7981 win.releaseInputGrab(); 7982 } 7983 7984 /++ 7985 Sets the window icon which is often seen in title bars and taskbars. 7986 7987 History: 7988 Added April 5, 2022 (dub v10.8) 7989 +/ 7990 @property void icon(MemoryImage icon) { 7991 if(win && icon) 7992 win.icon = icon; 7993 } 7994 7995 /// 7996 @scriptable 7997 @property bool focused() { 7998 return win.focused; 7999 } 8000 8001 static class Style : Widget.Style { 8002 override WidgetBackground background() { 8003 version(custom_widgets) 8004 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 8005 else version(win32_widgets) 8006 return WidgetBackground(Color.transparent); 8007 else static assert(0); 8008 } 8009 } 8010 mixin OverrideStyle!Style; 8011 8012 /++ 8013 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. 8014 +/ 8015 deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() { 8016 return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback(); 8017 } 8018 8019 private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() { 8020 OperatingSystemFont font; 8021 if(auto vt = WidgetPainter.visualTheme) { 8022 font = vt.defaultFontCached(96); // FIXME 8023 } 8024 8025 if(font is null) { 8026 static int defaultHeightCache; 8027 if(defaultHeightCache == 0) { 8028 font = new OperatingSystemFont; 8029 font.loadDefault; 8030 defaultHeightCache = font.height();// * 5 / 4; 8031 } 8032 return defaultHeightCache; 8033 } 8034 8035 return font.height();// * 5 / 4; 8036 } 8037 8038 Widget focusedWidget; 8039 8040 private SimpleWindow win_; 8041 8042 @property { 8043 /++ 8044 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 8045 8046 History: 8047 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 8048 +/ 8049 public SimpleWindow win() { 8050 return win_; 8051 } 8052 /// 8053 protected void win(SimpleWindow w) { 8054 win_ = w; 8055 } 8056 } 8057 8058 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 8059 this(Widget p) { 8060 tabStop = false; 8061 super(p); 8062 } 8063 8064 private void actualRedraw() { 8065 if(recomputeChildLayoutRequired) 8066 recomputeChildLayoutEntry(); 8067 if(!showing) return; 8068 8069 assert(parentWindow !is null); 8070 8071 auto w = drawableWindow; 8072 if(w is null) 8073 w = parentWindow.win; 8074 8075 if(w.closed()) 8076 return; 8077 8078 auto ugh = this.parent; 8079 int lox, loy; 8080 while(ugh) { 8081 lox += ugh.x; 8082 loy += ugh.y; 8083 ugh = ugh.parent; 8084 } 8085 auto painter = w.draw(true); 8086 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 8087 // RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN); 8088 } 8089 8090 8091 private bool skipNextChar = false; 8092 8093 /++ 8094 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 8095 8096 This constructor is intended primarily for internal use and may be changed to `protected` later. 8097 +/ 8098 this(SimpleWindow win) { 8099 8100 static if(UsingSimpledisplayX11) { 8101 win.discardAdditionalConnectionState = &discardXConnectionState; 8102 win.recreateAdditionalConnectionState = &recreateXConnectionState; 8103 } 8104 8105 tabStop = false; 8106 super(null); 8107 this.win = win; 8108 8109 win.addEventListener((Widget.RedrawEvent) { 8110 if(win.eventQueued!RecomputeEvent) { 8111 // writeln("skipping"); 8112 return; // let the recompute event do the actual redraw 8113 } 8114 this.actualRedraw(); 8115 }); 8116 8117 win.addEventListener((Widget.RecomputeEvent) { 8118 recomputeChildLayoutEntry(); 8119 if(win.eventQueued!RedrawEvent) 8120 return; // let the queued one do it 8121 else { 8122 // writeln("drawing"); 8123 this.actualRedraw(); // if not queued, it needs to be done now anyway 8124 } 8125 }); 8126 8127 this.width = win.width; 8128 this.height = win.height; 8129 this.parentWindow = this; 8130 8131 win.closeQuery = () { 8132 if(this.emit!ClosingEvent()) 8133 win.close(); 8134 }; 8135 win.onClosing = () { 8136 this.emit!ClosedEvent(); 8137 }; 8138 8139 win.windowResized = (int w, int h) { 8140 this.width = w; 8141 this.height = h; 8142 recomputeChildLayout(); 8143 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 8144 //version(win32_widgets) 8145 //InvalidateRect(hwnd, null, true); 8146 redraw(); 8147 }; 8148 8149 win.onFocusChange = (bool getting) { 8150 if(this.focusedWidget) { 8151 if(getting) { 8152 this.focusedWidget.emit!FocusEvent(); 8153 this.focusedWidget.emit!FocusInEvent(); 8154 } else { 8155 this.focusedWidget.emit!BlurEvent(); 8156 this.focusedWidget.emit!FocusOutEvent(); 8157 } 8158 } 8159 8160 if(getting) { 8161 this.emit!FocusEvent(); 8162 this.emit!FocusInEvent(); 8163 } else { 8164 this.emit!BlurEvent(); 8165 this.emit!FocusOutEvent(); 8166 } 8167 }; 8168 8169 win.onDpiChanged = { 8170 this.queueRecomputeChildLayout(); 8171 auto event = new DpiChangedEvent(this); 8172 event.sendDirectly(); 8173 8174 privateDpiChanged(); 8175 }; 8176 8177 win.setEventHandlers( 8178 (MouseEvent e) { 8179 dispatchMouseEvent(e); 8180 }, 8181 (KeyEvent e) { 8182 //writefln("%x %s", cast(uint) e.key, e.key); 8183 dispatchKeyEvent(e); 8184 }, 8185 (dchar e) { 8186 if(e == 13) e = 10; // hack? 8187 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8188 dispatchCharEvent(e); 8189 }, 8190 ); 8191 8192 addEventListener("char", (Widget, Event ev) { 8193 if(skipNextChar) { 8194 ev.preventDefault(); 8195 skipNextChar = false; 8196 } 8197 }); 8198 8199 version(win32_widgets) 8200 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 8201 if(hwnd !is this.win.impl.hwnd) 8202 return 1; // we don't care... pass it on 8203 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 8204 if(mustReturn) 8205 return ret; 8206 return 1; // pass it on 8207 }; 8208 8209 if(Window.newWindowCreated) 8210 Window.newWindowCreated(this); 8211 } 8212 8213 version(custom_widgets) 8214 override void defaultEventHandler_click(ClickEvent event) { 8215 if(event.target && event.target.tabStop) 8216 event.target.focus(); 8217 } 8218 8219 private static void delegate(Window) newWindowCreated; 8220 8221 version(win32_widgets) 8222 override void paint(WidgetPainter painter) { 8223 /* 8224 RECT rect; 8225 rect.right = this.width; 8226 rect.bottom = this.height; 8227 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 8228 */ 8229 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 8230 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 8231 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 8232 // since the pen is null, to fill the whole space, we need the +1 on both. 8233 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 8234 SelectObject(painter.impl.hdc, p); 8235 SelectObject(painter.impl.hdc, b); 8236 } 8237 version(custom_widgets) 8238 override void paint(WidgetPainter painter) { 8239 auto cs = getComputedStyle(); 8240 painter.fillColor = cs.windowBackgroundColor; 8241 painter.outlineColor = cs.windowBackgroundColor; 8242 painter.drawRectangle(Point(0, 0), this.width, this.height); 8243 } 8244 8245 8246 override void defaultEventHandler_keydown(KeyDownEvent event) { 8247 Widget _this = event.target; 8248 8249 if(event.key == Key.Tab) { 8250 /* Window tab ordering is a recursive thingy with each group */ 8251 8252 // FIXME inefficient 8253 Widget[] helper(Widget p) { 8254 if(p.hidden) 8255 return null; 8256 Widget[] childOrdering; 8257 8258 auto children = p.children.dup; 8259 8260 while(true) { 8261 // UIs should be generally small, so gonna brute force it a little 8262 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 8263 8264 Widget smallestTab; 8265 foreach(ref c; children) { 8266 if(c is null) continue; 8267 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 8268 smallestTab = c; 8269 c = null; 8270 } 8271 } 8272 if(smallestTab !is null) { 8273 if(smallestTab.tabStop && !smallestTab.hidden) 8274 childOrdering ~= smallestTab; 8275 if(!smallestTab.hidden) 8276 childOrdering ~= helper(smallestTab); 8277 } else 8278 break; 8279 8280 } 8281 8282 return childOrdering; 8283 } 8284 8285 Widget[] tabOrdering = helper(this); 8286 8287 Widget recipient; 8288 8289 if(tabOrdering.length) { 8290 bool seenThis = false; 8291 Widget previous; 8292 foreach(idx, child; tabOrdering) { 8293 if(child is focusedWidget) { 8294 8295 if(event.shiftKey) { 8296 if(idx == 0) 8297 recipient = tabOrdering[$-1]; 8298 else 8299 recipient = tabOrdering[idx - 1]; 8300 break; 8301 } 8302 8303 seenThis = true; 8304 if(idx + 1 == tabOrdering.length) { 8305 // we're at the end, either move to the next group 8306 // or start back over 8307 recipient = tabOrdering[0]; 8308 } 8309 continue; 8310 } 8311 if(seenThis) { 8312 recipient = child; 8313 break; 8314 } 8315 previous = child; 8316 } 8317 } 8318 8319 if(recipient !is null) { 8320 // writeln(typeid(recipient)); 8321 recipient.focus(); 8322 8323 skipNextChar = true; 8324 } 8325 } 8326 8327 debug if(event.key == Key.F12) { 8328 if(devTools) { 8329 devTools.close(); 8330 devTools = null; 8331 } else { 8332 devTools = new DevToolWindow(this); 8333 devTools.show(); 8334 } 8335 } 8336 } 8337 8338 debug DevToolWindow devTools; 8339 8340 8341 /++ 8342 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 8343 8344 History: 8345 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 8346 8347 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 8348 +/ 8349 this(int width = 500, int height = 500, string title = null) { 8350 if(title is null) { 8351 import core.runtime; 8352 if(Runtime.args.length) 8353 title = Runtime.args[0]; 8354 } 8355 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus); 8356 8357 static if(UsingSimpledisplayX11) { 8358 ///+ 8359 // for input proxy 8360 auto display = XDisplayConnection.get; 8361 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 8362 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 8363 XMapWindow(display, inputProxy); 8364 // writefln("input proxy: 0x%0x", inputProxy); 8365 this.inputProxy = new SimpleWindow(inputProxy); 8366 8367 XEvent lastEvent; 8368 this.inputProxy.handleNativeEvent = (XEvent ev) { 8369 lastEvent = ev; 8370 return 1; 8371 }; 8372 this.inputProxy.setEventHandlers( 8373 (MouseEvent e) { 8374 dispatchMouseEvent(e); 8375 }, 8376 (KeyEvent e) { 8377 //writefln("%x %s", cast(uint) e.key, e.key); 8378 if(dispatchKeyEvent(e)) { 8379 // FIXME: i should trap error 8380 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 8381 auto thing = nw.focusableWindow(); 8382 if(thing && thing.window) { 8383 lastEvent.xkey.window = thing.window; 8384 // writeln("sending event ", lastEvent.xkey); 8385 trapXErrors( { 8386 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 8387 }); 8388 } 8389 } 8390 } 8391 }, 8392 (dchar e) { 8393 if(e == 13) e = 10; // hack? 8394 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8395 dispatchCharEvent(e); 8396 }, 8397 ); 8398 8399 this.inputProxy.populateXic(); 8400 // done 8401 //+/ 8402 } 8403 8404 8405 8406 win.setRequestedInputFocus = &this.setRequestedInputFocus; 8407 8408 this(win); 8409 } 8410 8411 SimpleWindow inputProxy; 8412 8413 private SimpleWindow setRequestedInputFocus() { 8414 return inputProxy; 8415 } 8416 8417 /// ditto 8418 this(string title, int width = 500, int height = 500) { 8419 this(width, height, title); 8420 } 8421 8422 /// 8423 @property string title() { return parentWindow.win.title; } 8424 /// 8425 @property void title(string title) { parentWindow.win.title = title; } 8426 8427 /// 8428 @scriptable 8429 void close() { 8430 win.close(); 8431 // I synchronize here upon window closing to ensure all child windows 8432 // get updated too before the event loop. This avoids some random X errors. 8433 static if(UsingSimpledisplayX11) { 8434 runInGuiThread( { 8435 XSync(XDisplayConnection.get, false); 8436 }); 8437 } 8438 } 8439 8440 bool dispatchKeyEvent(KeyEvent ev) { 8441 auto wid = focusedWidget; 8442 if(wid is null) 8443 wid = this; 8444 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 8445 event.originalKeyEvent = ev; 8446 event.key = ev.key; 8447 event.state = ev.modifierState; 8448 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8449 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8450 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8451 event.dispatch(); 8452 8453 return !event.propagationStopped; 8454 } 8455 8456 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 8457 bool dispatchCharEvent(dchar ch) { 8458 if(focusedWidget) { 8459 auto event = new CharEvent(focusedWidget, ch); 8460 event.dispatch(); 8461 return !event.propagationStopped; 8462 } 8463 return true; 8464 } 8465 8466 Widget mouseLastOver; 8467 Widget mouseLastDownOn; 8468 bool lastWasDoubleClick; 8469 bool dispatchMouseEvent(MouseEvent ev) { 8470 auto eleR = widgetAtPoint(this, ev.x, ev.y); 8471 auto ele = eleR.widget; 8472 8473 auto captureEle = ele; 8474 8475 if(mouseCapturedBy !is null) { 8476 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 8477 captureEle = mouseCapturedBy; 8478 } 8479 8480 // a hack to get it relative to the widget. 8481 eleR.x = ev.x; 8482 eleR.y = ev.y; 8483 auto pain = captureEle; 8484 while(pain) { 8485 eleR.x -= pain.x; 8486 eleR.y -= pain.y; 8487 pain.addScrollPosition(eleR.x, eleR.y); 8488 pain = pain.parent; 8489 } 8490 8491 void populateMouseEventBase(MouseEventBase event) { 8492 event.button = ev.button; 8493 event.buttonLinear = ev.buttonLinear; 8494 event.state = ev.modifierState; 8495 event.clientX = eleR.x; 8496 event.clientY = eleR.y; 8497 8498 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8499 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8500 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8501 } 8502 8503 if(ev.type == MouseEventType.buttonPressed) { 8504 { 8505 auto event = new MouseDownEvent(captureEle); 8506 populateMouseEventBase(event); 8507 event.dispatch(); 8508 } 8509 8510 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 8511 auto event = new DoubleClickEvent(captureEle); 8512 populateMouseEventBase(event); 8513 event.dispatch(); 8514 lastWasDoubleClick = ev.doubleClick; 8515 } else { 8516 lastWasDoubleClick = false; 8517 } 8518 8519 mouseLastDownOn = ele; 8520 } else if(ev.type == MouseEventType.buttonReleased) { 8521 { 8522 auto event = new MouseUpEvent(captureEle); 8523 populateMouseEventBase(event); 8524 event.dispatch(); 8525 } 8526 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 8527 auto event = new ClickEvent(captureEle); 8528 populateMouseEventBase(event); 8529 event.dispatch(); 8530 } 8531 } else if(ev.type == MouseEventType.motion) { 8532 // motion 8533 { 8534 auto event = new MouseMoveEvent(captureEle); 8535 populateMouseEventBase(event); // fills in button which is meaningless but meh 8536 event.dispatch(); 8537 } 8538 8539 if(mouseLastOver !is ele) { 8540 if(ele !is null) { 8541 if(!isAParentOf(ele, mouseLastOver)) { 8542 ele.setDynamicState(DynamicState.hover, true); 8543 auto event = new MouseEnterEvent(ele); 8544 event.relatedTarget = mouseLastOver; 8545 event.sendDirectly(); 8546 8547 ele.useStyleProperties((scope Widget.Style s) { 8548 ele.parentWindow.win.cursor = s.cursor; 8549 }); 8550 } 8551 } 8552 8553 if(mouseLastOver !is null) { 8554 if(!isAParentOf(mouseLastOver, ele)) { 8555 mouseLastOver.setDynamicState(DynamicState.hover, false); 8556 auto event = new MouseLeaveEvent(mouseLastOver); 8557 event.relatedTarget = ele; 8558 event.sendDirectly(); 8559 } 8560 } 8561 8562 if(ele !is null) { 8563 auto event = new MouseOverEvent(ele); 8564 event.relatedTarget = mouseLastOver; 8565 event.dispatch(); 8566 } 8567 8568 if(mouseLastOver !is null) { 8569 auto event = new MouseOutEvent(mouseLastOver); 8570 event.relatedTarget = ele; 8571 event.dispatch(); 8572 } 8573 8574 mouseLastOver = ele; 8575 } 8576 } 8577 8578 return true; // FIXME: the event default prevented? 8579 } 8580 8581 /++ 8582 Shows the window and runs the application event loop. 8583 8584 Blocks until this window is closed. 8585 8586 Bugs: 8587 8588 $(PITFALL 8589 You should always have one event loop live for your application. 8590 If you make two windows in sequence, the second call to loop (or 8591 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 8592 might fail: 8593 8594 --- 8595 // don't do this! 8596 auto window = new Window(); 8597 window.loop(); 8598 8599 // or new Window or new MainWindow, all the same 8600 auto window2 = new SimpleWindow(); 8601 window2.eventLoop(0); // problematic! might crash 8602 --- 8603 8604 simpledisplay's current implementation assumes that final cleanup is 8605 done when the event loop refcount reaches zero. So after the first 8606 eventLoop returns, when there isn't already another one active, it assumes 8607 the program will exit soon and cleans up. 8608 8609 This is arguably a bug that it doesn't reinitialize, and I'll probably change 8610 it eventually, but in the mean time, there's an easy solution: 8611 8612 --- 8613 // do this 8614 EventLoop mainEventLoop = EventLoop.get; // just add this line 8615 8616 auto window = new Window(); 8617 window.loop(); 8618 8619 // or any other type of Window etc. 8620 auto window2 = new Window(); 8621 window2.loop(); // perfectly fine since mainEventLoop still alive 8622 --- 8623 8624 By adding a top-level reference to the event loop, it ensures the final cleanup 8625 is not performed until it goes out of scope too, letting the individual window loops 8626 work without trouble despite the bug. 8627 ) 8628 8629 History: 8630 The [BlockingMode] parameter was added on December 8, 2021. 8631 The default behavior is to block until the application quits 8632 (so all windows have been closed), unless another minigui or 8633 simpledisplay event loop is already running, in which case it 8634 will block until this window closes specifically. 8635 +/ 8636 @scriptable 8637 void loop(BlockingMode bm = BlockingMode.automatic) { 8638 if(win.closed) 8639 return; // otherwise show will throw 8640 show(); 8641 win.eventLoopWithBlockingMode(bm, 0); 8642 } 8643 8644 private bool firstShow = true; 8645 8646 @scriptable 8647 override void show() { 8648 bool rd = false; 8649 if(firstShow) { 8650 firstShow = false; 8651 recomputeChildLayout(); 8652 auto f = getFirstFocusable(this); // FIXME: autofocus? 8653 if(f) 8654 f.focus(); 8655 redraw(); 8656 } 8657 win.show(); 8658 super.show(); 8659 } 8660 @scriptable 8661 override void hide() { 8662 win.hide(); 8663 super.hide(); 8664 } 8665 8666 static Widget getFirstFocusable(Widget start) { 8667 if(start is null) 8668 return null; 8669 8670 foreach(widget; &start.focusableWidgets) { 8671 return widget; 8672 } 8673 8674 return null; 8675 } 8676 8677 static Widget getLastFocusable(Widget start) { 8678 if(start is null) 8679 return null; 8680 8681 Widget last; 8682 foreach(widget; &start.focusableWidgets) { 8683 last = widget; 8684 } 8685 8686 return last; 8687 } 8688 8689 8690 mixin Emits!ClosingEvent; 8691 mixin Emits!ClosedEvent; 8692 } 8693 8694 /++ 8695 History: 8696 Added January 12, 2022 8697 +/ 8698 class DpiChangedEvent : Event { 8699 enum EventString = "dpichanged"; 8700 8701 this(Widget target) { 8702 super(EventString, target); 8703 } 8704 } 8705 8706 debug private class DevToolWindow : Window { 8707 Window p; 8708 8709 TextEdit parentList; 8710 TextEdit logWindow; 8711 TextLabel clickX, clickY; 8712 8713 this(Window p) { 8714 this.p = p; 8715 super(400, 300, "Developer Toolbox"); 8716 8717 logWindow = new TextEdit(this); 8718 parentList = new TextEdit(this); 8719 8720 auto hl = new HorizontalLayout(this); 8721 clickX = new TextLabel("", TextAlignment.Right, hl); 8722 clickY = new TextLabel("", TextAlignment.Right, hl); 8723 8724 parentListeners ~= p.addEventListener("*", (Event ev) { 8725 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 8726 }); 8727 8728 parentListeners ~= p.addEventListener((ClickEvent ev) { 8729 auto s = ev.srcElement; 8730 8731 string list; 8732 8733 void addInfo(Widget s) { 8734 list ~= s.toString(); 8735 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 8736 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 8737 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 8738 list ~= "\n\theight: " ~ toInternal!string(s.height); 8739 list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth); 8740 list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth); 8741 list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness); 8742 list ~= "\n\twidth: " ~ toInternal!string(s.width); 8743 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 8744 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 8745 } 8746 8747 addInfo(s); 8748 8749 s = s.parent; 8750 while(s) { 8751 list ~= "\n"; 8752 addInfo(s); 8753 s = s.parent; 8754 } 8755 parentList.content = list; 8756 8757 clickX.label = toInternal!string(ev.clientX); 8758 clickY.label = toInternal!string(ev.clientY); 8759 }); 8760 } 8761 8762 EventListener[] parentListeners; 8763 8764 override void close() { 8765 assert(p !is null); 8766 foreach(p; parentListeners) 8767 p.disconnect(); 8768 parentListeners = null; 8769 p.devTools = null; 8770 p = null; 8771 super.close(); 8772 } 8773 8774 override void defaultEventHandler_keydown(KeyDownEvent ev) { 8775 if(ev.key == Key.F12) { 8776 this.close(); 8777 if(p) 8778 p.devTools = null; 8779 } else { 8780 super.defaultEventHandler_keydown(ev); 8781 } 8782 } 8783 8784 void log(T...)(T t) { 8785 string str; 8786 import std.conv; 8787 foreach(i; t) 8788 str ~= to!string(i); 8789 str ~= "\n"; 8790 logWindow.addText(str); 8791 8792 //version(custom_widgets) 8793 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 8794 } 8795 } 8796 8797 /++ 8798 A dialog is a transient window that intends to get information from 8799 the user before being dismissed. 8800 +/ 8801 abstract class Dialog : Window { 8802 /// 8803 this(int width, int height, string title = null) { 8804 super(width, height, title); 8805 } 8806 8807 /// 8808 abstract void OK(); 8809 8810 /// 8811 void Cancel() { 8812 this.close(); 8813 } 8814 } 8815 8816 /++ 8817 A custom widget similar to the HTML5 <details> tag. 8818 +/ 8819 version(none) 8820 class DetailsView : Widget { 8821 8822 } 8823 8824 // FIXME: maybe i should expose the other list views Windows offers too 8825 8826 /++ 8827 A TableView is a widget made to display a table of data strings. 8828 8829 8830 Future_Directions: 8831 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 8832 8833 I will add a selection changed event at some point, as well as item clicked events. 8834 History: 8835 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 8836 See_Also: 8837 [ListWidget] which displays a list of strings without additional columns. 8838 +/ 8839 class TableView : Widget { 8840 /++ 8841 8842 +/ 8843 this(Widget parent) { 8844 super(parent); 8845 8846 version(win32_widgets) { 8847 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 8848 } else version(custom_widgets) { 8849 auto smw = new ScrollMessageWidget(this); 8850 smw.addDefaultKeyboardListeners(); 8851 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 8852 tvwi = new TableViewWidgetInner(this, smw); 8853 } 8854 } 8855 8856 // FIXME: auto-size columns on double click of header thing like in Windows 8857 // it need only make the currently displayed things fit well. 8858 8859 8860 private ColumnInfo[] columns; 8861 private int itemCount; 8862 8863 version(custom_widgets) private { 8864 TableViewWidgetInner tvwi; 8865 } 8866 8867 /// Passed to [setColumnInfo] 8868 static struct ColumnInfo { 8869 const(char)[] name; /// the name displayed in the header 8870 /++ 8871 The default width, in pixels. As a special case, you can set this to -1 8872 if you want the system to try to automatically size the width to fit visible 8873 content. If it can't, it will try to pick a sensible default size. 8874 8875 Any other negative value is not allowed and may lead to unpredictable results. 8876 8877 History: 8878 The -1 behavior was specified on December 3, 2021. It actually worked before 8879 anyway on Win32 but now it is a formal feature with partial Linux support. 8880 8881 Bugs: 8882 It doesn't actually attempt to calculate a best-fit width on Linux as of 8883 December 3, 2021. I do plan to fix this in the future, but Windows is the 8884 priority right now. At least it doesn't break things when you use it now. 8885 +/ 8886 int width; 8887 8888 /++ 8889 Alignment of the text in the cell. Applies to the header as well as all data in this 8890 column. 8891 8892 Bugs: 8893 On Windows, the first column ignores this member and is always left aligned. 8894 You can work around this by inserting a dummy first column with width = 0 8895 then putting your actual data in the second column, which does respect the 8896 alignment. 8897 8898 This is a quirk of the operating system's implementation going back a very 8899 long time and is unlikely to ever be fixed. 8900 +/ 8901 TextAlignment alignment; 8902 8903 /++ 8904 After all the pixel widths have been assigned, any left over 8905 space is divided up among all columns and distributed to according 8906 to the widthPercent field. 8907 8908 8909 For example, if you have two fields, both with width 50 and one with 8910 widthPercent of 25 and the other with widthPercent of 75, and the 8911 container is 200 pixels wide, first both get their width of 50. 8912 then the 100 remaining pixels are split up, so the one gets a total 8913 of 75 pixels and the other gets a total of 125. 8914 8915 This is automatically applied as the window is resized. 8916 8917 If there is not enough space - that is, when a horizontal scrollbar 8918 needs to appear - there are 0 pixels divided up, and thus everyone 8919 gets 0. This can cause a column to shrink out of proportion when 8920 passing the scroll threshold. 8921 8922 It is important to still set a fixed width (that is, to populate the 8923 `width` field) even if you use the percents because that will be the 8924 default minimum in the event of a scroll bar appearing. 8925 8926 The percents total in the column can never exceed 100 or be less than 0. 8927 Doing this will trigger an assert error. 8928 8929 Implementation note: 8930 8931 Please note that percentages are only recalculated 1) upon original 8932 construction and 2) upon resizing the control. If the user adjusts the 8933 width of a column, the percentage items will not be updated. 8934 8935 On the other hand, if the user adjusts the width of a percentage column 8936 then resizes the window, it is recalculated, meaning their hand adjustment 8937 is discarded. This specific behavior may change in the future as it is 8938 arguably a bug, but I'm not certain yet. 8939 8940 History: 8941 Added November 10, 2021 (dub v10.4) 8942 +/ 8943 int widthPercent; 8944 8945 8946 private int calculatedWidth; 8947 } 8948 /++ 8949 Sets the number of columns along with information about the headers. 8950 8951 Please note: on Windows, the first column ignores your alignment preference 8952 and is always left aligned. 8953 +/ 8954 void setColumnInfo(ColumnInfo[] columns...) { 8955 8956 foreach(ref c; columns) { 8957 c.name = c.name.idup; 8958 } 8959 this.columns = columns.dup; 8960 8961 updateCalculatedWidth(false); 8962 8963 version(custom_widgets) { 8964 tvwi.header.updateHeaders(); 8965 tvwi.updateScrolls(); 8966 } else version(win32_widgets) 8967 foreach(i, column; this.columns) { 8968 LVCOLUMN lvColumn; 8969 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 8970 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 8971 8972 auto bfr = WCharzBuffer(column.name); 8973 lvColumn.pszText = bfr.ptr; 8974 8975 if(column.alignment & TextAlignment.Center) 8976 lvColumn.fmt = LVCFMT_CENTER; 8977 else if(column.alignment & TextAlignment.Right) 8978 lvColumn.fmt = LVCFMT_RIGHT; 8979 else 8980 lvColumn.fmt = LVCFMT_LEFT; 8981 8982 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 8983 throw new WindowsApiException("Insert Column Fail", GetLastError()); 8984 } 8985 } 8986 8987 private int getActualSetSize(size_t i, bool askWindows) { 8988 version(win32_widgets) 8989 if(askWindows) 8990 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 8991 auto w = columns[i].width; 8992 if(w == -1) 8993 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 8994 return w; 8995 } 8996 8997 private void updateCalculatedWidth(bool informWindows) { 8998 int padding; 8999 version(win32_widgets) 9000 padding = 4; 9001 int remaining = this.width; 9002 foreach(i, column; columns) 9003 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 9004 remaining -= padding; 9005 if(remaining < 0) 9006 remaining = 0; 9007 9008 int percentTotal; 9009 foreach(i, ref column; columns) { 9010 percentTotal += column.widthPercent; 9011 9012 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 9013 9014 column.calculatedWidth = c; 9015 9016 version(win32_widgets) 9017 if(informWindows) 9018 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 9019 } 9020 9021 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 9022 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)."); 9023 9024 9025 } 9026 9027 override void registerMovement() { 9028 super.registerMovement(); 9029 9030 updateCalculatedWidth(true); 9031 } 9032 9033 /++ 9034 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. 9035 +/ 9036 void setItemCount(int count) { 9037 this.itemCount = count; 9038 version(custom_widgets) { 9039 tvwi.updateScrolls(); 9040 redraw(); 9041 } else version(win32_widgets) { 9042 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 9043 } 9044 } 9045 9046 /++ 9047 Clears all items; 9048 +/ 9049 void clear() { 9050 this.itemCount = 0; 9051 this.columns = null; 9052 version(custom_widgets) { 9053 tvwi.header.updateHeaders(); 9054 tvwi.updateScrolls(); 9055 redraw(); 9056 } else version(win32_widgets) { 9057 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 9058 } 9059 } 9060 9061 /+ 9062 version(win32_widgets) 9063 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 9064 auto itemId = dis.itemID; 9065 auto hdc = dis.hDC; 9066 auto rect = dis.rcItem; 9067 switch(dis.itemAction) { 9068 case ODA_DRAWENTIRE: 9069 9070 // FIXME: do other items 9071 // FIXME: do the focus rectangle i guess 9072 // FIXME: alignment 9073 // FIXME: column width 9074 // FIXME: padding left 9075 // FIXME: check dpi scaling 9076 // FIXME: don't owner draw unless it is necessary. 9077 9078 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 9079 RECT itemRect; 9080 itemRect.top = 1; // subitem idx, 1-based 9081 itemRect.left = LVIR_BOUNDS; 9082 9083 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 9084 itemRect.left += padding; 9085 9086 getData(itemId, 0, (in char[] data) { 9087 auto wdata = WCharzBuffer(data); 9088 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 9089 9090 }); 9091 goto case; 9092 case ODA_FOCUS: 9093 if(dis.itemState & ODS_FOCUS) 9094 DrawFocusRect(hdc, &rect); 9095 break; 9096 case ODA_SELECT: 9097 // itemState & ODS_SELECTED 9098 break; 9099 default: 9100 } 9101 return 1; 9102 } 9103 +/ 9104 9105 version(win32_widgets) { 9106 CellStyle last; 9107 COLORREF defaultColor; 9108 COLORREF defaultBackground; 9109 } 9110 9111 version(win32_widgets) 9112 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 9113 switch(code) { 9114 case NM_CUSTOMDRAW: 9115 auto s = cast(NMLVCUSTOMDRAW*) hdr; 9116 switch(s.nmcd.dwDrawStage) { 9117 case CDDS_PREPAINT: 9118 if(getCellStyle is null) 9119 return 0; 9120 9121 mustReturn = true; 9122 return CDRF_NOTIFYITEMDRAW; 9123 case CDDS_ITEMPREPAINT: 9124 mustReturn = true; 9125 return CDRF_NOTIFYSUBITEMDRAW; 9126 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 9127 mustReturn = true; 9128 9129 if(getCellStyle is null) // this SHOULD never happen... 9130 return 0; 9131 9132 if(s.iSubItem == 0) { 9133 // Windows resets it per row so we'll use item 0 as a chance 9134 // to capture these for later 9135 defaultColor = s.clrText; 9136 defaultBackground = s.clrTextBk; 9137 } 9138 9139 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 9140 // if no special style and no reset needed... 9141 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 9142 return 0; // allow default processing to continue 9143 9144 last = style; 9145 9146 // might still need to reset or use the preference. 9147 9148 if(style.flags & CellStyle.Flags.textColorSet) 9149 s.clrText = style.textColor.asWindowsColorRef; 9150 else 9151 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 9152 if(style.flags & CellStyle.Flags.backgroundColorSet) 9153 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 9154 else 9155 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 9156 9157 return CDRF_NEWFONT; 9158 default: 9159 return 0; 9160 9161 } 9162 case NM_RETURN: // no need since i subclass keydown 9163 break; 9164 case LVN_COLUMNCLICK: 9165 auto info = cast(LPNMLISTVIEW) hdr; 9166 this.emit!HeaderClickedEvent(info.iSubItem); 9167 break; 9168 case NM_CLICK: 9169 case NM_DBLCLK: 9170 case NM_RCLICK: 9171 case NM_RDBLCLK: 9172 // the item/subitem is set here and that can be a useful notification 9173 // even beyond the normal click notification 9174 break; 9175 case LVN_GETDISPINFO: 9176 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 9177 if(info.item.mask & LVIF_TEXT) { 9178 if(getData) { 9179 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 9180 auto bfr = WCharzBuffer(dataReceived); 9181 auto len = info.item.cchTextMax; 9182 if(bfr.length < len) 9183 len = cast(typeof(len)) bfr.length; 9184 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 9185 info.item.pszText[len] = 0; 9186 }); 9187 } else { 9188 info.item.pszText[0] = 0; 9189 } 9190 //info.item.iItem 9191 //if(info.item.iSubItem) 9192 } 9193 break; 9194 default: 9195 } 9196 return 0; 9197 } 9198 9199 override bool encapsulatedChildren() { 9200 return true; 9201 } 9202 9203 /++ 9204 Informs the control that content has changed. 9205 9206 History: 9207 Added November 10, 2021 (dub v10.4) 9208 +/ 9209 void update() { 9210 version(custom_widgets) 9211 redraw(); 9212 else { 9213 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 9214 UpdateWindow(hwnd); 9215 } 9216 9217 9218 } 9219 9220 /++ 9221 Called by the system to request the text content of an individual cell. You 9222 should pass the text into the provided `sink` delegate. This function will be 9223 called for each visible cell as-needed when drawing. 9224 +/ 9225 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 9226 9227 /++ 9228 Available per-cell style customization options. Use one of the constructors 9229 provided to set the values conveniently, or default construct it and set individual 9230 values yourself. Just remember to set the `flags` so your values are actually used. 9231 If the flag isn't set, the field is ignored and the system default is used instead. 9232 9233 This is returned by the [getCellStyle] delegate. 9234 9235 Examples: 9236 --- 9237 // assumes you have a variables called `my_data` which is an array of arrays of numbers 9238 auto table = new TableView(window); 9239 // snip: you would set up columns here 9240 9241 // this is how you provide data to the table view class 9242 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 9243 import std.conv; 9244 sink(to!string(my_data[row][column])); 9245 }; 9246 9247 // and this is how you customize the colors 9248 table.getCellStyle = delegate(int row, int column) { 9249 return (my_data[row][column] < 0) ? 9250 TableView.CellStyle(Color.red); // make negative numbers red 9251 : TableView.CellStyle.init; // leave the rest alone 9252 }; 9253 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 9254 --- 9255 9256 History: 9257 Added November 27, 2021 (dub v10.4) 9258 +/ 9259 struct CellStyle { 9260 /// 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. 9261 this(Color textColor) { 9262 this.textColor = textColor; 9263 this.flags |= Flags.textColorSet; 9264 } 9265 /// Sets a custom text and background color. 9266 this(Color textColor, Color backgroundColor) { 9267 this.textColor = textColor; 9268 this.backgroundColor = backgroundColor; 9269 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 9270 } 9271 9272 Color textColor; 9273 Color backgroundColor; 9274 int flags; /// bitmask of [Flags] 9275 /// available options to combine into [flags] 9276 enum Flags { 9277 textColorSet = 1 << 0, 9278 backgroundColorSet = 1 << 1, 9279 } 9280 } 9281 /++ 9282 Companion delegate to [getData] that allows you to custom style each 9283 cell of the table. 9284 9285 Returns: 9286 A [CellStyle] structure that describes the desired style for the 9287 given cell. `return CellStyle.init` if you want the default style. 9288 9289 History: 9290 Added November 27, 2021 (dub v10.4) 9291 +/ 9292 CellStyle delegate(int row, int column) getCellStyle; 9293 9294 // i want to be able to do things like draw little colored things to show red for negative numbers 9295 // or background color indicators or even in-cell charts 9296 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 9297 9298 /++ 9299 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 9300 +/ 9301 mixin Emits!HeaderClickedEvent; 9302 } 9303 9304 /++ 9305 This is emitted by the [TableView] when a user clicks on a column header. 9306 9307 Its member `columnIndex` has the zero-based index of the column that was clicked. 9308 9309 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 9310 9311 History: 9312 Added November 27, 2021 (dub v10.4) 9313 +/ 9314 class HeaderClickedEvent : Event { 9315 enum EventString = "HeaderClicked"; 9316 this(Widget target, int columnIndex) { 9317 this.columnIndex = columnIndex; 9318 super(EventString, target); 9319 } 9320 9321 /// The index of the column 9322 int columnIndex; 9323 9324 /// 9325 override @property int intValue() { 9326 return columnIndex; 9327 } 9328 } 9329 9330 version(custom_widgets) 9331 private class TableViewWidgetInner : Widget { 9332 9333 // wrap this thing in a ScrollMessageWidget 9334 9335 TableView tvw; 9336 ScrollMessageWidget smw; 9337 HeaderWidget header; 9338 9339 this(TableView tvw, ScrollMessageWidget smw) { 9340 this.tvw = tvw; 9341 this.smw = smw; 9342 super(smw); 9343 9344 this.tabStop = true; 9345 9346 header = new HeaderWidget(this, smw.getHeader()); 9347 9348 smw.addEventListener("scroll", () { 9349 this.redraw(); 9350 header.redraw(); 9351 }); 9352 9353 9354 // I need headers outside the scroll area but rendered on the same line as the up arrow 9355 // FIXME: add a fixed header to the SMW 9356 } 9357 9358 enum padding = 3; 9359 9360 void updateScrolls() { 9361 int w; 9362 foreach(idx, column; tvw.columns) { 9363 if(column.width == 0) continue; 9364 w += tvw.getActualSetSize(idx, false);// + padding; 9365 } 9366 smw.setTotalArea(w, tvw.itemCount); 9367 columnsWidth = w; 9368 } 9369 9370 private int columnsWidth; 9371 9372 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 9373 9374 override void registerMovement() { 9375 super.registerMovement(); 9376 // FIXME: actual column width. it might need to be done per-pixel instead of per-colum 9377 smw.setViewableArea(this.width, this.height / lh); 9378 } 9379 9380 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 9381 int x; 9382 int y; 9383 9384 int row = smw.position.y; 9385 9386 foreach(lol; 0 .. this.height / lh) { 9387 if(row >= tvw.itemCount) 9388 break; 9389 x = 0; 9390 foreach(columnNumber, column; tvw.columns) { 9391 auto x2 = x + column.calculatedWidth; 9392 auto smwx = smw.position.x; 9393 9394 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 9395 auto startX = x; 9396 auto endX = x + column.calculatedWidth; 9397 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 9398 case TextAlignment.Left: startX += padding; break; 9399 case TextAlignment.Center: startX += padding; endX -= padding; break; 9400 case TextAlignment.Right: endX -= padding; break; 9401 default: /* broken */ break; 9402 } 9403 if(column.width != 0) // no point drawing an invisible column 9404 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 9405 // auto clip = painter.setClipRectangle( 9406 9407 void dotext(WidgetPainter painter) { 9408 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 9409 } 9410 9411 if(tvw.getCellStyle !is null) { 9412 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 9413 9414 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 9415 auto tempPainter = painter; 9416 tempPainter.fillColor = style.backgroundColor; 9417 tempPainter.outlineColor = style.backgroundColor; 9418 9419 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 9420 Point(endX - smw.position.x, y + lh)); 9421 } 9422 auto tempPainter = painter; 9423 if(style.flags & TableView.CellStyle.Flags.textColorSet) 9424 tempPainter.outlineColor = style.textColor; 9425 9426 dotext(tempPainter); 9427 } else { 9428 dotext(painter); 9429 } 9430 }); 9431 } 9432 9433 x += column.calculatedWidth; 9434 } 9435 row++; 9436 y += lh; 9437 } 9438 return bounds; 9439 } 9440 9441 static class Style : Widget.Style { 9442 override WidgetBackground background() { 9443 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 9444 } 9445 } 9446 mixin OverrideStyle!Style; 9447 9448 private static class HeaderWidget : Widget { 9449 this(TableViewWidgetInner tvw, Widget parent) { 9450 super(parent); 9451 this.tvw = tvw; 9452 9453 this.remainder = new Button("", this); 9454 9455 this.addEventListener((scope ClickEvent ev) { 9456 int header = -1; 9457 foreach(idx, child; this.children[1 .. $]) { 9458 if(child is ev.target) { 9459 header = cast(int) idx; 9460 break; 9461 } 9462 } 9463 9464 if(header != -1) { 9465 auto hce = new HeaderClickedEvent(tvw.tvw, header); 9466 hce.dispatch(); 9467 } 9468 9469 }); 9470 } 9471 9472 void updateHeaders() { 9473 foreach(child; children[1 .. $]) 9474 child.removeWidget(); 9475 9476 foreach(column; tvw.tvw.columns) { 9477 // the cast is ok because I dup it above, just the type is never changed. 9478 // all this is private so it should never get messed up. 9479 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 9480 } 9481 } 9482 9483 Button remainder; 9484 TableViewWidgetInner tvw; 9485 9486 override void recomputeChildLayout() { 9487 registerMovement(); 9488 int pos; 9489 foreach(idx, child; children[1 .. $]) { 9490 if(idx >= tvw.tvw.columns.length) 9491 continue; 9492 child.x = pos; 9493 child.y = 0; 9494 child.width = tvw.tvw.columns[idx].calculatedWidth; 9495 child.height = scaleWithDpi(16);// this.height; 9496 pos += child.width; 9497 9498 child.recomputeChildLayout(); 9499 } 9500 9501 if(remainder is null) 9502 return; 9503 9504 remainder.x = pos; 9505 remainder.y = 0; 9506 if(pos < this.width) 9507 remainder.width = this.width - pos;// + 4; 9508 else 9509 remainder.width = 0; 9510 remainder.height = scaleWithDpi(16); 9511 9512 remainder.recomputeChildLayout(); 9513 } 9514 9515 // for the scrollable children mixin 9516 Point scrollOrigin() { 9517 return Point(tvw.smw.position.x, 0); 9518 } 9519 void paintFrameAndBackground(WidgetPainter painter) { } 9520 9521 mixin ScrollableChildren; 9522 } 9523 } 9524 9525 /+ 9526 9527 // given struct / array / number / string / etc, make it viewable and editable 9528 class DataViewerWidget : Widget { 9529 9530 } 9531 +/ 9532 9533 /++ 9534 A line edit box with an associated label. 9535 9536 History: 9537 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 9538 9539 ``` 9540 Old: ________ 9541 9542 New: 9543 ____________ 9544 ``` 9545 9546 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 9547 9548 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 9549 horizontal label but left aligned. You may also consider a [GridLayout]. 9550 +/ 9551 alias LabeledLineEdit = Labeled!LineEdit; 9552 9553 /++ 9554 History: 9555 Added May 19, 2021 9556 +/ 9557 class Labeled(T) : Widget { 9558 /// 9559 this(string label, Widget parent) { 9560 super(parent); 9561 initialize!VerticalLayout(label, TextAlignment.Left, parent); 9562 } 9563 9564 /++ 9565 History: 9566 The alignment parameter was added May 17, 2021 9567 +/ 9568 this(string label, TextAlignment alignment, Widget parent) { 9569 super(parent); 9570 initialize!HorizontalLayout(label, alignment, parent); 9571 } 9572 9573 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 9574 tabStop = false; 9575 horizontal = is(L == HorizontalLayout); 9576 auto hl = new L(this); 9577 if(horizontal) { 9578 static class SpecialTextLabel : TextLabel { 9579 this(string label, TextAlignment alignment, Widget parent) { 9580 super(label, alignment, parent); 9581 } 9582 9583 override int paddingTop() { return 6; } 9584 } 9585 this.label = new SpecialTextLabel(label, alignment, hl); 9586 } else 9587 this.label = new TextLabel(label, alignment, hl); 9588 this.lineEdit = new T(hl); 9589 9590 this.label.labelFor = this.lineEdit; 9591 } 9592 9593 private bool horizontal; 9594 9595 TextLabel label; /// 9596 T lineEdit; /// 9597 9598 override int flexBasisWidth() { return 250; } 9599 9600 override int minHeight() { 9601 return this.children[0].minHeight; 9602 } 9603 override int maxHeight() { return minHeight(); } 9604 override int marginTop() { return 4; } 9605 override int marginBottom() { return 4; } 9606 9607 // FIXME: i should prolly call it value as well as content tbh 9608 9609 /// 9610 @property string content() { 9611 return lineEdit.content; 9612 } 9613 /// 9614 @property void content(string c) { 9615 return lineEdit.content(c); 9616 } 9617 9618 /// 9619 void selectAll() { 9620 lineEdit.selectAll(); 9621 } 9622 9623 override void focus() { 9624 lineEdit.focus(); 9625 } 9626 } 9627 9628 /++ 9629 A labeled password edit. 9630 9631 History: 9632 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 9633 9634 The default parameters for the constructors were also removed on May 19, 2021 9635 +/ 9636 alias LabeledPasswordEdit = Labeled!PasswordEdit; 9637 9638 private string toMenuLabel(string s) { 9639 string n; 9640 n.reserve(s.length); 9641 foreach(c; s) 9642 if(c == '_') 9643 n ~= ' '; 9644 else 9645 n ~= c; 9646 return n; 9647 } 9648 9649 private void autoExceptionHandler(Exception e) { 9650 messageBox(e.msg); 9651 } 9652 9653 private void delegate() makeAutomaticHandler(alias fn, T)(T t) { 9654 static if(is(T : void delegate())) { 9655 return () { 9656 try 9657 t(); 9658 catch(Exception e) 9659 autoExceptionHandler(e); 9660 }; 9661 } else static if(is(typeof(fn) Params == __parameters)) { 9662 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 9663 return () { 9664 void onOK(string s) { 9665 member = s; 9666 try 9667 t(Params[0](s)); 9668 catch(Exception e) 9669 autoExceptionHandler(e); 9670 } 9671 9672 if( 9673 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 9674 || type == FileDialogType.Save) 9675 { 9676 getSaveFileName(&onOK, member, filters, null); 9677 } else 9678 getOpenFileName(&onOK, member, filters, null); 9679 }; 9680 } else { 9681 struct S { 9682 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 9683 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 9684 } else mixin(q{ 9685 static foreach(idx, ignore; Params) { 9686 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 9687 } 9688 }); 9689 } 9690 return () { 9691 dialog((S s) { 9692 try { 9693 static if(is(typeof(t) Ret == return)) { 9694 static if(is(Ret == void)) { 9695 t(s.tupleof); 9696 } else { 9697 auto ret = t(s.tupleof); 9698 import std.conv; 9699 messageBox(to!string(ret), "Returned Value"); 9700 } 9701 } 9702 } catch(Exception e) 9703 autoExceptionHandler(e); 9704 }, null, __traits(identifier, fn)); 9705 }; 9706 } 9707 } 9708 } 9709 9710 private template hasAnyRelevantAnnotations(a...) { 9711 bool helper() { 9712 bool any; 9713 foreach(attr; a) { 9714 static if(is(typeof(attr) == .menu)) 9715 any = true; 9716 else static if(is(typeof(attr) == .toolbar)) 9717 any = true; 9718 else static if(is(attr == .separator)) 9719 any = true; 9720 else static if(is(typeof(attr) == .accelerator)) 9721 any = true; 9722 else static if(is(typeof(attr) == .hotkey)) 9723 any = true; 9724 else static if(is(typeof(attr) == .icon)) 9725 any = true; 9726 else static if(is(typeof(attr) == .label)) 9727 any = true; 9728 else static if(is(typeof(attr) == .tip)) 9729 any = true; 9730 } 9731 return any; 9732 } 9733 9734 enum bool hasAnyRelevantAnnotations = helper(); 9735 } 9736 9737 /++ 9738 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. 9739 +/ 9740 class MainWindow : Window { 9741 /// 9742 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 9743 super(initialWidth, initialHeight, title); 9744 9745 _clientArea = new ClientAreaWidget(); 9746 _clientArea.x = 0; 9747 _clientArea.y = 0; 9748 _clientArea.width = this.width; 9749 _clientArea.height = this.height; 9750 _clientArea.tabStop = false; 9751 9752 super.addChild(_clientArea); 9753 9754 statusBar = new StatusBar(this); 9755 } 9756 9757 /++ 9758 Adds a menu and toolbar from annotated functions. 9759 9760 --- 9761 struct Commands { 9762 @menu("File") { 9763 void New() {} 9764 void Open() {} 9765 void Save() {} 9766 @separator 9767 void Exit() @accelerator("Alt+F4") @hotkey('x') { 9768 window.close(); 9769 } 9770 } 9771 9772 @menu("Edit") { 9773 void Undo() { 9774 undo(); 9775 } 9776 @separator 9777 void Cut() {} 9778 void Copy() {} 9779 void Paste() {} 9780 } 9781 9782 @menu("Help") { 9783 void About() {} 9784 } 9785 } 9786 9787 Commands commands; 9788 9789 window.setMenuAndToolbarFromAnnotatedCode(commands); 9790 --- 9791 9792 Note that you can call this function multiple times and it will add the items in order to the given items. 9793 9794 +/ 9795 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 9796 setMenuAndToolbarFromAnnotatedCode_internal(t); 9797 } 9798 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 9799 setMenuAndToolbarFromAnnotatedCode_internal(t); 9800 } 9801 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 9802 Action[] toolbarActions; 9803 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 9804 Menu[string] mcs; 9805 9806 foreach(menu; menuBar.subMenus) { 9807 mcs[menu.label] = menu; 9808 } 9809 9810 foreach(memberName; __traits(derivedMembers, T)) { 9811 static if(memberName != "this") 9812 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 9813 .menu menu; 9814 .toolbar toolbar; 9815 bool separator; 9816 .accelerator accelerator; 9817 .hotkey hotkey; 9818 .icon icon; 9819 string label; 9820 string tip; 9821 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 9822 static if(is(typeof(attr) == .menu)) 9823 menu = attr; 9824 else static if(is(typeof(attr) == .toolbar)) 9825 toolbar = attr; 9826 else static if(is(attr == .separator)) 9827 separator = true; 9828 else static if(is(typeof(attr) == .accelerator)) 9829 accelerator = attr; 9830 else static if(is(typeof(attr) == .hotkey)) 9831 hotkey = attr; 9832 else static if(is(typeof(attr) == .icon)) 9833 icon = attr; 9834 else static if(is(typeof(attr) == .label)) 9835 label = attr.label; 9836 else static if(is(typeof(attr) == .tip)) 9837 tip = attr.tip; 9838 } 9839 9840 if(menu !is .menu.init || toolbar !is .toolbar.init) { 9841 ushort correctIcon = icon.id; // FIXME 9842 if(label.length == 0) 9843 label = memberName.toMenuLabel; 9844 9845 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName)); 9846 9847 auto action = new Action(label, correctIcon, handler); 9848 9849 if(accelerator.keyString.length) { 9850 auto ke = KeyEvent.parse(accelerator.keyString); 9851 action.accelerator = ke; 9852 accelerators[ke.toStr] = handler; 9853 } 9854 9855 if(toolbar !is .toolbar.init) 9856 toolbarActions ~= action; 9857 if(menu !is .menu.init) { 9858 Menu mc; 9859 if(menu.name in mcs) { 9860 mc = mcs[menu.name]; 9861 } else { 9862 mc = new Menu(menu.name, this); 9863 menuBar.addItem(mc); 9864 mcs[menu.name] = mc; 9865 } 9866 9867 if(separator) 9868 mc.addSeparator(); 9869 mc.addItem(new MenuItem(action)); 9870 } 9871 } 9872 } 9873 } 9874 9875 this.menuBar = menuBar; 9876 9877 if(toolbarActions.length) { 9878 auto tb = new ToolBar(toolbarActions, this); 9879 } 9880 } 9881 9882 void delegate()[string] accelerators; 9883 9884 override void defaultEventHandler_keydown(KeyDownEvent event) { 9885 auto str = event.originalKeyEvent.toStr; 9886 if(auto acl = str in accelerators) 9887 (*acl)(); 9888 super.defaultEventHandler_keydown(event); 9889 } 9890 9891 override void defaultEventHandler_mouseover(MouseOverEvent event) { 9892 super.defaultEventHandler_mouseover(event); 9893 if(this.statusBar !is null && event.target.statusTip.length) 9894 this.statusBar.parts[0].content = event.target.statusTip; 9895 else if(this.statusBar !is null && this.statusTip.length) 9896 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 9897 } 9898 9899 override void addChild(Widget c, int position = int.max) { 9900 if(auto tb = cast(ToolBar) c) 9901 version(win32_widgets) 9902 super.addChild(c, 0); 9903 else version(custom_widgets) 9904 super.addChild(c, menuBar ? 1 : 0); 9905 else static assert(0); 9906 else 9907 clientArea.addChild(c, position); 9908 } 9909 9910 ToolBar _toolBar; 9911 /// 9912 ToolBar toolBar() { return _toolBar; } 9913 /// 9914 ToolBar toolBar(ToolBar t) { 9915 _toolBar = t; 9916 foreach(child; this.children) 9917 if(child is t) 9918 return t; 9919 version(win32_widgets) 9920 super.addChild(t, 0); 9921 else version(custom_widgets) 9922 super.addChild(t, menuBar ? 1 : 0); 9923 else static assert(0); 9924 return t; 9925 } 9926 9927 MenuBar _menu; 9928 /// 9929 MenuBar menuBar() { return _menu; } 9930 /// 9931 MenuBar menuBar(MenuBar m) { 9932 if(m is _menu) { 9933 version(custom_widgets) 9934 recomputeChildLayout(); 9935 return m; 9936 } 9937 9938 if(_menu !is null) { 9939 // make sure it is sanely removed 9940 // FIXME 9941 } 9942 9943 _menu = m; 9944 9945 version(win32_widgets) { 9946 SetMenu(parentWindow.win.impl.hwnd, m.handle); 9947 } else version(custom_widgets) { 9948 super.addChild(m, 0); 9949 9950 // clientArea.y = menu.height; 9951 // clientArea.height = this.height - menu.height; 9952 9953 recomputeChildLayout(); 9954 } else static assert(false); 9955 9956 return _menu; 9957 } 9958 private Widget _clientArea; 9959 /// 9960 @property Widget clientArea() { return _clientArea; } 9961 protected @property void clientArea(Widget wid) { 9962 _clientArea = wid; 9963 } 9964 9965 private StatusBar _statusBar; 9966 /++ 9967 Returns the window's [StatusBar]. Be warned it may be `null`. 9968 +/ 9969 @property StatusBar statusBar() { return _statusBar; } 9970 /// ditto 9971 @property void statusBar(StatusBar bar) { 9972 if(_statusBar !is null) 9973 _statusBar.removeWidget(); 9974 _statusBar = bar; 9975 if(bar !is null) 9976 super.addChild(_statusBar); 9977 } 9978 } 9979 9980 /+ 9981 This is really an implementation detail of [MainWindow] 9982 +/ 9983 private class ClientAreaWidget : Widget { 9984 this() { 9985 this.tabStop = false; 9986 super(null); 9987 //sa = new ScrollableWidget(this); 9988 } 9989 /* 9990 ScrollableWidget sa; 9991 override void addChild(Widget w, int position) { 9992 if(sa is null) 9993 super.addChild(w, position); 9994 else { 9995 sa.addChild(w, position); 9996 sa.setContentSize(this.minWidth + 1, this.minHeight); 9997 writeln(sa.contentWidth, "x", sa.contentHeight); 9998 } 9999 } 10000 */ 10001 } 10002 10003 /** 10004 Toolbars are lists of buttons (typically icons) that appear under the menu. 10005 Each button ought to correspond to a menu item, represented by [Action] objects. 10006 */ 10007 class ToolBar : Widget { 10008 version(win32_widgets) { 10009 private int idealHeight; 10010 override int minHeight() { return idealHeight; } 10011 override int maxHeight() { return idealHeight; } 10012 } else version(custom_widgets) { 10013 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 10014 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 10015 } else static assert(false); 10016 override int heightStretchiness() { return 0; } 10017 10018 version(win32_widgets) { 10019 HIMAGELIST imageListSmall; 10020 HIMAGELIST imageListLarge; 10021 } 10022 10023 this(Widget parent) { 10024 this(null, parent); 10025 } 10026 10027 version(win32_widgets) 10028 void changeIconSize(bool useLarge) { 10029 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall)); 10030 10031 /+ 10032 SIZE size; 10033 import core.sys.windows.commctrl; 10034 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 10035 idealHeight = size.cy + 4; // the plus 4 is a hack 10036 +/ 10037 10038 idealHeight = useLarge ? 34 : 26; 10039 10040 if(parent) { 10041 parent.recomputeChildLayout(); 10042 parent.redraw(); 10043 } 10044 10045 SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4)); 10046 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 10047 } 10048 10049 /// 10050 this(Action[] actions, Widget parent) { 10051 super(parent); 10052 10053 tabStop = false; 10054 10055 version(win32_widgets) { 10056 // so i like how the flat thing looks on windows, but not on wine 10057 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 10058 // leave it commented 10059 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 10060 10061 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 10062 10063 imageListSmall = ImageList_Create( 10064 // width, height 10065 16, 16, 10066 ILC_COLOR16 | ILC_MASK, 10067 16 /*numberOfButtons*/, 0); 10068 10069 imageListLarge = ImageList_Create( 10070 // width, height 10071 24, 24, 10072 ILC_COLOR16 | ILC_MASK, 10073 16 /*numberOfButtons*/, 0); 10074 10075 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall); 10076 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 10077 10078 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge); 10079 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL); 10080 10081 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 10082 10083 TBBUTTON[] buttons; 10084 10085 // FIXME: I_IMAGENONE is if here is no icon 10086 foreach(action; actions) 10087 buttons ~= TBBUTTON( 10088 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 10089 action.id, 10090 TBSTATE_ENABLED, // state 10091 0, // style 10092 0, // reserved array, just zero it out 10093 0, // dwData 10094 cast(size_t) toWstringzInternal(action.label) // INT_PTR 10095 ); 10096 10097 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 10098 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 10099 10100 /* 10101 RECT rect; 10102 GetWindowRect(hwnd, &rect); 10103 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 10104 */ 10105 10106 dpiChanged(); // to load the things calling changeIconSize the first time 10107 10108 assert(idealHeight); 10109 } else version(custom_widgets) { 10110 foreach(action; actions) 10111 new ToolButton(action, this); 10112 } else static assert(false); 10113 } 10114 10115 override void recomputeChildLayout() { 10116 .recomputeChildLayout!"width"(this); 10117 } 10118 10119 10120 version(win32_widgets) 10121 override protected void dpiChanged() { 10122 auto sz = scaleWithDpi(16); 10123 if(sz >= 20) 10124 changeIconSize(true); 10125 else 10126 changeIconSize(false); 10127 } 10128 } 10129 10130 enum toolbarIconSize = 24; 10131 10132 /// 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. 10133 class ToolButton : Button { 10134 /// 10135 this(string label, Widget parent) { 10136 super(label, parent); 10137 tabStop = false; 10138 } 10139 /// 10140 this(Action action, Widget parent) { 10141 super(action.label, parent); 10142 tabStop = false; 10143 this.action = action; 10144 } 10145 10146 version(custom_widgets) 10147 override void defaultEventHandler_click(ClickEvent event) { 10148 foreach(handler; action.triggered) 10149 handler(); 10150 } 10151 10152 Action action; 10153 10154 override int maxWidth() { return toolbarIconSize; } 10155 override int minWidth() { return toolbarIconSize; } 10156 override int maxHeight() { return toolbarIconSize; } 10157 override int minHeight() { return toolbarIconSize; } 10158 10159 version(custom_widgets) 10160 override void paint(WidgetPainter painter) { 10161 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 10162 painter.outlineColor = Color.black; 10163 10164 // I want to get from 16 to 24. that's * 3 / 2 10165 static assert(toolbarIconSize >= 16); 10166 enum multiplier = toolbarIconSize / 8; 10167 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 10168 switch(action.iconId) { 10169 case GenericIcons.New: 10170 painter.fillColor = Color.white; 10171 painter.drawPolygon( 10172 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 10173 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 10174 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 10175 ); 10176 break; 10177 case GenericIcons.Save: 10178 painter.fillColor = Color.white; 10179 painter.outlineColor = Color.black; 10180 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10181 10182 // the label 10183 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 10184 10185 // the slider 10186 painter.fillColor = Color.black; 10187 painter.outlineColor = Color.black; 10188 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 10189 10190 painter.fillColor = Color.white; 10191 painter.outlineColor = Color.white; 10192 // the disc window 10193 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 10194 break; 10195 case GenericIcons.Open: 10196 painter.fillColor = Color.white; 10197 painter.drawPolygon( 10198 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 10199 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 10200 painter.drawPolygon( 10201 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 10202 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 10203 Point(2, 6) * multiplier / divisor); 10204 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 10205 break; 10206 case GenericIcons.Copy: 10207 painter.fillColor = Color.white; 10208 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 10209 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 10210 break; 10211 case GenericIcons.Cut: 10212 painter.fillColor = Color.transparent; 10213 painter.outlineColor = getComputedStyle.foregroundColor(); 10214 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 10215 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 10216 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 10217 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 10218 break; 10219 case GenericIcons.Paste: 10220 painter.fillColor = Color.white; 10221 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 10222 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10223 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 10224 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 10225 painter.fillColor = Color.black; 10226 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 10227 break; 10228 case GenericIcons.Help: 10229 painter.outlineColor = getComputedStyle.foregroundColor(); 10230 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10231 break; 10232 case GenericIcons.Undo: 10233 painter.fillColor = Color.transparent; 10234 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10235 painter.outlineColor = Color.black; 10236 painter.fillColor = Color.black; 10237 painter.drawPolygon( 10238 Point(4, 4) * multiplier / divisor, 10239 Point(8, 2) * multiplier / divisor, 10240 Point(8, 6) * multiplier / divisor, 10241 Point(4, 4) * multiplier / divisor, 10242 ); 10243 break; 10244 case GenericIcons.Redo: 10245 painter.fillColor = Color.transparent; 10246 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10247 painter.outlineColor = Color.black; 10248 painter.fillColor = Color.black; 10249 painter.drawPolygon( 10250 Point(10, 4) * multiplier / divisor, 10251 Point(6, 2) * multiplier / divisor, 10252 Point(6, 6) * multiplier / divisor, 10253 Point(10, 4) * multiplier / divisor, 10254 ); 10255 break; 10256 default: 10257 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10258 } 10259 return bounds; 10260 }); 10261 } 10262 10263 } 10264 10265 10266 /// 10267 class MenuBar : Widget { 10268 MenuItem[] items; 10269 Menu[] subMenus; 10270 10271 version(win32_widgets) { 10272 HMENU handle; 10273 /// 10274 this(Widget parent = null) { 10275 super(parent); 10276 10277 handle = CreateMenu(); 10278 tabStop = false; 10279 } 10280 } else version(custom_widgets) { 10281 /// 10282 this(Widget parent = null) { 10283 tabStop = false; // these are selected some other way 10284 super(parent); 10285 } 10286 10287 mixin Padding!q{2}; 10288 } else static assert(false); 10289 10290 version(custom_widgets) 10291 override void paint(WidgetPainter painter) { 10292 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 10293 } 10294 10295 /// 10296 MenuItem addItem(MenuItem item) { 10297 this.addChild(item); 10298 items ~= item; 10299 version(win32_widgets) { 10300 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10301 } 10302 return item; 10303 } 10304 10305 10306 /// 10307 Menu addItem(Menu item) { 10308 10309 subMenus ~= item; 10310 10311 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 10312 10313 addChild(mbItem); 10314 items ~= mbItem; 10315 10316 version(win32_widgets) { 10317 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 10318 } else version(custom_widgets) { 10319 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 10320 item.popup(mbItem); 10321 }; 10322 } else static assert(false); 10323 10324 return item; 10325 } 10326 10327 override void recomputeChildLayout() { 10328 .recomputeChildLayout!"width"(this); 10329 } 10330 10331 override int maxHeight() { return defaultLineHeight + 4; } 10332 override int minHeight() { return defaultLineHeight + 4; } 10333 } 10334 10335 10336 /** 10337 Status bars appear at the bottom of a MainWindow. 10338 They are made out of Parts, with a width and content. 10339 10340 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 10341 10342 10343 sb.parts[0].content = "Status bar text!"; 10344 */ 10345 class StatusBar : Widget { 10346 private Part[] partsArray; 10347 /// 10348 struct Parts { 10349 @disable this(); 10350 this(StatusBar owner) { this.owner = owner; } 10351 //@disable this(this); 10352 /// 10353 @property int length() { return cast(int) owner.partsArray.length; } 10354 private StatusBar owner; 10355 private this(StatusBar owner, Part[] parts) { 10356 this.owner.partsArray = parts; 10357 this.owner = owner; 10358 } 10359 /// 10360 Part opIndex(int p) { 10361 if(owner.partsArray.length == 0) 10362 this ~= new StatusBar.Part(0); 10363 return owner.partsArray[p]; 10364 } 10365 10366 /// 10367 Part opOpAssign(string op : "~" )(Part p) { 10368 assert(owner.partsArray.length < 255); 10369 p.owner = this.owner; 10370 p.idx = cast(int) owner.partsArray.length; 10371 owner.partsArray ~= p; 10372 10373 owner.recomputeChildLayout(); 10374 10375 version(win32_widgets) { 10376 int[256] pos; 10377 int cpos; 10378 foreach(idx, part; owner.partsArray) { 10379 if(idx + 1 == owner.partsArray.length) 10380 pos[idx] = -1; 10381 else { 10382 cpos += part.currentlyAssignedWidth; 10383 pos[idx] = cpos; 10384 } 10385 } 10386 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 10387 } else version(custom_widgets) { 10388 owner.redraw(); 10389 } else static assert(false); 10390 10391 return p; 10392 } 10393 } 10394 10395 private Parts _parts; 10396 /// 10397 final @property Parts parts() { 10398 return _parts; 10399 } 10400 10401 /++ 10402 10403 +/ 10404 static class Part { 10405 /++ 10406 History: 10407 Added September 1, 2023 (dub v11.1) 10408 +/ 10409 enum WidthUnits { 10410 /++ 10411 Unscaled pixels as they appear on screen. 10412 10413 If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui. 10414 +/ 10415 DeviceDependentPixels, 10416 /++ 10417 Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui. 10418 +/ 10419 DeviceIndependentPixels, 10420 /++ 10421 An approximate character count in the currently selected font (at layout time) of the status bar. This will use the x-width (similar to css `ch`). 10422 +/ 10423 ApproximateCharacters, 10424 /++ 10425 These take a proportion of the remaining space in the window after all other parts have been assigned. The sum of all proportional parts is then divided by the current item to get the amount of space it uses. 10426 10427 If you pass 0, it will assume that this item takes an average of all remaining proportional space. This is there primarily to provide compatibility with code written against older versions of minigui. 10428 +/ 10429 Proportional 10430 } 10431 private WidthUnits units; 10432 private int width; 10433 private StatusBar owner; 10434 10435 private int currentlyAssignedWidth; 10436 10437 /++ 10438 History: 10439 Prior to September 1, 2023, this took a default value of 100 and was interpreted as pixels, unless the value was 0 and it was the last item in the list, in which case it would use the remaining space in the window. 10440 10441 It now allows you to provide your own value for [WidthUnits]. 10442 10443 Additionally, the default value used to be an arbitrary value of 100. It is now 0, to take advantage of the automatic proportional calculator in the new version. If you want the old behavior, pass `100, StatusBar.Part.WidthUnits.DeviceIndependentPixels`. 10444 +/ 10445 this(int w, WidthUnits units = WidthUnits.Proportional) { 10446 this.units = units; 10447 this.width = w; 10448 } 10449 10450 /// ditto 10451 this(int w = 0) { 10452 if(w == 0) 10453 this(w, WidthUnits.Proportional); 10454 else 10455 this(w, WidthUnits.DeviceDependentPixels); 10456 } 10457 10458 private int idx; 10459 private string _content; 10460 /// 10461 @property string content() { return _content; } 10462 /// 10463 @property void content(string s) { 10464 version(win32_widgets) { 10465 _content = s; 10466 WCharzBuffer bfr = WCharzBuffer(s); 10467 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 10468 } else version(custom_widgets) { 10469 if(_content != s) { 10470 _content = s; 10471 owner.redraw(); 10472 } 10473 } else static assert(false); 10474 } 10475 } 10476 string simpleModeContent; 10477 bool inSimpleMode; 10478 10479 10480 /// 10481 this(Widget parent) { 10482 super(null); // FIXME 10483 _parts = Parts(this); 10484 tabStop = false; 10485 version(win32_widgets) { 10486 parentWindow = parent.parentWindow; 10487 createWin32Window(this, "msctls_statusbar32"w, "", 0); 10488 10489 RECT rect; 10490 GetWindowRect(hwnd, &rect); 10491 idealHeight = rect.bottom - rect.top; 10492 assert(idealHeight); 10493 } else version(custom_widgets) { 10494 } else static assert(false); 10495 } 10496 10497 override void recomputeChildLayout() { 10498 int remainingLength = this.width; 10499 10500 int proportionalSum; 10501 int proportionalCount; 10502 foreach(idx, part; this.partsArray) { 10503 with(Part.WidthUnits) 10504 final switch(part.units) { 10505 case DeviceDependentPixels: 10506 part.currentlyAssignedWidth = part.width; 10507 remainingLength -= part.currentlyAssignedWidth; 10508 break; 10509 case DeviceIndependentPixels: 10510 part.currentlyAssignedWidth = scaleWithDpi(part.width); 10511 remainingLength -= part.currentlyAssignedWidth; 10512 break; 10513 case ApproximateCharacters: 10514 auto cs = getComputedStyle(); 10515 auto font = cs.font; 10516 10517 part.currentlyAssignedWidth = font.averageWidth * this.width; 10518 remainingLength -= part.currentlyAssignedWidth; 10519 break; 10520 case Proportional: 10521 proportionalSum += part.width; 10522 proportionalCount ++; 10523 break; 10524 } 10525 } 10526 10527 foreach(part; this.partsArray) { 10528 if(part.units == Part.WidthUnits.Proportional) { 10529 auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width; 10530 if(proportion == 0) 10531 proportion = 1; 10532 10533 if(proportionalSum == 0) 10534 proportionalSum = proportionalCount; 10535 10536 part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum; 10537 } 10538 } 10539 10540 super.recomputeChildLayout(); 10541 } 10542 10543 version(win32_widgets) 10544 override protected void dpiChanged() { 10545 RECT rect; 10546 GetWindowRect(hwnd, &rect); 10547 idealHeight = rect.bottom - rect.top; 10548 assert(idealHeight); 10549 } 10550 10551 version(custom_widgets) 10552 override void paint(WidgetPainter painter) { 10553 auto cs = getComputedStyle(); 10554 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10555 int cpos = 0; 10556 foreach(idx, part; this.partsArray) { 10557 auto partWidth = part.currentlyAssignedWidth; 10558 // part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 10559 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 10560 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 10561 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 10562 10563 painter.outlineColor = cs.foregroundColor(); 10564 painter.fillColor = cs.foregroundColor(); 10565 10566 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 10567 cpos += partWidth; 10568 } 10569 } 10570 10571 10572 version(win32_widgets) { 10573 private int idealHeight; 10574 override int maxHeight() { return idealHeight; } 10575 override int minHeight() { return idealHeight; } 10576 } else version(custom_widgets) { 10577 override int maxHeight() { return defaultLineHeight + 4; } 10578 override int minHeight() { return defaultLineHeight + 4; } 10579 } else static assert(false); 10580 } 10581 10582 /// Displays an in-progress indicator without known values 10583 version(none) 10584 class IndefiniteProgressBar : Widget { 10585 version(win32_widgets) 10586 this(Widget parent) { 10587 super(parent); 10588 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 10589 tabStop = false; 10590 } 10591 override int minHeight() { return 10; } 10592 } 10593 10594 /// A progress bar with a known endpoint and completion amount 10595 class ProgressBar : Widget { 10596 /++ 10597 History: 10598 Added March 16, 2022 (dub v10.7) 10599 +/ 10600 this(int min, int max, Widget parent) { 10601 this(parent); 10602 setRange(cast(ushort) min, cast(ushort) max); // FIXME 10603 } 10604 this(Widget parent) { 10605 version(win32_widgets) { 10606 super(parent); 10607 createWin32Window(this, "msctls_progress32"w, "", 0); 10608 tabStop = false; 10609 } else version(custom_widgets) { 10610 super(parent); 10611 max = 100; 10612 step = 10; 10613 tabStop = false; 10614 } else static assert(0); 10615 } 10616 10617 version(custom_widgets) 10618 override void paint(WidgetPainter painter) { 10619 auto cs = getComputedStyle(); 10620 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10621 painter.fillColor = cs.progressBarColor; 10622 painter.drawRectangle(Point(0, 0), width * current / max, height); 10623 } 10624 10625 10626 version(custom_widgets) { 10627 int current; 10628 int max; 10629 int step; 10630 } 10631 10632 /// 10633 void advanceOneStep() { 10634 version(win32_widgets) 10635 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 10636 else version(custom_widgets) 10637 addToPosition(step); 10638 else static assert(false); 10639 } 10640 10641 /// 10642 void setStepIncrement(int increment) { 10643 version(win32_widgets) 10644 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 10645 else version(custom_widgets) 10646 step = increment; 10647 else static assert(false); 10648 } 10649 10650 /// 10651 void addToPosition(int amount) { 10652 version(win32_widgets) 10653 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 10654 else version(custom_widgets) 10655 setPosition(current + amount); 10656 else static assert(false); 10657 } 10658 10659 /// 10660 void setPosition(int pos) { 10661 version(win32_widgets) 10662 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 10663 else version(custom_widgets) { 10664 current = pos; 10665 if(current > max) 10666 current = max; 10667 redraw(); 10668 } 10669 else static assert(false); 10670 } 10671 10672 /// 10673 void setRange(ushort min, ushort max) { 10674 version(win32_widgets) 10675 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 10676 else version(custom_widgets) { 10677 this.max = max; 10678 } 10679 else static assert(false); 10680 } 10681 10682 override int minHeight() { return 10; } 10683 } 10684 10685 version(custom_widgets) 10686 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 10687 thisLabel.reserve(label.length); 10688 bool justSawAmpersand; 10689 foreach(ch; label) { 10690 if(justSawAmpersand) { 10691 justSawAmpersand = false; 10692 if(ch == '&') { 10693 goto plain; 10694 } 10695 thisAccelerator = ch; 10696 } else { 10697 if(ch == '&') { 10698 justSawAmpersand = true; 10699 continue; 10700 } 10701 plain: 10702 thisLabel ~= ch; 10703 } 10704 } 10705 } 10706 10707 /++ 10708 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. 10709 10710 10711 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 10712 10713 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10714 10715 History: 10716 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. 10717 +/ 10718 class Fieldset : Widget { 10719 // FIXME: on Windows,it doesn't draw the background on the label 10720 // on X, it doesn't fix the clipping rectangle for it 10721 version(win32_widgets) 10722 override int paddingTop() { return defaultLineHeight; } 10723 else version(custom_widgets) 10724 override int paddingTop() { return defaultLineHeight + 2; } 10725 else static assert(false); 10726 override int paddingBottom() { return 6; } 10727 override int paddingLeft() { return 6; } 10728 override int paddingRight() { return 6; } 10729 10730 override int marginLeft() { return 6; } 10731 override int marginRight() { return 6; } 10732 override int marginTop() { return 2; } 10733 override int marginBottom() { return 2; } 10734 10735 string legend; 10736 10737 version(custom_widgets) private dchar accelerator; 10738 10739 this(string legend, Widget parent) { 10740 version(win32_widgets) { 10741 super(parent); 10742 this.legend = legend; 10743 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 10744 tabStop = false; 10745 } else version(custom_widgets) { 10746 super(parent); 10747 tabStop = false; 10748 10749 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 10750 } else static assert(0); 10751 } 10752 10753 version(custom_widgets) 10754 override void paint(WidgetPainter painter) { 10755 auto dlh = defaultLineHeight; 10756 10757 painter.fillColor = Color.transparent; 10758 auto cs = getComputedStyle(); 10759 painter.pen = Pen(cs.foregroundColor, 1); 10760 painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2); 10761 10762 auto tx = painter.textSize(legend); 10763 painter.outlineColor = Color.transparent; 10764 10765 static if(UsingSimpledisplayX11) { 10766 painter.fillColor = getComputedStyle().windowBackgroundColor; 10767 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 10768 } else version(Windows) { 10769 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 10770 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 10771 SelectObject(painter.impl.hdc, b); 10772 } else static assert(0); 10773 painter.outlineColor = cs.foregroundColor; 10774 painter.drawText(Point(8, 0), legend); 10775 } 10776 10777 override int maxHeight() { 10778 auto m = paddingTop() + paddingBottom(); 10779 foreach(child; children) { 10780 auto mh = child.maxHeight(); 10781 if(mh == int.max) 10782 return int.max; 10783 m += mh; 10784 m += child.marginBottom(); 10785 m += child.marginTop(); 10786 } 10787 m += 6; 10788 if(m < minHeight) 10789 return minHeight; 10790 return m; 10791 } 10792 10793 override int minHeight() { 10794 auto m = paddingTop() + paddingBottom(); 10795 foreach(child; children) { 10796 m += child.minHeight(); 10797 m += child.marginBottom(); 10798 m += child.marginTop(); 10799 } 10800 return m + 6; 10801 } 10802 10803 override int minWidth() { 10804 return 6 + cast(int) this.legend.length * 7; 10805 } 10806 } 10807 10808 /++ 10809 $(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") 10810 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 10811 +/ 10812 version(minigui_screenshots) 10813 @Screenshot("Fieldset") 10814 unittest { 10815 auto window = new Window(200, 100); 10816 auto set = new Fieldset("Baby will", window); 10817 auto option1 = new Radiobox("Eat", set); 10818 auto option2 = new Radiobox("Cry", set); 10819 auto option3 = new Radiobox("Sleep", set); 10820 window.loop(); 10821 } 10822 10823 /// Draws a line 10824 class HorizontalRule : Widget { 10825 mixin Margin!q{ 2 }; 10826 override int minHeight() { return 2; } 10827 override int maxHeight() { return 2; } 10828 10829 /// 10830 this(Widget parent) { 10831 super(parent); 10832 } 10833 10834 override void paint(WidgetPainter painter) { 10835 auto cs = getComputedStyle(); 10836 painter.outlineColor = cs.darkAccentColor; 10837 painter.drawLine(Point(0, 0), Point(width, 0)); 10838 painter.outlineColor = cs.lightAccentColor; 10839 painter.drawLine(Point(0, 1), Point(width, 1)); 10840 } 10841 } 10842 10843 version(minigui_screenshots) 10844 @Screenshot("HorizontalRule") 10845 /++ 10846 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 10847 10848 +/ 10849 unittest { 10850 auto window = new Window(200, 100); 10851 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 10852 new HorizontalRule(window); 10853 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 10854 window.loop(); 10855 } 10856 10857 /// ditto 10858 class VerticalRule : Widget { 10859 mixin Margin!q{ 2 }; 10860 override int minWidth() { return 2; } 10861 override int maxWidth() { return 2; } 10862 10863 /// 10864 this(Widget parent) { 10865 super(parent); 10866 } 10867 10868 override void paint(WidgetPainter painter) { 10869 auto cs = getComputedStyle(); 10870 painter.outlineColor = cs.darkAccentColor; 10871 painter.drawLine(Point(0, 0), Point(0, height)); 10872 painter.outlineColor = cs.lightAccentColor; 10873 painter.drawLine(Point(1, 0), Point(1, height)); 10874 } 10875 } 10876 10877 10878 /// 10879 class Menu : Window { 10880 void remove() { 10881 foreach(i, child; parentWindow.children) 10882 if(child is this) { 10883 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 10884 break; 10885 } 10886 parentWindow.redraw(); 10887 10888 parentWindow.releaseMouseCapture(); 10889 } 10890 10891 /// 10892 void addSeparator() { 10893 version(win32_widgets) 10894 AppendMenu(handle, MF_SEPARATOR, 0, null); 10895 else version(custom_widgets) 10896 auto hr = new HorizontalRule(this); 10897 else static assert(0); 10898 } 10899 10900 override int paddingTop() { return 4; } 10901 override int paddingBottom() { return 4; } 10902 override int paddingLeft() { return 2; } 10903 override int paddingRight() { return 2; } 10904 10905 version(win32_widgets) {} 10906 else version(custom_widgets) { 10907 SimpleWindow dropDown; 10908 Widget menuParent; 10909 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 10910 this.menuParent = parent; 10911 10912 int w = 150; 10913 int h = paddingTop + paddingBottom; 10914 if(this.children.length) { 10915 // hacking it to get the ideal height out of recomputeChildLayout 10916 this.width = w; 10917 this.height = h; 10918 this.recomputeChildLayout(); 10919 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 10920 h += paddingBottom; 10921 10922 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 10923 } 10924 10925 if(offsetY == int.min) 10926 offsetY = parent.defaultLineHeight; 10927 10928 auto coord = parent.globalCoordinates(); 10929 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 10930 this.x = 0; 10931 this.y = 0; 10932 this.width = dropDown.width; 10933 this.height = dropDown.height; 10934 this.drawableWindow = dropDown; 10935 this.recomputeChildLayout(); 10936 10937 static if(UsingSimpledisplayX11) 10938 XSync(XDisplayConnection.get, 0); 10939 10940 dropDown.visibilityChanged = (bool visible) { 10941 if(visible) { 10942 this.redraw(); 10943 dropDown.grabInput(); 10944 } else { 10945 dropDown.releaseInputGrab(); 10946 } 10947 }; 10948 10949 dropDown.show(); 10950 10951 clickListener = this.addEventListener((scope ClickEvent ev) { 10952 unpopup(); 10953 // need to unlock asap just in case other user handlers block... 10954 static if(UsingSimpledisplayX11) 10955 flushGui(); 10956 }, true /* again for asap action */); 10957 } 10958 10959 EventListener clickListener; 10960 } 10961 else static assert(false); 10962 10963 version(custom_widgets) 10964 void unpopup() { 10965 mouseLastOver = mouseLastDownOn = null; 10966 dropDown.hide(); 10967 if(!menuParent.parentWindow.win.closed) { 10968 if(auto maw = cast(MouseActivatedWidget) menuParent) { 10969 maw.setDynamicState(DynamicState.depressed, false); 10970 maw.setDynamicState(DynamicState.hover, false); 10971 maw.redraw(); 10972 } 10973 // menuParent.parentWindow.win.focus(); 10974 } 10975 clickListener.disconnect(); 10976 } 10977 10978 MenuItem[] items; 10979 10980 /// 10981 MenuItem addItem(MenuItem item) { 10982 addChild(item); 10983 items ~= item; 10984 version(win32_widgets) { 10985 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10986 } 10987 return item; 10988 } 10989 10990 string label; 10991 10992 version(win32_widgets) { 10993 HMENU handle; 10994 /// 10995 this(string label, Widget parent) { 10996 // not actually passing the parent since it effs up the drawing 10997 super(cast(Widget) null);// parent); 10998 this.label = label; 10999 handle = CreatePopupMenu(); 11000 } 11001 } else version(custom_widgets) { 11002 /// 11003 this(string label, Widget parent) { 11004 11005 if(dropDown) { 11006 dropDown.close(); 11007 } 11008 dropDown = new SimpleWindow( 11009 150, 4, 11010 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 11011 11012 this.label = label; 11013 11014 super(dropDown); 11015 } 11016 } else static assert(false); 11017 11018 override int maxHeight() { return defaultLineHeight; } 11019 override int minHeight() { return defaultLineHeight; } 11020 11021 version(custom_widgets) 11022 override void paint(WidgetPainter painter) { 11023 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 11024 } 11025 } 11026 11027 /++ 11028 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 11029 +/ 11030 class MenuItem : MouseActivatedWidget { 11031 Menu submenu; 11032 11033 Action action; 11034 string label; 11035 11036 override int paddingLeft() { return 4; } 11037 11038 override int maxHeight() { return defaultLineHeight + 4; } 11039 override int minHeight() { return defaultLineHeight + 4; } 11040 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 11041 override int maxWidth() { 11042 if(cast(MenuBar) parent) { 11043 return minWidth(); 11044 } 11045 return int.max; 11046 } 11047 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 11048 this(string lbl, Widget parent = null) { 11049 super(parent); 11050 //label = lbl; // FIXME 11051 foreach(char ch; lbl) // FIXME 11052 if(ch != '&') // FIXME 11053 label ~= ch; // FIXME 11054 tabStop = false; // these are selected some other way 11055 } 11056 11057 /// 11058 this(Action action, Widget parent = null) { 11059 assert(action !is null); 11060 this(action.label, parent); 11061 this.action = action; 11062 tabStop = false; // these are selected some other way 11063 } 11064 11065 version(custom_widgets) 11066 override void paint(WidgetPainter painter) { 11067 auto cs = getComputedStyle(); 11068 if(dynamicState & DynamicState.depressed) 11069 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 11070 if(dynamicState & DynamicState.hover) 11071 painter.outlineColor = cs.activeMenuItemColor; 11072 else 11073 painter.outlineColor = cs.foregroundColor; 11074 painter.fillColor = Color.transparent; 11075 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11076 if(action && action.accelerator !is KeyEvent.init) { 11077 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 11078 11079 } 11080 } 11081 11082 static class Style : Widget.Style { 11083 override bool variesWithState(ulong dynamicStateFlags) { 11084 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11085 } 11086 } 11087 mixin OverrideStyle!Style; 11088 11089 override void defaultEventHandler_triggered(Event event) { 11090 if(action) 11091 foreach(handler; action.triggered) 11092 handler(); 11093 11094 if(auto pmenu = cast(Menu) this.parent) 11095 pmenu.remove(); 11096 11097 super.defaultEventHandler_triggered(event); 11098 } 11099 } 11100 11101 version(win32_widgets) 11102 /// A "mouse activiated widget" is really just an abstract variant of button. 11103 class MouseActivatedWidget : Widget { 11104 @property bool isChecked() { 11105 assert(hwnd); 11106 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 11107 11108 } 11109 @property void isChecked(bool state) { 11110 assert(hwnd); 11111 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 11112 11113 } 11114 11115 override void handleWmCommand(ushort cmd, ushort id) { 11116 if(cmd == 0) { 11117 auto event = new Event(EventType.triggered, this); 11118 event.dispatch(); 11119 } 11120 } 11121 11122 this(Widget parent) { 11123 super(parent); 11124 } 11125 } 11126 else version(custom_widgets) 11127 /// ditto 11128 class MouseActivatedWidget : Widget { 11129 @property bool isChecked() { return isChecked_; } 11130 @property bool isChecked(bool b) { return isChecked_ = b; } 11131 11132 private bool isChecked_; 11133 11134 this(Widget parent) { 11135 super(parent); 11136 11137 addEventListener((MouseDownEvent ev) { 11138 if(ev.button == MouseButton.left) { 11139 setDynamicState(DynamicState.depressed, true); 11140 setDynamicState(DynamicState.hover, true); 11141 redraw(); 11142 } 11143 }); 11144 11145 addEventListener((MouseUpEvent ev) { 11146 if(ev.button == MouseButton.left) { 11147 setDynamicState(DynamicState.depressed, false); 11148 setDynamicState(DynamicState.hover, false); 11149 redraw(); 11150 } 11151 }); 11152 11153 addEventListener((MouseMoveEvent mme) { 11154 if(!(mme.state & ModifierState.leftButtonDown)) { 11155 if(dynamicState_ & DynamicState.depressed) { 11156 setDynamicState(DynamicState.depressed, false); 11157 redraw(); 11158 } 11159 } 11160 }); 11161 } 11162 11163 override void defaultEventHandler_focus(Event ev) { 11164 super.defaultEventHandler_focus(ev); 11165 this.redraw(); 11166 } 11167 override void defaultEventHandler_blur(Event ev) { 11168 super.defaultEventHandler_blur(ev); 11169 setDynamicState(DynamicState.depressed, false); 11170 this.redraw(); 11171 } 11172 override void defaultEventHandler_keydown(KeyDownEvent ev) { 11173 super.defaultEventHandler_keydown(ev); 11174 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 11175 setDynamicState(DynamicState.depressed, true); 11176 setDynamicState(DynamicState.hover, true); 11177 this.redraw(); 11178 } 11179 } 11180 override void defaultEventHandler_keyup(KeyUpEvent ev) { 11181 super.defaultEventHandler_keyup(ev); 11182 if(!(dynamicState & DynamicState.depressed)) 11183 return; 11184 setDynamicState(DynamicState.depressed, false); 11185 setDynamicState(DynamicState.hover, false); 11186 this.redraw(); 11187 11188 auto event = new Event(EventType.triggered, this); 11189 event.sendDirectly(); 11190 } 11191 override void defaultEventHandler_click(ClickEvent ev) { 11192 super.defaultEventHandler_click(ev); 11193 if(ev.button == MouseButton.left) { 11194 auto event = new Event(EventType.triggered, this); 11195 event.sendDirectly(); 11196 } 11197 } 11198 11199 } 11200 else static assert(false); 11201 11202 /* 11203 /++ 11204 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 11205 11206 Basically the same as a checkbox. 11207 +/ 11208 class OnOffSwitch : MouseActivatedWidget { 11209 11210 } 11211 */ 11212 11213 /++ 11214 History: 11215 Added June 15, 2021 (dub v10.1) 11216 +/ 11217 struct ImageLabel { 11218 /++ 11219 Defines a label+image combo used by some widgets. 11220 11221 If you provide just a text label, that is all the widget will try to 11222 display. Or just an image will display just that. If you provide both, 11223 it may display both text and image side by side or display the image 11224 and offer text on an input event depending on the widget. 11225 11226 History: 11227 The `alignment` parameter was added on September 27, 2021 11228 +/ 11229 this(string label, TextAlignment alignment = TextAlignment.Center) { 11230 this.label = label; 11231 this.displayFlags = DisplayFlags.displayText; 11232 this.alignment = alignment; 11233 } 11234 11235 /// ditto 11236 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11237 this.label = label; 11238 this.image = image; 11239 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11240 this.alignment = alignment; 11241 } 11242 11243 /// ditto 11244 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11245 this.image = image; 11246 this.displayFlags = DisplayFlags.displayImage; 11247 this.alignment = alignment; 11248 } 11249 11250 /// ditto 11251 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 11252 this.label = label; 11253 this.image = image; 11254 this.alignment = alignment; 11255 this.displayFlags = displayFlags; 11256 } 11257 11258 string label; 11259 MemoryImage image; 11260 11261 enum DisplayFlags { 11262 displayText = 1 << 0, 11263 displayImage = 1 << 1, 11264 } 11265 11266 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11267 11268 TextAlignment alignment; 11269 } 11270 11271 /++ 11272 A basic checked or not checked box with an attached label. 11273 11274 11275 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 11276 11277 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11278 11279 History: 11280 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. 11281 +/ 11282 class Checkbox : MouseActivatedWidget { 11283 version(win32_widgets) { 11284 override int maxHeight() { return scaleWithDpi(16); } 11285 override int minHeight() { return scaleWithDpi(16); } 11286 } else version(custom_widgets) { 11287 private enum buttonSize = 16; 11288 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 11289 override int minHeight() { return maxHeight(); } 11290 } else static assert(0); 11291 11292 override int marginLeft() { return 4; } 11293 11294 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 11295 11296 /++ 11297 Just an alias because I keep typing checked out of web habit. 11298 11299 History: 11300 Added May 31, 2021 11301 +/ 11302 alias checked = isChecked; 11303 11304 private string label; 11305 private dchar accelerator; 11306 11307 /++ 11308 +/ 11309 this(string label, Widget parent) { 11310 this(ImageLabel(label), Appearance.checkbox, parent); 11311 } 11312 11313 /// ditto 11314 this(string label, Appearance appearance, Widget parent) { 11315 this(ImageLabel(label), appearance, parent); 11316 } 11317 11318 /++ 11319 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 11320 11321 History: 11322 Added June 29, 2021 (dub v10.2) 11323 +/ 11324 enum Appearance { 11325 checkbox, /// a normal checkbox 11326 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 11327 //sliderswitch, 11328 } 11329 private Appearance appearance; 11330 11331 /// ditto 11332 private this(ImageLabel label, Appearance appearance, Widget parent) { 11333 super(parent); 11334 version(win32_widgets) { 11335 this.label = label.label; 11336 11337 uint extraStyle; 11338 final switch(appearance) { 11339 case Appearance.checkbox: 11340 break; 11341 case Appearance.pushbutton: 11342 extraStyle |= BS_PUSHLIKE; 11343 break; 11344 } 11345 11346 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 11347 } else version(custom_widgets) { 11348 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 11349 } else static assert(0); 11350 } 11351 11352 version(custom_widgets) 11353 override void paint(WidgetPainter painter) { 11354 auto cs = getComputedStyle(); 11355 if(isFocused()) { 11356 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11357 painter.fillColor = cs.windowBackgroundColor; 11358 painter.drawRectangle(Point(0, 0), width, height); 11359 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11360 } else { 11361 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 11362 painter.fillColor = cs.windowBackgroundColor; 11363 painter.drawRectangle(Point(0, 0), width, height); 11364 } 11365 11366 11367 painter.outlineColor = Color.black; 11368 painter.fillColor = Color.white; 11369 enum rectOffset = 2; 11370 painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset)); 11371 11372 if(isChecked) { 11373 auto size = scaleWithDpi(2); 11374 painter.pen = Pen(Color.black, size); 11375 // I'm using height so the checkbox is square 11376 enum padding = 3; 11377 painter.drawLine( 11378 scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)), 11379 scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2) 11380 ); 11381 painter.drawLine( 11382 scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0), 11383 scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 - size % 2) 11384 ); 11385 11386 painter.pen = Pen(Color.black, 1); 11387 } 11388 11389 if(label !is null) { 11390 painter.outlineColor = cs.foregroundColor(); 11391 painter.fillColor = cs.foregroundColor(); 11392 11393 // i want the centerline of the text to be aligned with the centerline of the checkbox 11394 /+ 11395 auto font = cs.font(); 11396 auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2; 11397 painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label); 11398 +/ 11399 painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter); 11400 } 11401 } 11402 11403 override void defaultEventHandler_triggered(Event ev) { 11404 isChecked = !isChecked; 11405 11406 this.emit!(ChangeEvent!bool)(&isChecked); 11407 11408 redraw(); 11409 } 11410 11411 /// Emits a change event with the checked state 11412 mixin Emits!(ChangeEvent!bool); 11413 } 11414 11415 /// Adds empty space to a layout. 11416 class VerticalSpacer : Widget { 11417 /// 11418 this(Widget parent) { 11419 super(parent); 11420 } 11421 } 11422 11423 /// ditto 11424 class HorizontalSpacer : Widget { 11425 /// 11426 this(Widget parent) { 11427 super(parent); 11428 this.tabStop = false; 11429 } 11430 } 11431 11432 11433 /++ 11434 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 11435 11436 11437 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 11438 11439 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11440 11441 History: 11442 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. 11443 +/ 11444 class Radiobox : MouseActivatedWidget { 11445 11446 version(win32_widgets) { 11447 override int maxHeight() { return scaleWithDpi(16); } 11448 override int minHeight() { return scaleWithDpi(16); } 11449 } else version(custom_widgets) { 11450 private enum buttonSize = 16; 11451 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 11452 override int minHeight() { return maxHeight(); } 11453 } else static assert(0); 11454 11455 override int marginLeft() { return 4; } 11456 11457 // FIXME: make a label getter 11458 private string label; 11459 private dchar accelerator; 11460 11461 version(win32_widgets) 11462 this(string label, Widget parent) { 11463 super(parent); 11464 this.label = label; 11465 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 11466 } 11467 else version(custom_widgets) 11468 this(string label, Widget parent) { 11469 super(parent); 11470 label.extractWindowsStyleLabel(this.label, this.accelerator); 11471 height = 16; 11472 width = height + 4 + cast(int) label.length * 16; 11473 } 11474 else static assert(false); 11475 11476 version(custom_widgets) 11477 override void paint(WidgetPainter painter) { 11478 auto cs = getComputedStyle(); 11479 11480 if(isFocused) { 11481 painter.fillColor = cs.windowBackgroundColor; 11482 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11483 } else { 11484 painter.fillColor = cs.windowBackgroundColor; 11485 painter.outlineColor = cs.windowBackgroundColor; 11486 } 11487 painter.drawRectangle(Point(0, 0), width, height); 11488 11489 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11490 11491 painter.outlineColor = Color.black; 11492 painter.fillColor = Color.white; 11493 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 11494 if(isChecked) { 11495 painter.outlineColor = Color.black; 11496 painter.fillColor = Color.black; 11497 // I'm using height so the checkbox is square 11498 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5))); 11499 } 11500 11501 painter.outlineColor = cs.foregroundColor(); 11502 painter.fillColor = cs.foregroundColor(); 11503 11504 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11505 } 11506 11507 11508 override void defaultEventHandler_triggered(Event ev) { 11509 isChecked = true; 11510 11511 if(this.parent) { 11512 foreach(child; this.parent.children) { 11513 if(child is this) continue; 11514 if(auto rb = cast(Radiobox) child) { 11515 rb.isChecked = false; 11516 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 11517 rb.redraw(); 11518 } 11519 } 11520 } 11521 11522 this.emit!(ChangeEvent!bool)(&this.isChecked); 11523 11524 redraw(); 11525 } 11526 11527 /// 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. 11528 mixin Emits!(ChangeEvent!bool); 11529 } 11530 11531 11532 /++ 11533 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 11534 11535 11536 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 11537 11538 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11539 11540 History: 11541 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. 11542 +/ 11543 class Button : MouseActivatedWidget { 11544 override int heightStretchiness() { return 3; } 11545 override int widthStretchiness() { return 3; } 11546 11547 /++ 11548 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. 11549 11550 History: 11551 Added July 2, 2021 11552 +/ 11553 public bool triggersOnMultiClick; 11554 11555 private string label_; 11556 private TextAlignment alignment; 11557 private dchar accelerator; 11558 11559 /// 11560 string label() { return label_; } 11561 /// 11562 void label(string l) { 11563 label_ = l; 11564 version(win32_widgets) { 11565 WCharzBuffer bfr = WCharzBuffer(l); 11566 SetWindowTextW(hwnd, bfr.ptr); 11567 } else version(custom_widgets) { 11568 redraw(); 11569 } 11570 } 11571 11572 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 11573 super.defaultEventHandler_dblclick(ev); 11574 if(triggersOnMultiClick) { 11575 if(ev.button == MouseButton.left) { 11576 auto event = new Event(EventType.triggered, this); 11577 event.sendDirectly(); 11578 } 11579 } 11580 } 11581 11582 private Sprite sprite; 11583 private int displayFlags; 11584 11585 /++ 11586 Creates a push button with the given label, which may be an image or some text. 11587 11588 Bugs: 11589 If the image is bigger than the button, it may not be displayed in the right position on Linux. 11590 11591 History: 11592 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 11593 11594 The button with label and image will respect requests to show both on Windows as 11595 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 11596 +/ 11597 this(ImageLabel label, Widget parent) { 11598 version(win32_widgets) { 11599 // FIXME: use ideal button size instead 11600 width = 50; 11601 height = 30; 11602 super(parent); 11603 11604 // BS_BITMAP is set when we want image only, so checking for exactly that combination 11605 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 11606 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 11607 11608 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 11609 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 11610 11611 if(label.image) { 11612 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 11613 11614 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 11615 } 11616 11617 this.label = label.label; 11618 } else version(custom_widgets) { 11619 width = 50; 11620 height = 30; 11621 super(parent); 11622 11623 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 11624 11625 if(label.image) { 11626 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 11627 this.displayFlags = label.displayFlags; 11628 } 11629 11630 this.alignment = label.alignment; 11631 } 11632 } 11633 11634 /// 11635 this(string label, Widget parent) { 11636 this(ImageLabel(label), parent); 11637 } 11638 11639 override int minHeight() { return defaultLineHeight + 4; } 11640 11641 static class Style : Widget.Style { 11642 override WidgetBackground background() { 11643 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 11644 11645 auto pressed = DynamicState.depressed | DynamicState.hover; 11646 if((widget.dynamicState & pressed) == pressed) { 11647 return WidgetBackground(cs.depressedButtonColor()); 11648 } else if(widget.dynamicState & DynamicState.hover) { 11649 return WidgetBackground(cs.hoveringColor()); 11650 } else { 11651 return WidgetBackground(cs.buttonColor()); 11652 } 11653 } 11654 11655 override FrameStyle borderStyle() { 11656 auto pressed = DynamicState.depressed | DynamicState.hover; 11657 if((widget.dynamicState & pressed) == pressed) { 11658 return FrameStyle.sunk; 11659 } else { 11660 return FrameStyle.risen; 11661 } 11662 11663 } 11664 11665 override bool variesWithState(ulong dynamicStateFlags) { 11666 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11667 } 11668 } 11669 mixin OverrideStyle!Style; 11670 11671 version(custom_widgets) 11672 override void paint(WidgetPainter painter) { 11673 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 11674 if(sprite) { 11675 sprite.drawAt( 11676 painter, 11677 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 11678 Point(0, 0) 11679 ); 11680 } else { 11681 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 11682 } 11683 return bounds; 11684 }); 11685 } 11686 11687 override int flexBasisWidth() { 11688 version(win32_widgets) { 11689 SIZE size; 11690 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11691 if(size.cx == 0) 11692 goto fallback; 11693 return size.cx + scaleWithDpi(16); 11694 } 11695 fallback: 11696 return scaleWithDpi(cast(int) label.length * 8 + 16); 11697 } 11698 11699 override int flexBasisHeight() { 11700 version(win32_widgets) { 11701 SIZE size; 11702 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11703 if(size.cy == 0) 11704 goto fallback; 11705 return size.cy + scaleWithDpi(6); 11706 } 11707 fallback: 11708 return defaultLineHeight + 4; 11709 } 11710 } 11711 11712 /++ 11713 A button with a consistent size, suitable for user commands like OK and CANCEL. 11714 +/ 11715 class CommandButton : Button { 11716 this(string label, Widget parent) { 11717 super(label, parent); 11718 } 11719 11720 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 11721 11722 override int maxHeight() { 11723 return defaultLineHeight + 4; 11724 } 11725 11726 override int maxWidth() { 11727 return defaultLineHeight * 4; 11728 } 11729 11730 override int marginLeft() { return 12; } 11731 override int marginRight() { return 12; } 11732 override int marginTop() { return 12; } 11733 override int marginBottom() { return 12; } 11734 } 11735 11736 /// 11737 enum ArrowDirection { 11738 left, /// 11739 right, /// 11740 up, /// 11741 down /// 11742 } 11743 11744 /// 11745 version(custom_widgets) 11746 class ArrowButton : Button { 11747 /// 11748 this(ArrowDirection direction, Widget parent) { 11749 super("", parent); 11750 this.direction = direction; 11751 triggersOnMultiClick = true; 11752 } 11753 11754 private ArrowDirection direction; 11755 11756 override int minHeight() { return scaleWithDpi(16); } 11757 override int maxHeight() { return scaleWithDpi(16); } 11758 override int minWidth() { return scaleWithDpi(16); } 11759 override int maxWidth() { return scaleWithDpi(16); } 11760 11761 override void paint(WidgetPainter painter) { 11762 super.paint(painter); 11763 11764 auto cs = getComputedStyle(); 11765 11766 painter.outlineColor = cs.foregroundColor; 11767 painter.fillColor = cs.foregroundColor; 11768 11769 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 11770 11771 final switch(direction) { 11772 case ArrowDirection.up: 11773 painter.drawPolygon( 11774 scaleWithDpi(Point(2, 10) + offset), 11775 scaleWithDpi(Point(7, 5) + offset), 11776 scaleWithDpi(Point(12, 10) + offset), 11777 scaleWithDpi(Point(2, 10) + offset) 11778 ); 11779 break; 11780 case ArrowDirection.down: 11781 painter.drawPolygon( 11782 scaleWithDpi(Point(2, 6) + offset), 11783 scaleWithDpi(Point(7, 11) + offset), 11784 scaleWithDpi(Point(12, 6) + offset), 11785 scaleWithDpi(Point(2, 6) + offset) 11786 ); 11787 break; 11788 case ArrowDirection.left: 11789 painter.drawPolygon( 11790 scaleWithDpi(Point(10, 2) + offset), 11791 scaleWithDpi(Point(5, 7) + offset), 11792 scaleWithDpi(Point(10, 12) + offset), 11793 scaleWithDpi(Point(10, 2) + offset) 11794 ); 11795 break; 11796 case ArrowDirection.right: 11797 painter.drawPolygon( 11798 scaleWithDpi(Point(6, 2) + offset), 11799 scaleWithDpi(Point(11, 7) + offset), 11800 scaleWithDpi(Point(6, 12) + offset), 11801 scaleWithDpi(Point(6, 2) + offset) 11802 ); 11803 break; 11804 } 11805 } 11806 } 11807 11808 private 11809 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 11810 int x, y; 11811 Widget par = c; 11812 while(par) { 11813 x += par.x; 11814 y += par.y; 11815 par = par.parent; 11816 } 11817 return [x, y]; 11818 } 11819 11820 version(win32_widgets) 11821 private 11822 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 11823 // MapWindowPoints? 11824 int x, y; 11825 Widget par = c; 11826 while(par) { 11827 x += par.x; 11828 y += par.y; 11829 par = par.parent; 11830 if(par !is null && par.useNativeDrawing()) 11831 break; 11832 } 11833 return [x, y]; 11834 } 11835 11836 /// 11837 class ImageBox : Widget { 11838 private MemoryImage image_; 11839 11840 override int widthStretchiness() { return 1; } 11841 override int heightStretchiness() { return 1; } 11842 override int widthShrinkiness() { return 1; } 11843 override int heightShrinkiness() { return 1; } 11844 11845 override int flexBasisHeight() { 11846 return image_.height; 11847 } 11848 11849 override int flexBasisWidth() { 11850 return image_.width; 11851 } 11852 11853 /// 11854 public void setImage(MemoryImage image){ 11855 this.image_ = image; 11856 if(this.parentWindow && this.parentWindow.win) { 11857 if(sprite) 11858 sprite.dispose(); 11859 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11860 } 11861 redraw(); 11862 } 11863 11864 /// How to fit the image in the box if they aren't an exact match in size? 11865 enum HowToFit { 11866 center, /// centers the image, cropping around all the edges as needed 11867 crop, /// always draws the image in the upper left, cropping the lower right if needed 11868 // stretch, /// not implemented 11869 } 11870 11871 private Sprite sprite; 11872 private HowToFit howToFit_; 11873 11874 private Color backgroundColor_; 11875 11876 /// 11877 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 11878 this.image_ = image; 11879 this.tabStop = false; 11880 this.howToFit_ = howToFit; 11881 this.backgroundColor_ = backgroundColor; 11882 super(parent); 11883 updateSprite(); 11884 } 11885 11886 /// ditto 11887 this(MemoryImage image, HowToFit howToFit, Widget parent) { 11888 this(image, howToFit, Color.transparent, parent); 11889 } 11890 11891 private void updateSprite() { 11892 if(sprite is null && this.parentWindow && this.parentWindow.win) { 11893 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11894 } 11895 } 11896 11897 override void paint(WidgetPainter painter) { 11898 updateSprite(); 11899 if(backgroundColor_.a) { 11900 painter.fillColor = backgroundColor_; 11901 painter.drawRectangle(Point(0, 0), width, height); 11902 } 11903 if(howToFit_ == HowToFit.crop) 11904 sprite.drawAt(painter, Point(0, 0)); 11905 else if(howToFit_ == HowToFit.center) { 11906 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 11907 } 11908 } 11909 } 11910 11911 /// 11912 class TextLabel : Widget { 11913 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; } 11914 override int maxHeight() { return minHeight; } 11915 override int minWidth() { return 32; } 11916 11917 override int flexBasisHeight() { return minHeight(); } 11918 override int flexBasisWidth() { return defaultTextWidth(label); } 11919 11920 string label_; 11921 11922 /++ 11923 Indicates which other control this label is here for. Similar to HTML `for` attribute. 11924 11925 In practice this means a click on the label will focus the `labelFor`. In future versions 11926 it will also set screen reader hints but that is not yet implemented. 11927 11928 History: 11929 Added October 3, 2021 (dub v10.4) 11930 +/ 11931 Widget labelFor; 11932 11933 /// 11934 @scriptable 11935 string label() { return label_; } 11936 11937 /// 11938 @scriptable 11939 void label(string l) { 11940 label_ = l; 11941 version(win32_widgets) { 11942 WCharzBuffer bfr = WCharzBuffer(l); 11943 SetWindowTextW(hwnd, bfr.ptr); 11944 } else version(custom_widgets) 11945 redraw(); 11946 } 11947 11948 override void defaultEventHandler_click(scope ClickEvent ce) { 11949 if(this.labelFor !is null) 11950 this.labelFor.focus(); 11951 } 11952 11953 /++ 11954 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 11955 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 11956 +/ 11957 this(string label, TextAlignment alignment, Widget parent) { 11958 this.label_ = label; 11959 this.alignment = alignment; 11960 this.tabStop = false; 11961 super(parent); 11962 11963 version(win32_widgets) 11964 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 11965 } 11966 11967 /// ditto 11968 this(string label, Widget parent) { 11969 this(label, TextAlignment.Right, parent); 11970 } 11971 11972 TextAlignment alignment; 11973 11974 version(custom_widgets) 11975 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 11976 painter.outlineColor = getComputedStyle().foregroundColor; 11977 painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment); 11978 return bounds; 11979 } 11980 11981 } 11982 11983 version(custom_widgets) 11984 private struct etc { 11985 mixin ExperimentalTextComponent; 11986 } 11987 11988 version(win32_widgets) 11989 alias EditableTextWidgetParent = Widget; /// 11990 else version(custom_widgets) { 11991 version(trash_text) { 11992 alias EditableTextWidgetParent = ScrollableWidget; /// 11993 } else { 11994 alias EditableTextWidgetParent = Widget; 11995 version=use_new_text_system; 11996 import arsd.textlayouter; 11997 } 11998 } else static assert(0); 11999 12000 version(use_new_text_system) 12001 class TextDisplayHelper : Widget { 12002 protected TextLayouter l; 12003 protected ScrollMessageWidget smw; 12004 12005 private const(TextLayouter.State)*[] undoStack; 12006 private const(TextLayouter.State)*[] redoStack; 12007 12008 bool readonly; 12009 bool caretNavigation; // scroll lock can flip this 12010 bool singleLine; 12011 bool acceptsTabInput; 12012 12013 private Menu ctx; 12014 override Menu contextMenu(int x, int y) { 12015 if(ctx is null) { 12016 ctx = new Menu("Actions", this); 12017 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 12018 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 12019 ctx.addSeparator(); 12020 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 12021 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 12022 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 12023 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 12024 ctx.addSeparator(); 12025 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 12026 } 12027 return ctx; 12028 } 12029 12030 override void defaultEventHandler_blur(Event ev) { 12031 super.defaultEventHandler_blur(ev); 12032 if(l.wasMutated()) { 12033 auto evt = new ChangeEvent!string(this, &this.content); 12034 evt.dispatch(); 12035 l.clearWasMutatedFlag(); 12036 } 12037 } 12038 12039 private string content() { 12040 return l.getTextString(); 12041 } 12042 12043 void undo() { 12044 if(undoStack.length) { 12045 auto state = undoStack[$-1]; 12046 undoStack = undoStack[0 .. $-1]; 12047 undoStack.assumeSafeAppend(); 12048 redoStack ~= l.saveState(); 12049 l.restoreState(state); 12050 adjustScrollbarSizes(); 12051 scrollForCaret(); 12052 redraw(); 12053 stateCheckpoint = true; 12054 } 12055 } 12056 12057 void redo() { 12058 if(redoStack.length) { 12059 doStateCheckpoint(); 12060 auto state = redoStack[$-1]; 12061 redoStack = redoStack[0 .. $-1]; 12062 redoStack.assumeSafeAppend(); 12063 l.restoreState(state); 12064 adjustScrollbarSizes(); 12065 scrollForCaret(); 12066 redraw(); 12067 stateCheckpoint = true; 12068 } 12069 } 12070 12071 void cut() { 12072 with(l.selection()) { 12073 if(!isEmpty()) { 12074 setClipboardText(parentWindow.win, getContentString()); 12075 doStateCheckpoint(); 12076 replaceContent(""); 12077 adjustScrollbarSizes(); 12078 scrollForCaret(); 12079 this.redraw(); 12080 } 12081 } 12082 12083 } 12084 12085 void copy() { 12086 with(l.selection()) { 12087 if(!isEmpty()) { 12088 setClipboardText(parentWindow.win, getContentString()); 12089 this.redraw(); 12090 } 12091 } 12092 } 12093 12094 void paste() { 12095 getClipboardText(parentWindow.win, (txt) { 12096 doStateCheckpoint(); 12097 l.selection.replaceContent(txt); 12098 adjustScrollbarSizes(); 12099 scrollForCaret(); 12100 this.redraw(); 12101 }); 12102 } 12103 12104 void deleteContentOfSelection() { 12105 doStateCheckpoint(); 12106 l.selection.replaceContent(""); 12107 l.selection.setUserXCoordinate(); 12108 adjustScrollbarSizes(); 12109 scrollForCaret(); 12110 redraw(); 12111 } 12112 12113 void selectAll() { 12114 with(l.selection) { 12115 moveToStartOfDocument(); 12116 setAnchor(); 12117 moveToEndOfDocument(); 12118 setFocus(); 12119 } 12120 redraw(); 12121 } 12122 12123 protected bool stateCheckpoint = true; 12124 12125 protected void doStateCheckpoint() { 12126 if(stateCheckpoint) { 12127 undoStack ~= l.saveState(); 12128 stateCheckpoint = false; 12129 } 12130 } 12131 12132 protected void adjustScrollbarSizes() { 12133 // FIXME: will want a content area helper function instead of doing all these subtractions myself 12134 auto borderWidth = 2; 12135 this.smw.setTotalArea(l.width, l.height); 12136 this.smw.setViewableArea( 12137 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 12138 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 12139 } 12140 12141 protected void scrollForCaret() { 12142 // writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 12143 smw.scrollIntoView(l.selection.focusBoundingBox()); 12144 } 12145 12146 // FIXME: this should be a theme changed event listener instead 12147 private BaseVisualTheme currentTheme; 12148 override void recomputeChildLayout() { 12149 if(currentTheme is null) 12150 currentTheme = WidgetPainter.visualTheme; 12151 if(WidgetPainter.visualTheme !is currentTheme) { 12152 currentTheme = WidgetPainter.visualTheme; 12153 auto ds = this.l.defaultStyle; 12154 if(auto ms = cast(MyTextStyle) ds) { 12155 auto cs = getComputedStyle(); 12156 auto font = cs.font(); 12157 if(font !is null) 12158 ms.font_ = font; 12159 else { 12160 auto osc = new OperatingSystemFont(); 12161 osc.loadDefault; 12162 ms.font_ = osc; 12163 } 12164 } 12165 } 12166 super.recomputeChildLayout(); 12167 } 12168 12169 private Point adjustForSingleLine(Point p) { 12170 if(singleLine) 12171 return Point(p.x, this.height / 2); 12172 else 12173 return p; 12174 } 12175 12176 private bool wordWrapEnabled_; 12177 12178 this(TextLayouter l, ScrollMessageWidget parent) { 12179 this.smw = parent; 12180 12181 smw.addDefaultWheelListeners(16, 16, 8); 12182 smw.movementPerButtonClick(16, 16); 12183 12184 this.defaultPadding = Rectangle(2, 2, 2, 2); 12185 12186 this.l = l; 12187 super(parent); 12188 12189 smw.addEventListener((scope ScrollEvent se) { 12190 this.redraw(); 12191 }); 12192 12193 bool mouseDown; 12194 12195 this.addEventListener((scope ResizeEvent re) { 12196 // FIXME: I should add a method to give this client area width thing 12197 if(wordWrapEnabled_) 12198 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 12199 12200 adjustScrollbarSizes(); 12201 scrollForCaret(); 12202 12203 this.redraw(); 12204 }); 12205 12206 this.addEventListener((scope KeyDownEvent kde) { 12207 switch(kde.key) { 12208 case Key.Up, Key.Down, Key.Left, Key.Right: 12209 case Key.Home, Key.End: 12210 stateCheckpoint = true; 12211 bool setPosition = false; 12212 switch(kde.key) { 12213 case Key.Up: l.selection.moveUp(); break; 12214 case Key.Down: l.selection.moveDown(); break; 12215 case Key.Left: l.selection.moveLeft(); setPosition = true; break; 12216 case Key.Right: l.selection.moveRight(); setPosition = true; break; 12217 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 12218 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 12219 default: assert(0); 12220 } 12221 12222 if(kde.shiftKey) 12223 l.selection.setFocus(); 12224 else 12225 l.selection.setAnchor(); 12226 if(setPosition) 12227 l.selection.setUserXCoordinate(); 12228 scrollForCaret(); 12229 redraw(); 12230 break; 12231 case Key.PageUp, Key.PageDown: 12232 // FIXME 12233 scrollForCaret(); 12234 break; 12235 case Key.Delete: 12236 if(l.selection.isEmpty()) { 12237 l.selection.setAnchor(); 12238 l.selection.moveRight(); 12239 l.selection.setFocus(); 12240 } 12241 deleteContentOfSelection(); 12242 adjustScrollbarSizes(); 12243 scrollForCaret(); 12244 break; 12245 case Key.Insert: 12246 break; 12247 case Key.A: 12248 if(kde.ctrlKey) 12249 selectAll(); 12250 break; 12251 case Key.F: 12252 // find 12253 break; 12254 case Key.Z: 12255 if(kde.ctrlKey) 12256 undo(); 12257 break; 12258 case Key.R: 12259 if(kde.ctrlKey) 12260 redo(); 12261 break; 12262 case Key.X: 12263 if(kde.ctrlKey) 12264 cut(); 12265 break; 12266 case Key.C: 12267 if(kde.ctrlKey) 12268 copy(); 12269 break; 12270 case Key.V: 12271 if(kde.ctrlKey) 12272 paste(); 12273 break; 12274 case Key.F1: 12275 with(l.selection()) { 12276 moveToStartOfLine(); 12277 setAnchor(); 12278 moveToEndOfLine(); 12279 moveToIncludeAdjacentEndOfLineMarker(); 12280 setFocus(); 12281 replaceContent(""); 12282 } 12283 12284 redraw(); 12285 break; 12286 /* 12287 case Key.F2: 12288 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 12289 //(cast(MyTextStyle) old).font, 12290 font2, 12291 Color.red))); 12292 redraw(); 12293 break; 12294 */ 12295 case Key.Tab: 12296 // we process the char event, so don't want to change focus on it 12297 if(acceptsTabInput) 12298 kde.preventDefault(); 12299 break; 12300 default: 12301 } 12302 }); 12303 12304 Point downAt; 12305 12306 static if(UsingSimpledisplayX11) 12307 this.addEventListener((scope ClickEvent ce) { 12308 if(ce.button == MouseButton.middle) { 12309 parentWindow.win.getPrimarySelection((txt) { 12310 l.selection.replaceContent(txt); 12311 redraw(); 12312 }); 12313 } 12314 }); 12315 12316 this.addEventListener((scope MouseDownEvent ce) { 12317 if(ce.button == MouseButton.left) { 12318 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12319 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 12320 l.selection.setAnchor(); 12321 mouseDown = true; 12322 parentWindow.captureMouse(this); 12323 this.redraw(); 12324 } else if(ce.button == MouseButton.right) { 12325 this.showContextMenu(ce.clientX, ce.clientY); 12326 } 12327 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12328 }); 12329 12330 Timer autoscrollTimer; 12331 int autoscrollDirection; 12332 int autoscrollAmount; 12333 12334 void autoscroll() { 12335 switch(autoscrollDirection) { 12336 case 0: smw.scrollUp(autoscrollAmount); break; 12337 case 1: smw.scrollDown(autoscrollAmount); break; 12338 case 2: smw.scrollLeft(autoscrollAmount); break; 12339 case 3: smw.scrollRight(autoscrollAmount); break; 12340 default: assert(0); 12341 } 12342 12343 this.redraw(); 12344 } 12345 12346 void setAutoscrollTimer(int direction, int amount) { 12347 if(autoscrollTimer is null) { 12348 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 12349 } 12350 12351 autoscrollDirection = direction; 12352 autoscrollAmount = amount; 12353 } 12354 12355 void stopAutoscrollTimer() { 12356 if(autoscrollTimer !is null) { 12357 autoscrollTimer.dispose(); 12358 autoscrollTimer = null; 12359 } 12360 autoscrollAmount = 0; 12361 autoscrollDirection = 0; 12362 } 12363 12364 this.addEventListener((scope MouseMoveEvent ce) { 12365 if(mouseDown) { 12366 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12367 12368 // FIXME: when scrolling i actually do want a timer. 12369 // i also want a zone near the sides of the window where i can auto scroll 12370 12371 auto scrollMultiplier = scaleWithDpi(16); 12372 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 12373 12374 if(!singleLine && movedTo.y < 4) { 12375 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 12376 } else 12377 if(!singleLine && (movedTo.y + 6) > this.height) { 12378 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 12379 } else 12380 if(movedTo.x < 4) { 12381 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 12382 } else 12383 if((movedTo.x + 6) > this.width) { 12384 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 12385 } else 12386 stopAutoscrollTimer(); 12387 12388 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 12389 l.selection.setFocus(); 12390 this.redraw(); 12391 } 12392 }); 12393 12394 this.addEventListener((scope MouseUpEvent ce) { 12395 // FIXME: assert primary selection 12396 if(mouseDown && ce.button == MouseButton.left) { 12397 stateCheckpoint = true; 12398 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 12399 //l.selection.setFocus(); 12400 mouseDown = false; 12401 parentWindow.releaseMouseCapture(); 12402 stopAutoscrollTimer(); 12403 this.redraw(); 12404 } 12405 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12406 }); 12407 12408 this.addEventListener((scope CharEvent ce) { 12409 if(readonly) 12410 return; 12411 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 12412 return; // skip the ctrl+x characters we don't care about as plain text 12413 12414 if(singleLine && ce.character == '\n') 12415 return; 12416 if(!acceptsTabInput && ce.character == '\t') 12417 return; 12418 12419 doStateCheckpoint(); 12420 12421 char[4] buffer; 12422 import std.utf; // FIXME: i should remove this. compile time not significant but the logs get spammed with phobos' import web 12423 auto stride = encode(buffer, ce.character); 12424 l.selection.replaceContent(buffer[0 .. stride]); 12425 l.selection.setUserXCoordinate(); 12426 adjustScrollbarSizes(); 12427 scrollForCaret(); 12428 redraw(); 12429 }); 12430 } 12431 12432 static class Style : Widget.Style { 12433 override WidgetBackground background() { 12434 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 12435 } 12436 12437 override Color foregroundColor() { 12438 return WidgetPainter.visualTheme.foregroundColor; 12439 } 12440 12441 override FrameStyle borderStyle() { 12442 return FrameStyle.sunk; 12443 } 12444 12445 override MouseCursor cursor() { 12446 return GenericCursor.Text; 12447 } 12448 } 12449 mixin OverrideStyle!Style; 12450 12451 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; } 12452 override int maxHeight() { 12453 if(singleLine) 12454 return minHeight; 12455 else 12456 return super.maxHeight(); 12457 } 12458 12459 void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 12460 painter.drawText(upperLeft, text); 12461 } 12462 12463 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 12464 //painter.setFont(font); 12465 12466 auto cs = getComputedStyle(); 12467 auto defaultColor = cs.foregroundColor; 12468 12469 auto old = painter.setClipRectangle(bounds); 12470 scope(exit) painter.setClipRectangle(old); 12471 12472 l.getDrawableText(delegate bool(txt, style, info, carets...) { 12473 //writeln("Segment: ", txt); 12474 assert(style !is null); 12475 12476 auto myStyle = cast(MyTextStyle) style; 12477 assert(myStyle !is null); 12478 12479 painter.setFont(myStyle.font); 12480 // defaultColor = myStyle.color; // FIXME: so wrong 12481 12482 if(info.selections && info.boundingBox.width > 0) { 12483 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 12484 painter.fillColor = color; 12485 painter.outlineColor = color; 12486 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 12487 painter.outlineColor = cs.selectionForegroundColor; 12488 //painter.fillColor = Color.white; 12489 } else { 12490 painter.outlineColor = defaultColor; 12491 } 12492 12493 if(this.isFocused) 12494 foreach(idx, caret; carets) { 12495 if(idx == 0) 12496 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 12497 painter.drawLine( 12498 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 12499 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 12500 ); 12501 } 12502 12503 if(txt.stripInternal.length) { 12504 drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 12505 } 12506 12507 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) { 12508 return false; 12509 } else { 12510 return true; 12511 } 12512 }, Rectangle(smw.position(), bounds.size)); 12513 12514 /+ 12515 int place = 0; 12516 int y = 75; 12517 foreach(width; widths) { 12518 painter.fillColor = Color.red; 12519 painter.drawRectangle(Point(place, y), Size(width, 75)); 12520 //y += 15; 12521 place += width; 12522 } 12523 +/ 12524 12525 return bounds; 12526 } 12527 12528 static class MyTextStyle : TextStyle { 12529 OperatingSystemFont font_; 12530 this(OperatingSystemFont font, bool passwordMode = false) { 12531 this.font_ = font; 12532 } 12533 12534 override OperatingSystemFont font() { 12535 return font_; 12536 } 12537 } 12538 } 12539 12540 /+ 12541 version(use_new_text_system) 12542 class TextWidget : Widget { 12543 TextLayouter l; 12544 ScrollMessageWidget smw; 12545 TextDisplayHelper helper; 12546 this(TextLayouter l, Widget parent) { 12547 this.l = l; 12548 super(parent); 12549 12550 smw = new ScrollMessageWidget(this); 12551 //smw.horizontalScrollBar.hide; 12552 //smw.verticalScrollBar.hide; 12553 smw.addDefaultWheelListeners(16, 16, 8); 12554 smw.movementPerButtonClick(16, 16); 12555 helper = new TextDisplayHelper(l, smw); 12556 12557 // no need to do this here since there's gonna be a resize 12558 // event immediately before any drawing 12559 // smw.setTotalArea(l.width, l.height); 12560 smw.setViewableArea( 12561 this.width - this.paddingLeft - this.paddingRight, 12562 this.height - this.paddingTop - this.paddingBottom); 12563 12564 /+ 12565 writeln(l.width, "x", l.height); 12566 +/ 12567 } 12568 } 12569 +/ 12570 12571 12572 12573 12574 /+ 12575 This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too 12576 +/ 12577 12578 /// Contains the implementation of text editing 12579 abstract class EditableTextWidget : EditableTextWidgetParent { 12580 this(Widget parent) { 12581 super(parent); 12582 12583 version(custom_widgets) 12584 setupCustomTextEditing(); 12585 } 12586 12587 private bool wordWrapEnabled_; 12588 void wordWrapEnabled(bool enabled) { 12589 version(win32_widgets) { 12590 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 12591 } else version(custom_widgets) { 12592 wordWrapEnabled_ = enabled; 12593 version(use_new_text_system) 12594 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 12595 } else static assert(false); 12596 } 12597 12598 override int minWidth() { return scaleWithDpi(16); } 12599 override int widthStretchiness() { return 7; } 12600 12601 version(use_new_text_system) 12602 override int maxHeight() { return tdh.maxHeight; } 12603 12604 version(use_new_text_system) 12605 override void focus() { if(tdh) tdh.focus(); else super.focus(); } 12606 12607 void selectAll() { 12608 version(win32_widgets) 12609 SendMessage(hwnd, EM_SETSEL, 0, -1); 12610 else version(custom_widgets) { 12611 version(use_new_text_system) 12612 tdh.selectAll(); 12613 else 12614 textLayout.selectAll(); 12615 redraw(); 12616 } 12617 } 12618 12619 version(use_new_text_system) 12620 TextDisplayHelper tdh; 12621 12622 @property string content() { 12623 version(win32_widgets) { 12624 wchar[4096] bufferstack; 12625 wchar[] buffer; 12626 auto len = GetWindowTextLength(hwnd); 12627 if(len < bufferstack.length) 12628 buffer = bufferstack[0 .. len + 1]; 12629 else 12630 buffer = new wchar[](len + 1); 12631 12632 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 12633 if(l >= 0) 12634 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 12635 else 12636 return null; 12637 } else version(custom_widgets) { 12638 version(use_new_text_system) { 12639 return textLayout.getTextString(); 12640 } else 12641 return textLayout.getPlainText(); 12642 } else static assert(false); 12643 } 12644 @property void content(string s) { 12645 version(win32_widgets) { 12646 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 12647 SetWindowTextW(hwnd, bfr.ptr); 12648 } else version(custom_widgets) { 12649 version(use_new_text_system) { 12650 selectAll(); 12651 textLayout.selection.replaceContent(s); 12652 12653 tdh.adjustScrollbarSizes(); 12654 // these don't seem to help 12655 // tdh.smw.setPosition(0, 0); 12656 // tdh.scrollForCaret(); 12657 12658 redraw(); 12659 } else { 12660 textLayout.clear(); 12661 textLayout.addText(s); 12662 12663 { 12664 // FIXME: it should be able to get this info easier 12665 auto painter = draw(); 12666 textLayout.redoLayout(painter); 12667 } 12668 auto cbb = textLayout.contentBoundingBox(); 12669 setContentSize(cbb.width, cbb.height); 12670 /* 12671 textLayout.addText(ForegroundColor.red, s); 12672 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 12673 textLayout.addText(" is the best!"); 12674 */ 12675 redraw(); 12676 } 12677 } 12678 else static assert(false); 12679 } 12680 12681 void addText(string txt) { 12682 version(custom_widgets) { 12683 version(use_new_text_system) { 12684 textLayout.appendText(txt); 12685 tdh.adjustScrollbarSizes(); 12686 redraw(); 12687 } else { 12688 textLayout.addText(txt); 12689 12690 { 12691 // FIXME: it should be able to get this info easier 12692 auto painter = draw(); 12693 textLayout.redoLayout(painter); 12694 } 12695 auto cbb = textLayout.contentBoundingBox(); 12696 setContentSize(cbb.width, cbb.height); 12697 } 12698 } else version(win32_widgets) { 12699 // get the current selection 12700 DWORD StartPos, EndPos; 12701 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 12702 12703 // move the caret to the end of the text 12704 int outLength = GetWindowTextLengthW(hwnd); 12705 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 12706 12707 // insert the text at the new caret position 12708 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 12709 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 12710 12711 // restore the previous selection 12712 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 12713 } else static assert(0); 12714 } 12715 12716 version(custom_widgets) 12717 version(trash_text) 12718 override void paintFrameAndBackground(WidgetPainter painter) { 12719 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 12720 } 12721 12722 version(use_new_text_system) 12723 TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 12724 return new TextDisplayHelper(textLayout, smw); 12725 } 12726 12727 version(use_new_text_system) 12728 TextStyle defaultTextStyle() { 12729 return new TextDisplayHelper.MyTextStyle(getUsedFont()); 12730 } 12731 12732 version(use_new_text_system) 12733 private OperatingSystemFont getUsedFont() { 12734 auto cs = getComputedStyle(); 12735 auto font = cs.font; 12736 if(font is null) { 12737 font = new OperatingSystemFont; 12738 font.loadDefault(); 12739 } 12740 return font; 12741 } 12742 12743 version(win32_widgets) { /* will do it with Windows calls in the classes */ } 12744 else version(custom_widgets) { 12745 // FIXME 12746 version(use_new_text_system) { 12747 TextLayouter textLayout; 12748 12749 void setupCustomTextEditing() { 12750 textLayout = new TextLayouter(defaultTextStyle()); 12751 auto smw = new ScrollMessageWidget(this); 12752 if(!showingHorizontalScroll) 12753 smw.horizontalScrollBar.hide(); 12754 if(!showingVerticalScroll) 12755 smw.verticalScrollBar.hide(); 12756 this.tabStop = false; 12757 smw.tabStop = false; 12758 tdh = textDisplayHelperFactory(textLayout, smw); 12759 12760 this.parentWindow.addEventListener((scope DpiChangedEvent dce) { 12761 if(textLayout) { 12762 if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) { 12763 // the dpi change can change the font, so this informs the layouter that it has changed too 12764 style.font_ = getUsedFont(); 12765 12766 // arsd.core.writeln(this.parentWindow.win.actualDpi); 12767 } 12768 } 12769 }); 12770 } 12771 12772 } else { 12773 12774 static if(SimpledisplayTimerAvailable) 12775 Timer caretTimer; 12776 etc.TextLayout textLayout; 12777 12778 void setupCustomTextEditing() { 12779 textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4)); 12780 textLayout.selectionXorColor = getComputedStyle().activeListXorColor; 12781 } 12782 12783 override void paint(WidgetPainter painter) { 12784 if(parentWindow.win.closed) return; 12785 12786 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 12787 12788 /* 12789 painter.outlineColor = Color.white; 12790 painter.fillColor = Color.white; 12791 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 12792 */ 12793 12794 painter.outlineColor = Color.black; 12795 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 12796 12797 textLayout.caretShowingOnScreen = false; 12798 12799 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 12800 } 12801 } 12802 12803 static class Style : Widget.Style { 12804 override FrameStyle borderStyle() { 12805 return FrameStyle.sunk; 12806 } 12807 override MouseCursor cursor() { 12808 return GenericCursor.Text; 12809 } 12810 } 12811 mixin OverrideStyle!Style; 12812 } 12813 else static assert(false); 12814 12815 version(trash_text) 12816 version(custom_widgets) 12817 override void defaultEventHandler_mousedown(MouseDownEvent ev) { 12818 super.defaultEventHandler_mousedown(ev); 12819 if(parentWindow.win.closed) return; 12820 if(ev.button == MouseButton.left) { 12821 if(textLayout.selectNone()) 12822 redraw(); 12823 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 12824 this.focus(); 12825 //this.parentWindow.win.grabInput(); 12826 } else if(ev.button == MouseButton.middle) { 12827 static if(UsingSimpledisplayX11) { 12828 getPrimarySelection(parentWindow.win, (in char[] txt) { 12829 textLayout.insert(txt); 12830 redraw(); 12831 12832 auto cbb = textLayout.contentBoundingBox(); 12833 setContentSize(cbb.width, cbb.height); 12834 }); 12835 } 12836 } 12837 } 12838 12839 version(trash_text) 12840 version(custom_widgets) 12841 override void defaultEventHandler_mouseup(MouseUpEvent ev) { 12842 //this.parentWindow.win.releaseInputGrab(); 12843 super.defaultEventHandler_mouseup(ev); 12844 } 12845 12846 version(trash_text) 12847 version(custom_widgets) 12848 override void defaultEventHandler_mousemove(MouseMoveEvent ev) { 12849 super.defaultEventHandler_mousemove(ev); 12850 if(ev.state & ModifierState.leftButtonDown) { 12851 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 12852 redraw(); 12853 } 12854 } 12855 12856 version(trash_text) 12857 version(custom_widgets) 12858 override void defaultEventHandler_focus(Event ev) { 12859 super.defaultEventHandler_focus(ev); 12860 if(parentWindow.win.closed) return; 12861 auto painter = this.draw(); 12862 textLayout.drawCaret(painter); 12863 12864 static if(SimpledisplayTimerAvailable) 12865 if(caretTimer) { 12866 caretTimer.destroy(); 12867 caretTimer = null; 12868 } 12869 12870 bool blinkingCaret = true; 12871 static if(UsingSimpledisplayX11) 12872 if(!Image.impl.xshmAvailable) 12873 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 12874 12875 if(blinkingCaret) 12876 static if(SimpledisplayTimerAvailable) 12877 caretTimer = new Timer(500, { 12878 if(parentWindow.win.closed) { 12879 caretTimer.destroy(); 12880 return; 12881 } 12882 if(isFocused()) { 12883 auto painter = this.draw(); 12884 textLayout.drawCaret(painter); 12885 } else if(textLayout.caretShowingOnScreen) { 12886 auto painter = this.draw(); 12887 textLayout.eraseCaret(painter); 12888 } 12889 }); 12890 } 12891 12892 version(trash_text) { 12893 private string lastContentBlur; 12894 12895 override void defaultEventHandler_blur(Event ev) { 12896 super.defaultEventHandler_blur(ev); 12897 if(parentWindow.win.closed) return; 12898 version(custom_widgets) { 12899 auto painter = this.draw(); 12900 textLayout.eraseCaret(painter); 12901 static if(SimpledisplayTimerAvailable) 12902 if(caretTimer) { 12903 caretTimer.destroy(); 12904 caretTimer = null; 12905 } 12906 } 12907 12908 if(this.content != lastContentBlur) { 12909 auto evt = new ChangeEvent!string(this, &this.content); 12910 evt.dispatch(); 12911 lastContentBlur = this.content; 12912 } 12913 } 12914 } 12915 12916 version(win32_widgets) { 12917 private string lastContentBlur; 12918 12919 override void defaultEventHandler_blur(Event ev) { 12920 super.defaultEventHandler_blur(ev); 12921 12922 if(this.content != lastContentBlur) { 12923 auto evt = new ChangeEvent!string(this, &this.content); 12924 evt.dispatch(); 12925 lastContentBlur = this.content; 12926 } 12927 } 12928 } 12929 12930 12931 version(trash_text) 12932 version(custom_widgets) 12933 override void defaultEventHandler_char(CharEvent ev) { 12934 super.defaultEventHandler_char(ev); 12935 textLayout.insert(ev.character); 12936 redraw(); 12937 12938 // FIXME: too inefficient 12939 auto cbb = textLayout.contentBoundingBox(); 12940 setContentSize(cbb.width, cbb.height); 12941 } 12942 version(trash_text) 12943 version(custom_widgets) 12944 override void defaultEventHandler_keydown(KeyDownEvent ev) { 12945 //super.defaultEventHandler_keydown(ev); 12946 switch(ev.key) { 12947 case Key.Delete: 12948 textLayout.delete_(); 12949 redraw(); 12950 break; 12951 case Key.Left: 12952 textLayout.moveLeft(); 12953 redraw(); 12954 break; 12955 case Key.Right: 12956 textLayout.moveRight(); 12957 redraw(); 12958 break; 12959 case Key.Up: 12960 textLayout.moveUp(); 12961 redraw(); 12962 break; 12963 case Key.Down: 12964 textLayout.moveDown(); 12965 redraw(); 12966 break; 12967 case Key.Home: 12968 textLayout.moveHome(); 12969 redraw(); 12970 break; 12971 case Key.End: 12972 textLayout.moveEnd(); 12973 redraw(); 12974 break; 12975 case Key.PageUp: 12976 foreach(i; 0 .. 32) 12977 textLayout.moveUp(); 12978 redraw(); 12979 break; 12980 case Key.PageDown: 12981 foreach(i; 0 .. 32) 12982 textLayout.moveDown(); 12983 redraw(); 12984 break; 12985 12986 default: 12987 {} // intentionally blank, let "char" handle it 12988 } 12989 /* 12990 if(ev.key == Key.Backspace) { 12991 textLayout.backspace(); 12992 redraw(); 12993 } 12994 */ 12995 ensureVisibleInScroll(textLayout.caretBoundingBox()); 12996 } 12997 12998 version(use_new_text_system) { 12999 bool showingVerticalScroll() { return true; } 13000 bool showingHorizontalScroll() { return true; } 13001 } 13002 } 13003 13004 /// 13005 class LineEdit : EditableTextWidget { 13006 // FIXME: hack 13007 version(custom_widgets) { 13008 override bool showingVerticalScroll() { return false; } 13009 override bool showingHorizontalScroll() { return false; } 13010 } 13011 13012 override int flexBasisWidth() { return 250; } 13013 13014 /// 13015 this(Widget parent) { 13016 super(parent); 13017 version(win32_widgets) { 13018 createWin32Window(this, "edit"w, "", 13019 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 13020 } else version(custom_widgets) { 13021 version(trash_text) { 13022 setupCustomTextEditing(); 13023 addEventListener(delegate(CharEvent ev) { 13024 if(ev.character == '\n') 13025 ev.preventDefault(); 13026 }); 13027 } 13028 } else static assert(false); 13029 } 13030 13031 version(use_new_text_system) 13032 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13033 auto tdh = new TextDisplayHelper(textLayout, smw); 13034 tdh.singleLine = true; 13035 return tdh; 13036 } 13037 13038 version(win32_widgets) { 13039 mixin Padding!q{2}; 13040 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 13041 override int maxHeight() { return minHeight; } 13042 } 13043 13044 /+ 13045 @property void passwordMode(bool p) { 13046 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 13047 } 13048 +/ 13049 } 13050 13051 /++ 13052 A [LineEdit] that displays `*` in place of the actual characters. 13053 13054 Alas, Windows requires the window to be created differently to use this style, 13055 so it had to be a new class instead of a toggle on and off on an existing object. 13056 13057 FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now. 13058 13059 History: 13060 Added January 24, 2021 13061 +/ 13062 class PasswordEdit : EditableTextWidget { 13063 version(custom_widgets) { 13064 override bool showingVerticalScroll() { return false; } 13065 override bool showingHorizontalScroll() { return false; } 13066 } 13067 13068 override int flexBasisWidth() { return 250; } 13069 13070 version(use_new_text_system) 13071 override TextStyle defaultTextStyle() { 13072 auto cs = getComputedStyle(); 13073 13074 auto osf = new class OperatingSystemFont { 13075 this() { 13076 super(cs.font); 13077 } 13078 override int stringWidth(scope const(char)[] text, SimpleWindow window = null) { 13079 int count = 0; 13080 foreach(dchar ch; text) 13081 count++; 13082 return count * super.stringWidth("*", window); 13083 } 13084 }; 13085 13086 return new TextDisplayHelper.MyTextStyle(osf); 13087 } 13088 13089 version(use_new_text_system) 13090 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13091 static class TDH : TextDisplayHelper { 13092 this(TextLayouter textLayout, ScrollMessageWidget smw) { 13093 singleLine = true; 13094 super(textLayout, smw); 13095 } 13096 13097 override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 13098 char[256] buffer = void; 13099 int bufferLength = 0; 13100 foreach(dchar ch; text) 13101 buffer[bufferLength++] = '*'; 13102 painter.drawText(upperLeft, buffer[0..bufferLength]); 13103 } 13104 } 13105 13106 return new TDH(textLayout, smw); 13107 } 13108 13109 /// 13110 this(Widget parent) { 13111 super(parent); 13112 version(win32_widgets) { 13113 createWin32Window(this, "edit"w, "", 13114 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 13115 } else version(custom_widgets) { 13116 version(trash_text) 13117 setupCustomTextEditing(); 13118 addEventListener(delegate(CharEvent ev) { 13119 if(ev.character == '\n') 13120 ev.preventDefault(); 13121 }); 13122 } else static assert(false); 13123 } 13124 version(win32_widgets) { 13125 mixin Padding!q{2}; 13126 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 13127 override int maxHeight() { return minHeight; } 13128 } 13129 } 13130 13131 13132 /// 13133 class TextEdit : EditableTextWidget { 13134 /// 13135 this(Widget parent) { 13136 super(parent); 13137 version(win32_widgets) { 13138 createWin32Window(this, "edit"w, "", 13139 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 13140 } else version(custom_widgets) { 13141 version(trash_text) 13142 setupCustomTextEditing(); 13143 } else static assert(false); 13144 } 13145 override int maxHeight() { return int.max; } 13146 override int heightStretchiness() { return 7; } 13147 13148 override int flexBasisWidth() { return 250; } 13149 override int flexBasisHeight() { return 25; } 13150 } 13151 13152 13153 /++ 13154 13155 +/ 13156 version(none) 13157 class RichTextDisplay : Widget { 13158 @property void content(string c) {} 13159 void appendContent(string c) {} 13160 } 13161 13162 /// 13163 class MessageBox : Window { 13164 private string message; 13165 MessageBoxButton buttonPressed = MessageBoxButton.None; 13166 /// 13167 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 13168 super(300, 100); 13169 13170 assert(buttons.length); 13171 assert(buttons.length == buttonIds.length); 13172 13173 this.message = message; 13174 13175 auto label = new TextLabel(message, TextAlignment.Center, this); 13176 13177 auto hl = new HorizontalLayout(this); 13178 auto spacer = new HorizontalSpacer(hl); // to right align 13179 13180 foreach(idx, buttonText; buttons) { 13181 auto button = new CommandButton(buttonText, hl); 13182 13183 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 13184 this.buttonPressed = buttonIds[idx]; 13185 win.close(); 13186 }; })(idx)); 13187 13188 if(idx == 0) 13189 button.focus(); 13190 } 13191 13192 if(buttons.length == 1) 13193 auto spacer2 = new HorizontalSpacer(hl); // to center it 13194 13195 win.resize(scaleWithDpi(300), this.minHeight()); 13196 13197 win.show(); 13198 redraw(); 13199 } 13200 13201 mixin Padding!q{16}; 13202 } 13203 13204 /// 13205 enum MessageBoxStyle { 13206 OK, /// 13207 OKCancel, /// 13208 RetryCancel, /// 13209 YesNo, /// 13210 YesNoCancel, /// 13211 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. 13212 } 13213 13214 /// 13215 enum MessageBoxIcon { 13216 None, /// 13217 Info, /// 13218 Warning, /// 13219 Error /// 13220 } 13221 13222 /// Identifies the button the user pressed on a message box. 13223 enum MessageBoxButton { 13224 None, /// The user closed the message box without clicking any of the buttons. 13225 OK, /// 13226 Cancel, /// 13227 Retry, /// 13228 Yes, /// 13229 No, /// 13230 Continue /// 13231 } 13232 13233 13234 /++ 13235 Displays a modal message box, blocking until the user dismisses it. 13236 13237 Returns: the button pressed. 13238 +/ 13239 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13240 version(win32_widgets) { 13241 WCharzBuffer t = WCharzBuffer(title); 13242 WCharzBuffer m = WCharzBuffer(message); 13243 UINT type; 13244 with(MessageBoxStyle) 13245 final switch(style) { 13246 case OK: type |= MB_OK; break; 13247 case OKCancel: type |= MB_OKCANCEL; break; 13248 case RetryCancel: type |= MB_RETRYCANCEL; break; 13249 case YesNo: type |= MB_YESNO; break; 13250 case YesNoCancel: type |= MB_YESNOCANCEL; break; 13251 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 13252 } 13253 with(MessageBoxIcon) 13254 final switch(icon) { 13255 case None: break; 13256 case Info: type |= MB_ICONINFORMATION; break; 13257 case Warning: type |= MB_ICONWARNING; break; 13258 case Error: type |= MB_ICONERROR; break; 13259 } 13260 switch(MessageBoxW(null, m.ptr, t.ptr, type)) { 13261 case IDOK: return MessageBoxButton.OK; 13262 case IDCANCEL: return MessageBoxButton.Cancel; 13263 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 13264 case IDYES: return MessageBoxButton.Yes; 13265 case IDNO: return MessageBoxButton.No; 13266 case IDCONTINUE: return MessageBoxButton.Continue; 13267 default: return MessageBoxButton.None; 13268 } 13269 } else { 13270 string[] buttons; 13271 MessageBoxButton[] buttonIds; 13272 with(MessageBoxStyle) 13273 final switch(style) { 13274 case OK: 13275 buttons = ["OK"]; 13276 buttonIds = [MessageBoxButton.OK]; 13277 break; 13278 case OKCancel: 13279 buttons = ["OK", "Cancel"]; 13280 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 13281 break; 13282 case RetryCancel: 13283 buttons = ["Retry", "Cancel"]; 13284 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 13285 break; 13286 case YesNo: 13287 buttons = ["Yes", "No"]; 13288 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 13289 break; 13290 case YesNoCancel: 13291 buttons = ["Yes", "No", "Cancel"]; 13292 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 13293 break; 13294 case RetryCancelContinue: 13295 buttons = ["Try Again", "Cancel", "Continue"]; 13296 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 13297 break; 13298 } 13299 auto mb = new MessageBox(message, buttons, buttonIds); 13300 EventLoop el = EventLoop.get; 13301 el.run(() { return !mb.win.closed; }); 13302 return mb.buttonPressed; 13303 } 13304 } 13305 13306 /// ditto 13307 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13308 return messageBox(null, message, style, icon); 13309 } 13310 13311 13312 13313 /// 13314 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 13315 13316 /++ 13317 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 13318 13319 History: 13320 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. 13321 +/ 13322 struct EventListener { 13323 private Widget widget; 13324 private string event; 13325 private EventHandler handler; 13326 private bool useCapture; 13327 13328 /// 13329 void disconnect() { 13330 widget.removeEventListener(this); 13331 } 13332 } 13333 13334 /++ 13335 The purpose of this enum was to give a compile-time checked version of various standard event strings. 13336 13337 Now, I recommend you use a statically typed event object instead. 13338 13339 See_Also: [Event] 13340 +/ 13341 enum EventType : string { 13342 click = "click", /// 13343 13344 mouseenter = "mouseenter", /// 13345 mouseleave = "mouseleave", /// 13346 mousein = "mousein", /// 13347 mouseout = "mouseout", /// 13348 mouseup = "mouseup", /// 13349 mousedown = "mousedown", /// 13350 mousemove = "mousemove", /// 13351 13352 keydown = "keydown", /// 13353 keyup = "keyup", /// 13354 char_ = "char", /// 13355 13356 focus = "focus", /// 13357 blur = "blur", /// 13358 13359 triggered = "triggered", /// 13360 13361 change = "change", /// 13362 } 13363 13364 /++ 13365 Represents an event that is currently being processed. 13366 13367 13368 Minigui's event model is based on the web browser. An event has a name, a target, 13369 and an associated data object. It starts from the window and works its way down through 13370 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 13371 then goes back up again all the way back to the window, triggering bubble phase handlers. At 13372 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 13373 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 13374 was called; that just stops it from calling other handlers in the widget tree, but the default happens 13375 whenever propagation is done, not only if it gets to the end of the chain). 13376 13377 This model has several nice points: 13378 13379 $(LIST 13380 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 13381 with event handlers set, then add/remove children as much as you want without needing 13382 to manage the event handlers on them - the parent alone can manage everything. 13383 13384 * It is easy to create new custom events in your application. 13385 13386 * It is familiar to many web developers. 13387 ) 13388 13389 There's a few downsides though: 13390 13391 $(LIST 13392 * There's not a lot of type safety. 13393 13394 * You don't get a static list of what events a widget can emit. 13395 13396 * Tracing where an event got cancelled along the chain can get difficult; the downside of 13397 the central delegation benefit is it can be lead to debugging of action at a distance. 13398 ) 13399 13400 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 13401 while keeping the benefits - and most compatibility with - the existing model. The main idea is 13402 to simply use a D object type which provides a static interface as well as a built-in event name. 13403 Then, a new static interface allows you to see what an event can emit and attach handlers to it 13404 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 13405 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 13406 to having a little more help from the D compiler and documentation generator. 13407 13408 Your code would change like this: 13409 13410 --- 13411 // old 13412 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 13413 13414 // new 13415 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 13416 --- 13417 13418 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. 13419 13420 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 13421 13422 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 13423 13424 Thus the family of functions are: 13425 13426 [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. 13427 13428 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 13429 13430 [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. 13431 13432 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 13433 13434 --- 13435 class MyCheckbox : Widget { 13436 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 13437 /// It is NOT actually required but should be used whenever possible. 13438 mixin Emits!(ChangeEvent!bool); 13439 13440 this(Widget parent) { 13441 super(parent); 13442 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 13443 } 13444 13445 private bool _checked; 13446 @property bool checked() { return _checked; } 13447 @property void checked(bool set) { 13448 _checked = set; 13449 emit!(ChangeEvent!bool)(&checked); 13450 } 13451 } 13452 --- 13453 13454 ## Creating Your Own Events 13455 13456 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. 13457 13458 --- 13459 class MyEvent : Event { 13460 this(Widget target) { super(EventString, target); } 13461 mixin Register; // adds EventString and other reflection information 13462 } 13463 --- 13464 13465 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 13466 13467 History: 13468 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. 13469 13470 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. 13471 +/ 13472 /+ 13473 13474 ## General Conventions 13475 13476 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. 13477 13478 13479 ## Qt-style signals and slots 13480 13481 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. 13482 13483 The intention is for events to be used when 13484 13485 --- 13486 class Demo : Widget { 13487 this() { 13488 myPropertyChanged = Signal!int(this); 13489 } 13490 @property myProperty(int v) { 13491 myPropertyChanged.emit(v); 13492 } 13493 13494 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 13495 // but it can just genuinely not care about `this` since that's not really passed. 13496 } 13497 13498 class Foo : Widget { 13499 // the slot uda is not necessary, but it helps the script and ui builder find it. 13500 @slot void setValue(int v) { ... } 13501 } 13502 13503 demo.myPropertyChanged.connect(&foo.setValue); 13504 --- 13505 13506 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 13507 13508 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 13509 13510 class StringChangeEvent : ChangeEvent, Signal!string { 13511 mixin SignalImpl 13512 } 13513 13514 +/ 13515 class Event : ReflectableProperties { 13516 /// Creates an event without populating any members and without sending it. See [dispatch] 13517 this(string eventName, Widget emittedBy) { 13518 this.eventName = eventName; 13519 this.srcElement = emittedBy; 13520 } 13521 13522 13523 /// Implementations for the [ReflectableProperties] interface/ 13524 void getPropertiesList(scope void delegate(string name) sink) const {} 13525 /// ditto 13526 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 13527 /// ditto 13528 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 13529 return SetPropertyResult.notPermitted; 13530 } 13531 13532 13533 /+ 13534 /++ 13535 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 13536 13537 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. 13538 +/ 13539 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 13540 if(value.length == 0) { 13541 finalSink(memberName, `""`); 13542 return; 13543 } 13544 13545 char[1024] bufferBacking; 13546 char[] buffer = bufferBacking; 13547 int bufferPosition; 13548 13549 void sink(char ch) { 13550 if(bufferPosition >= buffer.length) 13551 buffer.length = buffer.length + 1024; 13552 buffer[bufferPosition++] = ch; 13553 } 13554 13555 sink('"'); 13556 13557 foreach(ch; value) { 13558 switch(ch) { 13559 case '\\': 13560 sink('\\'); sink('\\'); 13561 break; 13562 case '"': 13563 sink('\\'); sink('"'); 13564 break; 13565 case '\n': 13566 sink('\\'); sink('n'); 13567 break; 13568 case '\r': 13569 sink('\\'); sink('r'); 13570 break; 13571 case '\t': 13572 sink('\\'); sink('t'); 13573 break; 13574 default: 13575 sink(ch); 13576 } 13577 } 13578 13579 sink('"'); 13580 13581 finalSink(memberName, buffer[0 .. bufferPosition]); 13582 } 13583 +/ 13584 13585 /+ 13586 enum EventInitiator { 13587 system, 13588 minigui, 13589 user 13590 } 13591 13592 immutable EventInitiator; initiatedBy; 13593 +/ 13594 13595 /++ 13596 Events should generally follow the propagation model, but there's some exceptions 13597 to that rule. If so, they should override this to return false. In that case, only 13598 bubbling event handlers on the target itself and capturing event handlers on the containing 13599 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 13600 capture -> target -> bubble process.) 13601 13602 History: 13603 Added May 12, 2021 13604 +/ 13605 bool propagates() const pure nothrow @nogc @safe { 13606 return true; 13607 } 13608 13609 /++ 13610 hints as to whether preventDefault will actually do anything. not entirely reliable. 13611 13612 History: 13613 Added May 14, 2021 13614 +/ 13615 bool cancelable() const pure nothrow @nogc @safe { 13616 return true; 13617 } 13618 13619 /++ 13620 You can mix this into child class to register some boilerplate. It includes the `EventString` 13621 member, a constructor, and implementations of the dynamic get data interfaces. 13622 13623 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 13624 13625 13626 You can override the default EventString by simply providing your own in the form of 13627 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 13628 which provides some namespace protection against conflicts in other libraries while still being fairly 13629 easy to use. 13630 13631 If you provide your own constructor, it will override the default constructor provided here. A constructor 13632 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 13633 first argument to your constructor. 13634 13635 History: 13636 Added May 13, 2021. 13637 +/ 13638 protected static mixin template Register() { 13639 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 13640 this(Widget target) { super(EventString, target); } 13641 13642 mixin ReflectableProperties.RegisterGetters; 13643 } 13644 13645 /++ 13646 This is the widget that emitted the event. 13647 13648 13649 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 13650 13651 History: 13652 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 13653 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 13654 so I don't intend to remove these aliases. 13655 +/ 13656 Widget source; 13657 /// ditto 13658 alias source target; 13659 /// ditto 13660 alias source srcElement; 13661 13662 Widget relatedTarget; /// Note: likely to be deprecated at some point. 13663 13664 /// Prevents the default event handler (if there is one) from being called 13665 void preventDefault() { 13666 lastDefaultPrevented = true; 13667 defaultPrevented = true; 13668 } 13669 13670 /// Stops the event propagation immediately. 13671 void stopPropagation() { 13672 propagationStopped = true; 13673 } 13674 13675 private bool defaultPrevented; 13676 private bool propagationStopped; 13677 private string eventName; 13678 13679 private bool isBubbling; 13680 13681 /// 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. 13682 protected void adjustScrolling() { } 13683 /// ditto 13684 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 13685 13686 /++ 13687 this sends it only to the target. If you want propagation, use dispatch() instead. 13688 13689 This should be made private!!! 13690 13691 +/ 13692 void sendDirectly() { 13693 if(srcElement is null) 13694 return; 13695 13696 // 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. 13697 13698 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 13699 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 13700 13701 adjustScrolling(); 13702 13703 if(auto e = target.parentWindow) { 13704 if(auto handlers = "*" in e.capturingEventHandlers) 13705 foreach(handler; *handlers) 13706 if(handler) handler(e, this); 13707 if(auto handlers = eventName in e.capturingEventHandlers) 13708 foreach(handler; *handlers) 13709 if(handler) handler(e, this); 13710 } 13711 13712 auto e = srcElement; 13713 13714 if(auto handlers = eventName in e.bubblingEventHandlers) 13715 foreach(handler; *handlers) 13716 if(handler) handler(e, this); 13717 13718 if(auto handlers = "*" in e.bubblingEventHandlers) 13719 foreach(handler; *handlers) 13720 if(handler) handler(e, this); 13721 13722 // there's never a default for a catch-all event 13723 if(!defaultPrevented) 13724 if(eventName in e.defaultEventHandlers) 13725 e.defaultEventHandlers[eventName](e, this); 13726 } 13727 13728 /// this dispatches the element using the capture -> target -> bubble process 13729 void dispatch() { 13730 if(srcElement is null) 13731 return; 13732 13733 if(!propagates) { 13734 sendDirectly; 13735 return; 13736 } 13737 13738 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 13739 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 13740 13741 adjustScrolling(); 13742 // first capture, then bubble 13743 13744 Widget[] chain; 13745 Widget curr = srcElement; 13746 while(curr) { 13747 auto l = curr; 13748 chain ~= l; 13749 curr = curr.parent; 13750 } 13751 13752 isBubbling = false; 13753 13754 foreach_reverse(e; chain) { 13755 if(auto handlers = "*" in e.capturingEventHandlers) 13756 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13757 13758 if(propagationStopped) 13759 break; 13760 13761 if(auto handlers = eventName in e.capturingEventHandlers) 13762 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13763 13764 // the default on capture should really be to always do nothing 13765 13766 //if(!defaultPrevented) 13767 // if(eventName in e.defaultEventHandlers) 13768 // e.defaultEventHandlers[eventName](e.element, this); 13769 13770 if(propagationStopped) 13771 break; 13772 } 13773 13774 int adjustX; 13775 int adjustY; 13776 13777 isBubbling = true; 13778 if(!propagationStopped) 13779 foreach(e; chain) { 13780 if(auto handlers = eventName in e.bubblingEventHandlers) 13781 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13782 13783 if(propagationStopped) 13784 break; 13785 13786 if(auto handlers = "*" in e.bubblingEventHandlers) 13787 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13788 13789 if(propagationStopped) 13790 break; 13791 13792 if(e.encapsulatedChildren()) { 13793 adjustClientCoordinates(adjustX, adjustY); 13794 target = e; 13795 } else { 13796 adjustX += e.x; 13797 adjustY += e.y; 13798 } 13799 } 13800 13801 if(!defaultPrevented) 13802 foreach(e; chain) { 13803 if(eventName in e.defaultEventHandlers) 13804 e.defaultEventHandlers[eventName](e, this); 13805 } 13806 } 13807 13808 13809 /* old compatibility things */ 13810 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 13811 final @property { 13812 Key key() { return (cast(KeyEventBase) this).key; } 13813 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 13814 13815 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 13816 bool altKey() { return (cast(KeyEventBase) this).altKey; } 13817 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 13818 } 13819 13820 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 13821 final @property { 13822 int clientX() { return (cast(MouseEventBase) this).clientX; } 13823 int clientY() { return (cast(MouseEventBase) this).clientY; } 13824 13825 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 13826 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 13827 13828 int button() { return (cast(MouseEventBase) this).button; } 13829 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 13830 } 13831 13832 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 13833 final @property { 13834 int state() { 13835 if(auto meb = cast(MouseEventBase) this) 13836 return meb.state; 13837 if(auto keb = cast(KeyEventBase) this) 13838 return keb.state; 13839 assert(0); 13840 } 13841 } 13842 13843 deprecated("Use a CharEvent instead of Event in your handler going forward") 13844 final @property { 13845 dchar character() { 13846 if(auto ce = cast(CharEvent) this) 13847 return ce.character; 13848 return dchar.init; 13849 } 13850 } 13851 13852 // for change events 13853 @property { 13854 /// 13855 int intValue() { return 0; } 13856 /// 13857 string stringValue() { return null; } 13858 } 13859 } 13860 13861 /++ 13862 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 13863 13864 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 13865 dynamic and custom events, but the static list helps ensure you get them right. 13866 13867 If this is declared, you can use [Widget.emit] to send the event. 13868 13869 All events work the same way though, following the capture->widget->bubble model described under [Event]. 13870 13871 History: 13872 Added May 4, 2021 13873 +/ 13874 mixin template Emits(EventType) { 13875 import arsd.minigui : EventString; 13876 static if(is(EventType : Event) && !is(EventType == Event)) 13877 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 13878 else 13879 static assert(0, "You can only emit subclasses of Event"); 13880 } 13881 13882 /// ditto 13883 mixin template Emits(string eventString) { 13884 mixin("private Event[0] emits_" ~ eventString ~";"); 13885 } 13886 13887 /* 13888 class SignalEvent(string name) : Event { 13889 13890 } 13891 */ 13892 13893 /++ 13894 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". 13895 13896 13897 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. 13898 13899 History: 13900 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 13901 +/ 13902 class CommandEvent : Event { 13903 enum EventString = "command"; 13904 this(Widget source, string CommandString = EventString) { 13905 super(CommandString, source); 13906 } 13907 } 13908 13909 /++ 13910 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 13911 +/ 13912 class CommandEventWithArgs(Args...) : CommandEvent { 13913 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 13914 Args args; 13915 } 13916 13917 /++ 13918 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. 13919 13920 See [CommandEvent] for more information. 13921 13922 Returns: 13923 The [EventListener] you can use to remove the handler. 13924 +/ 13925 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 13926 return w.addEventListener(CommandString, (Event ev) { 13927 if(ev.target is w) 13928 return; // it does not consume its own commands! 13929 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 13930 handler(cev.args); 13931 ev.stopPropagation(); 13932 } 13933 }); 13934 } 13935 13936 /++ 13937 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. 13938 +/ 13939 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 13940 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 13941 event.dispatch(); 13942 } 13943 13944 class ResizeEvent : Event { 13945 enum EventString = "resize"; 13946 13947 this(Widget target) { super(EventString, target); } 13948 13949 override bool propagates() const { return false; } 13950 } 13951 13952 /++ 13953 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 13954 13955 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. 13956 13957 History: 13958 Added June 21, 2021 (dub v10.1) 13959 +/ 13960 class ClosingEvent : Event { 13961 enum EventString = "closing"; 13962 13963 this(Widget target) { super(EventString, target); } 13964 13965 override bool propagates() const { return false; } 13966 override bool cancelable() const { return true; } 13967 } 13968 13969 /// ditto 13970 class ClosedEvent : Event { 13971 enum EventString = "closed"; 13972 13973 this(Widget target) { super(EventString, target); } 13974 13975 override bool propagates() const { return false; } 13976 override bool cancelable() const { return false; } 13977 } 13978 13979 /// 13980 class BlurEvent : Event { 13981 enum EventString = "blur"; 13982 13983 // FIXME: related target? 13984 this(Widget target) { super(EventString, target); } 13985 13986 override bool propagates() const { return false; } 13987 } 13988 13989 /// 13990 class FocusEvent : Event { 13991 enum EventString = "focus"; 13992 13993 // FIXME: related target? 13994 this(Widget target) { super(EventString, target); } 13995 13996 override bool propagates() const { return false; } 13997 } 13998 13999 /++ 14000 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 14001 14002 History: 14003 Added July 3, 2021 14004 +/ 14005 class FocusInEvent : Event { 14006 enum EventString = "focusin"; 14007 14008 // FIXME: related target? 14009 this(Widget target) { super(EventString, target); } 14010 14011 override bool cancelable() const { return false; } 14012 } 14013 14014 /// ditto 14015 class FocusOutEvent : Event { 14016 enum EventString = "focusout"; 14017 14018 // FIXME: related target? 14019 this(Widget target) { super(EventString, target); } 14020 14021 override bool cancelable() const { return false; } 14022 } 14023 14024 /// 14025 class ScrollEvent : Event { 14026 enum EventString = "scroll"; 14027 this(Widget target) { super(EventString, target); } 14028 14029 override bool cancelable() const { return false; } 14030 } 14031 14032 /++ 14033 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 14034 14035 History: 14036 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 14037 +/ 14038 class CharEvent : Event { 14039 enum EventString = "char"; 14040 this(Widget target, dchar ch) { 14041 character = ch; 14042 super(EventString, target); 14043 } 14044 14045 immutable dchar character; 14046 } 14047 14048 /++ 14049 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 14050 +/ 14051 abstract class ChangeEventBase : Event { 14052 enum EventString = "change"; 14053 this(Widget target) { 14054 super(EventString, target); 14055 } 14056 14057 /+ 14058 // idk where or how exactly i want to do this. 14059 // i might come back to it later. 14060 14061 // If a widget itself broadcasts one of theses itself, it stops propagation going down 14062 // this way the source doesn't get too confused (think of a nested scroll widget) 14063 // 14064 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 14065 // then you consume that command and change you scroll x position to whatever. then you do 14066 // some kind of change event that is broadcast back to the children and any horizontal scroll 14067 // listeners are now able to update, without having an explicit connection between them. 14068 void broadcastToChildren(string fieldName) { 14069 14070 } 14071 +/ 14072 } 14073 14074 /++ 14075 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. 14076 14077 14078 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). 14079 14080 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);` 14081 14082 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 14083 14084 History: 14085 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. 14086 +/ 14087 class ChangeEvent(T) : ChangeEventBase { 14088 this(Widget target, T delegate() getNewValue) { 14089 assert(getNewValue !is null); 14090 this.getNewValue = getNewValue; 14091 super(target); 14092 } 14093 14094 private T delegate() getNewValue; 14095 14096 /++ 14097 Gets the new value that just changed. 14098 +/ 14099 @property T value() { 14100 return getNewValue(); 14101 } 14102 14103 /// compatibility method for old generic Events 14104 static if(is(immutable T == immutable int)) 14105 override int intValue() { return value; } 14106 /// ditto 14107 static if(is(immutable T == immutable string)) 14108 override string stringValue() { return value; } 14109 } 14110 14111 /++ 14112 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 14113 14114 14115 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14116 14117 History: 14118 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14119 +/ 14120 abstract class KeyEventBase : Event { 14121 this(string name, Widget target) { 14122 super(name, target); 14123 } 14124 14125 // for key events 14126 Key key; /// 14127 14128 KeyEvent originalKeyEvent; 14129 14130 /++ 14131 Indicates the current state of the given keyboard modifier keys. 14132 14133 History: 14134 Added to events on April 15, 2020. 14135 +/ 14136 bool ctrlKey; 14137 14138 /// ditto 14139 bool altKey; 14140 14141 /// ditto 14142 bool shiftKey; 14143 14144 /++ 14145 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 14146 14147 See [arsd.simpledisplay.ModifierState] for other possible flags. 14148 +/ 14149 int state; 14150 14151 mixin Register; 14152 } 14153 14154 /++ 14155 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]. 14156 14157 14158 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14159 14160 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. 14161 14162 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. 14163 14164 See_Also: [KeyUpEvent], [CharEvent] 14165 14166 History: 14167 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 14168 +/ 14169 class KeyDownEvent : KeyEventBase { 14170 enum EventString = "keydown"; 14171 this(Widget target) { super(EventString, target); } 14172 } 14173 14174 /++ 14175 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 14176 14177 14178 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14179 14180 See_Also: [KeyDownEvent], [CharEvent] 14181 14182 History: 14183 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 14184 +/ 14185 class KeyUpEvent : KeyEventBase { 14186 enum EventString = "keyup"; 14187 this(Widget target) { super(EventString, target); } 14188 } 14189 14190 /++ 14191 Contains shared properties for various mouse events; 14192 14193 14194 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14195 14196 History: 14197 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14198 +/ 14199 abstract class MouseEventBase : Event { 14200 this(string name, Widget target) { 14201 super(name, target); 14202 } 14203 14204 // for mouse events 14205 int clientX; /// The mouse event location relative to the target widget 14206 int clientY; /// ditto 14207 14208 int viewportX; /// The mouse event location relative to the window origin 14209 int viewportY; /// ditto 14210 14211 int button; /// See: [MouseEvent.button] 14212 int buttonLinear; /// See: [MouseEvent.buttonLinear] 14213 14214 /++ 14215 Indicates the current state of the given keyboard modifier keys. 14216 14217 History: 14218 Added to mouse events on September 28, 2010. 14219 +/ 14220 bool ctrlKey; 14221 14222 /// ditto 14223 bool altKey; 14224 14225 /// ditto 14226 bool shiftKey; 14227 14228 14229 14230 int state; /// 14231 14232 /++ 14233 for consistent names with key event. 14234 14235 History: 14236 Added September 28, 2021 (dub v10.3) 14237 +/ 14238 alias modifierState = state; 14239 14240 /++ 14241 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 14242 14243 History: 14244 Added May 15, 2021 14245 +/ 14246 bool isMouseWheel() { 14247 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 14248 } 14249 14250 // private 14251 override void adjustClientCoordinates(int deltaX, int deltaY) { 14252 clientX += deltaX; 14253 clientY += deltaY; 14254 } 14255 14256 override void adjustScrolling() { 14257 version(custom_widgets) { // TEMP 14258 viewportX = clientX; 14259 viewportY = clientY; 14260 if(auto se = cast(ScrollableWidget) srcElement) { 14261 clientX += se.scrollOrigin.x; 14262 clientY += se.scrollOrigin.y; 14263 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 14264 //clientX += se.scrollX_; 14265 //clientY += se.scrollY_; 14266 } 14267 } 14268 } 14269 14270 mixin Register; 14271 } 14272 14273 /++ 14274 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 14275 14276 14277 $(WARNING 14278 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 14279 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 14280 behavior. 14281 ) 14282 14283 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 14284 14285 [MouseUpEvent] is sent when the user releases a mouse button. 14286 14287 [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.) 14288 14289 [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. 14290 14291 [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. 14292 14293 [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. 14294 14295 [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. 14296 14297 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 14298 14299 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 14300 14301 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14302 14303 Rationale: 14304 14305 If you only want to do drag, mousedown/up works just fine being consistently sent. 14306 14307 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). 14308 14309 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. 14310 14311 History: 14312 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. 14313 +/ 14314 class MouseUpEvent : MouseEventBase { 14315 enum EventString = "mouseup"; /// 14316 this(Widget target) { super(EventString, target); } 14317 } 14318 /// ditto 14319 class MouseDownEvent : MouseEventBase { 14320 enum EventString = "mousedown"; /// 14321 this(Widget target) { super(EventString, target); } 14322 } 14323 /// ditto 14324 class MouseMoveEvent : MouseEventBase { 14325 enum EventString = "mousemove"; /// 14326 this(Widget target) { super(EventString, target); } 14327 } 14328 /// ditto 14329 class ClickEvent : MouseEventBase { 14330 enum EventString = "click"; /// 14331 this(Widget target) { super(EventString, target); } 14332 } 14333 /// ditto 14334 class DoubleClickEvent : MouseEventBase { 14335 enum EventString = "dblclick"; /// 14336 this(Widget target) { super(EventString, target); } 14337 } 14338 /// ditto 14339 class MouseOverEvent : Event { 14340 enum EventString = "mouseover"; /// 14341 this(Widget target) { super(EventString, target); } 14342 } 14343 /// ditto 14344 class MouseOutEvent : Event { 14345 enum EventString = "mouseout"; /// 14346 this(Widget target) { super(EventString, target); } 14347 } 14348 /// ditto 14349 class MouseEnterEvent : Event { 14350 enum EventString = "mouseenter"; /// 14351 this(Widget target) { super(EventString, target); } 14352 14353 override bool propagates() const { return false; } 14354 } 14355 /// ditto 14356 class MouseLeaveEvent : Event { 14357 enum EventString = "mouseleave"; /// 14358 this(Widget target) { super(EventString, target); } 14359 14360 override bool propagates() const { return false; } 14361 } 14362 14363 private bool isAParentOf(Widget a, Widget b) { 14364 if(a is null || b is null) 14365 return false; 14366 14367 while(b !is null) { 14368 if(a is b) 14369 return true; 14370 b = b.parent; 14371 } 14372 14373 return false; 14374 } 14375 14376 private struct WidgetAtPointResponse { 14377 Widget widget; 14378 14379 // x, y relative to the widget in the response. 14380 int x; 14381 int y; 14382 } 14383 14384 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 14385 assert(starting !is null); 14386 14387 starting.addScrollPosition(x, y); 14388 14389 auto child = starting.getChildAtPosition(x, y); 14390 while(child) { 14391 if(child.hidden) 14392 continue; 14393 starting = child; 14394 x -= child.x; 14395 y -= child.y; 14396 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 14397 child = r.widget; 14398 if(child is starting) 14399 break; 14400 } 14401 return WidgetAtPointResponse(starting, x, y); 14402 } 14403 14404 version(win32_widgets) { 14405 private: 14406 import core.sys.windows.commctrl; 14407 14408 pragma(lib, "comctl32"); 14409 shared static this() { 14410 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 14411 INITCOMMONCONTROLSEX ic; 14412 ic.dwSize = cast(DWORD) ic.sizeof; 14413 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 14414 if(!InitCommonControlsEx(&ic)) { 14415 //writeln("ICC failed"); 14416 } 14417 } 14418 14419 14420 // everything from here is just win32 headers copy pasta 14421 private: 14422 extern(Windows): 14423 14424 alias HANDLE HMENU; 14425 HMENU CreateMenu(); 14426 bool SetMenu(HWND, HMENU); 14427 HMENU CreatePopupMenu(); 14428 enum MF_POPUP = 0x10; 14429 enum MF_STRING = 0; 14430 14431 14432 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 14433 struct INITCOMMONCONTROLSEX { 14434 DWORD dwSize; 14435 DWORD dwICC; 14436 } 14437 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 14438 enum { 14439 IDB_STD_SMALL_COLOR, 14440 IDB_STD_LARGE_COLOR, 14441 IDB_VIEW_SMALL_COLOR = 4, 14442 IDB_VIEW_LARGE_COLOR = 5 14443 } 14444 enum { 14445 STD_CUT, 14446 STD_COPY, 14447 STD_PASTE, 14448 STD_UNDO, 14449 STD_REDOW, 14450 STD_DELETE, 14451 STD_FILENEW, 14452 STD_FILEOPEN, 14453 STD_FILESAVE, 14454 STD_PRINTPRE, 14455 STD_PROPERTIES, 14456 STD_HELP, 14457 STD_FIND, 14458 STD_REPLACE, 14459 STD_PRINT // = 14 14460 } 14461 14462 alias HANDLE HIMAGELIST; 14463 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 14464 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 14465 BOOL ImageList_Destroy(HIMAGELIST); 14466 14467 uint MAKELONG(ushort a, ushort b) { 14468 return cast(uint) ((b << 16) | a); 14469 } 14470 14471 14472 struct TBBUTTON { 14473 int iBitmap; 14474 int idCommand; 14475 BYTE fsState; 14476 BYTE fsStyle; 14477 version(Win64) 14478 BYTE[6] bReserved; 14479 else 14480 BYTE[2] bReserved; 14481 DWORD dwData; 14482 INT_PTR iString; 14483 } 14484 14485 enum { 14486 TB_ADDBUTTONSA = WM_USER + 20, 14487 TB_INSERTBUTTONA = WM_USER + 21, 14488 TB_GETIDEALSIZE = WM_USER + 99, 14489 } 14490 14491 struct SIZE { 14492 LONG cx; 14493 LONG cy; 14494 } 14495 14496 14497 enum { 14498 TBSTATE_CHECKED = 1, 14499 TBSTATE_PRESSED = 2, 14500 TBSTATE_ENABLED = 4, 14501 TBSTATE_HIDDEN = 8, 14502 TBSTATE_INDETERMINATE = 16, 14503 TBSTATE_WRAP = 32 14504 } 14505 14506 14507 14508 enum { 14509 ILC_COLOR = 0, 14510 ILC_COLOR4 = 4, 14511 ILC_COLOR8 = 8, 14512 ILC_COLOR16 = 16, 14513 ILC_COLOR24 = 24, 14514 ILC_COLOR32 = 32, 14515 ILC_COLORDDB = 254, 14516 ILC_MASK = 1, 14517 ILC_PALETTE = 2048 14518 } 14519 14520 14521 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 14522 14523 14524 enum { 14525 TB_ENABLEBUTTON = WM_USER + 1, 14526 TB_CHECKBUTTON, 14527 TB_PRESSBUTTON, 14528 TB_HIDEBUTTON, 14529 TB_INDETERMINATE, // = WM_USER + 5, 14530 TB_ISBUTTONENABLED = WM_USER + 9, 14531 TB_ISBUTTONCHECKED, 14532 TB_ISBUTTONPRESSED, 14533 TB_ISBUTTONHIDDEN, 14534 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 14535 TB_SETSTATE = WM_USER + 17, 14536 TB_GETSTATE = WM_USER + 18, 14537 TB_ADDBITMAP = WM_USER + 19, 14538 TB_DELETEBUTTON = WM_USER + 22, 14539 TB_GETBUTTON, 14540 TB_BUTTONCOUNT, 14541 TB_COMMANDTOINDEX, 14542 TB_SAVERESTOREA, 14543 TB_CUSTOMIZE, 14544 TB_ADDSTRINGA, 14545 TB_GETITEMRECT, 14546 TB_BUTTONSTRUCTSIZE, 14547 TB_SETBUTTONSIZE, 14548 TB_SETBITMAPSIZE, 14549 TB_AUTOSIZE, // = WM_USER + 33, 14550 TB_GETTOOLTIPS = WM_USER + 35, 14551 TB_SETTOOLTIPS = WM_USER + 36, 14552 TB_SETPARENT = WM_USER + 37, 14553 TB_SETROWS = WM_USER + 39, 14554 TB_GETROWS, 14555 TB_GETBITMAPFLAGS, 14556 TB_SETCMDID, 14557 TB_CHANGEBITMAP, 14558 TB_GETBITMAP, 14559 TB_GETBUTTONTEXTA, 14560 TB_REPLACEBITMAP, // = WM_USER + 46, 14561 TB_GETBUTTONSIZE = WM_USER + 58, 14562 TB_SETBUTTONWIDTH = WM_USER + 59, 14563 TB_GETBUTTONTEXTW = WM_USER + 75, 14564 TB_SAVERESTOREW = WM_USER + 76, 14565 TB_ADDSTRINGW = WM_USER + 77, 14566 } 14567 14568 extern(Windows) 14569 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 14570 14571 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 14572 14573 14574 enum { 14575 TB_SETINDENT = WM_USER + 47, 14576 TB_SETIMAGELIST, 14577 TB_GETIMAGELIST, 14578 TB_LOADIMAGES, 14579 TB_GETRECT, 14580 TB_SETHOTIMAGELIST, 14581 TB_GETHOTIMAGELIST, 14582 TB_SETDISABLEDIMAGELIST, 14583 TB_GETDISABLEDIMAGELIST, 14584 TB_SETSTYLE, 14585 TB_GETSTYLE, 14586 //TB_GETBUTTONSIZE, 14587 //TB_SETBUTTONWIDTH, 14588 TB_SETMAXTEXTROWS, 14589 TB_GETTEXTROWS // = WM_USER + 61 14590 } 14591 14592 enum { 14593 CCM_FIRST = 0x2000, 14594 CCM_LAST = CCM_FIRST + 0x200, 14595 CCM_SETBKCOLOR = 8193, 14596 CCM_SETCOLORSCHEME = 8194, 14597 CCM_GETCOLORSCHEME = 8195, 14598 CCM_GETDROPTARGET = 8196, 14599 CCM_SETUNICODEFORMAT = 8197, 14600 CCM_GETUNICODEFORMAT = 8198, 14601 CCM_SETVERSION = 0x2007, 14602 CCM_GETVERSION = 0x2008, 14603 CCM_SETNOTIFYWINDOW = 0x2009 14604 } 14605 14606 14607 enum { 14608 PBM_SETRANGE = WM_USER + 1, 14609 PBM_SETPOS, 14610 PBM_DELTAPOS, 14611 PBM_SETSTEP, 14612 PBM_STEPIT, // = WM_USER + 5 14613 PBM_SETRANGE32 = 1030, 14614 PBM_GETRANGE, 14615 PBM_GETPOS, 14616 PBM_SETBARCOLOR, // = 1033 14617 PBM_SETBKCOLOR = CCM_SETBKCOLOR 14618 } 14619 14620 enum { 14621 PBS_SMOOTH = 1, 14622 PBS_VERTICAL = 4 14623 } 14624 14625 enum { 14626 ICC_LISTVIEW_CLASSES = 1, 14627 ICC_TREEVIEW_CLASSES = 2, 14628 ICC_BAR_CLASSES = 4, 14629 ICC_TAB_CLASSES = 8, 14630 ICC_UPDOWN_CLASS = 16, 14631 ICC_PROGRESS_CLASS = 32, 14632 ICC_HOTKEY_CLASS = 64, 14633 ICC_ANIMATE_CLASS = 128, 14634 ICC_WIN95_CLASSES = 255, 14635 ICC_DATE_CLASSES = 256, 14636 ICC_USEREX_CLASSES = 512, 14637 ICC_COOL_CLASSES = 1024, 14638 ICC_STANDARD_CLASSES = 0x00004000, 14639 } 14640 14641 enum WM_USER = 1024; 14642 } 14643 14644 version(win32_widgets) 14645 pragma(lib, "comdlg32"); 14646 14647 14648 /// 14649 enum GenericIcons : ushort { 14650 None, /// 14651 // these happen to match the win32 std icons numerically if you just subtract one from the value 14652 Cut, /// 14653 Copy, /// 14654 Paste, /// 14655 Undo, /// 14656 Redo, /// 14657 Delete, /// 14658 New, /// 14659 Open, /// 14660 Save, /// 14661 PrintPreview, /// 14662 Properties, /// 14663 Help, /// 14664 Find, /// 14665 Replace, /// 14666 Print, /// 14667 } 14668 14669 enum FileDialogType { 14670 Automatic, 14671 Open, 14672 Save 14673 } 14674 string previousFileReferenced; 14675 14676 /++ 14677 Used in automatic menu functions to indicate that the user should be able to browse for a file. 14678 14679 Params: 14680 storage = an alias to a `static string` variable that stores the last file referenced. It will 14681 use this to pre-fill the dialog with a suggestion. 14682 14683 Please note that it MUST be `static` or you will get compile errors. 14684 14685 filters = the filters param to [getFileName] 14686 14687 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 14688 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 14689 a save dialog box. Otherwise, it will show an open dialog box. 14690 +/ 14691 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 14692 string name; 14693 alias name this; 14694 } 14695 14696 /++ 14697 Gets a file name for an open or save operation, calling your `onOK` function when the user has selected one. This function may or may not block depending on the operating system, you MUST assume it will complete asynchronously. 14698 14699 History: 14700 onCancel was added November 6, 2021. 14701 14702 The dialog itself on Linux was modified on December 2, 2021 to include 14703 a directory picker in addition to the command line completion view. 14704 14705 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 14706 Future_directions: 14707 I want to add some kind of custom preview and maybe thumbnail thing in the future, 14708 at least on Linux, maybe on Windows too. 14709 +/ 14710 void getOpenFileName( 14711 void delegate(string) onOK, 14712 string prefilledName = null, 14713 string[] filters = null, 14714 void delegate() onCancel = null, 14715 string initialDirectory = null, 14716 ) 14717 { 14718 return getFileName(true, onOK, prefilledName, filters, onCancel, initialDirectory); 14719 } 14720 14721 /// ditto 14722 void getSaveFileName( 14723 void delegate(string) onOK, 14724 string prefilledName = null, 14725 string[] filters = null, 14726 void delegate() onCancel = null, 14727 string initialDirectory = null, 14728 ) 14729 { 14730 return getFileName(false, onOK, prefilledName, filters, onCancel, initialDirectory); 14731 } 14732 14733 void getFileName( 14734 bool openOrSave, 14735 void delegate(string) onOK, 14736 string prefilledName = null, 14737 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 14738 void delegate() onCancel = null, 14739 string initialDirectory = null, 14740 ) 14741 { 14742 14743 version(win32_widgets) { 14744 import core.sys.windows.commdlg; 14745 /* 14746 Ofn.lStructSize = sizeof(OPENFILENAME); 14747 Ofn.hwndOwner = hWnd; 14748 Ofn.lpstrFilter = szFilter; 14749 Ofn.lpstrFile= szFile; 14750 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 14751 Ofn.lpstrFileTitle = szFileTitle; 14752 Ofn.nMaxFileTitle = sizeof(szFileTitle); 14753 Ofn.lpstrInitialDir = (LPSTR)NULL; 14754 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 14755 Ofn.lpstrTitle = szTitle; 14756 */ 14757 14758 14759 wchar[1024] file = 0; 14760 wchar[1024] filterBuffer = 0; 14761 makeWindowsString(prefilledName, file[]); 14762 OPENFILENAME ofn; 14763 ofn.lStructSize = ofn.sizeof; 14764 if(filters.length) { 14765 string filter; 14766 foreach(i, f; filters) { 14767 filter ~= f; 14768 filter ~= "\0"; 14769 } 14770 filter ~= "\0"; 14771 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 14772 } 14773 ofn.lpstrFile = file.ptr; 14774 ofn.nMaxFile = file.length; 14775 14776 wchar[1024] initialDir = 0; 14777 if(initialDirectory !is null) { 14778 makeWindowsString(initialDirectory, initialDir[]); 14779 ofn.lpstrInitialDir = file.ptr; 14780 } 14781 14782 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 14783 { 14784 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 14785 if(okString.length && okString[$-1] == '\0') 14786 okString = okString[0..$-1]; 14787 onOK(okString); 14788 } else { 14789 if(onCancel) 14790 onCancel(); 14791 } 14792 } else version(custom_widgets) { 14793 if(filters.length == 0) 14794 filters = ["All Files\0*.*"]; 14795 auto picker = new FilePicker(prefilledName, filters, initialDirectory); 14796 picker.onOK = onOK; 14797 picker.onCancel = onCancel; 14798 picker.show(); 14799 } 14800 } 14801 14802 version(custom_widgets) 14803 private 14804 class FilePicker : Dialog { 14805 void delegate(string) onOK; 14806 void delegate() onCancel; 14807 LineEdit lineEdit; 14808 14809 // returns common prefix 14810 string loadFiles(string cwd, string[] filters...) { 14811 string[] files; 14812 string[] dirs; 14813 14814 string commonPrefix; 14815 14816 getFiles(cwd, (string name, bool isDirectory) { 14817 if(name == ".") 14818 return; // skip this as unnecessary 14819 if(isDirectory) 14820 dirs ~= name; 14821 else { 14822 foreach(filter; filters) 14823 if( 14824 filter.length <= 1 || 14825 filter == "*.*" || 14826 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 14827 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 14828 ) 14829 { 14830 files ~= name; 14831 14832 if(filter.length > 0 && filter[$-1] == '*') { 14833 if(commonPrefix is null) { 14834 commonPrefix = name; 14835 } else { 14836 foreach(idx, char i; name) { 14837 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 14838 commonPrefix = commonPrefix[0 .. idx]; 14839 break; 14840 } 14841 } 14842 } 14843 } 14844 14845 break; 14846 } 14847 } 14848 }); 14849 14850 extern(C) static int comparator(scope const void* a, scope const void* b) { 14851 auto sa = *cast(string*) a; 14852 auto sb = *cast(string*) b; 14853 14854 for(int i = 0; i < sa.length; i++) { 14855 if(i == sb.length) 14856 return 1; 14857 return sa[i] - sb[i]; 14858 } 14859 14860 return 0; 14861 } 14862 14863 nonPhobosSort(files, &comparator); 14864 nonPhobosSort(dirs, &comparator); 14865 14866 listWidget.clear(); 14867 dirWidget.clear(); 14868 foreach(name; dirs) 14869 dirWidget.addOption(name); 14870 foreach(name; files) 14871 listWidget.addOption(name); 14872 14873 return commonPrefix; 14874 } 14875 14876 ListWidget listWidget; 14877 ListWidget dirWidget; 14878 14879 string currentDirectory; 14880 string[] processedFilters; 14881 14882 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"] 14883 this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) { 14884 super(300, 200, "Choose File..."); // owner); 14885 14886 foreach(filter; filters) { 14887 while(filter.length && filter[0] != 0) { 14888 filter = filter[1 .. $]; 14889 } 14890 if(filter.length) 14891 filter = filter[1 .. $]; // trim off the 0 14892 14893 while(filter.length) { 14894 int idx = 0; 14895 while(idx < filter.length && filter[idx] != ';') { 14896 idx++; 14897 } 14898 14899 processedFilters ~= filter[0 .. idx]; 14900 if(idx < filter.length) 14901 idx++; // skip the ; 14902 filter = filter[idx .. $]; 14903 } 14904 } 14905 14906 currentDirectory = initialDirectory is null ? "." : initialDirectory; 14907 14908 { 14909 auto hl = new HorizontalLayout(this); 14910 dirWidget = new ListWidget(hl); 14911 listWidget = new ListWidget(hl); 14912 14913 // double click events normally trigger something else but 14914 // here user might be clicking kinda fast and we'd rather just 14915 // keep it 14916 dirWidget.addEventListener((scope DoubleClickEvent dev) { 14917 auto ce = new ChangeEvent!void(dirWidget, () {}); 14918 ce.dispatch(); 14919 }); 14920 14921 dirWidget.addEventListener((scope ChangeEvent!void sce) { 14922 string v; 14923 foreach(o; dirWidget.options) 14924 if(o.selected) { 14925 v = o.label; 14926 break; 14927 } 14928 if(v.length) { 14929 currentDirectory ~= "/" ~ v; 14930 loadFiles(currentDirectory, processedFilters); 14931 } 14932 }); 14933 14934 // double click here, on the other hand, selects the file 14935 // and moves on 14936 listWidget.addEventListener((scope DoubleClickEvent dev) { 14937 OK(); 14938 }); 14939 } 14940 14941 lineEdit = new LineEdit(this); 14942 lineEdit.focus(); 14943 lineEdit.addEventListener(delegate(CharEvent event) { 14944 if(event.character == '\t' || event.character == '\n') 14945 event.preventDefault(); 14946 }); 14947 14948 listWidget.addEventListener(EventType.change, () { 14949 foreach(o; listWidget.options) 14950 if(o.selected) 14951 lineEdit.content = o.label; 14952 }); 14953 14954 loadFiles(currentDirectory, processedFilters); 14955 14956 lineEdit.addEventListener((KeyDownEvent event) { 14957 if(event.key == Key.Tab) { 14958 14959 auto current = lineEdit.content; 14960 if(current.length >= 2 && current[0 ..2] == "./") 14961 current = current[2 .. $]; 14962 14963 auto commonPrefix = loadFiles(".", current ~ "*"); 14964 14965 if(commonPrefix.length) 14966 lineEdit.content = commonPrefix; 14967 14968 // FIXME: if that is a directory, add the slash? or even go inside? 14969 14970 event.preventDefault(); 14971 } 14972 }); 14973 14974 lineEdit.content = prefilledName; 14975 14976 auto hl = new HorizontalLayout(60, this); 14977 auto cancelButton = new Button("Cancel", hl); 14978 auto okButton = new Button("OK", hl); 14979 14980 cancelButton.addEventListener(EventType.triggered, &Cancel); 14981 okButton.addEventListener(EventType.triggered, &OK); 14982 14983 this.addEventListener((KeyDownEvent event) { 14984 if(event.key == Key.Enter || event.key == Key.PadEnter) { 14985 event.preventDefault(); 14986 OK(); 14987 } 14988 if(event.key == Key.Escape) 14989 Cancel(); 14990 }); 14991 14992 } 14993 14994 override void OK() { 14995 if(lineEdit.content.length) { 14996 string accepted; 14997 auto c = lineEdit.content; 14998 if(c.length && c[0] == '/') 14999 accepted = c; 15000 else 15001 accepted = currentDirectory ~ "/" ~ lineEdit.content; 15002 15003 if(isDir(accepted)) { 15004 // FIXME: would be kinda nice to support ~ and collapse these paths too 15005 // FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later. 15006 currentDirectory = accepted; 15007 loadFiles(currentDirectory, processedFilters); 15008 lineEdit.content = ""; 15009 return; 15010 } 15011 15012 if(onOK) 15013 onOK(accepted); 15014 } 15015 close(); 15016 } 15017 15018 override void Cancel() { 15019 if(onCancel) 15020 onCancel(); 15021 close(); 15022 } 15023 } 15024 15025 private bool isDir(string name) { 15026 version(Windows) { 15027 auto ws = WCharzBuffer(name); 15028 auto ret = GetFileAttributesW(ws.ptr); 15029 if(ret == INVALID_FILE_ATTRIBUTES) 15030 return false; 15031 return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0; 15032 } else version(Posix) { 15033 import core.sys.posix.sys.stat; 15034 stat_t buf; 15035 auto ret = stat((name ~ '\0').ptr, &buf); 15036 if(ret == -1) 15037 return false; // I could probably check more specific errors tbh 15038 return (buf.st_mode & S_IFMT) == S_IFDIR; 15039 } else return false; 15040 } 15041 15042 /* 15043 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 15044 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 15045 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 15046 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 15047 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 15048 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 15049 http://www.sbin.org/doc/Xlib/chapt_03.html 15050 15051 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 15052 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 15053 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 15054 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 15055 */ 15056 15057 15058 // These are all for setMenuAndToolbarFromAnnotatedCode 15059 /// This item in the menu will be preceded by a separator line 15060 /// Group: generating_from_code 15061 struct separator {} 15062 deprecated("It was misspelled, use separator instead") alias seperator = separator; 15063 /// Program-wide keyboard shortcut to trigger the action 15064 /// Group: generating_from_code 15065 struct accelerator { string keyString; } 15066 /// tells which menu the action will be on 15067 /// Group: generating_from_code 15068 struct menu { string name; } 15069 /// Describes which toolbar section the action appears on 15070 /// Group: generating_from_code 15071 struct toolbar { string groupName; } 15072 /// 15073 /// Group: generating_from_code 15074 struct icon { ushort id; } 15075 /// 15076 /// Group: generating_from_code 15077 struct label { string label; } 15078 /// 15079 /// Group: generating_from_code 15080 struct hotkey { dchar ch; } 15081 /// 15082 /// Group: generating_from_code 15083 struct tip { string tip; } 15084 15085 15086 /++ 15087 Observes and allows inspection of an object via automatic gui 15088 +/ 15089 /// Group: generating_from_code 15090 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 15091 return new ObjectInspectionWindowImpl!(T)(t); 15092 } 15093 15094 class ObjectInspectionWindow : Window { 15095 this(int a, int b, string c) { 15096 super(a, b, c); 15097 } 15098 15099 abstract void readUpdatesFromObject(); 15100 } 15101 15102 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 15103 T t; 15104 this(T t) { 15105 this.t = t; 15106 15107 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 15108 15109 foreach(memberName; __traits(derivedMembers, T)) {{ 15110 alias member = I!(__traits(getMember, t, memberName))[0]; 15111 alias type = typeof(member); 15112 static if(is(type == int)) { 15113 auto le = new LabeledLineEdit(memberName ~ ": ", this); 15114 //le.addEventListener("char", (Event ev) { 15115 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 15116 //ev.preventDefault(); 15117 //}); 15118 le.addEventListener(EventType.change, (Event ev) { 15119 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 15120 }); 15121 15122 updateMemberDelegates[memberName] = () { 15123 le.content = toInternal!string(__traits(getMember, t, memberName)); 15124 }; 15125 } 15126 }} 15127 } 15128 15129 void delegate()[string] updateMemberDelegates; 15130 15131 override void readUpdatesFromObject() { 15132 foreach(k, v; updateMemberDelegates) 15133 v(); 15134 } 15135 } 15136 15137 /++ 15138 Creates a dialog based on a data structure. 15139 15140 --- 15141 dialog((YourStructure value) { 15142 // the user filled in the struct and clicked OK, 15143 // you can check the members now 15144 }); 15145 --- 15146 15147 Params: 15148 initialData = the initial value to show in the dialog. It will not modify this unless 15149 it is a class then it might, no promises. 15150 15151 History: 15152 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 15153 +/ 15154 /// Group: generating_from_code 15155 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15156 dialog(T.init, onOK, onCancel, title); 15157 } 15158 /// ditto 15159 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15160 auto dg = new AutomaticDialog!T(initialData, onOK, onCancel, title); 15161 dg.show(); 15162 } 15163 15164 private static template I(T...) { alias I = T; } 15165 15166 15167 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 15168 if(name == "id") 15169 return allLowerCase ? name : "ID"; 15170 15171 char[160] buffer; 15172 int bufferIndex = 0; 15173 bool shouldCap = true; 15174 bool shouldSpace; 15175 bool lastWasCap; 15176 foreach(idx, char ch; name) { 15177 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15178 15179 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 15180 if(lastWasCap) { 15181 // two caps in a row, don't change. Prolly acronym. 15182 } else { 15183 if(idx) 15184 shouldSpace = true; // new word, add space 15185 } 15186 15187 lastWasCap = true; 15188 } else { 15189 lastWasCap = false; 15190 } 15191 15192 if(shouldSpace) { 15193 buffer[bufferIndex++] = space; 15194 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15195 shouldSpace = false; 15196 } 15197 if(shouldCap) { 15198 if(ch >= 'a' && ch <= 'z') 15199 ch -= 32; 15200 shouldCap = false; 15201 } 15202 if(allLowerCase && ch >= 'A' && ch <= 'Z') 15203 ch += 32; 15204 buffer[bufferIndex++] = ch; 15205 } 15206 return buffer[0 .. bufferIndex].idup; 15207 } 15208 15209 /++ 15210 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. 15211 +/ 15212 class AutomaticDialog(T) : Dialog { 15213 T t; 15214 15215 void delegate(T) onOK; 15216 void delegate() onCancel; 15217 15218 override int paddingTop() { return defaultLineHeight; } 15219 override int paddingBottom() { return defaultLineHeight; } 15220 override int paddingRight() { return defaultLineHeight; } 15221 override int paddingLeft() { return defaultLineHeight; } 15222 15223 this(T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 15224 assert(onOK !is null); 15225 15226 t = initialData; 15227 15228 static if(is(T == class)) { 15229 if(t is null) 15230 t = new T(); 15231 } 15232 this.onOK = onOK; 15233 this.onCancel = onCancel; 15234 super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title); 15235 15236 static if(is(T == class)) 15237 this.addDataControllerWidget(t); 15238 else 15239 this.addDataControllerWidget(&t); 15240 15241 auto hl = new HorizontalLayout(this); 15242 auto stretch = new HorizontalSpacer(hl); // to right align 15243 auto ok = new CommandButton("OK", hl); 15244 auto cancel = new CommandButton("Cancel", hl); 15245 ok.addEventListener(EventType.triggered, &OK); 15246 cancel.addEventListener(EventType.triggered, &Cancel); 15247 15248 this.addEventListener((KeyDownEvent ev) { 15249 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 15250 ok.focus(); 15251 OK(); 15252 ev.preventDefault(); 15253 } 15254 if(ev.key == Key.Escape) { 15255 Cancel(); 15256 ev.preventDefault(); 15257 } 15258 }); 15259 15260 this.addEventListener((scope ClosedEvent ce) { 15261 if(onCancel) 15262 onCancel(); 15263 }); 15264 15265 //this.children[0].focus(); 15266 } 15267 15268 override void OK() { 15269 onOK(t); 15270 close(); 15271 } 15272 15273 override void Cancel() { 15274 if(onCancel) 15275 onCancel(); 15276 close(); 15277 } 15278 } 15279 15280 private template baseClassCount(Class) { 15281 private int helper() { 15282 int count = 0; 15283 static if(is(Class bases == super)) { 15284 foreach(base; bases) 15285 static if(is(base == class)) 15286 count += 1 + baseClassCount!base; 15287 } 15288 return count; 15289 } 15290 15291 enum int baseClassCount = helper(); 15292 } 15293 15294 private long stringToLong(string s) { 15295 long ret; 15296 if(s.length == 0) 15297 return ret; 15298 bool negative = s[0] == '-'; 15299 if(negative) 15300 s = s[1 .. $]; 15301 foreach(ch; s) { 15302 if(ch >= '0' && ch <= '9') { 15303 ret *= 10; 15304 ret += ch - '0'; 15305 } 15306 } 15307 if(negative) 15308 ret = -ret; 15309 return ret; 15310 } 15311 15312 15313 interface ReflectableProperties { 15314 /++ 15315 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 15316 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 15317 json in the current implementation. 15318 15319 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 15320 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 15321 as of the June 2, 2021 release. 15322 15323 History: 15324 Added June 2, 2021. 15325 15326 See_Also: [getPropertyAsString], [setPropertyFromString] 15327 +/ 15328 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 15329 /++ 15330 Requests a property to be delivered to you as a string, through your `sink` delegate. 15331 15332 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 15333 be interpreted as json, otherwise, it is just a plain string. 15334 15335 The sink should always be called exactly once for each call (it is basically a return value, but it might 15336 use a local buffer it maintains instead of allocating a return value). 15337 15338 History: 15339 Added June 2, 2021. 15340 15341 See_Also: [getPropertiesList], [setPropertyFromString] 15342 +/ 15343 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 15344 /++ 15345 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. 15346 15347 History: 15348 Added June 2, 2021. 15349 15350 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 15351 +/ 15352 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 15353 15354 /// [setPropertyFromString] possible return values 15355 enum SetPropertyResult { 15356 success = 0, /// the property has been successfully set to the request value 15357 notPermitted = -1, /// the property exists but it cannot be changed at this time 15358 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 15359 noSuchProperty = -3, /// there is no property by that name 15360 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 15361 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) 15362 } 15363 15364 /++ 15365 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 15366 15367 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15368 15369 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15370 rarely need to use these building blocks directly. 15371 +/ 15372 mixin template RegisterSetters() { 15373 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 15374 switch(name) { 15375 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15376 case memberName: 15377 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15378 if(value != "true" && value != "false") 15379 return SetPropertyResult.wrongFormat; 15380 __traits(getMember, this, memberName) = value == "true" ? true : false; 15381 return SetPropertyResult.success; 15382 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15383 import core.stdc.stdlib; 15384 char[128] zero = 0; 15385 if(buffer.length + 1 >= zero.length) 15386 return SetPropertyResult.wrongFormat; 15387 zero[0 .. buffer.length] = buffer[]; 15388 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 15389 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15390 import core.stdc.stdlib; 15391 char[128] zero = 0; 15392 if(buffer.length + 1 >= zero.length) 15393 return SetPropertyResult.wrongFormat; 15394 zero[0 .. buffer.length] = buffer[]; 15395 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 15396 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15397 __traits(getMember, this, memberName) = value.idup; 15398 } else { 15399 return SetPropertyResult.notImplemented; 15400 } 15401 15402 } 15403 default: 15404 return super.setPropertyFromString(name, value, valueIsJson); 15405 } 15406 } 15407 } 15408 15409 /++ 15410 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 15411 15412 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15413 15414 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15415 rarely need to use these building blocks directly. 15416 +/ 15417 mixin template RegisterGetters() { 15418 override void getPropertiesList(scope void delegate(string name) sink) const { 15419 super.getPropertiesList(sink); 15420 15421 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15422 sink(memberName); 15423 } 15424 } 15425 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 15426 switch(name) { 15427 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15428 case memberName: 15429 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15430 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 15431 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15432 import core.stdc.stdio; 15433 char[32] buffer; 15434 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 15435 sink(name, buffer[0 .. len], true); 15436 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15437 import core.stdc.stdio; 15438 char[32] buffer; 15439 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 15440 sink(name, buffer[0 .. len], true); 15441 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15442 sink(name, __traits(getMember, this, memberName), false); 15443 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 15444 } else { 15445 sink(name, null, true); 15446 } 15447 15448 return; 15449 } 15450 default: 15451 return super.getPropertyAsString(name, sink); 15452 } 15453 } 15454 } 15455 } 15456 15457 private struct Stack(T) { 15458 this(int maxSize) { 15459 internalLength = 0; 15460 arr = initialBuffer[]; 15461 } 15462 15463 ///. 15464 void push(T t) { 15465 if(internalLength >= arr.length) { 15466 auto oldarr = arr; 15467 if(arr.length < 4096) 15468 arr = new T[arr.length * 2]; 15469 else 15470 arr = new T[arr.length + 4096]; 15471 arr[0 .. oldarr.length] = oldarr[]; 15472 } 15473 15474 arr[internalLength] = t; 15475 internalLength++; 15476 } 15477 15478 ///. 15479 T pop() { 15480 assert(internalLength); 15481 internalLength--; 15482 return arr[internalLength]; 15483 } 15484 15485 ///. 15486 T peek() { 15487 assert(internalLength); 15488 return arr[internalLength - 1]; 15489 } 15490 15491 ///. 15492 @property bool empty() { 15493 return internalLength ? false : true; 15494 } 15495 15496 ///. 15497 private T[] arr; 15498 private size_t internalLength; 15499 private T[64] initialBuffer; 15500 // 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), 15501 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 15502 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 15503 } 15504 15505 /// 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. 15506 private struct WidgetStream { 15507 15508 ///. 15509 @property Widget front() { 15510 return current.widget; 15511 } 15512 15513 /// Use Widget.tree instead. 15514 this(Widget start) { 15515 current.widget = start; 15516 current.childPosition = -1; 15517 isEmpty = false; 15518 stack = typeof(stack)(0); 15519 } 15520 15521 /* 15522 Handle it 15523 handle its children 15524 15525 */ 15526 15527 ///. 15528 void popFront() { 15529 more: 15530 if(isEmpty) return; 15531 15532 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 15533 15534 current.childPosition++; 15535 if(current.childPosition >= current.widget.children.length) { 15536 if(stack.empty()) 15537 isEmpty = true; 15538 else { 15539 current = stack.pop(); 15540 goto more; 15541 } 15542 } else { 15543 stack.push(current); 15544 current.widget = current.widget.children[current.childPosition]; 15545 current.childPosition = -1; 15546 } 15547 } 15548 15549 ///. 15550 @property bool empty() { 15551 return isEmpty; 15552 } 15553 15554 private: 15555 15556 struct Current { 15557 Widget widget; 15558 int childPosition; 15559 } 15560 15561 Current current; 15562 15563 Stack!(Current) stack; 15564 15565 bool isEmpty; 15566 } 15567 15568 15569 /+ 15570 15571 I could fix up the hierarchy kinda like this 15572 15573 class Widget { 15574 Widget[] children() { return null; } 15575 } 15576 interface WidgetContainer { 15577 Widget asWidget(); 15578 void addChild(Widget w); 15579 15580 // alias asWidget this; // but meh 15581 } 15582 15583 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 15584 15585 class Layout : Widget, WidgetContainer {} 15586 15587 class Window : WidgetContainer {} 15588 15589 15590 All constructors that previously took Widgets should now take WidgetContainers instead 15591 15592 15593 15594 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". 15595 +/ 15596 15597 /+ 15598 LAYOUTS 2.0 15599 15600 can just be assigned as a function. assigning a new one will cause it to be immediately called. 15601 15602 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 15603 15604 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 15605 15606 and even Paint can just use computedStyle... 15607 15608 background color 15609 font 15610 border color and style 15611 15612 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 15613 please note that many widgets and in some modes will completely ignore properties as they will. 15614 they are just hints you set, not promises. 15615 15616 15617 15618 15619 15620 So generally the existing virtual functions are just the default for the class. But individual objects 15621 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 15622 +/ 15623 15624 /++ 15625 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. 15626 15627 History: 15628 Added May 24, 2021. 15629 +/ 15630 struct WidgetBackground { 15631 /++ 15632 A background with the given solid color. 15633 +/ 15634 this(Color color) { 15635 this.color = color; 15636 } 15637 15638 this(WidgetBackground bg) { 15639 this = bg; 15640 } 15641 15642 /++ 15643 Creates a widget from the string. 15644 15645 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 15646 +/ 15647 static WidgetBackground fromString(string s) { 15648 return WidgetBackground(Color.fromString(s)); 15649 } 15650 15651 /++ 15652 The background is not necessarily a solid color, but you can always specify a color as a fallback. 15653 15654 History: 15655 Made `public` on December 18, 2022 (dub v10.10). 15656 +/ 15657 Color color; 15658 } 15659 15660 /++ 15661 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!) 15662 15663 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. 15664 15665 You should not inherit from this directly, but instead use [VisualTheme]. 15666 15667 History: 15668 Added May 8, 2021 15669 +/ 15670 abstract class BaseVisualTheme { 15671 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 15672 abstract void doPaint(Widget widget, WidgetPainter painter); 15673 15674 /+ 15675 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 15676 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 15677 +/ 15678 15679 /++ 15680 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 15681 where the interpretation of the string varies for each property and may include things like measurement units. 15682 +/ 15683 abstract string getPropertyString(Widget widget, string propertyName); 15684 15685 /++ 15686 Default background color of the window. Widgets also use this to simulate transparency. 15687 15688 Probably some shade of grey. 15689 +/ 15690 abstract Color windowBackgroundColor(); 15691 abstract Color widgetBackgroundColor(); 15692 abstract Color foregroundColor(); 15693 abstract Color lightAccentColor(); 15694 abstract Color darkAccentColor(); 15695 15696 /++ 15697 Colors used to indicate active selections in lists and text boxes, etc. 15698 +/ 15699 abstract Color selectionForegroundColor(); 15700 /// ditto 15701 abstract Color selectionBackgroundColor(); 15702 15703 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 15704 15705 /++ 15706 If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally. 15707 +/ 15708 abstract OperatingSystemFont defaultFont(int dpi); 15709 15710 private OperatingSystemFont[int] defaultFontCache_; 15711 private OperatingSystemFont defaultFontCached(int dpi) { 15712 if(dpi !in defaultFontCache_) { 15713 // FIXME: set this to false if X disconnect or if visual theme changes 15714 defaultFontCache_[dpi] = defaultFont(dpi); 15715 } 15716 return defaultFontCache_[dpi]; 15717 } 15718 } 15719 15720 /+ 15721 A widget should have: 15722 classList 15723 dataset 15724 attributes 15725 computedStyles 15726 state (persistent) 15727 dynamic state (focused, hover, etc) 15728 +/ 15729 15730 // visualTheme.computedStyle(this).paddingLeft 15731 15732 15733 /++ 15734 This is your entry point to create your own visual theme for custom widgets. 15735 15736 You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods. 15737 15738 Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating. 15739 +/ 15740 abstract class VisualTheme(CRTP) : BaseVisualTheme { 15741 override string getPropertyString(Widget widget, string propertyName) { 15742 return null; 15743 } 15744 15745 /+ 15746 mixin StyleOverride!Widget 15747 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 15748 w.useStyleProperties(dg); 15749 } 15750 +/ 15751 15752 final override void doPaint(Widget widget, WidgetPainter painter) { 15753 auto derived = cast(CRTP) cast(void*) this; 15754 15755 scope void delegate(Widget, WidgetPainter) bestMatch; 15756 int bestMatchScore; 15757 15758 static if(__traits(hasMember, CRTP, "paint")) 15759 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 15760 static if(is(typeof(overload) Params == __parameters)) { 15761 static assert(Params.length == 2); 15762 static assert(is(Params[0] : Widget)); 15763 static assert(is(Params[1] == WidgetPainter)); 15764 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); 15765 15766 alias type = Params[0]; 15767 if(cast(type) widget) { 15768 auto score = baseClassCount!type; 15769 15770 if(score > bestMatchScore) { 15771 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 15772 bestMatchScore = score; 15773 } 15774 } 15775 } else static assert(0, "paint should be a method."); 15776 } 15777 15778 if(bestMatch) 15779 bestMatch(widget, painter); 15780 else 15781 widget.paint(painter); 15782 } 15783 15784 deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; } 15785 15786 // 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 15787 // mixin Beautiful95Theme; 15788 mixin DefaultLightTheme; 15789 15790 private static struct Cached { 15791 // i prolly want to do this 15792 } 15793 } 15794 15795 /// ditto 15796 mixin template Beautiful95Theme() { 15797 override Color windowBackgroundColor() { return Color(212, 212, 212); } 15798 override Color widgetBackgroundColor() { return Color.white; } 15799 override Color foregroundColor() { return Color.black; } 15800 override Color darkAccentColor() { return Color(172, 172, 172); } 15801 override Color lightAccentColor() { return Color(223, 223, 223); } 15802 override Color selectionForegroundColor() { return Color.white; } 15803 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 15804 override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr 15805 } 15806 15807 /// ditto 15808 mixin template DefaultLightTheme() { 15809 override Color windowBackgroundColor() { return Color(232, 232, 232); } 15810 override Color widgetBackgroundColor() { return Color.white; } 15811 override Color foregroundColor() { return Color.black; } 15812 override Color darkAccentColor() { return Color(172, 172, 172); } 15813 override Color lightAccentColor() { return Color(223, 223, 223); } 15814 override Color selectionForegroundColor() { return Color.white; } 15815 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 15816 override OperatingSystemFont defaultFont(int dpi) { 15817 version(Windows) 15818 return new OperatingSystemFont("Segoe UI"); 15819 else { 15820 // FIXME: undo xft's scaling so we don't end up double scaled 15821 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 15822 } 15823 } 15824 } 15825 15826 /// ditto 15827 mixin template DefaultDarkTheme() { 15828 override Color windowBackgroundColor() { return Color(64, 64, 64); } 15829 override Color widgetBackgroundColor() { return Color.black; } 15830 override Color foregroundColor() { return Color.white; } 15831 override Color darkAccentColor() { return Color(20, 20, 20); } 15832 override Color lightAccentColor() { return Color(80, 80, 80); } 15833 override Color selectionForegroundColor() { return Color.white; } 15834 override Color selectionBackgroundColor() { return Color(128, 0, 128); } 15835 override OperatingSystemFont defaultFont(int dpi) { 15836 version(Windows) 15837 return new OperatingSystemFont("Segoe UI", 12); 15838 else 15839 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 15840 } 15841 } 15842 15843 /// ditto 15844 alias DefaultTheme = DefaultLightTheme; 15845 15846 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 15847 /+ 15848 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 15849 Color windowBackgroundColor() { return Color(242, 242, 242); } 15850 Color darkAccentColor() { return windowBackgroundColor; } 15851 Color lightAccentColor() { return windowBackgroundColor; } 15852 +/ 15853 } 15854 15855 /++ 15856 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 15857 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 15858 15859 History: 15860 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 15861 +/ 15862 class StateChanged(alias field) : Event { 15863 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 15864 override bool cancelable() const { return false; } 15865 this(Widget target, typeof(field) newValue) { 15866 this.newValue = newValue; 15867 super(EventString, target); 15868 } 15869 15870 typeof(field) newValue; 15871 } 15872 15873 /++ 15874 Convenience function to add a `triggered` event listener. 15875 15876 Its implementation is simply `w.addEventListener("triggered", dg);` 15877 15878 History: 15879 Added November 27, 2021 (dub v10.4) 15880 +/ 15881 void addWhenTriggered(Widget w, void delegate() dg) { 15882 w.addEventListener("triggered", dg); 15883 } 15884 15885 /++ 15886 Observable varables can be added to widgets and when they are changed, it fires 15887 off a [StateChanged] event so you can react to it. 15888 15889 It is implemented as a getter and setter property, along with another helper you 15890 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 15891 event through the usual means. Just give the name of the variable. See [StateChanged] for an 15892 example. 15893 15894 History: 15895 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 15896 +/ 15897 mixin template Observable(T, string name) { 15898 private T backing; 15899 15900 mixin(q{ 15901 void } ~ name ~ q{_changed (void delegate(T) dg) { 15902 this.addEventListener((StateChanged!this_thing ev) { 15903 dg(ev.newValue); 15904 }); 15905 } 15906 15907 @property T } ~ name ~ q{ () { 15908 return backing; 15909 } 15910 15911 @property void } ~ name ~ q{ (T t) { 15912 backing = t; 15913 auto event = new StateChanged!this_thing(this, t); 15914 event.dispatch(); 15915 } 15916 }); 15917 15918 mixin("private alias this_thing = " ~ name ~ ";"); 15919 } 15920 15921 15922 private bool startsWith(string test, string thing) { 15923 if(test.length < thing.length) 15924 return false; 15925 return test[0 .. thing.length] == thing; 15926 } 15927 15928 private bool endsWith(string test, string thing) { 15929 if(test.length < thing.length) 15930 return false; 15931 return test[$ - thing.length .. $] == thing; 15932 } 15933 15934 // still do layout delegation 15935 // and... split off Window from Widget. 15936 15937 version(minigui_screenshots) 15938 struct Screenshot { 15939 string name; 15940 } 15941 15942 version(minigui_screenshots) 15943 static if(__VERSION__ > 2092) 15944 mixin(q{ 15945 shared static this() { 15946 import core.runtime; 15947 15948 static UnitTestResult screenshotMagic() { 15949 string name; 15950 15951 import arsd.png; 15952 15953 auto results = new Window(); 15954 auto button = new Button("do it", results); 15955 15956 Window.newWindowCreated = delegate(Window w) { 15957 Timer timer; 15958 timer = new Timer(250, { 15959 auto img = w.win.takeScreenshot(); 15960 timer.destroy(); 15961 15962 version(Windows) 15963 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 15964 else 15965 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 15966 15967 w.close(); 15968 }); 15969 }; 15970 15971 button.addWhenTriggered( { 15972 15973 foreach(test; __traits(getUnitTests, mixin(__MODULE__))) { 15974 name = null; 15975 static foreach(attr; __traits(getAttributes, test)) { 15976 static if(is(typeof(attr) == Screenshot)) 15977 name = attr.name; 15978 } 15979 if(name.length) { 15980 test(); 15981 } 15982 } 15983 15984 }); 15985 15986 results.loop(); 15987 15988 return UnitTestResult(0, 0, false, false); 15989 } 15990 15991 15992 Runtime.extendedModuleUnitTester = &screenshotMagic; 15993 } 15994 }); 15995 version(minigui_screenshots) { 15996 version(unittest) 15997 void main() {} 15998 else static assert(0, "dont forget the -unittest flag to dmd"); 15999 } 16000 16001 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 16002 // FIXME: make multiple accelerators disambiguate based ona rgs 16003 // FIXME: MainWindow ctor should have same arg order as Window 16004 // FIXME: mainwindow ctor w/ client area size instead of total size. 16005 // 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. 16006 // FIXME: tri-state checkbox 16007 // FIXME: subordinate controls grouping...