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` and -L/entry:mainCRTStartup`. If using ldc instead 136 of dmd, use `-L/entry:wmainCRTStartup` instead of `mainCRTStartup`; note the "w". 137 138 Otherwise you'll get a console and possibly other visual bugs. But if you do use 139 the subsystem:windows, note that Phobos' writeln will crash the program! 140 141 HTML_To_Classes: 142 $(SMALL_TABLE 143 HTML Code | Minigui Class 144 145 `<input type="text">` | [LineEdit] 146 `<textarea>` | [TextEdit] 147 `<select>` | [DropDownSelection] 148 `<input type="checkbox">` | [Checkbox] 149 `<input type="radio">` | [Radiobox] 150 `<button>` | [Button] 151 ) 152 153 154 Stretchiness: 155 The default is 4. You can use larger numbers for things that should 156 consume a lot of space, and lower numbers for ones that are better at 157 smaller sizes. 158 159 Overlapped_input: 160 COMING EVENTUALLY: 161 minigui will include a little bit of I/O functionality that just works 162 with the event loop. If you want to get fancy, I suggest spinning up 163 another thread and posting events back and forth. 164 165 $(H2 Add ons) 166 See the `minigui_addons` directory in the arsd repo for some add on widgets 167 you can import separately too. 168 169 $(H3 XML definitions) 170 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 171 172 $(H3 Scriptability) 173 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 174 in this documentation, it means you can call it from the script language. 175 176 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 177 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 178 179 --- 180 import arsd.minigui_xml; 181 import arsd.script; 182 183 var globals = var.emptyObject; 184 globals.makeWidgetFromString = &makeWidgetFromString; 185 186 // this now works 187 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 188 --- 189 190 More to come. 191 192 History: 193 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 194 195 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 196 tag this as version 2.0. 197 198 Among the changes: 199 $(LIST 200 * 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. 201 202 See [Event] for details. 203 204 * 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. 205 206 See [DoubleClickEvent] for details. 207 208 * 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. 209 210 See [Widget.Style] for details. 211 212 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 213 214 * 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. 215 216 * 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. 217 218 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 219 220 * 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. 221 222 * Various non-breaking additions. 223 ) 224 +/ 225 module arsd.minigui; 226 227 /++ 228 This hello world sample will have an oversized button, but that's ok, you see your first window! 229 +/ 230 version(Demo) 231 unittest { 232 import arsd.minigui; 233 234 void main() { 235 auto window = new MainWindow(); 236 237 // note the parent widget is almost always passed as the last argument to a constructor 238 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 239 auto button = new Button("Close", window); 240 button.addWhenTriggered({ 241 window.close(); 242 }); 243 244 window.loop(); 245 } 246 247 main(); // exclude from docs 248 } 249 250 /++ 251 This example shows one way you can partition your window into a header 252 and sidebar. Here, the header and sidebar have a fixed width, while the 253 rest of the content sizes with the window. 254 255 It might be a new way of thinking about window layout to do things this 256 way - perhaps [GridLayout] more matches your style of thought - but the 257 concept here is to partition the window into sub-boxes with a particular 258 size, then partition those boxes into further boxes. 259 260 $(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.) 261 262 So to make the header, start with a child layout that has a max height. 263 It will use that space from the top, then the remaining children will 264 split the remaining area, meaning you can think of is as just being another 265 box you can split again. Keep splitting until you have the look you desire. 266 +/ 267 // https://github.com/adamdruppe/arsd/issues/310 268 version(minigui_screenshots) 269 @Screenshot("layout") 270 unittest { 271 import arsd.minigui; 272 273 // This helper class is just to help make the layout boxes visible. 274 // think of it like a <div style="background-color: whatever;"></div> in HTML. 275 class ColorWidget : Widget { 276 this(Color color, Widget parent) { 277 this.color = color; 278 super(parent); 279 } 280 Color color; 281 class Style : Widget.Style { 282 override WidgetBackground background() { return WidgetBackground(color); } 283 } 284 mixin OverrideStyle!Style; 285 } 286 287 void main() { 288 auto window = new Window; 289 290 // the key is to give it a max height. This is one way to do it: 291 auto header = new class HorizontalLayout { 292 this() { super(window); } 293 override int maxHeight() { return 50; } 294 }; 295 // this next line is a shortcut way of doing it too, but it only works 296 // for HorizontalLayout and VerticalLayout, and is less explicit, so it 297 // is good to know how to make a new class like above anyway. 298 // auto header = new HorizontalLayout(50, window); 299 300 auto bar = new HorizontalLayout(window); 301 302 // or since this is so common, VerticalLayout and HorizontalLayout both 303 // can just take an argument in their constructor for max width/height respectively 304 305 // (could have tone this above too, but I wanted to demo both techniques) 306 auto left = new VerticalLayout(100, bar); 307 308 // and this is the main section's container. A plain Widget instance is good enough here. 309 auto container = new Widget(bar); 310 311 // and these just add color to the containers we made above for the screenshot. 312 // in a real application, you can just add your actual controls instead of these. 313 auto headerColorBox = new ColorWidget(Color.teal, header); 314 auto leftColorBox = new ColorWidget(Color.green, left); 315 auto rightColorBox = new ColorWidget(Color.purple, container); 316 317 window.loop(); 318 } 319 320 main(); // exclude from docs 321 } 322 323 324 import arsd.core; 325 alias Timer = arsd.simpledisplay.Timer; 326 public import arsd.simpledisplay; 327 /++ 328 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 329 330 History: 331 Was private until May 15, 2021. 332 +/ 333 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 334 335 version(Windows) { 336 import core.sys.windows.winnls; 337 import core.sys.windows.windef; 338 import core.sys.windows.basetyps; 339 import core.sys.windows.winbase; 340 import core.sys.windows.winuser; 341 import core.sys.windows.wingdi; 342 static import gdi = core.sys.windows.wingdi; 343 } 344 345 version(Windows) { 346 version(minigui_manifest) {} else version=minigui_no_manifest; 347 348 version(minigui_no_manifest) {} else 349 static if(__VERSION__ >= 2_083) 350 version(CRuntime_Microsoft) { // FIXME: mingw? 351 // assume we want commctrl6 whenever possible since there's really no reason not to 352 // and this avoids some of the manifest hassle 353 pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""); 354 } 355 } 356 357 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 358 private bool lastDefaultPrevented; 359 360 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 361 alias scriptable = arsd_jsvar_compatible; 362 363 version(Windows) { 364 // use native widgets when available unless specifically asked otherwise 365 version(custom_widgets) { 366 enum bool UsingCustomWidgets = true; 367 enum bool UsingWin32Widgets = false; 368 } else { 369 version = win32_widgets; 370 enum bool UsingCustomWidgets = false; 371 enum bool UsingWin32Widgets = true; 372 373 // give access to my text system for the rich text cross platform stuff 374 version = use_new_text_system; 375 import arsd.textlayouter; 376 } 377 // and native theming when needed 378 //version = win32_theming; 379 } else { 380 enum bool UsingCustomWidgets = true; 381 enum bool UsingWin32Widgets = false; 382 version=custom_widgets; 383 } 384 385 386 387 /* 388 389 The main goals of minigui.d are to: 390 1) Provide basic widgets that just work in a lightweight lib. 391 I basically want things comparable to a plain HTML form, 392 plus the easy and obvious things you expect from Windows 393 apps like a menu. 394 2) Use native things when possible for best functionality with 395 least library weight. 396 3) Give building blocks to provide easy extension for your 397 custom widgets, or hooking into additional native widgets 398 I didn't wrap. 399 4) Provide interfaces for easy interaction between third 400 party minigui extensions. (event model, perhaps 401 signals/slots, drop-in ease of use bits.) 402 5) Zero non-system dependencies, including Phobos as much as 403 I reasonably can. It must only import arsd.color and 404 my simpledisplay.d. If you need more, it will have to be 405 an extension module. 406 6) An easy layout system that generally works. 407 408 A stretch goal is to make it easy to make gui forms with code, 409 some kind of resource file (xml?) and even a wysiwyg designer. 410 411 Another stretch goal is to make it easy to hook data into the gui, 412 including from reflection. So like auto-generate a form from a 413 function signature or struct definition, or show a list from an 414 array that automatically updates as the array is changed. Then, 415 your program focuses on the data more than the gui interaction. 416 417 418 419 STILL NEEDED: 420 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 421 * slider 422 * listbox 423 * spinner 424 * label? 425 * rich text 426 */ 427 428 429 /+ 430 enum LayoutMethods { 431 verticalFlex, 432 horizontalFlex, 433 inlineBlock, // left to right, no stretch, goes to next line as needed 434 static, // just set to x, y 435 verticalNoStretch, // browser style default 436 437 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 438 439 grid, // magic 440 } 441 +/ 442 443 /++ 444 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. 445 446 447 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. 448 449 --- 450 class MinimalWidget : Widget { 451 this(Widget parent) { 452 super(parent); 453 } 454 } 455 --- 456 457 $(SIDEBAR 458 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. 459 ) 460 461 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. 462 463 Among the things you'll most likely want to change in your custom widget: 464 465 $(LIST 466 * 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.) 467 468 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. 469 470 Do this $(I after) calling the `super` constructor. 471 472 * 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. 473 474 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 475 476 * 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. 477 478 * 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. 479 ) 480 481 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. 482 483 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 484 485 Your own custom-drawn and native system controls can exist side-by-side. 486 487 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. 488 +/ 489 class Widget : ReflectableProperties { 490 491 private bool willDraw() { 492 return true; 493 } 494 495 /+ 496 /++ 497 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 498 499 History: 500 Added September 15, 2021 501 implemented.... ??? 502 +/ 503 void prepareReflection(this This)() { 504 505 } 506 +/ 507 508 private bool _enabled = true; 509 510 /++ 511 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. 512 513 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. 514 515 History: 516 Added November 23, 2021 (dub v10.4) 517 518 Warning: the specific behavior of disabling with parents may change in the future. 519 Bugs: 520 Currently only implemented for widgets backed by native Windows controls. 521 522 See_Also: [disabledReason], [disabledBy] 523 +/ 524 @property bool enabled() { 525 return disabledBy() is null; 526 } 527 528 /// ditto 529 @property void enabled(bool yes) { 530 _enabled = yes; 531 version(win32_widgets) { 532 if(hwnd) 533 EnableWindow(hwnd, yes); 534 } 535 setDynamicState(DynamicState.disabled, yes); 536 } 537 538 private string disabledReason_; 539 540 /++ 541 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. 542 543 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 544 545 History: 546 Added November 23, 2021 (dub v10.4) 547 See_Also: [enabled], [disabledBy] 548 +/ 549 @property string disabledReason() { 550 auto w = disabledBy(); 551 return (w is null) ? null : w.disabledReason_; 552 } 553 554 /// ditto 555 @property void disabledReason(string reason) { 556 disabledReason_ = reason; 557 } 558 559 /++ 560 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. 561 562 History: 563 Added November 25, 2021 (dub v10.4) 564 See_Also: [enabled], [disabledReason] 565 +/ 566 Widget disabledBy() { 567 Widget p = this; 568 while(p) { 569 if(!p._enabled) 570 return p; 571 p = p.parent; 572 } 573 return null; 574 } 575 576 /// Implementations of [ReflectableProperties] interface. See the interface for details. 577 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 578 if(valueIsJson) 579 return SetPropertyResult.wrongFormat; 580 switch(name) { 581 case "name": 582 this.name = value.idup; 583 return SetPropertyResult.success; 584 case "statusTip": 585 this.statusTip = value.idup; 586 return SetPropertyResult.success; 587 default: 588 return SetPropertyResult.noSuchProperty; 589 } 590 } 591 /// ditto 592 void getPropertiesList(scope void delegate(string name) sink) const { 593 sink("name"); 594 sink("statusTip"); 595 } 596 /// ditto 597 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 598 switch(name) { 599 case "name": 600 sink(name, this.name, false); 601 return; 602 case "statusTip": 603 sink(name, this.statusTip, false); 604 return; 605 default: 606 sink(name, null, true); 607 } 608 } 609 610 /++ 611 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 612 613 History: 614 Added November 25, 2021 (dub v10.5) 615 `Point` overload added January 12, 2022 (dub v10.6) 616 +/ 617 int scaleWithDpi(int value, int assumedDpi = 96) { 618 // avoid potential overflow with common special values 619 if(value == int.max) 620 return int.max; 621 if(value == int.min) 622 return int.min; 623 if(value == 0) 624 return 0; 625 return value * currentDpi(assumedDpi) / assumedDpi; 626 } 627 628 /// ditto 629 Point scaleWithDpi(Point value, int assumedDpi = 96) { 630 return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi)); 631 } 632 633 /++ 634 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. 635 636 Not entirely stable. 637 638 History: 639 Added August 25, 2023 (dub v11.1) 640 +/ 641 final int currentDpi(int assumedDpi = 96) { 642 // assert(parentWindow !is null); 643 // assert(parentWindow.win !is null); 644 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 645 //divide = 138; // to test 1.5x 646 // 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. 647 // this also covers the case when actualDpi returns 0. 648 if(divide < 96) 649 divide = 96; 650 return divide; 651 } 652 653 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 654 // I'll think up something better eventually 655 656 // FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight. 657 protected final int defaultLineHeight() { 658 auto cs = getComputedStyle(); 659 if(cs.font && !cs.font.isNull) 660 return cs.font.height() * 5 / 4; 661 else 662 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4); 663 } 664 665 /++ 666 667 History: 668 Added August 25, 2023 (dub v11.1) 669 +/ 670 protected final int defaultTextHeight(int numberOfLines = 1) { 671 auto cs = getComputedStyle(); 672 if(cs.font && !cs.font.isNull) 673 return cs.font.height() * numberOfLines; 674 else 675 return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines; 676 } 677 678 protected final int defaultTextWidth(const(char)[] text) { 679 auto cs = getComputedStyle(); 680 if(cs.font && !cs.font.isNull) 681 return cs.font.stringWidth(text); 682 else 683 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2); 684 } 685 686 /++ 687 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. 688 689 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. 690 691 History: 692 Added May 22, 2021 693 +/ 694 protected bool encapsulatedChildren() { 695 return false; 696 } 697 698 private void privateDpiChanged() { 699 dpiChanged(); 700 foreach(child; children) 701 child.privateDpiChanged(); 702 } 703 704 /++ 705 Virtual hook to update any caches or fonts you need on the event of a dpi scaling change. 706 707 History: 708 Added January 12, 2022 (dub v10.6) 709 +/ 710 protected void dpiChanged() { 711 712 } 713 714 // Default layout properties { 715 716 int minWidth() { return 0; } 717 int minHeight() { 718 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 719 int sum = this.paddingTop + this.paddingBottom; 720 foreach(child; children) { 721 if(child.hidden) 722 continue; 723 sum += child.minHeight(); 724 sum += child.marginTop(); 725 sum += child.marginBottom(); 726 } 727 728 return sum; 729 } 730 int maxWidth() { return int.max; } 731 int maxHeight() { return int.max; } 732 int widthStretchiness() { return 4; } 733 int heightStretchiness() { return 4; } 734 735 /++ 736 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 737 738 History: 739 Added June 15, 2021 (dub v10.1) 740 +/ 741 int widthShrinkiness() { return 0; } 742 /// ditto 743 int heightShrinkiness() { return 0; } 744 745 /++ 746 The initial size of the widget for layout calculations. Default is 0. 747 748 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 749 750 History: 751 Added June 15, 2021 (dub v10.1) 752 +/ 753 int flexBasisWidth() { return 0; } 754 /// ditto 755 int flexBasisHeight() { return 0; } 756 757 /++ 758 Not stable. 759 760 Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored. 761 762 So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6. 763 764 History: 765 Added January 5, 2023 766 +/ 767 Rectangle defaultMargin; 768 /// ditto 769 Rectangle defaultPadding; 770 771 int marginLeft() { return scaleWithDpi(defaultMargin.left); } 772 int marginRight() { return scaleWithDpi(defaultMargin.right); } 773 int marginTop() { return scaleWithDpi(defaultMargin.top); } 774 int marginBottom() { return scaleWithDpi(defaultMargin.bottom); } 775 int paddingLeft() { return scaleWithDpi(defaultPadding.left); } 776 int paddingRight() { return scaleWithDpi(defaultPadding.right); } 777 int paddingTop() { return scaleWithDpi(defaultPadding.top); } 778 int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); } 779 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 780 781 private bool recomputeChildLayoutRequired = true; 782 private static class RecomputeEvent {} 783 private __gshared rce = new RecomputeEvent(); 784 protected final void queueRecomputeChildLayout() { 785 recomputeChildLayoutRequired = true; 786 787 if(this.parentWindow) { 788 auto sw = this.parentWindow.win; 789 assert(sw !is null); 790 if(!sw.eventQueued!RecomputeEvent) { 791 sw.postEvent(rce); 792 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 793 } 794 } 795 796 } 797 798 protected final void recomputeChildLayoutEntry() { 799 if(recomputeChildLayoutRequired) { 800 recomputeChildLayout(); 801 recomputeChildLayoutRequired = false; 802 redraw(); 803 } else { 804 // I still need to check the tree just in case one of them was queued up 805 // and the event came up here instead of there. 806 foreach(child; children) 807 child.recomputeChildLayoutEntry(); 808 } 809 } 810 811 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 812 void recomputeChildLayout() { 813 .recomputeChildLayout!"height"(this); 814 } 815 816 // } 817 818 819 /++ 820 Returns the style's tag name string this object uses. 821 822 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. 823 824 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 825 826 History: 827 Added May 10, 2021 828 +/ 829 string styleTagName() const { 830 string n = typeid(this).name; 831 foreach_reverse(idx, ch; n) 832 if(ch == '.') { 833 n = n[idx + 1 .. $]; 834 break; 835 } 836 return n; 837 } 838 839 /// API for the [styleClassList] 840 static struct ClassList { 841 private Widget widget; 842 843 /// 844 void add(string s) { 845 widget.styleClassList_ ~= s; 846 } 847 848 /// 849 void remove(string s) { 850 foreach(idx, s1; widget.styleClassList_) 851 if(s1 == s) { 852 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 853 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 854 widget.styleClassList_.assumeSafeAppend(); 855 return; 856 } 857 } 858 859 /// Returns true if it was added, false if it was removed. 860 bool toggle(string s) { 861 if(contains(s)) { 862 remove(s); 863 return false; 864 } else { 865 add(s); 866 return true; 867 } 868 } 869 870 /// 871 bool contains(string s) const { 872 foreach(s1; widget.styleClassList_) 873 if(s1 == s) 874 return true; 875 return false; 876 877 } 878 } 879 880 private string[] styleClassList_; 881 882 /++ 883 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. 884 885 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 886 887 History: 888 Added May 10, 2021 889 +/ 890 inout(ClassList) styleClassList() inout { 891 return cast(inout(ClassList)) ClassList(cast() this); 892 } 893 894 /++ 895 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. 896 897 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. 898 899 The upper 32 bits are available for your own extensions. 900 901 History: 902 Added May 10, 2021 903 +/ 904 enum DynamicState : ulong { 905 focus = (1 << 0), /// the widget currently has the keyboard focus 906 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 907 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!) 908 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!) 909 checked = (1 << 4), /// the widget is toggleable and currently toggled on 910 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 911 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 912 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 913 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. 914 915 USER_BEGIN = (1UL << 32), 916 } 917 918 // I want to add the primary and cancel styles to buttons at least at some point somehow. 919 920 /// ditto 921 @property ulong dynamicState() { return dynamicState_; } 922 /// ditto 923 @property ulong dynamicState(ulong newValue) { 924 if(dynamicState != newValue) { 925 auto old = dynamicState_; 926 dynamicState_ = newValue; 927 928 useStyleProperties((scope Widget.Style s) { 929 if(s.variesWithState(old ^ newValue)) 930 redraw(); 931 }); 932 } 933 return dynamicState_; 934 } 935 936 /// ditto 937 void setDynamicState(ulong flags, bool state) { 938 auto ds = dynamicState_; 939 if(state) 940 ds |= flags; 941 else 942 ds &= ~flags; 943 944 dynamicState = ds; 945 } 946 947 private ulong dynamicState_; 948 949 deprecated("Use dynamic styles instead now") { 950 Color backgroundColor() { return backgroundColor_; } 951 void backgroundColor(Color c){ this.backgroundColor_ = c; } 952 953 MouseCursor cursor() { return GenericCursor.Default; } 954 } private Color backgroundColor_ = Color.transparent; 955 956 957 /++ 958 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). 959 960 It is here so there can be a specificity switch. 961 962 See [OverrideStyle] for a helper function to use your own. 963 964 History: 965 Added May 11, 2021 966 +/ 967 static class Style/* : StyleProperties*/ { 968 public Widget widget; // public because the mixin template needs access to it 969 970 /++ 971 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 972 973 History: 974 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. 975 +/ 976 bool variesWithState(ulong dynamicStateFlags) { 977 version(win32_widgets) { 978 if(widget.hwnd) 979 return false; 980 } 981 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 982 } 983 984 /// 985 Color foregroundColor() { 986 return WidgetPainter.visualTheme.foregroundColor; 987 } 988 989 /// 990 WidgetBackground background() { 991 // the default is a "transparent" background, which means 992 // it goes as far up as it can to get the color 993 if (widget.backgroundColor_ != Color.transparent) 994 return WidgetBackground(widget.backgroundColor_); 995 if (widget.parent) 996 return widget.parent.getComputedStyle.background; 997 return WidgetBackground(widget.backgroundColor_); 998 } 999 1000 private static OperatingSystemFont fontCached_; 1001 private OperatingSystemFont fontCached() { 1002 if(fontCached_ is null) 1003 fontCached_ = font(); 1004 return fontCached_; 1005 } 1006 1007 /++ 1008 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. 1009 +/ 1010 OperatingSystemFont font() { 1011 return null; 1012 } 1013 1014 /++ 1015 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. 1016 1017 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 1018 1019 History: 1020 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 1021 +/ 1022 MouseCursor cursor() { 1023 return GenericCursor.Default; 1024 } 1025 1026 FrameStyle borderStyle() { 1027 return FrameStyle.none; 1028 } 1029 1030 /++ 1031 +/ 1032 Color borderColor() { 1033 return Color.transparent; 1034 } 1035 1036 FrameStyle outlineStyle() { 1037 if(widget.dynamicState & DynamicState.focus) 1038 return FrameStyle.dotted; 1039 else 1040 return FrameStyle.none; 1041 } 1042 1043 Color outlineColor() { 1044 return foregroundColor; 1045 } 1046 } 1047 1048 /++ 1049 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 1050 The basic usage is simple: 1051 1052 --- 1053 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 1054 // override style hints as-needed here 1055 } 1056 OverrideStyle!Style; // add the method 1057 --- 1058 1059 $(TIP 1060 While the class is not forced to be `static`, for best results, it should be. A non-static class 1061 can not be inherited by other objects whereas the static one can. A property on the base class, 1062 called [Widget.Style.widget|widget], is available for you to access its properties. 1063 ) 1064 1065 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 1066 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 1067 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 1068 1069 1070 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 1071 You may also just override `variesWithState` when you use this flag. 1072 1073 --- 1074 mixin OverrideStyle!( 1075 DynamicState.focus, YourFocusedStyle, 1076 DynamicState.hover, YourHoverStyle, 1077 YourDefaultStyle 1078 ) 1079 --- 1080 1081 It checks if `dynamicState` matches the state and if so, returns the object given. 1082 1083 If there is no state mask given, the next one matches everything. The first match given is used. 1084 1085 However, since in most cases you'll want check state inside your individual methods, you probably won't 1086 find much use for this whole-class swap out. 1087 1088 History: 1089 Added May 16, 2021 1090 +/ 1091 static protected mixin template OverrideStyle(S...) { 1092 static import amg = arsd.minigui; 1093 override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) { 1094 ulong mask = 0; 1095 foreach(idx, thing; S) { 1096 static if(is(typeof(thing) : ulong)) { 1097 mask = thing; 1098 } else { 1099 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 1100 //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."); 1101 scope amg.Widget.Style s = new thing(); 1102 s.widget = this; 1103 dg(s); 1104 return; 1105 } 1106 } 1107 } 1108 } 1109 } 1110 /++ 1111 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 1112 +/ 1113 void useStyleProperties(scope void delegate(scope Style props) dg) { 1114 scope Style s = new Style(); 1115 s.widget = this; 1116 dg(s); 1117 } 1118 1119 1120 protected void sendResizeEvent() { 1121 this.emit!ResizeEvent(); 1122 } 1123 1124 Menu contextMenu(int x, int y) { return null; } 1125 1126 final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) { 1127 if(parentWindow is null || parentWindow.win is null) return false; 1128 1129 auto menu = this.contextMenu(x, y); 1130 if(menu is null) 1131 return false; 1132 1133 version(win32_widgets) { 1134 // FIXME: if it is -1, -1, do it at the current selection location instead 1135 // tho the corner of the window, whcih it does now, isn't the literal worst. 1136 1137 if(screenX < 0 && screenY < 0) { 1138 auto p = this.globalCoordinates(); 1139 if(screenX == -2) 1140 p.x += x; 1141 if(screenY == -2) 1142 p.y += y; 1143 1144 screenX = p.x; 1145 screenY = p.y; 1146 } 1147 1148 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1149 throw new Exception("TrackContextMenuEx"); 1150 } else version(custom_widgets) { 1151 menu.popup(this, x, y); 1152 } 1153 1154 return true; 1155 } 1156 1157 /++ 1158 Removes this widget from its parent. 1159 1160 History: 1161 `removeWidget` was made `final` on May 11, 2021. 1162 +/ 1163 @scriptable 1164 final void removeWidget() { 1165 auto p = this.parent; 1166 if(p) { 1167 int item; 1168 for(item = 0; item < p._children.length; item++) 1169 if(p._children[item] is this) 1170 break; 1171 auto idx = item; 1172 for(; item < p._children.length - 1; item++) 1173 p._children[item] = p._children[item + 1]; 1174 p._children = p._children[0 .. $-1]; 1175 1176 this.parent.widgetRemoved(idx, this); 1177 //this.parent = null; 1178 1179 p.queueRecomputeChildLayout(); 1180 } 1181 version(win32_widgets) { 1182 removeAllChildren(); 1183 if(hwnd) { 1184 DestroyWindow(hwnd); 1185 hwnd = null; 1186 } 1187 } 1188 } 1189 1190 /++ 1191 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. 1192 1193 History: 1194 Added September 19, 2021 1195 +/ 1196 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 1197 1198 /++ 1199 Removes all child widgets from `this`. You should not use the removed widgets again. 1200 1201 Note that on Windows, it also destroys the native handles for the removed children recursively. 1202 1203 History: 1204 Added July 1, 2021 (dub v10.2) 1205 +/ 1206 void removeAllChildren() { 1207 version(win32_widgets) 1208 foreach(child; _children) { 1209 child.removeAllChildren(); 1210 if(child.hwnd) { 1211 DestroyWindow(child.hwnd); 1212 child.hwnd = null; 1213 } 1214 } 1215 auto orig = this._children; 1216 this._children = null; 1217 foreach(idx, w; orig) 1218 this.widgetRemoved(idx, w); 1219 1220 queueRecomputeChildLayout(); 1221 } 1222 1223 /++ 1224 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1225 +/ 1226 @scriptable 1227 Widget getChildByName(string name) { 1228 return getByName(name); 1229 } 1230 /++ 1231 Finds the nearest descendant with the requested type and [name]. May return `this`. 1232 +/ 1233 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1234 if(this.name == name) 1235 if(auto c = cast(WidgetClass) this) 1236 return c; 1237 foreach(child; children) { 1238 auto w = child.getByName(name); 1239 if(auto c = cast(WidgetClass) w) 1240 return c; 1241 } 1242 return null; 1243 } 1244 1245 /++ 1246 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. 1247 Names should be unique in a window. 1248 1249 See_Also: [getByName], [getChildByName] 1250 +/ 1251 @scriptable string name; 1252 1253 private EventHandler[][string] bubblingEventHandlers; 1254 private EventHandler[][string] capturingEventHandlers; 1255 1256 /++ 1257 Default event handlers. These are called on the appropriate 1258 event unless [Event.preventDefault] is called on the event at 1259 some point through the bubbling process. 1260 1261 1262 If you are implementing your own widget and want to add custom 1263 events, you should follow the same pattern here: create a virtual 1264 function named `defaultEventHandler_eventname` with the implementation, 1265 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1266 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1267 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1268 This ensures virtual dispatch based on the correct subclass. 1269 1270 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1271 overridden version. 1272 1273 You only need to do that on parent classes adding NEW event types. If you 1274 just want to change the default behavior of an existing event type in a subclass, 1275 you override the function (and optionally call `super.method_name`) like normal. 1276 1277 +/ 1278 protected EventHandler[string] defaultEventHandlers; 1279 1280 /// ditto 1281 void setupDefaultEventHandlers() { 1282 defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); }; 1283 defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); }; 1284 defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); }; 1285 defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); }; 1286 defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); }; 1287 defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); }; 1288 defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); }; 1289 defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); }; 1290 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); }; 1291 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); }; 1292 defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); }; 1293 defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); }; 1294 defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); }; 1295 defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); }; 1296 defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); }; 1297 defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); }; 1298 defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); }; 1299 defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); }; 1300 } 1301 1302 /// ditto 1303 void defaultEventHandler_click(ClickEvent event) {} 1304 /// ditto 1305 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1306 /// ditto 1307 void defaultEventHandler_keydown(KeyDownEvent event) {} 1308 /// ditto 1309 void defaultEventHandler_keyup(KeyUpEvent event) {} 1310 /// ditto 1311 void defaultEventHandler_mousedown(MouseDownEvent event) { 1312 if(event.button == MouseButton.left) { 1313 if(this.tabStop) { 1314 this.focus(); 1315 } 1316 } 1317 } 1318 /// ditto 1319 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1320 /// ditto 1321 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1322 /// ditto 1323 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1324 /// ditto 1325 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1326 /// ditto 1327 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1328 /// ditto 1329 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1330 /// ditto 1331 void defaultEventHandler_char(CharEvent event) {} 1332 /// ditto 1333 void defaultEventHandler_triggered(Event event) {} 1334 /// ditto 1335 void defaultEventHandler_change(Event event) {} 1336 /// ditto 1337 void defaultEventHandler_focus(Event event) {} 1338 /// ditto 1339 void defaultEventHandler_blur(Event event) {} 1340 /// ditto 1341 void defaultEventHandler_focusin(Event event) {} 1342 /// ditto 1343 void defaultEventHandler_focusout(Event event) {} 1344 1345 /++ 1346 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1347 1348 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1349 1350 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1351 of participating in handler delegation. 1352 1353 $(TIP 1354 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. 1355 ) 1356 +/ 1357 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1358 return addEventListener(event, (Widget, scope Event e) { 1359 if(e.srcElement is this) 1360 handler(); 1361 }, useCapture); 1362 } 1363 1364 /// ditto 1365 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1366 return addEventListener(event, (Widget, Event e) { 1367 if(e.srcElement is this) 1368 handler(e); 1369 }, useCapture); 1370 } 1371 1372 /// ditto 1373 EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) { 1374 static if(is(Handler Fn == delegate)) { 1375 static if(is(Fn Params == __parameters)) { 1376 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1377 if(e.srcElement !is this) 1378 return; 1379 auto ty = cast(Params[0]) e; 1380 if(ty !is null) 1381 handler(ty); 1382 }, useCapture); 1383 } else static assert(0); 1384 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1385 } 1386 1387 /// ditto 1388 @scriptable 1389 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1390 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1391 } 1392 1393 /// ditto 1394 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1395 static if(is(Handler Fn == delegate)) { 1396 static if(is(Fn Params == __parameters)) { 1397 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1398 auto ty = cast(Params[0]) e; 1399 if(ty !is null) 1400 handler(ty); 1401 }, useCapture); 1402 } else static assert(0); 1403 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1404 } 1405 1406 /// ditto 1407 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1408 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1409 } 1410 1411 /// ditto 1412 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1413 if(event.length > 2 && event[0..2] == "on") 1414 event = event[2 .. $]; 1415 1416 if(useCapture) 1417 capturingEventHandlers[event] ~= handler; 1418 else 1419 bubblingEventHandlers[event] ~= handler; 1420 1421 return EventListener(this, event, handler, useCapture); 1422 } 1423 1424 /// ditto 1425 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1426 if(event.length > 2 && event[0..2] == "on") 1427 event = event[2 .. $]; 1428 1429 if(useCapture) { 1430 if(event in capturingEventHandlers) 1431 foreach(ref evt; capturingEventHandlers[event]) 1432 if(evt is handler) evt = null; 1433 } else { 1434 if(event in bubblingEventHandlers) 1435 foreach(ref evt; bubblingEventHandlers[event]) 1436 if(evt is handler) evt = null; 1437 } 1438 } 1439 1440 /// ditto 1441 void removeEventListener(EventListener listener) { 1442 removeEventListener(listener.event, listener.handler, listener.useCapture); 1443 } 1444 1445 static if(UsingSimpledisplayX11) { 1446 void discardXConnectionState() { 1447 foreach(child; children) 1448 child.discardXConnectionState(); 1449 } 1450 1451 void recreateXConnectionState() { 1452 foreach(child; children) 1453 child.recreateXConnectionState(); 1454 redraw(); 1455 } 1456 } 1457 1458 /++ 1459 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1460 1461 History: 1462 `globalCoordinates` was made `final` on May 11, 2021. 1463 +/ 1464 Point globalCoordinates() { 1465 int x = this.x; 1466 int y = this.y; 1467 auto p = this.parent; 1468 while(p) { 1469 x += p.x; 1470 y += p.y; 1471 p = p.parent; 1472 } 1473 1474 static if(UsingSimpledisplayX11) { 1475 auto dpy = XDisplayConnection.get; 1476 arsd.simpledisplay.Window dummyw; 1477 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1478 } else version(Windows) { 1479 POINT pt; 1480 pt.x = x; 1481 pt.y = y; 1482 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1483 x = pt.x; 1484 y = pt.y; 1485 } else { 1486 featureNotImplemented(); 1487 } 1488 1489 return Point(x, y); 1490 } 1491 1492 version(win32_widgets) 1493 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1494 1495 version(win32_widgets) 1496 /// Called when a WM_COMMAND is sent to the associated hwnd. 1497 void handleWmCommand(ushort cmd, ushort id) {} 1498 1499 version(win32_widgets) 1500 /++ 1501 Called when a WM_NOTIFY is sent to the associated hwnd. 1502 1503 History: 1504 +/ 1505 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1506 1507 version(win32_widgets) 1508 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); } 1509 1510 /++ 1511 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1512 1513 Updates to this variable will only be made visible on the next mouse enter event. 1514 +/ 1515 @scriptable string statusTip; 1516 // string toolTip; 1517 // string helpText; 1518 1519 /++ 1520 If true, this widget can be focused via keyboard control with the tab key. 1521 1522 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1523 +/ 1524 bool tabStop = true; 1525 /++ 1526 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.) 1527 +/ 1528 int tabOrder; 1529 1530 version(win32_widgets) { 1531 static Widget[HWND] nativeMapping; 1532 /// The native handle, if there is one. 1533 HWND hwnd; 1534 WNDPROC originalWindowProcedure; 1535 1536 SimpleWindow simpleWindowWrappingHwnd; 1537 1538 // please note it IGNORES your return value and does NOT forward it to Windows! 1539 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1540 return 0; 1541 } 1542 } 1543 private bool implicitlyCreated; 1544 1545 /// 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. 1546 int x; 1547 /// ditto 1548 int y; 1549 private int _width; 1550 private int _height; 1551 private Widget[] _children; 1552 private Widget _parent; 1553 private Window _parentWindow; 1554 1555 /++ 1556 Returns the window to which this widget is attached. 1557 1558 History: 1559 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. 1560 +/ 1561 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1562 private @property void parentWindow(Window parent) { 1563 auto old = _parentWindow; 1564 _parentWindow = parent; 1565 newParentWindow(old, _parentWindow); 1566 foreach(child; children) 1567 child.parentWindow = parent; // please note that this is recursive 1568 } 1569 1570 /++ 1571 Called when the widget has been added to or remove from a parent window. 1572 1573 Note that either oldParent and/or newParent may be null any time this is called. 1574 1575 History: 1576 Added September 13, 2024 1577 +/ 1578 protected void newParentWindow(Window oldParent, Window newParent) {} 1579 1580 /++ 1581 Returns the list of the widget's children. 1582 1583 History: 1584 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1585 1586 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1587 +/ 1588 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1589 1590 /++ 1591 Returns the widget's parent. 1592 1593 History: 1594 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1595 1596 The parent should only be managed by the [addChild] and [removeWidget] method. 1597 +/ 1598 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1599 1600 /// The widget's current size. 1601 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1602 /// ditto 1603 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1604 1605 /// Only the layout manager should be calling these. 1606 final protected @property int width(int a) @safe { return _width = a; } 1607 /// ditto 1608 final protected @property int height(int a) @safe { return _height = a; } 1609 1610 /++ 1611 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. 1612 1613 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 1614 +/ 1615 protected void registerMovement() { 1616 version(win32_widgets) { 1617 if(hwnd) { 1618 auto pos = getChildPositionRelativeToParentHwnd(this); 1619 MoveWindow(hwnd, pos[0], pos[1], width, height, true); // 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 1620 this.redraw(); 1621 } 1622 } 1623 sendResizeEvent(); 1624 } 1625 1626 /// Creates the widget and adds it to the parent. 1627 this(Widget parent) { 1628 if(parent !is null) 1629 parent.addChild(this); 1630 setupDefaultEventHandlers(); 1631 } 1632 1633 /// 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. 1634 @scriptable 1635 bool isFocused() { 1636 return parentWindow && parentWindow.focusedWidget is this; 1637 } 1638 1639 private bool showing_ = true; 1640 /// 1641 bool showing() const { return showing_; } 1642 /// 1643 bool hidden() const { return !showing_; } 1644 /++ 1645 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. 1646 1647 Note that a widget only ever shows if all its parents are showing too. 1648 +/ 1649 void showing(bool s, bool recalculate = true) { 1650 if(s != showing_) { 1651 showing_ = s; 1652 // writeln(typeid(this).toString, " ", this.parent ? typeid(this.parent).toString : "null", " ", s); 1653 1654 showNativeWindowChildren(s); 1655 1656 if(parent && recalculate) { 1657 parent.queueRecomputeChildLayout(); 1658 parent.redraw(); 1659 } 1660 1661 if(s) { 1662 queueRecomputeChildLayout(); 1663 redraw(); 1664 } 1665 } 1666 } 1667 /// Convenience method for `showing = true` 1668 @scriptable 1669 void show() { 1670 showing = true; 1671 } 1672 /// Convenience method for `showing = false` 1673 @scriptable 1674 void hide() { 1675 showing = false; 1676 } 1677 1678 /++ 1679 If you are a native window, show/hide it based on shouldShow and return `true`. 1680 1681 Otherwise, do nothing and return false. 1682 +/ 1683 protected bool showOrHideIfNativeWindow(bool shouldShow) { 1684 version(win32_widgets) { 1685 if(hwnd) { 1686 ShowWindow(hwnd, shouldShow ? SW_SHOW : SW_HIDE); 1687 return true; 1688 } else { 1689 return false; 1690 } 1691 } else { 1692 return false; 1693 } 1694 } 1695 1696 private void showNativeWindowChildren(bool s) { 1697 if(!showOrHideIfNativeWindow(s && showing)) 1698 foreach(child; children) 1699 child.showNativeWindowChildren(s); 1700 } 1701 1702 /// 1703 @scriptable 1704 void focus() { 1705 assert(parentWindow !is null); 1706 if(isFocused()) 1707 return; 1708 1709 if(parentWindow.focusedWidget) { 1710 // FIXME: more details here? like from and to 1711 auto from = parentWindow.focusedWidget; 1712 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 1713 parentWindow.focusedWidget = null; 1714 from.emit!BlurEvent(); 1715 this.emit!FocusOutEvent(); 1716 } 1717 1718 1719 version(win32_widgets) { 1720 if(this.hwnd !is null) 1721 SetFocus(this.hwnd); 1722 } 1723 //else static if(UsingSimpledisplayX11) 1724 //this.parentWindow.win.focus(); 1725 1726 parentWindow.focusedWidget = this; 1727 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 1728 this.emit!FocusEvent(); 1729 this.emit!FocusInEvent(); 1730 } 1731 1732 /+ 1733 /++ 1734 Unfocuses the widget. This may reset 1735 +/ 1736 @scriptable 1737 void blur() { 1738 1739 } 1740 +/ 1741 1742 1743 /++ 1744 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 1745 1746 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 1747 +/ 1748 void attachedToWindow(Window w) {} 1749 /++ 1750 Callback when the widget is added to another widget. 1751 1752 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 1753 +/ 1754 void addedTo(Widget w) {} 1755 1756 /++ 1757 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. 1758 1759 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 1760 +/ 1761 protected void addChild(Widget w, int position = int.max) { 1762 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 1763 assert(w !is this, "Child cannot be its own parent!"); 1764 w._parent = this; 1765 if(position == int.max || position == children.length) { 1766 _children ~= w; 1767 } else { 1768 assert(position < _children.length); 1769 _children.length = _children.length + 1; 1770 for(int i = cast(int) _children.length - 1; i > position; i--) 1771 _children[i] = _children[i - 1]; 1772 _children[position] = w; 1773 } 1774 1775 this.parentWindow = this._parentWindow; 1776 1777 w.addedTo(this); 1778 1779 bool parentIsNative; 1780 version(win32_widgets) { 1781 parentIsNative = hwnd !is null; 1782 } 1783 if(!parentIsNative && !showing) 1784 w.showOrHideIfNativeWindow(false); 1785 1786 if(parentWindow !is null) { 1787 w.attachedToWindow(parentWindow); 1788 parentWindow.queueRecomputeChildLayout(); 1789 parentWindow.redraw(); 1790 } 1791 } 1792 1793 /++ 1794 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. 1795 +/ 1796 Widget getChildAtPosition(int x, int y) { 1797 // it goes backward so the last one to show gets picked first 1798 // might use z-index later 1799 foreach_reverse(child; children) { 1800 if(child.hidden) 1801 continue; 1802 if(child.x <= x && child.y <= y 1803 && ((x - child.x) < child.width) 1804 && ((y - child.y) < child.height)) 1805 { 1806 return child; 1807 } 1808 } 1809 1810 return null; 1811 } 1812 1813 /++ 1814 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. 1815 1816 History: 1817 Added July 2, 2021 (v10.2) 1818 +/ 1819 protected void addScrollPosition(ref int x, ref int y) {}; 1820 1821 /++ 1822 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. 1823 1824 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. 1825 1826 [paint] is not called for system widgets as the OS library draws them instead. 1827 1828 1829 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. 1830 1831 You should also look at [WidgetPainter.visualTheme] to be theme aware. 1832 1833 History: 1834 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. 1835 +/ 1836 void paint(WidgetPainter painter) { 1837 version(win32_widgets) 1838 if(hwnd) { 1839 return; 1840 } 1841 painter.drawThemed(&paintContent); // note this refers to the following overload 1842 } 1843 1844 /++ 1845 Responsible for drawing the content as the theme engine is responsible for other elements. 1846 1847 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 1848 1849 Params: 1850 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. 1851 1852 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. 1853 1854 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. 1855 1856 Returns: 1857 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. 1858 1859 History: 1860 Added May 15, 2021 1861 +/ 1862 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 1863 return bounds; 1864 } 1865 1866 deprecated("Change ScreenPainter to WidgetPainter") 1867 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 1868 1869 /// I don't actually like the name of this 1870 /// this draws a background on it 1871 void erase(WidgetPainter painter) { 1872 version(win32_widgets) 1873 if(hwnd) return; // Windows will do it. I think. 1874 1875 auto c = getComputedStyle().background.color; 1876 painter.fillColor = c; 1877 painter.outlineColor = c; 1878 1879 version(win32_widgets) { 1880 HANDLE b, p; 1881 if(c.a == 0 && parent is parentWindow) { 1882 // I don't remember why I had this really... 1883 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 1884 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 1885 } 1886 } 1887 painter.drawRectangle(Point(0, 0), width, height); 1888 version(win32_widgets) { 1889 if(c.a == 0 && parent is parentWindow) { 1890 SelectObject(painter.impl.hdc, p); 1891 SelectObject(painter.impl.hdc, b); 1892 } 1893 } 1894 } 1895 1896 /// 1897 WidgetPainter draw() { 1898 int x = this.x, y = this.y; 1899 auto parent = this.parent; 1900 while(parent) { 1901 x += parent.x; 1902 y += parent.y; 1903 parent = parent.parent; 1904 } 1905 1906 auto painter = parentWindow.win.draw(true); 1907 painter.originX = x; 1908 painter.originY = y; 1909 painter.setClipRectangle(Point(0, 0), width, height); 1910 return WidgetPainter(painter, this); 1911 } 1912 1913 /// 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. 1914 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 1915 if(hidden) 1916 return; 1917 1918 int paintX = x; 1919 int paintY = y; 1920 if(this.useNativeDrawing()) { 1921 paintX = 0; 1922 paintY = 0; 1923 lox = 0; 1924 loy = 0; 1925 containment = Rectangle(0, 0, int.max, int.max); 1926 } 1927 1928 painter.originX = lox + paintX; 1929 painter.originY = loy + paintY; 1930 1931 bool actuallyPainted = false; 1932 1933 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 1934 if(clip == Rectangle.init) { 1935 // writeln(this, " clipped out"); 1936 return; 1937 } 1938 1939 bool invalidateChildren = invalidate; 1940 1941 if(redrawRequested || force) { 1942 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 1943 1944 painter.drawingUpon = this; 1945 1946 erase(painter); 1947 if(painter.visualTheme) 1948 painter.visualTheme.doPaint(this, painter); 1949 else 1950 paint(painter); 1951 1952 if(invalidate) { 1953 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 1954 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 1955 painter.invalidateRect(region); 1956 // children are contained inside this, so no need to do extra work 1957 invalidateChildren = false; 1958 } 1959 1960 redrawRequested = false; 1961 actuallyPainted = true; 1962 } 1963 1964 foreach(child; children) { 1965 version(win32_widgets) 1966 if(child.useNativeDrawing()) continue; 1967 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 1968 } 1969 1970 version(win32_widgets) 1971 foreach(child; children) { 1972 if(child.useNativeDrawing) { 1973 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 1974 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 1975 } 1976 } 1977 } 1978 1979 protected bool useNativeDrawing() nothrow { 1980 version(win32_widgets) 1981 return hwnd !is null; 1982 else 1983 return false; 1984 } 1985 1986 private static class RedrawEvent {} 1987 private __gshared re = new RedrawEvent(); 1988 1989 private bool redrawRequested; 1990 /// 1991 final void redraw(string file = __FILE__, size_t line = __LINE__) { 1992 redrawRequested = true; 1993 1994 if(this.parentWindow) { 1995 auto sw = this.parentWindow.win; 1996 assert(sw !is null); 1997 if(!sw.eventQueued!RedrawEvent) { 1998 sw.postEvent(re); 1999 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 2000 } 2001 } 2002 } 2003 2004 private SimpleWindow drawableWindow; 2005 2006 /++ 2007 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. 2008 2009 Returns: 2010 `true` if you should do your default behavior. 2011 2012 History: 2013 Added May 5, 2021 2014 2015 Bugs: 2016 It does not do the static checks on gdc right now. 2017 +/ 2018 final protected bool emit(EventType, this This, Args...)(Args args) { 2019 version(GNU) {} else 2020 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 2021 auto e = new EventType(this, args); 2022 e.dispatch(); 2023 return !e.defaultPrevented; 2024 } 2025 /// ditto 2026 final protected bool emit(string eventString, this This)() { 2027 auto e = new Event(eventString, this); 2028 e.dispatch(); 2029 return !e.defaultPrevented; 2030 } 2031 2032 /++ 2033 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. 2034 2035 History: 2036 Added May 5, 2021 2037 +/ 2038 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 2039 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 2040 return addEventListener(handler); 2041 } 2042 2043 /++ 2044 Gets the computed style properties from the visual theme. 2045 2046 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].) 2047 2048 History: 2049 Added May 8, 2021 2050 +/ 2051 final StyleInformation getComputedStyle() { 2052 return StyleInformation(this); 2053 } 2054 2055 int focusableWidgets(scope int delegate(Widget) dg) { 2056 foreach(widget; WidgetStream(this)) { 2057 if(widget.tabStop && !widget.hidden) { 2058 int result = dg(widget); 2059 if (result) 2060 return result; 2061 } 2062 } 2063 return 0; 2064 } 2065 2066 /++ 2067 Calculates the border box (that is, the full width/height of the widget, from border edge to border edge) 2068 for the given content box (the area between the padding) 2069 2070 History: 2071 Added January 4, 2023 (dub v11.0) 2072 +/ 2073 Rectangle borderBoxForContentBox(Rectangle contentBox) { 2074 auto cs = getComputedStyle(); 2075 2076 auto borderWidth = getBorderWidth(cs.borderStyle); 2077 2078 auto rect = contentBox; 2079 2080 rect.left -= borderWidth; 2081 rect.right += borderWidth; 2082 rect.top -= borderWidth; 2083 rect.bottom += borderWidth; 2084 2085 auto insideBorderRect = rect; 2086 2087 rect.left -= cs.paddingLeft; 2088 rect.right += cs.paddingRight; 2089 rect.top -= cs.paddingTop; 2090 rect.bottom += cs.paddingBottom; 2091 2092 return rect; 2093 } 2094 2095 2096 // FIXME: I kinda want to hide events from implementation widgets 2097 // so it just catches them all and stops propagation... 2098 // i guess i can do it with a event listener on star. 2099 2100 mixin Emits!KeyDownEvent; /// 2101 mixin Emits!KeyUpEvent; /// 2102 mixin Emits!CharEvent; /// 2103 2104 mixin Emits!MouseDownEvent; /// 2105 mixin Emits!MouseUpEvent; /// 2106 mixin Emits!ClickEvent; /// 2107 mixin Emits!DoubleClickEvent; /// 2108 mixin Emits!MouseMoveEvent; /// 2109 mixin Emits!MouseOverEvent; /// 2110 mixin Emits!MouseOutEvent; /// 2111 mixin Emits!MouseEnterEvent; /// 2112 mixin Emits!MouseLeaveEvent; /// 2113 2114 mixin Emits!ResizeEvent; /// 2115 2116 mixin Emits!BlurEvent; /// 2117 mixin Emits!FocusEvent; /// 2118 2119 mixin Emits!FocusInEvent; /// 2120 mixin Emits!FocusOutEvent; /// 2121 } 2122 2123 /+ 2124 /++ 2125 Interface to indicate that the widget has a simple value property. 2126 2127 History: 2128 Added August 26, 2021 2129 +/ 2130 interface HasValue!T { 2131 /// Getter 2132 @property T value(); 2133 /// Setter 2134 @property void value(T); 2135 } 2136 2137 /++ 2138 Interface to indicate that the widget has a range of possible values for its simple value property. 2139 This would be present on something like a slider or possibly a number picker. 2140 2141 History: 2142 Added September 11, 2021 2143 +/ 2144 interface HasRangeOfValues!T : HasValue!T { 2145 /// The minimum and maximum values in the range, inclusive. 2146 @property T minValue(); 2147 @property void minValue(T); /// ditto 2148 @property T maxValue(); /// ditto 2149 @property void maxValue(T); /// ditto 2150 2151 /// The smallest step the user interface allows. User may still type in values without this limitation. 2152 @property void step(T); 2153 @property T step(); /// ditto 2154 } 2155 2156 /++ 2157 Interface to indicate that the widget has a list of possible values the user can choose from. 2158 This would be present on something like a drop-down selector. 2159 2160 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2161 combobox. 2162 2163 History: 2164 Added September 11, 2021 2165 +/ 2166 interface HasListOfValues!T : HasValue!T { 2167 @property T[] values; 2168 @property void values(T[]); 2169 2170 @property int selectedIndex(); // note it may return -1! 2171 @property void selectedIndex(int); 2172 } 2173 +/ 2174 2175 /++ 2176 History: 2177 Added September 2021 (dub v10.4) 2178 +/ 2179 class GridLayout : Layout { 2180 2181 // 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. 2182 2183 /++ 2184 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2185 +/ 2186 enum Gravity { 2187 Center = 0, 2188 NorthWest = North | West, 2189 North = 0b10_00, 2190 NorthEast = North | East, 2191 West = 0b00_10, 2192 East = 0b00_01, 2193 SouthWest = South | West, 2194 South = 0b01_00, 2195 SouthEast = South | East, 2196 } 2197 2198 /++ 2199 The width and height are in some proportional units and can often just be 12. 2200 +/ 2201 this(int width, int height, Widget parent) { 2202 this.gridWidth = width; 2203 this.gridHeight = height; 2204 super(parent); 2205 } 2206 2207 /++ 2208 Sets the position of the given child. 2209 2210 The units of these arguments are in the proportional grid units you set in the constructor. 2211 +/ 2212 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2213 // ensure it is in bounds 2214 // then ensure no overlaps 2215 2216 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2217 2218 foreach(ref position; positions) { 2219 if(position.widget is child) { 2220 position = p; 2221 goto set; 2222 } 2223 } 2224 2225 positions ~= p; 2226 2227 set: 2228 2229 // FIXME: should this batch? 2230 queueRecomputeChildLayout(); 2231 2232 return child; 2233 } 2234 2235 override void addChild(Widget w, int position = int.max) { 2236 super.addChild(w, position); 2237 //positions ~= ChildPosition(w); 2238 if(position != int.max) { 2239 // FIXME: align it so they actually match. 2240 } 2241 } 2242 2243 override void widgetRemoved(size_t idx, Widget w) { 2244 // FIXME: keep the positions array aligned 2245 // positions[idx].widget = null; 2246 } 2247 2248 override void recomputeChildLayout() { 2249 registerMovement(); 2250 int onGrid = cast(int) positions.length; 2251 c: foreach(child; children) { 2252 // just snap it to the grid 2253 if(onGrid) 2254 foreach(position; positions) 2255 if(position.widget is child) { 2256 child.x = this.width * position.x / this.gridWidth; 2257 child.y = this.height * position.y / this.gridHeight; 2258 child.width = this.width * position.width / this.gridWidth; 2259 child.height = this.height * position.height / this.gridHeight; 2260 2261 auto diff = child.width - child.maxWidth(); 2262 // FIXME: gravity? 2263 if(diff > 0) { 2264 child.width = child.width - diff; 2265 2266 if(position.gravity & Gravity.West) { 2267 // nothing needed, already aligned 2268 } else if(position.gravity & Gravity.East) { 2269 child.x += diff; 2270 } else { 2271 child.x += diff / 2; 2272 } 2273 } 2274 2275 diff = child.height - child.maxHeight(); 2276 // FIXME: gravity? 2277 if(diff > 0) { 2278 child.height = child.height - diff; 2279 2280 if(position.gravity & Gravity.North) { 2281 // nothing needed, already aligned 2282 } else if(position.gravity & Gravity.South) { 2283 child.y += diff; 2284 } else { 2285 child.y += diff / 2; 2286 } 2287 } 2288 2289 2290 child.recomputeChildLayout(); 2291 onGrid--; 2292 continue c; 2293 } 2294 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2295 } 2296 } 2297 2298 private struct ChildPosition { 2299 Widget widget; 2300 int x; 2301 int y; 2302 int width; 2303 int height; 2304 Gravity gravity; 2305 } 2306 private ChildPosition[] positions; 2307 2308 int gridWidth = 12; 2309 int gridHeight = 12; 2310 } 2311 2312 /// 2313 abstract class ComboboxBase : Widget { 2314 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2315 // or to always show the list, we want CBS_SIMPLE == 1 2316 version(win32_widgets) 2317 this(uint style, Widget parent) { 2318 super(parent); 2319 createWin32Window(this, "ComboBox"w, null, style); 2320 } 2321 else version(custom_widgets) 2322 this(Widget parent) { 2323 super(parent); 2324 2325 addEventListener((KeyDownEvent event) { 2326 if(event.key == Key.Up) { 2327 if(selection_ > -1) { // -1 means select blank 2328 selection_--; 2329 fireChangeEvent(); 2330 } 2331 event.preventDefault(); 2332 } 2333 if(event.key == Key.Down) { 2334 if(selection_ + 1 < options.length) { 2335 selection_++; 2336 fireChangeEvent(); 2337 } 2338 event.preventDefault(); 2339 } 2340 2341 }); 2342 2343 } 2344 else static assert(false); 2345 2346 /++ 2347 Returns the current list of options in the selection. 2348 2349 History: 2350 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2351 +/ 2352 final @property string[] options() const { 2353 return cast(string[]) options_; 2354 } 2355 2356 private string[] options_; 2357 private int selection_ = -1; 2358 2359 /++ 2360 Adds an option to the end of options array. 2361 +/ 2362 void addOption(string s) { 2363 options_ ~= s; 2364 version(win32_widgets) 2365 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2366 } 2367 2368 /++ 2369 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2370 +/ 2371 int getSelection() { 2372 return selection_; 2373 } 2374 2375 /++ 2376 Returns the current selection as a string. 2377 2378 History: 2379 Added November 17, 2021 2380 +/ 2381 string getSelectionString() { 2382 return selection_ == -1 ? null : options[selection_]; 2383 } 2384 2385 /++ 2386 Sets the current selection to an index in the options array, or to the given option if present. 2387 Please note that the string version may do a linear lookup. 2388 2389 Returns: 2390 the index you passed in 2391 2392 History: 2393 The `string` based overload was added on March 1, 2022 (dub v10.7). 2394 2395 The return value was `void` prior to March 1, 2022. 2396 +/ 2397 int setSelection(int idx) { 2398 selection_ = idx; 2399 version(win32_widgets) 2400 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2401 2402 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2403 t.dispatch(); 2404 2405 return idx; 2406 } 2407 2408 /// ditto 2409 int setSelection(string s) { 2410 if(s !is null) 2411 foreach(idx, item; options) 2412 if(item == s) { 2413 return setSelection(cast(int) idx); 2414 } 2415 return setSelection(-1); 2416 } 2417 2418 /++ 2419 This event is fired when the selection changes. Note it inherits 2420 from ChangeEvent!string, meaning you can use that as well, and it also 2421 fills in [Event.intValue]. 2422 +/ 2423 static class SelectionChangedEvent : ChangeEvent!string { 2424 this(Widget target, int iv, string sv) { 2425 super(target, &stringValue); 2426 this.iv = iv; 2427 this.sv = sv; 2428 } 2429 immutable int iv; 2430 immutable string sv; 2431 2432 override @property string stringValue() { return sv; } 2433 override @property int intValue() { return iv; } 2434 } 2435 2436 version(win32_widgets) 2437 override void handleWmCommand(ushort cmd, ushort id) { 2438 if(cmd == CBN_SELCHANGE) { 2439 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2440 fireChangeEvent(); 2441 } 2442 } 2443 2444 private void fireChangeEvent() { 2445 if(selection_ >= options.length) 2446 selection_ = -1; 2447 2448 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2449 t.dispatch(); 2450 } 2451 2452 version(win32_widgets) { 2453 override int minHeight() { return defaultLineHeight + 6; } 2454 override int maxHeight() { return defaultLineHeight + 6; } 2455 } else { 2456 override int minHeight() { return defaultLineHeight + 4; } 2457 override int maxHeight() { return defaultLineHeight + 4; } 2458 } 2459 2460 version(custom_widgets) { 2461 2462 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2463 2464 SimpleWindow dropDown; 2465 void popup() { 2466 auto w = width; 2467 // FIXME: suggestedDropdownHeight see below 2468 auto h = cast(int) this.options.length * defaultLineHeight + 8; 2469 2470 auto coord = this.globalCoordinates(); 2471 auto dropDown = new SimpleWindow( 2472 w, h, 2473 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null); 2474 2475 dropDown.move(coord.x, coord.y + this.height); 2476 2477 { 2478 auto cs = getComputedStyle(); 2479 auto painter = dropDown.draw(); 2480 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2481 auto p = Point(4, 4); 2482 painter.outlineColor = cs.foregroundColor; 2483 foreach(option; options) { 2484 painter.drawText(p, option); 2485 p.y += defaultLineHeight; 2486 } 2487 } 2488 2489 dropDown.setEventHandlers( 2490 (MouseEvent event) { 2491 if(event.type == MouseEventType.buttonReleased) { 2492 dropDown.close(); 2493 auto element = (event.y - 4) / defaultLineHeight; 2494 if(element >= 0 && element <= options.length) { 2495 selection_ = element; 2496 2497 fireChangeEvent(); 2498 } 2499 } 2500 } 2501 ); 2502 2503 dropDown.visibilityChanged = (bool visible) { 2504 if(visible) { 2505 this.redraw(); 2506 dropDown.grabInput(); 2507 } else { 2508 dropDown.releaseInputGrab(); 2509 } 2510 }; 2511 2512 dropDown.show(); 2513 } 2514 2515 } 2516 } 2517 2518 /++ 2519 A drop-down list where the user must select one of the 2520 given options. Like `<select>` in HTML. 2521 +/ 2522 class DropDownSelection : ComboboxBase { 2523 this(Widget parent) { 2524 version(win32_widgets) 2525 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 2526 else version(custom_widgets) { 2527 super(parent); 2528 2529 addEventListener("focus", () { this.redraw; }); 2530 addEventListener("blur", () { this.redraw; }); 2531 addEventListener(EventType.change, () { this.redraw; }); 2532 addEventListener("mousedown", () { this.focus(); this.popup(); }); 2533 addEventListener((KeyDownEvent event) { 2534 if(event.key == Key.Space) 2535 popup(); 2536 }); 2537 } else static assert(false); 2538 } 2539 2540 mixin Padding!q{2}; 2541 static class Style : Widget.Style { 2542 override FrameStyle borderStyle() { return FrameStyle.risen; } 2543 } 2544 mixin OverrideStyle!Style; 2545 2546 version(custom_widgets) 2547 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2548 auto cs = getComputedStyle(); 2549 2550 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 2551 2552 painter.outlineColor = cs.foregroundColor; 2553 painter.fillColor = cs.foregroundColor; 2554 2555 /+ 2556 Point[4] triangle; 2557 enum padding = 6; 2558 enum paddingV = 7; 2559 enum triangleWidth = 10; 2560 triangle[0] = Point(width - padding - triangleWidth, paddingV); 2561 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 2562 triangle[2] = Point(width - padding - 0, paddingV); 2563 triangle[3] = triangle[0]; 2564 painter.drawPolygon(triangle[]); 2565 +/ 2566 2567 auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2); 2568 2569 painter.drawPolygon( 2570 scaleWithDpi(Point(2, 6) + offset), 2571 scaleWithDpi(Point(7, 11) + offset), 2572 scaleWithDpi(Point(12, 6) + offset), 2573 scaleWithDpi(Point(2, 6) + offset) 2574 ); 2575 2576 2577 return bounds; 2578 } 2579 2580 version(win32_widgets) 2581 override void registerMovement() { 2582 version(win32_widgets) { 2583 if(hwnd) { 2584 auto pos = getChildPositionRelativeToParentHwnd(this); 2585 // the height given to this from Windows' perspective is supposed 2586 // to include the drop down's height. so I add to it to give some 2587 // room for that. 2588 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 2589 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 2590 } 2591 } 2592 sendResizeEvent(); 2593 } 2594 } 2595 2596 /++ 2597 A text box with a drop down arrow listing selections. 2598 The user can choose from the list, or type their own. 2599 +/ 2600 class FreeEntrySelection : ComboboxBase { 2601 this(Widget parent) { 2602 version(win32_widgets) 2603 super(2 /* CBS_DROPDOWN */, parent); 2604 else version(custom_widgets) { 2605 super(parent); 2606 auto hl = new HorizontalLayout(this); 2607 lineEdit = new LineEdit(hl); 2608 2609 tabStop = false; 2610 2611 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2612 2613 auto btn = new class ArrowButton { 2614 this() { 2615 super(ArrowDirection.down, hl); 2616 } 2617 override int maxHeight() { 2618 return lineEdit.maxHeight; 2619 } 2620 }; 2621 //btn.addDirectEventListener("focus", &lineEdit.focus); 2622 btn.addEventListener("triggered", &this.popup); 2623 addEventListener(EventType.change, (Event event) { 2624 lineEdit.content = event.stringValue; 2625 lineEdit.focus(); 2626 redraw(); 2627 }); 2628 } 2629 else static assert(false); 2630 } 2631 2632 version(custom_widgets) { 2633 LineEdit lineEdit; 2634 } 2635 } 2636 2637 /++ 2638 A combination of free entry with a list below it. 2639 +/ 2640 class ComboBox : ComboboxBase { 2641 this(Widget parent) { 2642 version(win32_widgets) 2643 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 2644 else version(custom_widgets) { 2645 super(parent); 2646 lineEdit = new LineEdit(this); 2647 listWidget = new ListWidget(this); 2648 listWidget.multiSelect = false; 2649 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 2650 string c = null; 2651 foreach(option; listWidget.options) 2652 if(option.selected) { 2653 c = option.label; 2654 break; 2655 } 2656 lineEdit.content = c; 2657 }); 2658 2659 listWidget.tabStop = false; 2660 this.tabStop = false; 2661 listWidget.addEventListener("focus", &lineEdit.focus); 2662 this.addEventListener("focus", &lineEdit.focus); 2663 2664 addDirectEventListener(EventType.change, { 2665 listWidget.setSelection(selection_); 2666 if(selection_ != -1) 2667 lineEdit.content = options[selection_]; 2668 lineEdit.focus(); 2669 redraw(); 2670 }); 2671 2672 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2673 2674 listWidget.addDirectEventListener(EventType.change, { 2675 int set = -1; 2676 foreach(idx, opt; listWidget.options) 2677 if(opt.selected) { 2678 set = cast(int) idx; 2679 break; 2680 } 2681 if(set != selection_) 2682 this.setSelection(set); 2683 }); 2684 } else static assert(false); 2685 } 2686 2687 override int minHeight() { return defaultLineHeight * 3; } 2688 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 2689 override int heightStretchiness() { return 5; } 2690 2691 version(custom_widgets) { 2692 LineEdit lineEdit; 2693 ListWidget listWidget; 2694 2695 override void addOption(string s) { 2696 listWidget.options ~= ListWidget.Option(s); 2697 ComboboxBase.addOption(s); 2698 } 2699 } 2700 } 2701 2702 /+ 2703 class Spinner : Widget { 2704 version(win32_widgets) 2705 this(Widget parent) { 2706 super(parent); 2707 parentWindow = parent.parentWindow; 2708 auto hlayout = new HorizontalLayout(this); 2709 lineEdit = new LineEdit(hlayout); 2710 upDownControl = new UpDownControl(hlayout); 2711 } 2712 2713 LineEdit lineEdit; 2714 UpDownControl upDownControl; 2715 } 2716 2717 class UpDownControl : Widget { 2718 version(win32_widgets) 2719 this(Widget parent) { 2720 super(parent); 2721 parentWindow = parent.parentWindow; 2722 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 2723 } 2724 2725 override int minHeight() { return defaultLineHeight; } 2726 override int maxHeight() { return defaultLineHeight * 3/2; } 2727 2728 override int minWidth() { return defaultLineHeight * 3/2; } 2729 override int maxWidth() { return defaultLineHeight * 3/2; } 2730 } 2731 +/ 2732 2733 /+ 2734 class DataView : Widget { 2735 // this is the omnibus data viewer 2736 // the internal data layout is something like: 2737 // string[string][] but also each node can have parents 2738 } 2739 +/ 2740 2741 2742 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 2743 2744 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 2745 2746 // FIXME: menus should prolly capture the mouse. ugh i kno. 2747 /* 2748 TextEdit needs: 2749 2750 * caret manipulation 2751 * selection control 2752 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 2753 2754 For example: 2755 2756 connect(paste, &textEdit.insertTextAtCaret); 2757 2758 would be nice. 2759 2760 2761 2762 I kinda want an omnibus dataview that combines list, tree, 2763 and table - it can be switched dynamically between them. 2764 2765 Flattening policy: only show top level, show recursive, show grouped 2766 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 2767 2768 Single select, multi select, organization, drag+drop 2769 */ 2770 2771 //static if(UsingSimpledisplayX11) 2772 version(win32_widgets) {} 2773 else version(custom_widgets) { 2774 enum scrollClickRepeatInterval = 50; 2775 2776 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 2777 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 2778 enum activeTabColor = lightAccentColor; 2779 enum hoveringColor = Color(228, 228, 228); 2780 enum buttonColor = windowBackgroundColor; 2781 enum depressedButtonColor = darkAccentColor; 2782 enum activeListXorColor = Color(255, 255, 127); 2783 enum progressBarColor = Color(0, 0, 128); 2784 enum activeMenuItemColor = Color(0, 0, 128); 2785 2786 }} 2787 else static assert(false); 2788 deprecated("Get these properties off the `visualTheme` instead.") { 2789 // these are used by horizontal rule so not just custom_widgets. for now at least. 2790 enum darkAccentColor = Color(172, 172, 172); 2791 enum lightAccentColor = Color(223, 223, 223); // used to be 223 2792 } 2793 2794 private const(wchar)* toWstringzInternal(in char[] s) { 2795 wchar[] str; 2796 str.reserve(s.length + 1); 2797 foreach(dchar ch; s) 2798 str ~= ch; 2799 str ~= '\0'; 2800 return str.ptr; 2801 } 2802 2803 static if(SimpledisplayTimerAvailable) 2804 void setClickRepeat(Widget w, int interval, int delay = 250) { 2805 Timer timer; 2806 int delayRemaining = delay / interval; 2807 if(delayRemaining <= 1) 2808 delayRemaining = 2; 2809 2810 immutable originalDelayRemaining = delayRemaining; 2811 2812 w.addDirectEventListener((scope MouseDownEvent ev) { 2813 if(ev.srcElement !is w) 2814 return; 2815 if(timer !is null) { 2816 timer.destroy(); 2817 timer = null; 2818 } 2819 delayRemaining = originalDelayRemaining; 2820 timer = new Timer(interval, () { 2821 if(delayRemaining > 0) 2822 delayRemaining--; 2823 else { 2824 auto ev = new Event("triggered", w); 2825 ev.sendDirectly(); 2826 } 2827 }); 2828 }); 2829 2830 w.addDirectEventListener((scope MouseUpEvent ev) { 2831 if(ev.srcElement !is w) 2832 return; 2833 if(timer !is null) { 2834 timer.destroy(); 2835 timer = null; 2836 } 2837 }); 2838 2839 w.addDirectEventListener((scope MouseLeaveEvent ev) { 2840 if(ev.srcElement !is w) 2841 return; 2842 if(timer !is null) { 2843 timer.destroy(); 2844 timer = null; 2845 } 2846 }); 2847 2848 } 2849 else 2850 void setClickRepeat(Widget w, int interval, int delay = 250) {} 2851 2852 enum FrameStyle { 2853 none, /// 2854 risen, /// a 3d pop-out effect (think Windows 95 button) 2855 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 2856 solid, /// 2857 dotted, /// 2858 fantasy, /// a style based on a popular fantasy video game 2859 rounded, /// a rounded rectangle 2860 } 2861 2862 version(custom_widgets) 2863 deprecated 2864 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 2865 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2866 } 2867 2868 version(custom_widgets) 2869 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 2870 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 2871 } 2872 2873 version(custom_widgets) 2874 deprecated 2875 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 2876 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2877 } 2878 2879 int getBorderWidth(FrameStyle style) { 2880 final switch(style) { 2881 case FrameStyle.sunk, FrameStyle.risen: 2882 return 2; 2883 case FrameStyle.none: 2884 return 0; 2885 case FrameStyle.solid: 2886 return 1; 2887 case FrameStyle.dotted: 2888 return 1; 2889 case FrameStyle.fantasy: 2890 return 3; 2891 case FrameStyle.rounded: 2892 return 2; 2893 } 2894 } 2895 2896 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 2897 int borderWidth = getBorderWidth(style); 2898 final switch(style) { 2899 case FrameStyle.sunk, FrameStyle.risen: 2900 // outer layer 2901 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 2902 break; 2903 case FrameStyle.none: 2904 painter.outlineColor = background; 2905 break; 2906 case FrameStyle.solid: 2907 case FrameStyle.rounded: 2908 painter.pen = Pen(border, 1); 2909 break; 2910 case FrameStyle.dotted: 2911 painter.pen = Pen(border, 1, Pen.Style.Dotted); 2912 break; 2913 case FrameStyle.fantasy: 2914 painter.pen = Pen(border, 3); 2915 break; 2916 } 2917 2918 painter.fillColor = background; 2919 2920 if(style == FrameStyle.rounded) { 2921 painter.drawRectangleRounded(Point(x, y), Size(width, height), 6); 2922 } else { 2923 painter.drawRectangle(Point(x + 0, y + 0), width, height); 2924 2925 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 2926 // 3d effect 2927 auto vt = WidgetPainter.visualTheme; 2928 2929 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 2930 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 2931 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 2932 2933 // inner layer 2934 //right, bottom 2935 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 2936 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 2937 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 2938 // left, top 2939 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 2940 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 2941 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 2942 } else if(style == FrameStyle.fantasy) { 2943 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 2944 painter.fillColor = Color.transparent; 2945 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 2946 } 2947 } 2948 2949 return borderWidth; 2950 } 2951 2952 /++ 2953 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. 2954 2955 See_Also: 2956 [MenuItem] 2957 [ToolButton] 2958 [Menu.addItem] 2959 +/ 2960 class Action { 2961 version(win32_widgets) { 2962 private int id; 2963 private static int lastId = 9000; 2964 private static Action[int] mapping; 2965 } 2966 2967 KeyEvent accelerator; 2968 2969 // FIXME: disable message 2970 // and toggle thing? 2971 // ??? and trigger arguments too ??? 2972 2973 /++ 2974 Params: 2975 label = the textual label 2976 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 2977 triggered = initial handler, more can be added via the [triggered] member. 2978 +/ 2979 this(string label, ushort icon = 0, void delegate() triggered = null) { 2980 this.label = label; 2981 this.iconId = icon; 2982 if(triggered !is null) 2983 this.triggered ~= triggered; 2984 version(win32_widgets) { 2985 id = ++lastId; 2986 mapping[id] = this; 2987 } 2988 } 2989 2990 private string label; 2991 private ushort iconId; 2992 // icon 2993 2994 // when it is triggered, the triggered event is fired on the window 2995 /// The list of handlers when it is triggered. 2996 void delegate()[] triggered; 2997 } 2998 2999 /* 3000 plan: 3001 keyboard accelerators 3002 3003 * menus (and popups and tooltips) 3004 * status bar 3005 * toolbars and buttons 3006 3007 sortable table view 3008 3009 maybe notification area icons 3010 basic clipboard 3011 3012 * radio box 3013 splitter 3014 toggle buttons (optionally mutually exclusive, like in Paint) 3015 label, rich text display, multi line plain text (selectable) 3016 * fieldset 3017 * nestable grid layout 3018 single line text input 3019 * multi line text input 3020 slider 3021 spinner 3022 list box 3023 drop down 3024 combo box 3025 auto complete box 3026 * progress bar 3027 3028 terminal window/widget (on unix it might even be a pty but really idk) 3029 3030 ok button 3031 cancel button 3032 3033 keyboard hotkeys 3034 3035 scroll widget 3036 3037 event redirections and network transparency 3038 script integration 3039 */ 3040 3041 3042 /* 3043 MENUS 3044 3045 auto bar = new MenuBar(window); 3046 window.menuBar = bar; 3047 3048 auto fileMenu = bar.addItem(new Menu("&File")); 3049 fileMenu.addItem(new MenuItem("&Exit")); 3050 3051 3052 EVENTS 3053 3054 For controls, you should usually use "triggered" rather than "click", etc., because 3055 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 3056 This is the case on menus and pushbuttons. 3057 3058 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 3059 */ 3060 3061 3062 /* 3063 enum LinePreference { 3064 AlwaysOnOwnLine, // always on its own line 3065 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 3066 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 3067 } 3068 */ 3069 3070 /++ 3071 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. 3072 3073 --- 3074 class MyWidget : Widget { 3075 this(Widget parent) { super(parent); } 3076 3077 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 3078 mixin Padding!q{4}; 3079 3080 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 3081 mixin Margin!q{8}; 3082 3083 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 3084 // while Top/Bottom/Right remain 8 from the mixin above. 3085 override int marginLeft() { return 2; } 3086 } 3087 --- 3088 3089 3090 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]). 3091 3092 Padding is the area inside a widget where its background is drawn, but the content avoids. 3093 3094 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!). 3095 3096 * 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. 3097 +/ 3098 mixin template Padding(string code) { 3099 override int paddingLeft() { return mixin(code);} 3100 override int paddingRight() { return mixin(code);} 3101 override int paddingTop() { return mixin(code);} 3102 override int paddingBottom() { return mixin(code);} 3103 } 3104 3105 /// ditto 3106 mixin template Margin(string code) { 3107 override int marginLeft() { return mixin(code);} 3108 override int marginRight() { return mixin(code);} 3109 override int marginTop() { return mixin(code);} 3110 override int marginBottom() { return mixin(code);} 3111 } 3112 3113 private 3114 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3115 enum calcingV = relevantMeasure == "height"; 3116 3117 parent.registerMovement(); 3118 3119 if(parent.children.length == 0) 3120 return; 3121 3122 auto parentStyle = parent.getComputedStyle(); 3123 3124 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3125 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3126 3127 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3128 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3129 3130 // my own width and height should already be set by the caller of this function... 3131 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3132 mixin("parentStyle.padding"~firstThingy~"()") - 3133 mixin("parentStyle.padding"~secondThingy~"()"); 3134 3135 int stretchinessSum; 3136 int stretchyChildSum; 3137 int lastMargin = 0; 3138 3139 int shrinkinessSum; 3140 int shrinkyChildSum; 3141 3142 // set initial size 3143 foreach(child; parent.children) { 3144 3145 auto childStyle = child.getComputedStyle(); 3146 3147 if(cast(StaticPosition) child) 3148 continue; 3149 if(child.hidden) 3150 continue; 3151 3152 const iw = child.flexBasisWidth(); 3153 const ih = child.flexBasisHeight(); 3154 3155 static if(calcingV) { 3156 child.width = parent.width - 3157 mixin("childStyle.margin"~otherFirstThingy~"()") - 3158 mixin("childStyle.margin"~otherSecondThingy~"()") - 3159 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3160 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3161 3162 if(child.width < 0) 3163 child.width = 0; 3164 if(child.width > childStyle.maxWidth()) 3165 child.width = childStyle.maxWidth(); 3166 3167 if(iw > 0) { 3168 auto totalPossible = child.width; 3169 if(child.width > iw && child.widthStretchiness() == 0) 3170 child.width = iw; 3171 } 3172 3173 child.height = mymax(childStyle.minHeight(), ih); 3174 } else { 3175 // set to take all the space 3176 child.height = parent.height - 3177 mixin("childStyle.margin"~firstThingy~"()") - 3178 mixin("childStyle.margin"~secondThingy~"()") - 3179 mixin("parentStyle.padding"~firstThingy~"()") - 3180 mixin("parentStyle.padding"~secondThingy~"()"); 3181 3182 // then clamp it 3183 if(child.height < 0) 3184 child.height = 0; 3185 if(child.height > childStyle.maxHeight()) 3186 child.height = childStyle.maxHeight(); 3187 3188 // and if possible, respect the ideal target 3189 if(ih > 0) { 3190 auto totalPossible = child.height; 3191 if(child.height > ih && child.heightStretchiness() == 0) 3192 child.height = ih; 3193 } 3194 3195 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3196 child.width = mymax(childStyle.minWidth(), iw); 3197 } 3198 3199 spaceRemaining -= mixin("child." ~ relevantMeasure); 3200 3201 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3202 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3203 lastMargin = margin; 3204 spaceRemaining -= thisMargin + margin; 3205 3206 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3207 stretchinessSum += s; 3208 if(s > 0) 3209 stretchyChildSum++; 3210 3211 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3212 shrinkinessSum += s2; 3213 if(s2 > 0) 3214 shrinkyChildSum++; 3215 } 3216 3217 if(spaceRemaining < 0 && shrinkyChildSum) { 3218 // shrink to get into the space if it is possible 3219 auto toRemove = -spaceRemaining; 3220 auto removalPerItem = toRemove / shrinkinessSum; 3221 auto remainder = toRemove % shrinkinessSum; 3222 3223 // FIXME: wtf why am i shrinking things with no shrinkiness? 3224 3225 foreach(child; parent.children) { 3226 auto childStyle = child.getComputedStyle(); 3227 if(cast(StaticPosition) child) 3228 continue; 3229 if(child.hidden) 3230 continue; 3231 static if(calcingV) { 3232 auto minimum = childStyle.minHeight(); 3233 auto stretch = childStyle.heightShrinkiness(); 3234 } else { 3235 auto minimum = childStyle.minWidth(); 3236 auto stretch = childStyle.widthShrinkiness(); 3237 } 3238 3239 if(mixin("child._" ~ relevantMeasure) <= minimum) 3240 continue; 3241 // import arsd.core; writeln(typeid(child).toString, " ", child._width, " > ", minimum, " :: ", removalPerItem, "*", stretch); 3242 3243 mixin("child._" ~ relevantMeasure) -= removalPerItem * stretch + remainder / shrinkyChildSum; // this is removing more than needed to trigger the next thing. ugh. 3244 3245 spaceRemaining += removalPerItem * stretch + remainder / shrinkyChildSum; 3246 } 3247 } 3248 3249 // stretch to fill space 3250 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3251 auto spacePerChild = spaceRemaining / stretchinessSum; 3252 bool spreadEvenly; 3253 bool giveToBiggest; 3254 if(spacePerChild <= 0) { 3255 spacePerChild = spaceRemaining / stretchyChildSum; 3256 spreadEvenly = true; 3257 } 3258 if(spacePerChild <= 0) { 3259 giveToBiggest = true; 3260 } 3261 int previousSpaceRemaining = spaceRemaining; 3262 stretchinessSum = 0; 3263 Widget mostStretchy; 3264 int mostStretchyS; 3265 foreach(child; parent.children) { 3266 auto childStyle = child.getComputedStyle(); 3267 if(cast(StaticPosition) child) 3268 continue; 3269 if(child.hidden) 3270 continue; 3271 static if(calcingV) { 3272 auto maximum = childStyle.maxHeight(); 3273 } else { 3274 auto maximum = childStyle.maxWidth(); 3275 } 3276 3277 if(mixin("child." ~ relevantMeasure) >= maximum) { 3278 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3279 mixin("child._" ~ relevantMeasure) -= adj; 3280 spaceRemaining += adj; 3281 continue; 3282 } 3283 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3284 if(s <= 0) 3285 continue; 3286 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3287 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3288 spaceRemaining -= spaceAdjustment; 3289 if(mixin("child." ~ relevantMeasure) > maximum) { 3290 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3291 mixin("child._" ~ relevantMeasure) -= diff; 3292 spaceRemaining += diff; 3293 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3294 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3295 if(mostStretchy is null || s >= mostStretchyS) { 3296 mostStretchy = child; 3297 mostStretchyS = s; 3298 } 3299 } 3300 } 3301 3302 if(giveToBiggest && mostStretchy !is null) { 3303 auto child = mostStretchy; 3304 auto childStyle = child.getComputedStyle(); 3305 int spaceAdjustment = spaceRemaining; 3306 3307 static if(calcingV) 3308 auto maximum = childStyle.maxHeight(); 3309 else 3310 auto maximum = childStyle.maxWidth(); 3311 3312 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3313 spaceRemaining -= spaceAdjustment; 3314 if(mixin("child._" ~ relevantMeasure) > maximum) { 3315 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3316 mixin("child._" ~ relevantMeasure) -= diff; 3317 spaceRemaining += diff; 3318 } 3319 } 3320 3321 if(spaceRemaining == previousSpaceRemaining) { 3322 if(mostStretchy !is null) { 3323 static if(calcingV) 3324 auto maximum = mostStretchy.maxHeight(); 3325 else 3326 auto maximum = mostStretchy.maxWidth(); 3327 3328 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3329 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3330 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3331 } 3332 break; // apparently nothing more we can do 3333 } 3334 } 3335 3336 foreach(child; parent.children) { 3337 auto childStyle = child.getComputedStyle(); 3338 if(cast(StaticPosition) child) 3339 continue; 3340 if(child.hidden) 3341 continue; 3342 3343 static if(calcingV) 3344 auto maximum = childStyle.maxHeight(); 3345 else 3346 auto maximum = childStyle.maxWidth(); 3347 if(mixin("child._" ~ relevantMeasure) > maximum) 3348 mixin("child._" ~ relevantMeasure) = maximum; 3349 } 3350 3351 // position 3352 lastMargin = 0; 3353 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3354 foreach(child; parent.children) { 3355 auto childStyle = child.getComputedStyle(); 3356 if(cast(StaticPosition) child) { 3357 child.recomputeChildLayout(); 3358 continue; 3359 } 3360 if(child.hidden) 3361 continue; 3362 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3363 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3364 currentPos += thisMargin; 3365 static if(calcingV) { 3366 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3367 child.y = currentPos; 3368 } else { 3369 child.x = currentPos; 3370 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3371 3372 } 3373 currentPos += mixin("child." ~ relevantMeasure); 3374 currentPos += margin; 3375 lastMargin = margin; 3376 3377 child.recomputeChildLayout(); 3378 } 3379 } 3380 3381 int mymax(int a, int b) { return a > b ? a : b; } 3382 int mymax(int a, int b, int c) { 3383 auto d = mymax(a, b); 3384 return c > d ? c : d; 3385 } 3386 3387 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3388 // and here, it must be integrable with the layout, the event system, and not be painted over. 3389 version(win32_widgets) { 3390 3391 // this function just does stuff that a parent window needs for redirection 3392 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3393 this_.hookedWndProc(msg, wParam, lParam); 3394 3395 switch(msg) { 3396 3397 case WM_VSCROLL, WM_HSCROLL: 3398 auto pos = HIWORD(wParam); 3399 auto m = LOWORD(wParam); 3400 3401 auto scrollbarHwnd = cast(HWND) lParam; 3402 3403 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3404 3405 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3406 3407 switch(m) { 3408 /+ 3409 // I don't think those messages are ever actually sent normally by the widget itself, 3410 // they are more used for the keyboard interface. methinks. 3411 case SB_BOTTOM: 3412 // writeln("end"); 3413 auto event = new Event("scrolltoend", *widgetp); 3414 event.dispatch(); 3415 //if(!event.defaultPrevented) 3416 break; 3417 case SB_TOP: 3418 // writeln("top"); 3419 auto event = new Event("scrolltobeginning", *widgetp); 3420 event.dispatch(); 3421 break; 3422 case SB_ENDSCROLL: 3423 // idk 3424 break; 3425 +/ 3426 case SB_LINEDOWN: 3427 (*widgetp).emitCommand!"scrolltonextline"(); 3428 return 0; 3429 case SB_LINEUP: 3430 (*widgetp).emitCommand!"scrolltopreviousline"(); 3431 return 0; 3432 case SB_PAGEDOWN: 3433 (*widgetp).emitCommand!"scrolltonextpage"(); 3434 return 0; 3435 case SB_PAGEUP: 3436 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3437 return 0; 3438 case SB_THUMBPOSITION: 3439 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3440 ev.dispatch(); 3441 return 0; 3442 case SB_THUMBTRACK: 3443 // eh kinda lying but i like the real time update display 3444 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3445 ev.dispatch(); 3446 3447 // the event loop doesn't seem to carry on with a requested redraw.. 3448 // so we request it to get our dirty bit set... 3449 // then we need to immediately actually redraw it too for instant feedback to user 3450 SimpleWindow.processAllCustomEvents(); 3451 SimpleWindow.processAllCustomEvents(); 3452 //if(this_.parentWindow) 3453 //this_.parentWindow.actualRedraw(); 3454 3455 // and this ensures the WM_PAINT message is sent fairly quickly 3456 // still seems to lag a little in large windows but meh it basically works. 3457 if(this_.parentWindow) { 3458 // FIXME: if painting is slow, this does still lag 3459 // we probably will want to expose some user hook to ScrollWindowEx 3460 // or something. 3461 UpdateWindow(this_.parentWindow.hwnd); 3462 } 3463 return 0; 3464 default: 3465 } 3466 } 3467 break; 3468 3469 case WM_CONTEXTMENU: 3470 auto hwndFrom = cast(HWND) wParam; 3471 3472 auto xPos = cast(short) LOWORD(lParam); 3473 auto yPos = cast(short) HIWORD(lParam); 3474 3475 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3476 POINT p; 3477 p.x = xPos; 3478 p.y = yPos; 3479 ScreenToClient(hwnd, &p); 3480 auto clientX = cast(ushort) p.x; 3481 auto clientY = cast(ushort) p.y; 3482 3483 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 3484 3485 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 3486 return 0; 3487 } 3488 } 3489 break; 3490 3491 case WM_DRAWITEM: 3492 auto dis = cast(DRAWITEMSTRUCT*) lParam; 3493 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 3494 return (*widgetp).handleWmDrawItem(dis); 3495 } 3496 break; 3497 3498 case WM_NOTIFY: 3499 auto hdr = cast(NMHDR*) lParam; 3500 auto hwndFrom = hdr.hwndFrom; 3501 auto code = hdr.code; 3502 3503 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3504 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 3505 } 3506 break; 3507 case WM_COMMAND: 3508 auto handle = cast(HWND) lParam; 3509 auto cmd = HIWORD(wParam); 3510 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 3511 3512 default: 3513 // pass it on 3514 } 3515 return 0; 3516 } 3517 3518 3519 3520 extern(Windows) 3521 private 3522 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 3523 // but can i merge them?! 3524 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3525 // try { writeln(iMessage); } catch(Exception e) {}; 3526 3527 if(auto te = hWnd in Widget.nativeMapping) { 3528 try { 3529 3530 te.hookedWndProc(iMessage, wParam, lParam); 3531 3532 int mustReturn; 3533 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 3534 if(mustReturn) 3535 return ret; 3536 3537 if(iMessage == WM_SETFOCUS) { 3538 auto lol = *te; 3539 while(lol !is null && lol.implicitlyCreated) 3540 lol = lol.parent; 3541 lol.focus(); 3542 //(*te).parentWindow.focusedWidget = lol; 3543 } 3544 3545 3546 if(iMessage == WM_CTLCOLOREDIT) { 3547 3548 } 3549 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 3550 SetBkMode(cast(HDC) wParam, TRANSPARENT); 3551 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 3552 //GetStockObject(NULL_BRUSH); 3553 } 3554 3555 auto pos = getChildPositionRelativeToParentOrigin(*te); 3556 lastDefaultPrevented = false; 3557 // try { writeln(typeid(*te)); } catch(Exception e) {} 3558 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 3559 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 3560 else { 3561 // it was something we recognized, should only call the window procedure if the default was not prevented 3562 } 3563 } catch(Exception e) { 3564 assert(0, e.toString()); 3565 } 3566 return 0; 3567 } 3568 assert(0, "shouldn't be receiving messages for this window...."); 3569 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 3570 } 3571 3572 extern(Windows) 3573 private 3574 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 3575 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3576 if(iMessage == WM_ERASEBKGND) { 3577 auto dc = GetDC(hWnd); 3578 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 3579 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 3580 RECT r; 3581 GetWindowRect(hWnd, &r); 3582 // since the pen is null, to fill the whole space, we need the +1 on both. 3583 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 3584 SelectObject(dc, p); 3585 SelectObject(dc, b); 3586 ReleaseDC(hWnd, dc); 3587 InvalidateRect(hWnd, null, false); // redraw the border 3588 return 1; 3589 } 3590 return HookedWndProc(hWnd, iMessage, wParam, lParam); 3591 } 3592 3593 /++ 3594 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 3595 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 3596 of minigui's expectations. 3597 3598 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 3599 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 3600 3601 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 3602 3603 To check if you can use this, use `static if(UsingWin32Widgets)`. 3604 +/ 3605 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 3606 assert(p.parentWindow !is null); 3607 assert(p.parentWindow.win.impl.hwnd !is null); 3608 3609 auto bsgroupbox = style == BS_GROUPBOX; 3610 3611 HWND phwnd; 3612 3613 auto wtf = p.parent; 3614 while(wtf) { 3615 if(wtf.hwnd !is null) { 3616 phwnd = wtf.hwnd; 3617 break; 3618 } 3619 wtf = wtf.parent; 3620 } 3621 3622 if(phwnd is null) 3623 phwnd = p.parentWindow.win.impl.hwnd; 3624 3625 assert(phwnd !is null); 3626 3627 WCharzBuffer wt = WCharzBuffer(windowText); 3628 3629 style |= WS_VISIBLE | WS_CHILD; 3630 //if(className != WC_TABCONTROL) 3631 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 3632 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 3633 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 3634 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 3635 3636 assert(p.hwnd !is null); 3637 3638 3639 static HFONT font; 3640 if(font is null) { 3641 NONCLIENTMETRICS params; 3642 params.cbSize = params.sizeof; 3643 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 3644 font = CreateFontIndirect(¶ms.lfMessageFont); 3645 } 3646 } 3647 3648 if(font) 3649 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 3650 3651 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 3652 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 3653 Widget.nativeMapping[p.hwnd] = p; 3654 3655 if(bsgroupbox) 3656 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 3657 else 3658 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3659 3660 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 3661 3662 p.registerMovement(); 3663 } 3664 } 3665 3666 version(win32_widgets) 3667 private 3668 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 3669 if(hwnd is null || hwnd in Widget.nativeMapping) 3670 return true; 3671 auto parent = cast(Widget) cast(void*) lparam; 3672 Widget p = new Widget(null); 3673 p._parent = parent; 3674 p.parentWindow = parent.parentWindow; 3675 p.hwnd = hwnd; 3676 p.implicitlyCreated = true; 3677 Widget.nativeMapping[p.hwnd] = p; 3678 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3679 return true; 3680 } 3681 3682 /++ 3683 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 3684 +/ 3685 struct WidgetPainter { 3686 this(ScreenPainter screenPainter, Widget drawingUpon) { 3687 this.drawingUpon = drawingUpon; 3688 this.screenPainter = screenPainter; 3689 if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 3690 this.screenPainter.setFont(font); 3691 } 3692 3693 /++ 3694 EXPERIMENTAL. subject to change. 3695 3696 When you draw a cursor, you can draw this to notify your window of where it is, 3697 for IME systems to use. 3698 +/ 3699 void notifyCursorPosition(int x, int y, int width, int height) { 3700 if(auto a = drawingUpon.parentWindow) 3701 if(auto w = a.inputProxy) { 3702 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 3703 } 3704 } 3705 3706 3707 /// 3708 ScreenPainter screenPainter; 3709 /// Forward to the screen painter for other methods 3710 alias screenPainter this; 3711 3712 private Widget drawingUpon; 3713 3714 /++ 3715 This is the list of rectangles that actually need to be redrawn. 3716 3717 Not actually implemented yet. 3718 +/ 3719 Rectangle[] invalidatedRectangles; 3720 3721 private static BaseVisualTheme _visualTheme; 3722 3723 /++ 3724 Functions to access the visual theme and helpers to easily use it. 3725 3726 These are aware of the current widget's computed style out of the theme. 3727 +/ 3728 static @property BaseVisualTheme visualTheme() { 3729 if(_visualTheme is null) 3730 _visualTheme = new DefaultVisualTheme(); 3731 return _visualTheme; 3732 } 3733 3734 /// ditto 3735 static @property void visualTheme(BaseVisualTheme theme) { 3736 _visualTheme = theme; 3737 3738 // FIXME: notify all windows about the new theme, they should recompute layout and redraw. 3739 } 3740 3741 /// ditto 3742 Color themeForeground() { 3743 return drawingUpon.getComputedStyle().foregroundColor(); 3744 } 3745 3746 /// ditto 3747 Color themeBackground() { 3748 return drawingUpon.getComputedStyle().background.color; 3749 } 3750 3751 int isDarkTheme() { 3752 return 0; // unspecified, yes, no as enum. FIXME 3753 } 3754 3755 /++ 3756 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. 3757 3758 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 3759 3760 If you change teh clip rectangle, you should change it back before you return. 3761 3762 3763 The sequence it uses is: 3764 background 3765 content (delegated to you) 3766 border 3767 focused outline 3768 selected overlay 3769 3770 Example code: 3771 3772 --- 3773 void paint(WidgetPainter painter) { 3774 painter.drawThemed((bounds) { 3775 return bounds; // if the selection overlay should be contained, you can return it here. 3776 }); 3777 } 3778 --- 3779 +/ 3780 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 3781 drawThemed((WidgetPainter painter, const Rectangle bounds) { 3782 return drawBody(bounds); 3783 }); 3784 } 3785 // this overload is actually mroe for setting the delegate to a virtual function 3786 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 3787 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 3788 3789 auto cs = drawingUpon.getComputedStyle(); 3790 3791 auto bg = cs.background.color; 3792 3793 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 3794 3795 rect.left += borderWidth; 3796 rect.right -= borderWidth; 3797 rect.top += borderWidth; 3798 rect.bottom -= borderWidth; 3799 3800 auto insideBorderRect = rect; 3801 3802 rect.left += cs.paddingLeft; 3803 rect.right -= cs.paddingRight; 3804 rect.top += cs.paddingTop; 3805 rect.bottom -= cs.paddingBottom; 3806 3807 this.outlineColor = this.themeForeground; 3808 this.fillColor = bg; 3809 3810 auto widgetFont = cs.fontCached; 3811 if(widgetFont !is null) 3812 this.setFont(widgetFont); 3813 3814 rect = drawBody(this, rect); 3815 3816 if(widgetFont !is null) { 3817 if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 3818 this.setFont(vtFont); 3819 else 3820 this.setFont(null); 3821 } 3822 3823 if(auto os = cs.outlineStyle()) { 3824 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 3825 this.fillColor = Color.transparent; 3826 this.drawRectangle(insideBorderRect); 3827 } 3828 } 3829 3830 /++ 3831 First, draw the background. 3832 Then draw your content. 3833 Next, draw the border. 3834 And the focused indicator. 3835 And the is-selected box. 3836 3837 If it is focused i can draw the outline too... 3838 3839 If selected i can even do the xor action but that's at the end. 3840 +/ 3841 void drawThemeBackground() { 3842 3843 } 3844 3845 void drawThemeBorder() { 3846 3847 } 3848 3849 // all this stuff is a dangerous experiment.... 3850 static class ScriptableVersion { 3851 ScreenPainterImplementation* p; 3852 int originX, originY; 3853 3854 @scriptable: 3855 void drawRectangle(int x, int y, int width, int height) { 3856 p.drawRectangle(x + originX, y + originY, width, height); 3857 } 3858 void drawLine(int x1, int y1, int x2, int y2) { 3859 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 3860 } 3861 void drawText(int x, int y, string text) { 3862 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 3863 } 3864 void setOutlineColor(int r, int g, int b) { 3865 p.pen = Pen(Color(r,g,b), 1); 3866 } 3867 void setFillColor(int r, int g, int b) { 3868 p.fillColor = Color(r,g,b); 3869 } 3870 } 3871 3872 ScriptableVersion toArsdJsvar() { 3873 auto sv = new ScriptableVersion; 3874 sv.p = this.screenPainter.impl; 3875 sv.originX = this.screenPainter.originX; 3876 sv.originY = this.screenPainter.originY; 3877 return sv; 3878 } 3879 3880 static WidgetPainter fromJsVar(T)(T t) { 3881 return WidgetPainter.init; 3882 } 3883 // done.......... 3884 } 3885 3886 3887 struct Style { 3888 static struct helper(string m, T) { 3889 enum method = m; 3890 T v; 3891 3892 mixin template MethodOverride(typeof(this) v) { 3893 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 3894 } 3895 } 3896 3897 static auto opDispatch(string method, T)(T value) { 3898 return helper!(method, T)(value); 3899 } 3900 } 3901 3902 /++ 3903 Implementation detail of the [ControlledBy] UDA. 3904 3905 History: 3906 Added Oct 28, 2020 3907 +/ 3908 struct ControlledBy_(T, Args...) { 3909 Args args; 3910 3911 static if(Args.length) 3912 this(Args args) { 3913 this.args = args; 3914 } 3915 3916 private T construct(Widget parent) { 3917 return new T(args, parent); 3918 } 3919 } 3920 3921 /++ 3922 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 3923 3924 History: 3925 Added Oct 28, 2020 3926 +/ 3927 auto ControlledBy(T, Args...)(Args args) { 3928 return ControlledBy_!(T, Args)(args); 3929 } 3930 3931 struct ContainerMeta { 3932 string name; 3933 ContainerMeta[] children; 3934 Widget function(Widget parent) factory; 3935 3936 Widget instantiate(Widget parent) { 3937 auto n = factory(parent); 3938 n.name = name; 3939 foreach(child; children) 3940 child.instantiate(n); 3941 return n; 3942 } 3943 } 3944 3945 /++ 3946 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 3947 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 3948 3949 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 3950 structures. It works fine on structs declared inside functions though. 3951 3952 See: https://issues.dlang.org/show_bug.cgi?id=21984 3953 +/ 3954 template Container(CArgs...) { 3955 static if(CArgs.length && is(CArgs[0] : Widget)) { 3956 private alias Super = CArgs[0]; 3957 private alias CArgs2 = CArgs[1 .. $]; 3958 } else { 3959 private alias Super = Layout; 3960 private alias CArgs2 = CArgs; 3961 } 3962 3963 class Container : Super { 3964 this(Widget parent) { super(parent); } 3965 3966 // just to partially support old gdc versions 3967 version(GNU) { 3968 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 3969 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 3970 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 3971 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 3972 } else mixin(q{ 3973 static foreach(Arg; CArgs2) { 3974 mixin Arg.MethodOverride!(Arg); 3975 } 3976 }); 3977 3978 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 3979 return ContainerMeta( 3980 name, 3981 children.dup, 3982 function (Widget parent) { return new typeof(this)(parent); } 3983 ); 3984 } 3985 3986 static ContainerMeta opCall(ContainerMeta[] children...) { 3987 return opCall(null, children); 3988 } 3989 } 3990 } 3991 3992 /++ 3993 The data controller widget is created by reflecting over the given 3994 data type. You can use [ControlledBy] as a UDA on a struct or 3995 just let it create things automatically. 3996 3997 Unlike [dialog], this uses real-time updating of the data and 3998 you add it to another window yourself. 3999 4000 --- 4001 struct Test { 4002 int x; 4003 int y; 4004 } 4005 4006 auto window = new Window(); 4007 auto dcw = new DataControllerWidget!Test(new Test, window); 4008 --- 4009 4010 The way it works is any public members are given a widget based 4011 on their data type, and public methods trigger an action button 4012 if no relevant parameters or a dialog action if it does have 4013 parameters, similar to the [menu] facility. 4014 4015 If you change data programmatically, without going through the 4016 DataControllerWidget methods, you will have to tell it something 4017 has changed and it needs to redraw. This is done with the `invalidate` 4018 method. 4019 4020 History: 4021 Added Oct 28, 2020 4022 +/ 4023 /// Group: generating_from_code 4024 class DataControllerWidget(T) : WidgetContainer { 4025 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 4026 private alias Tref = T; 4027 else 4028 private alias Tref = T*; 4029 4030 Tref datum; 4031 4032 /++ 4033 See_also: [addDataControllerWidget] 4034 +/ 4035 this(Tref datum, Widget parent) { 4036 this.datum = datum; 4037 4038 Widget cp = this; 4039 4040 super(parent); 4041 4042 foreach(attr; __traits(getAttributes, T)) 4043 static if(is(typeof(attr) == ContainerMeta)) { 4044 cp = attr.instantiate(this); 4045 } 4046 4047 auto def = this.getByName("default"); 4048 if(def !is null) 4049 cp = def; 4050 4051 Widget helper(string name) { 4052 auto maybe = this.getByName(name); 4053 if(maybe is null) 4054 return cp; 4055 return maybe; 4056 4057 } 4058 4059 foreach(member; __traits(allMembers, T)) 4060 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 4061 static if(is(typeof(__traits(getMember, this.datum, member)))) 4062 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 4063 void delegate() update; 4064 4065 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 4066 4067 if(update) 4068 updaters ~= update; 4069 4070 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 4071 w.addEventListener("triggered", delegate() { 4072 makeAutomaticHandler!(__traits(getMember, this.datum, member))(this.parentWindow, &__traits(getMember, this.datum, member))(); 4073 notifyDataUpdated(); 4074 }); 4075 } else static if(is(typeof(w.isChecked) == bool)) { 4076 w.addEventListener(EventType.change, (Event ev) { 4077 __traits(getMember, this.datum, member) = w.isChecked; 4078 }); 4079 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 4080 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 4081 } else static if(is(typeof(w.value) == int)) { 4082 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4083 } else static if(is(typeof(w) == DropDownSelection)) { 4084 // special case for this to kinda support enums and such. coudl be better though 4085 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4086 } else { 4087 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 4088 } 4089 } 4090 } 4091 4092 /++ 4093 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 4094 4095 History: 4096 Added May 28, 2021 4097 +/ 4098 void notifyDataUpdated() { 4099 foreach(updater; updaters) 4100 updater(); 4101 4102 this.emit!(ChangeEvent!void)(delegate{}); 4103 } 4104 4105 private Widget[string] memberWidgets; 4106 private void delegate()[] updaters; 4107 4108 mixin Emits!(ChangeEvent!void); 4109 } 4110 4111 private int saturatedSum(int[] values...) { 4112 int sum; 4113 foreach(value; values) { 4114 if(value == int.max) 4115 return int.max; 4116 sum += value; 4117 } 4118 return sum; 4119 } 4120 4121 void genericSetValue(T, W)(T* where, W what) { 4122 import std.conv; 4123 *where = to!T(what); 4124 //*where = cast(T) stringToLong(what); 4125 } 4126 4127 /++ 4128 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4129 4130 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4131 4132 Note that this creates the widget but does not attach any event handlers to it. 4133 +/ 4134 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 4135 4136 string displayName = __traits(identifier, tt).beautify; 4137 4138 static if(controlledByCount!tt == 1) { 4139 foreach(i, attr; __traits(getAttributes, tt)) { 4140 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4141 auto w = attr.construct(parent); 4142 static if(__traits(compiles, w.setPosition(*valptr))) 4143 update = () { w.setPosition(*valptr); }; 4144 else static if(__traits(compiles, w.setValue(*valptr))) 4145 update = () { w.setValue(*valptr); }; 4146 4147 if(update) 4148 update(); 4149 return w; 4150 } 4151 } 4152 } else static if(controlledByCount!tt == 0) { 4153 static if(is(typeof(tt) == enum)) { 4154 // FIXME: update 4155 auto dds = new DropDownSelection(parent); 4156 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4157 dds.addOption(option); 4158 if(__traits(getMember, typeof(tt), option) == *valptr) 4159 dds.setSelection(cast(int) idx); 4160 } 4161 return dds; 4162 } else static if(is(typeof(tt) == bool)) { 4163 auto box = new Checkbox(displayName, parent); 4164 update = () { box.isChecked = *valptr; }; 4165 update(); 4166 return box; 4167 } else static if(is(typeof(tt) : const long)) { 4168 auto le = new LabeledLineEdit(displayName, parent); 4169 update = () { le.content = toInternal!string(*valptr); }; 4170 update(); 4171 return le; 4172 } else static if(is(typeof(tt) : const double)) { 4173 auto le = new LabeledLineEdit(displayName, parent); 4174 import std.conv; 4175 update = () { le.content = to!string(*valptr); }; 4176 update(); 4177 return le; 4178 } else static if(is(typeof(tt) : const string)) { 4179 auto le = new LabeledLineEdit(displayName, parent); 4180 update = () { le.content = *valptr; }; 4181 update(); 4182 return le; 4183 } else static if(is(typeof(tt) == function)) { 4184 auto w = new Button(displayName, parent); 4185 return w; 4186 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4187 return parent.addDataControllerWidget(tt); 4188 } else static assert(0, typeof(tt).stringof); 4189 } else static assert(0, "multiple controllers not yet supported"); 4190 } 4191 4192 private template controlledByCount(alias tt) { 4193 static int helper() { 4194 int count; 4195 foreach(i, attr; __traits(getAttributes, tt)) 4196 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 4197 count++; 4198 return count; 4199 } 4200 4201 enum controlledByCount = helper; 4202 } 4203 4204 /++ 4205 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 4206 4207 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 4208 4209 History: 4210 The `redrawOnChange` parameter was added on May 28, 2021. 4211 +/ 4212 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 4213 auto dcw = new DataControllerWidget!T(t, parent); 4214 initializeDataControllerWidget(dcw, redrawOnChange); 4215 return dcw; 4216 } 4217 4218 /// ditto 4219 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 4220 auto dcw = new DataControllerWidget!T(t, parent); 4221 initializeDataControllerWidget(dcw, redrawOnChange); 4222 return dcw; 4223 } 4224 4225 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 4226 if(redrawOnChange !is null) 4227 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 4228 } 4229 4230 /++ 4231 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. 4232 4233 History: 4234 Finalized on June 3, 2021 for the dub v10.0 release 4235 +/ 4236 struct StyleInformation { 4237 private Widget w; 4238 private BaseVisualTheme visualTheme; 4239 4240 private this(Widget w) { 4241 this.w = w; 4242 this.visualTheme = WidgetPainter.visualTheme; 4243 } 4244 4245 /++ 4246 Forwards to [Widget.Style] 4247 4248 Bugs: 4249 It is supposed to fall back to the [VisualTheme] if 4250 the style doesn't override the default, but that is 4251 not generally implemented. Many of them may end up 4252 being explicit overloads instead of the generic 4253 opDispatch fallback, like [font] is now. 4254 +/ 4255 public @property opDispatch(string name)() { 4256 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4257 w.useStyleProperties((scope Widget.Style props) { 4258 //visualTheme.useStyleProperties(w, (props) { 4259 prop = __traits(getMember, props, name); 4260 }); 4261 return prop; 4262 } 4263 4264 /++ 4265 Returns the cached font object associated with the widget, 4266 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 4267 4268 History: 4269 Prior to March 21, 2022 (dub v10.7), `font` went through 4270 [opDispatch], which did not use the cache. You can now call it 4271 repeatedly without guilt. 4272 +/ 4273 public @property OperatingSystemFont font() { 4274 OperatingSystemFont prop; 4275 w.useStyleProperties((scope Widget.Style props) { 4276 prop = props.fontCached; 4277 }); 4278 if(prop is null) { 4279 prop = visualTheme.defaultFontCached(w.currentDpi); 4280 } 4281 return prop; 4282 } 4283 4284 @property { 4285 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 4286 /** */ int paddingLeft() { return w.paddingLeft(); } 4287 /** */ int paddingRight() { return w.paddingRight(); } 4288 /** */ int paddingTop() { return w.paddingTop(); } 4289 /** */ int paddingBottom() { return w.paddingBottom(); } 4290 4291 /** */ int marginLeft() { return w.marginLeft(); } 4292 /** */ int marginRight() { return w.marginRight(); } 4293 /** */ int marginTop() { return w.marginTop(); } 4294 /** */ int marginBottom() { return w.marginBottom(); } 4295 4296 /** */ int maxHeight() { return w.maxHeight(); } 4297 /** */ int minHeight() { return w.minHeight(); } 4298 4299 /** */ int maxWidth() { return w.maxWidth(); } 4300 /** */ int minWidth() { return w.minWidth(); } 4301 4302 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 4303 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 4304 4305 /** */ int heightStretchiness() { return w.heightStretchiness(); } 4306 /** */ int widthStretchiness() { return w.widthStretchiness(); } 4307 4308 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 4309 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 4310 4311 // Global helpers some of these are unstable. 4312 static: 4313 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4314 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4315 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4316 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4317 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 4318 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4319 4320 /** */ Color activeTabColor() { return lightAccentColor; } 4321 /** */ Color buttonColor() { return windowBackgroundColor; } 4322 /** */ Color depressedButtonColor() { return darkAccentColor; } 4323 /** */ Color hoveringColor() { return lightAccentColor; } 4324 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 4325 auto c = WidgetPainter.visualTheme.selectionColor(); 4326 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4327 } 4328 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4329 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4330 } 4331 4332 4333 4334 /+ 4335 4336 private static auto extractStyleProperty(string name)(Widget w) { 4337 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4338 w.useStyleProperties((props) { 4339 prop = __traits(getMember, props, name); 4340 }); 4341 return prop; 4342 } 4343 4344 // FIXME: clear this upon a X server disconnect 4345 private static OperatingSystemFont[string] fontCache; 4346 4347 T getProperty(T)(string name, lazy T default_) { 4348 if(visualTheme !is null) { 4349 auto str = visualTheme.getPropertyString(w, name); 4350 if(str is null) 4351 return default_; 4352 static if(is(T == Color)) 4353 return Color.fromString(str); 4354 else static if(is(T == Measurement)) 4355 return Measurement(cast(int) toInternal!int(str)); 4356 else static if(is(T == WidgetBackground)) 4357 return WidgetBackground.fromString(str); 4358 else static if(is(T == OperatingSystemFont)) { 4359 if(auto f = str in fontCache) 4360 return *f; 4361 else 4362 return fontCache[str] = new OperatingSystemFont(str); 4363 } else static if(is(T == FrameStyle)) { 4364 switch(str) { 4365 default: 4366 return FrameStyle.none; 4367 foreach(style; __traits(allMembers, FrameStyle)) 4368 case style: 4369 return __traits(getMember, FrameStyle, style); 4370 } 4371 } else static assert(0); 4372 } else 4373 return default_; 4374 } 4375 4376 static struct Measurement { 4377 int value; 4378 alias value this; 4379 } 4380 4381 @property: 4382 4383 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 4384 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 4385 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 4386 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 4387 4388 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 4389 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 4390 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 4391 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 4392 4393 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 4394 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 4395 4396 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 4397 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 4398 4399 4400 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 4401 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 4402 4403 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 4404 4405 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 4406 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 4407 4408 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 4409 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 4410 4411 4412 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4413 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4414 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4415 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4416 4417 Color activeTabColor() { return lightAccentColor; } 4418 Color buttonColor() { return windowBackgroundColor; } 4419 Color depressedButtonColor() { return darkAccentColor; } 4420 Color hoveringColor() { return Color(228, 228, 228); } 4421 Color activeListXorColor() { 4422 auto c = WidgetPainter.visualTheme.selectionColor(); 4423 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4424 } 4425 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4426 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4427 +/ 4428 } 4429 4430 4431 4432 // pragma(msg, __traits(classInstanceSize, Widget)); 4433 4434 /*private*/ template EventString(E) { 4435 static if(is(typeof(E.EventString))) 4436 enum EventString = E.EventString; 4437 else 4438 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 4439 } 4440 4441 /*private*/ template EventStringIdentifier(E) { 4442 string helper() { 4443 auto es = EventString!E; 4444 char[] id = new char[](es.length * 2); 4445 size_t idx; 4446 foreach(char ch; es) { 4447 id[idx++] = cast(char)('a' + (ch >> 4)); 4448 id[idx++] = cast(char)('a' + (ch & 0x0f)); 4449 } 4450 return cast(string) id; 4451 } 4452 4453 enum EventStringIdentifier = helper(); 4454 } 4455 4456 4457 template classStaticallyEmits(This, EventType) { 4458 static if(is(This Base == super)) 4459 static if(is(Base : Widget)) 4460 enum baseEmits = classStaticallyEmits!(Base, EventType); 4461 else 4462 enum baseEmits = false; 4463 else 4464 enum baseEmits = false; 4465 4466 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 4467 4468 enum classStaticallyEmits = thisEmits || baseEmits; 4469 } 4470 4471 /++ 4472 A helper to make widgets out of other native windows. 4473 4474 History: 4475 Factored out of OpenGlWidget on November 5, 2021 4476 +/ 4477 class NestedChildWindowWidget : Widget { 4478 SimpleWindow win; 4479 4480 /++ 4481 Used on X to send focus to the appropriate child window when requested by the window manager. 4482 4483 Normally returns its own nested window. Can also return another child or null to revert to the parent 4484 if you override it in a child class. 4485 4486 History: 4487 Added April 2, 2022 (dub v10.8) 4488 +/ 4489 SimpleWindow focusableWindow() { 4490 return win; 4491 } 4492 4493 /// 4494 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4495 this(SimpleWindow win, Widget parent) { 4496 this.parentWindow = parent.parentWindow; 4497 this.win = win; 4498 4499 super(parent); 4500 windowsetup(win); 4501 } 4502 4503 static protected SimpleWindow getParentWindow(Widget parent) { 4504 assert(parent !is null); 4505 SimpleWindow pwin = parent.parentWindow.win; 4506 4507 version(win32_widgets) { 4508 HWND phwnd; 4509 auto wtf = parent; 4510 while(wtf) { 4511 if(wtf.hwnd) { 4512 phwnd = wtf.hwnd; 4513 break; 4514 } 4515 wtf = wtf.parent; 4516 } 4517 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 4518 if(phwnd) 4519 pwin = new SimpleWindow(phwnd); 4520 } 4521 4522 return pwin; 4523 } 4524 4525 /++ 4526 Called upon the nested window being destroyed. 4527 Remember the window has already been destroyed at 4528 this point, so don't use the native handle for anything. 4529 4530 History: 4531 Added April 3, 2022 (dub v10.8) 4532 +/ 4533 protected void dispose() { 4534 4535 } 4536 4537 protected void windowsetup(SimpleWindow w) { 4538 /* 4539 win.onFocusChange = (bool getting) { 4540 if(getting) 4541 this.focus(); 4542 }; 4543 */ 4544 4545 /+ 4546 win.onFocusChange = (bool getting) { 4547 if(getting) { 4548 this.parentWindow.focusedWidget = this; 4549 this.emit!FocusEvent(); 4550 this.emit!FocusInEvent(); 4551 } else { 4552 this.emit!BlurEvent(); 4553 this.emit!FocusOutEvent(); 4554 } 4555 }; 4556 +/ 4557 4558 win.onDestroyed = () { 4559 this.dispose(); 4560 }; 4561 4562 version(win32_widgets) { 4563 Widget.nativeMapping[win.hwnd] = this; 4564 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4565 } else { 4566 win.setEventHandlers( 4567 (MouseEvent e) { 4568 Widget p = this; 4569 while(p ! is parentWindow) { 4570 e.x += p.x; 4571 e.y += p.y; 4572 p = p.parent; 4573 } 4574 parentWindow.dispatchMouseEvent(e); 4575 }, 4576 (KeyEvent e) { 4577 //writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 4578 parentWindow.dispatchKeyEvent(e); 4579 }, 4580 (dchar e) { 4581 parentWindow.dispatchCharEvent(e); 4582 }, 4583 ); 4584 } 4585 4586 } 4587 4588 override bool showOrHideIfNativeWindow(bool shouldShow) { 4589 auto cur = hidden; 4590 win.hidden = !shouldShow; 4591 if(cur != shouldShow && shouldShow) 4592 redraw(); 4593 return true; 4594 } 4595 4596 /// OpenGL widgets cannot have child widgets. Do not call this. 4597 /* @disable */ final override void addChild(Widget, int) { 4598 throw new Error("cannot add children to OpenGL widgets"); 4599 } 4600 4601 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 4602 /// Keep in mind that events like mouse coordinates are still relative to your size. 4603 override void registerMovement() { 4604 // writefln("%d %d %d %d", x,y,width,height); 4605 version(win32_widgets) 4606 auto pos = getChildPositionRelativeToParentHwnd(this); 4607 else 4608 auto pos = getChildPositionRelativeToParentOrigin(this); 4609 win.moveResize(pos[0], pos[1], width, height); 4610 4611 registerMovementAdditionalWork(); 4612 sendResizeEvent(); 4613 } 4614 4615 abstract void registerMovementAdditionalWork(); 4616 } 4617 4618 /++ 4619 Nests an opengl capable window inside this window as a widget. 4620 4621 You may also just want to create an additional [SimpleWindow] with 4622 [OpenGlOptions.yes] yourself. 4623 4624 An OpenGL widget cannot have child widgets. It will throw if you try. 4625 +/ 4626 static if(OpenGlEnabled) 4627 class OpenGlWidget : NestedChildWindowWidget { 4628 4629 override void registerMovementAdditionalWork() { 4630 win.setAsCurrentOpenGlContext(); 4631 } 4632 4633 /// 4634 this(Widget parent) { 4635 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4636 super(win, parent); 4637 } 4638 4639 override void paint(WidgetPainter painter) { 4640 win.setAsCurrentOpenGlContext(); 4641 glViewport(0, 0, this.width, this.height); 4642 win.redrawOpenGlSceneNow(); 4643 } 4644 4645 void redrawOpenGlScene(void delegate() dg) { 4646 win.redrawOpenGlScene = dg; 4647 } 4648 } 4649 4650 /++ 4651 This demo shows how to draw text in an opengl scene. 4652 +/ 4653 unittest { 4654 import arsd.minigui; 4655 import arsd.ttf; 4656 4657 void main() { 4658 auto window = new Window(); 4659 4660 auto widget = new OpenGlWidget(window); 4661 4662 // old means non-shader code so compatible with glBegin etc. 4663 // tbh I haven't implemented new one in font yet... 4664 // anyway, declaring here, will construct soon. 4665 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 4666 4667 // this is a little bit awkward, calling some methods through 4668 // the underlying SimpleWindow `win` method, and you can't do this 4669 // on a nanovega widget due to conflicts so I should probably fix 4670 // the api to be a bit easier. But here it will work. 4671 // 4672 // Alternatively, you could load the font on the first draw, inside 4673 // the redrawOpenGlScene, and keep a flag so you don't do it every 4674 // time. That'd be a bit easier since the lib sets up the context 4675 // by then guaranteed. 4676 // 4677 // But still, I wanna show this. 4678 widget.win.visibleForTheFirstTime = delegate { 4679 // must set the opengl context 4680 widget.win.setAsCurrentOpenGlContext(); 4681 4682 // if you were doing a OpenGL 3+ shader, this 4683 // gets especially important to do in order. With 4684 // old-style opengl, I think you can even do it 4685 // in main(), but meh, let's show it more correctly. 4686 4687 // Anyway, now it is time to load the font from the 4688 // OS (you can alternatively load one from a .ttf file 4689 // you bundle with the application), then load the 4690 // font into texture for drawing. 4691 4692 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 4693 4694 assert(!osfont.isNull()); // make sure it actually loaded 4695 4696 // using typeof to avoid repeating the long name lol 4697 glfont = new typeof(glfont)( 4698 // get the raw data from the font for loading in here 4699 // since it doesn't use the OS function to draw the 4700 // text, we gotta treat it more as a file than as 4701 // a drawing api. 4702 osfont.getTtfBytes(), 4703 18, // need to respecify size since opengl world is different coordinate system 4704 4705 // these last two numbers are why it is called 4706 // "Limited" font. It only loads the characters 4707 // in the given range, since the texture atlas 4708 // it references is all a big image generated ahead 4709 // of time. You could maybe do the whole thing but 4710 // idk how much memory that is. 4711 // 4712 // But here, 0-128 represents the ASCII range, so 4713 // good enough for most English things, numeric labels, 4714 // etc. 4715 0, 4716 128 4717 ); 4718 }; 4719 4720 widget.redrawOpenGlScene = () { 4721 // now we can use the glfont's drawString function 4722 4723 // first some opengl setup. You can do this in one place 4724 // on window first visible too in many cases, just showing 4725 // here cuz it is easier for me. 4726 4727 // gonna need some alpha blending or it just looks awful 4728 glEnable(GL_BLEND); 4729 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 4730 glClearColor(0,0,0,0); 4731 glDepthFunc(GL_LEQUAL); 4732 4733 // Also need to enable 2d textures, since it draws the 4734 // font characters as images baked in 4735 glMatrixMode(GL_MODELVIEW); 4736 glLoadIdentity(); 4737 glDisable(GL_DEPTH_TEST); 4738 glEnable(GL_TEXTURE_2D); 4739 4740 // the orthographic matrix is best for 2d things like text 4741 // so let's set that up. This matrix makes the coordinates 4742 // in the opengl scene be one-to-one with the actual pixels 4743 // on screen. (Not necessarily best, you may wish to scale 4744 // things, but it does help keep fonts looking normal.) 4745 glMatrixMode(GL_PROJECTION); 4746 glLoadIdentity(); 4747 glOrtho(0, widget.width, widget.height, 0, 0, 1); 4748 4749 // you can do other glScale, glRotate, glTranslate, etc 4750 // to the matrix here of course if you want. 4751 4752 // note the x,y coordinates here are for the text baseline 4753 // NOT the upper-left corner. The baseline is like the line 4754 // in the notebook you write on. Most the letters are actually 4755 // above it, but some, like p and q, dip a bit below it. 4756 // 4757 // So if you're used to the upper left coordinate like the 4758 // rest of simpledisplay/minigui usually do, do the 4759 // y + glfont.ascent to bring it down a little. So this 4760 // example puts the string in the upper left of the window. 4761 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 4762 4763 // re color btw: the function sets a solid color internally, 4764 // but you actually COULD do your own thing for rainbow effects 4765 // and the sort if you wanted too, by pulling its guts out. 4766 // Just view its source for an idea of how it actually draws: 4767 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 4768 4769 // it gets a bit complicated with the character positioning, 4770 // but the opengl parts are fairly simple: bind a texture, 4771 // set the color, draw a quad for each letter. 4772 4773 4774 // the last optional argument there btw is a bounding box 4775 // it will/ use to word wrap and return an object you can 4776 // use to implement scrolling or pagination; it tells how 4777 // much of the string didn't fit in the box. But for simple 4778 // labels we can just ignore that. 4779 4780 4781 // I'd suggest drawing text as the last step, after you 4782 // do your other drawing. You might use the push/pop matrix 4783 // stuff to keep your place. You, in theory, should be able 4784 // to do text in a 3d space but I've never actually tried 4785 // that.... 4786 }; 4787 4788 window.loop(); 4789 } 4790 } 4791 4792 version(custom_widgets) 4793 private alias ListWidgetBase = ScrollableWidget; 4794 else 4795 private alias ListWidgetBase = Widget; 4796 4797 /++ 4798 A list widget contains a list of strings that the user can examine and select. 4799 4800 4801 In the future, items in the list may be possible to be more than just strings. 4802 4803 See_Also: 4804 [TableView] 4805 +/ 4806 class ListWidget : ListWidgetBase { 4807 /// 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. 4808 mixin Emits!(ChangeEvent!void); 4809 4810 static struct Option { 4811 string label; 4812 bool selected; 4813 void* tag; 4814 } 4815 4816 /++ 4817 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 4818 +/ 4819 void setSelection(int y) { 4820 if(!multiSelect) 4821 foreach(ref opt; options) 4822 opt.selected = false; 4823 if(y >= 0 && y < options.length) 4824 options[y].selected = !options[y].selected; 4825 4826 this.emit!(ChangeEvent!void)(delegate {}); 4827 4828 version(custom_widgets) 4829 redraw(); 4830 } 4831 4832 /++ 4833 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 4834 Returns -1 if nothing is selected. 4835 +/ 4836 int getSelection() 4837 { 4838 foreach(i, opt; options) { 4839 if (opt.selected) 4840 return cast(int) i; 4841 } 4842 return -1; 4843 } 4844 4845 version(custom_widgets) 4846 override void defaultEventHandler_click(ClickEvent event) { 4847 this.focus(); 4848 if(event.button == MouseButton.left) { 4849 auto y = (event.clientY - 4) / defaultLineHeight; 4850 if(y >= 0 && y < options.length) { 4851 setSelection(y); 4852 } 4853 } 4854 super.defaultEventHandler_click(event); 4855 } 4856 4857 this(Widget parent) { 4858 tabStop = false; 4859 super(parent); 4860 version(win32_widgets) 4861 createWin32Window(this, WC_LISTBOX, "", 4862 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 4863 } 4864 4865 version(win32_widgets) 4866 override void handleWmCommand(ushort code, ushort id) { 4867 switch(code) { 4868 case LBN_SELCHANGE: 4869 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 4870 setSelection(cast(int) sel); 4871 break; 4872 default: 4873 } 4874 } 4875 4876 4877 version(custom_widgets) 4878 override void paintFrameAndBackground(WidgetPainter painter) { 4879 draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor); 4880 } 4881 4882 version(custom_widgets) 4883 override void paint(WidgetPainter painter) { 4884 auto cs = getComputedStyle(); 4885 auto pos = Point(4, 4); 4886 foreach(idx, option; options) { 4887 painter.fillColor = painter.visualTheme.widgetBackgroundColor; 4888 painter.outlineColor = painter.visualTheme.widgetBackgroundColor; 4889 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4890 if(option.selected) { 4891 //painter.rasterOp = RasterOp.xor; 4892 painter.outlineColor = cs.selectionForegroundColor; 4893 painter.fillColor = cs.selectionBackgroundColor; 4894 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4895 //painter.rasterOp = RasterOp.normal; 4896 } 4897 painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor; 4898 painter.drawText(pos, option.label); 4899 pos.y += defaultLineHeight; 4900 } 4901 } 4902 4903 static class Style : Widget.Style { 4904 override WidgetBackground background() { 4905 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 4906 } 4907 } 4908 mixin OverrideStyle!Style; 4909 //mixin Padding!q{2}; 4910 4911 void addOption(string text, void* tag = null) { 4912 options ~= Option(text, false, tag); 4913 version(win32_widgets) { 4914 WCharzBuffer buffer = WCharzBuffer(text); 4915 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 4916 } 4917 version(custom_widgets) { 4918 setContentSize(width, cast(int) (options.length * defaultLineHeight)); 4919 redraw(); 4920 } 4921 } 4922 4923 void clear() { 4924 options = null; 4925 version(win32_widgets) { 4926 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 4927 {} 4928 4929 } else version(custom_widgets) { 4930 scrollTo(Point(0, 0)); 4931 redraw(); 4932 } 4933 } 4934 4935 Option[] options; 4936 version(win32_widgets) 4937 enum multiSelect = false; /// not implemented yet 4938 else 4939 bool multiSelect; 4940 4941 override int heightStretchiness() { return 6; } 4942 } 4943 4944 4945 4946 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 4947 enum ScrollBarShowPolicy { 4948 automatic, /// automatically show the scroll bar if it is necessary 4949 never, /// never show the scroll bar (scrolling must be done programmatically) 4950 always /// always show the scroll bar, even if it is disabled 4951 } 4952 4953 /++ 4954 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 4955 4956 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 4957 +/ 4958 // FIXME ScrollBarShowPolicy 4959 // FIXME: use the ScrollMessageWidget in here now that it exists 4960 class ScrollableWidget : Widget { 4961 // FIXME: make line size configurable 4962 // FIXME: add keyboard controls 4963 version(win32_widgets) { 4964 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 4965 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 4966 auto pos = HIWORD(wParam); 4967 auto m = LOWORD(wParam); 4968 4969 // FIXME: I can reintroduce the 4970 // scroll bars now by using this 4971 // in the top-level window handler 4972 // to forward comamnds 4973 auto scrollbarHwnd = lParam; 4974 switch(m) { 4975 case SB_BOTTOM: 4976 if(msg == WM_HSCROLL) 4977 horizontalScrollTo(contentWidth_); 4978 else 4979 verticalScrollTo(contentHeight_); 4980 break; 4981 case SB_TOP: 4982 if(msg == WM_HSCROLL) 4983 horizontalScrollTo(0); 4984 else 4985 verticalScrollTo(0); 4986 break; 4987 case SB_ENDSCROLL: 4988 // idk 4989 break; 4990 case SB_LINEDOWN: 4991 if(msg == WM_HSCROLL) 4992 horizontalScroll(scaleWithDpi(16)); 4993 else 4994 verticalScroll(scaleWithDpi(16)); 4995 break; 4996 case SB_LINEUP: 4997 if(msg == WM_HSCROLL) 4998 horizontalScroll(scaleWithDpi(-16)); 4999 else 5000 verticalScroll(scaleWithDpi(-16)); 5001 break; 5002 case SB_PAGEDOWN: 5003 if(msg == WM_HSCROLL) 5004 horizontalScroll(scaleWithDpi(100)); 5005 else 5006 verticalScroll(scaleWithDpi(100)); 5007 break; 5008 case SB_PAGEUP: 5009 if(msg == WM_HSCROLL) 5010 horizontalScroll(scaleWithDpi(-100)); 5011 else 5012 verticalScroll(scaleWithDpi(-100)); 5013 break; 5014 case SB_THUMBPOSITION: 5015 case SB_THUMBTRACK: 5016 if(msg == WM_HSCROLL) 5017 horizontalScrollTo(pos); 5018 else 5019 verticalScrollTo(pos); 5020 5021 if(m == SB_THUMBTRACK) { 5022 // the event loop doesn't seem to carry on with a requested redraw.. 5023 // so we request it to get our dirty bit set... 5024 redraw(); 5025 5026 // then we need to immediately actually redraw it too for instant feedback to user 5027 5028 SimpleWindow.processAllCustomEvents(); 5029 //if(parentWindow) 5030 //parentWindow.actualRedraw(); 5031 } 5032 break; 5033 default: 5034 } 5035 } 5036 return super.hookedWndProc(msg, wParam, lParam); 5037 } 5038 } 5039 /// 5040 this(Widget parent) { 5041 this.parentWindow = parent.parentWindow; 5042 5043 version(win32_widgets) { 5044 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 5045 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 5046 super(parent); 5047 } else version(custom_widgets) { 5048 outerContainer = new InternalScrollableContainerWidget(this, parent); 5049 super(outerContainer); 5050 } else static assert(0); 5051 } 5052 5053 version(custom_widgets) 5054 InternalScrollableContainerWidget outerContainer; 5055 5056 override void defaultEventHandler_click(ClickEvent event) { 5057 if(event.button == MouseButton.wheelUp) 5058 verticalScroll(scaleWithDpi(-16)); 5059 if(event.button == MouseButton.wheelDown) 5060 verticalScroll(scaleWithDpi(16)); 5061 super.defaultEventHandler_click(event); 5062 } 5063 5064 override void defaultEventHandler_keydown(KeyDownEvent event) { 5065 switch(event.key) { 5066 case Key.Left: 5067 horizontalScroll(scaleWithDpi(-16)); 5068 break; 5069 case Key.Right: 5070 horizontalScroll(scaleWithDpi(16)); 5071 break; 5072 case Key.Up: 5073 verticalScroll(scaleWithDpi(-16)); 5074 break; 5075 case Key.Down: 5076 verticalScroll(scaleWithDpi(16)); 5077 break; 5078 case Key.Home: 5079 verticalScrollTo(0); 5080 break; 5081 case Key.End: 5082 verticalScrollTo(contentHeight); 5083 break; 5084 case Key.PageUp: 5085 verticalScroll(scaleWithDpi(-160)); 5086 break; 5087 case Key.PageDown: 5088 verticalScroll(scaleWithDpi(160)); 5089 break; 5090 default: 5091 } 5092 super.defaultEventHandler_keydown(event); 5093 } 5094 5095 5096 version(win32_widgets) 5097 override void recomputeChildLayout() { 5098 super.recomputeChildLayout(); 5099 SCROLLINFO info; 5100 info.cbSize = info.sizeof; 5101 info.nPage = viewportHeight; 5102 info.fMask = SIF_PAGE | SIF_RANGE; 5103 info.nMin = 0; 5104 info.nMax = contentHeight_; 5105 SetScrollInfo(hwnd, SB_VERT, &info, true); 5106 5107 info.cbSize = info.sizeof; 5108 info.nPage = viewportWidth; 5109 info.fMask = SIF_PAGE | SIF_RANGE; 5110 info.nMin = 0; 5111 info.nMax = contentWidth_; 5112 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5113 } 5114 5115 /* 5116 Scrolling 5117 ------------ 5118 5119 You are assigned a width and a height by the layout engine, which 5120 is your viewport box. However, you may draw more than that by setting 5121 a contentWidth and contentHeight. 5122 5123 If these can be contained by the viewport, no scrollbar is displayed. 5124 If they cannot fit though, it will automatically show scroll as necessary. 5125 5126 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 5127 is zero, no vertical scrolling is performed. 5128 5129 If scrolling is necessary, the lib will automatically work with the bars. 5130 When you redraw, the origin and clipping info in the painter is set so if 5131 you just draw everything, it will work, but you can be more efficient by checking 5132 the viewportWidth, viewportHeight, and scrollOrigin members. 5133 */ 5134 5135 /// 5136 final @property int viewportWidth() { 5137 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 5138 } 5139 /// 5140 final @property int viewportHeight() { 5141 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 5142 } 5143 5144 // FIXME property 5145 Point scrollOrigin_; 5146 5147 /// 5148 final const(Point) scrollOrigin() { 5149 return scrollOrigin_; 5150 } 5151 5152 // the user sets these two 5153 private int contentWidth_ = 0; 5154 private int contentHeight_ = 0; 5155 5156 /// 5157 int contentWidth() { return contentWidth_; } 5158 /// 5159 int contentHeight() { return contentHeight_; } 5160 5161 /// 5162 void setContentSize(int width, int height) { 5163 contentWidth_ = width; 5164 contentHeight_ = height; 5165 5166 version(custom_widgets) { 5167 if(showingVerticalScroll || showingHorizontalScroll) { 5168 outerContainer.queueRecomputeChildLayout(); 5169 } 5170 5171 if(showingVerticalScroll()) 5172 outerContainer.verticalScrollBar.redraw(); 5173 if(showingHorizontalScroll()) 5174 outerContainer.horizontalScrollBar.redraw(); 5175 } else version(win32_widgets) { 5176 queueRecomputeChildLayout(); 5177 } else static assert(0); 5178 } 5179 5180 /// 5181 void verticalScroll(int delta) { 5182 verticalScrollTo(scrollOrigin.y + delta); 5183 } 5184 /// 5185 void verticalScrollTo(int pos) { 5186 scrollOrigin_.y = pos; 5187 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 5188 scrollOrigin_.y = contentHeight - viewportHeight; 5189 5190 if(scrollOrigin_.y < 0) 5191 scrollOrigin_.y = 0; 5192 5193 version(win32_widgets) { 5194 SCROLLINFO info; 5195 info.cbSize = info.sizeof; 5196 info.fMask = SIF_POS; 5197 info.nPos = scrollOrigin_.y; 5198 SetScrollInfo(hwnd, SB_VERT, &info, true); 5199 } else version(custom_widgets) { 5200 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 5201 } else static assert(0); 5202 5203 redraw(); 5204 } 5205 5206 /// 5207 void horizontalScroll(int delta) { 5208 horizontalScrollTo(scrollOrigin.x + delta); 5209 } 5210 /// 5211 void horizontalScrollTo(int pos) { 5212 scrollOrigin_.x = pos; 5213 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 5214 scrollOrigin_.x = contentWidth - viewportWidth; 5215 5216 if(scrollOrigin_.x < 0) 5217 scrollOrigin_.x = 0; 5218 5219 version(win32_widgets) { 5220 SCROLLINFO info; 5221 info.cbSize = info.sizeof; 5222 info.fMask = SIF_POS; 5223 info.nPos = scrollOrigin_.x; 5224 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5225 } else version(custom_widgets) { 5226 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 5227 } else static assert(0); 5228 5229 redraw(); 5230 } 5231 /// 5232 void scrollTo(Point p) { 5233 verticalScrollTo(p.y); 5234 horizontalScrollTo(p.x); 5235 } 5236 5237 /// 5238 void ensureVisibleInScroll(Point p) { 5239 auto rect = viewportRectangle(); 5240 if(rect.contains(p)) 5241 return; 5242 if(p.x < rect.left) 5243 horizontalScroll(p.x - rect.left); 5244 else if(p.x > rect.right) 5245 horizontalScroll(p.x - rect.right); 5246 5247 if(p.y < rect.top) 5248 verticalScroll(p.y - rect.top); 5249 else if(p.y > rect.bottom) 5250 verticalScroll(p.y - rect.bottom); 5251 } 5252 5253 /// 5254 void ensureVisibleInScroll(Rectangle rect) { 5255 ensureVisibleInScroll(rect.upperLeft); 5256 ensureVisibleInScroll(rect.lowerRight); 5257 } 5258 5259 /// 5260 Rectangle viewportRectangle() { 5261 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 5262 } 5263 5264 /// 5265 bool showingHorizontalScroll() { 5266 return contentWidth > width; 5267 } 5268 /// 5269 bool showingVerticalScroll() { 5270 return contentHeight > height; 5271 } 5272 5273 /// This is called before the ordinary paint delegate, 5274 /// giving you a chance to draw the window frame, etc, 5275 /// before the scroll clip takes effect 5276 void paintFrameAndBackground(WidgetPainter painter) { 5277 version(win32_widgets) { 5278 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5279 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5280 // since the pen is null, to fill the whole space, we need the +1 on both. 5281 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5282 SelectObject(painter.impl.hdc, p); 5283 SelectObject(painter.impl.hdc, b); 5284 } 5285 5286 } 5287 5288 // make space for the scroll bar, and that's it. 5289 final override int paddingRight() { return scaleWithDpi(16); } 5290 final override int paddingBottom() { return scaleWithDpi(16); } 5291 5292 /* 5293 END SCROLLING 5294 */ 5295 5296 override WidgetPainter draw() { 5297 int x = this.x, y = this.y; 5298 auto parent = this.parent; 5299 while(parent) { 5300 x += parent.x; 5301 y += parent.y; 5302 parent = parent.parent; 5303 } 5304 5305 //version(win32_widgets) { 5306 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5307 //} else { 5308 auto painter = parentWindow.win.draw(true); 5309 //} 5310 painter.originX = x; 5311 painter.originY = y; 5312 5313 painter.originX = painter.originX - scrollOrigin.x; 5314 painter.originY = painter.originY - scrollOrigin.y; 5315 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 5316 5317 return WidgetPainter(painter, this); 5318 } 5319 5320 mixin ScrollableChildren; 5321 } 5322 5323 // you need to have a Point scrollOrigin in the class somewhere 5324 // and a paintFrameAndBackground 5325 private mixin template ScrollableChildren() { 5326 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5327 if(hidden) 5328 return; 5329 5330 //version(win32_widgets) 5331 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5332 5333 painter.originX = lox + x; 5334 painter.originY = loy + y; 5335 5336 bool actuallyPainted = false; 5337 5338 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 5339 if(clip == Rectangle.init) 5340 return; 5341 5342 if(force || redrawRequested) { 5343 //painter.setClipRectangle(scrollOrigin, width, height); 5344 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5345 paintFrameAndBackground(painter); 5346 } 5347 5348 /+ 5349 version(win32_widgets) { 5350 if(hwnd) RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW);// | RDW_ALLCHILDREN | RDW_UPDATENOW); 5351 } 5352 +/ 5353 5354 painter.originX = painter.originX - scrollOrigin.x; 5355 painter.originY = painter.originY - scrollOrigin.y; 5356 if(force || redrawRequested) { 5357 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 5358 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 5359 5360 //erase(painter); // we paintFrameAndBackground above so no need 5361 if(painter.visualTheme) 5362 painter.visualTheme.doPaint(this, painter); 5363 else 5364 paint(painter); 5365 5366 if(invalidate) { 5367 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5368 // children are contained inside this, so no need to do extra work 5369 invalidate = false; 5370 } 5371 5372 5373 actuallyPainted = true; 5374 redrawRequested = false; 5375 } 5376 5377 foreach(child; children) { 5378 if(cast(FixedPosition) child) 5379 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5380 else 5381 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5382 } 5383 } 5384 } 5385 5386 private class InternalScrollableContainerInsideWidget : ContainerWidget { 5387 ScrollableContainerWidget scw; 5388 5389 this(ScrollableContainerWidget parent) { 5390 scw = parent; 5391 super(parent); 5392 } 5393 5394 version(custom_widgets) 5395 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5396 if(hidden) 5397 return; 5398 5399 bool actuallyPainted = false; 5400 5401 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 5402 5403 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 5404 if(clip == Rectangle.init) 5405 return; 5406 5407 painter.originX = lox + x - scrollOrigin.x; 5408 painter.originY = loy + y - scrollOrigin.y; 5409 if(force || redrawRequested) { 5410 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5411 5412 erase(painter); 5413 if(painter.visualTheme) 5414 painter.visualTheme.doPaint(this, painter); 5415 else 5416 paint(painter); 5417 5418 if(invalidate) { 5419 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5420 // children are contained inside this, so no need to do extra work 5421 invalidate = false; 5422 } 5423 5424 actuallyPainted = true; 5425 redrawRequested = false; 5426 } 5427 foreach(child; children) { 5428 if(cast(FixedPosition) child) 5429 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5430 else 5431 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5432 } 5433 } 5434 5435 version(custom_widgets) 5436 override protected void addScrollPosition(ref int x, ref int y) { 5437 x += scw.scrollX_; 5438 y += scw.scrollY_; 5439 } 5440 } 5441 5442 /++ 5443 A widget meant to contain other widgets that may need to scroll. 5444 5445 Currently buggy. 5446 5447 History: 5448 Added July 1, 2021 (dub v10.2) 5449 5450 On January 3, 2022, I tried to use it in a few other cases 5451 and found it only worked well in the original test case. Since 5452 it still sucks, I think I'm going to rewrite it again. 5453 +/ 5454 class ScrollableContainerWidget : ContainerWidget { 5455 /// 5456 this(Widget parent) { 5457 super(parent); 5458 5459 container = new InternalScrollableContainerInsideWidget(this); 5460 hsb = new HorizontalScrollbar(this); 5461 vsb = new VerticalScrollbar(this); 5462 5463 tabStop = false; 5464 container.tabStop = false; 5465 magic = true; 5466 5467 5468 vsb.addEventListener("scrolltonextline", () { 5469 scrollBy(0, scaleWithDpi(16)); 5470 }); 5471 vsb.addEventListener("scrolltopreviousline", () { 5472 scrollBy(0,scaleWithDpi( -16)); 5473 }); 5474 vsb.addEventListener("scrolltonextpage", () { 5475 scrollBy(0, container.height); 5476 }); 5477 vsb.addEventListener("scrolltopreviouspage", () { 5478 scrollBy(0, -container.height); 5479 }); 5480 vsb.addEventListener((scope ScrollToPositionEvent spe) { 5481 scrollTo(scrollX_, spe.value); 5482 }); 5483 5484 this.addEventListener(delegate (scope ClickEvent e) { 5485 if(e.button == MouseButton.wheelUp) { 5486 if(!e.defaultPrevented) 5487 scrollBy(0, scaleWithDpi(-16)); 5488 e.stopPropagation(); 5489 } else if(e.button == MouseButton.wheelDown) { 5490 if(!e.defaultPrevented) 5491 scrollBy(0, scaleWithDpi(16)); 5492 e.stopPropagation(); 5493 } 5494 }); 5495 } 5496 5497 /+ 5498 override void defaultEventHandler_click(ClickEvent e) { 5499 } 5500 +/ 5501 5502 override void removeAllChildren() { 5503 container.removeAllChildren(); 5504 } 5505 5506 void scrollTo(int x, int y) { 5507 scrollBy(x - scrollX_, y - scrollY_); 5508 } 5509 5510 void scrollBy(int x, int y) { 5511 auto ox = scrollX_; 5512 auto oy = scrollY_; 5513 5514 auto nx = ox + x; 5515 auto ny = oy + y; 5516 5517 if(nx < 0) 5518 nx = 0; 5519 if(ny < 0) 5520 ny = 0; 5521 5522 auto maxX = hsb.max - container.width; 5523 if(maxX < 0) maxX = 0; 5524 auto maxY = vsb.max - container.height; 5525 if(maxY < 0) maxY = 0; 5526 5527 if(nx > maxX) 5528 nx = maxX; 5529 if(ny > maxY) 5530 ny = maxY; 5531 5532 auto dx = nx - ox; 5533 auto dy = ny - oy; 5534 5535 if(dx || dy) { 5536 version(win32_widgets) 5537 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 5538 else { 5539 redraw(); 5540 } 5541 5542 hsb.setPosition = nx; 5543 vsb.setPosition = ny; 5544 5545 scrollX_ = nx; 5546 scrollY_ = ny; 5547 } 5548 } 5549 5550 private int scrollX_; 5551 private int scrollY_; 5552 5553 void setTotalArea(int width, int height) { 5554 hsb.setMax(width); 5555 vsb.setMax(height); 5556 } 5557 5558 /// 5559 void setViewableArea(int width, int height) { 5560 hsb.setViewableArea(width); 5561 vsb.setViewableArea(height); 5562 } 5563 5564 private bool magic; 5565 override void addChild(Widget w, int position = int.max) { 5566 if(magic) 5567 container.addChild(w, position); 5568 else 5569 super.addChild(w, position); 5570 } 5571 5572 override void recomputeChildLayout() { 5573 if(hsb is null || vsb is null || container is null) return; 5574 5575 /+ 5576 writeln(x, " ", y , " ", width, " ", height); 5577 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 5578 +/ 5579 5580 registerMovement(); 5581 5582 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 5583 hsb.x = 0; 5584 hsb.y = this.height - hsb.height; 5585 hsb.width = this.width - scaleWithDpi(16); 5586 hsb.recomputeChildLayout(); 5587 5588 vsb.width = scaleWithDpi(16); // FIXME? 5589 vsb.x = this.width - vsb.width; 5590 vsb.y = 0; 5591 vsb.height = this.height - scaleWithDpi(16); 5592 vsb.recomputeChildLayout(); 5593 5594 container.x = 0; 5595 container.y = 0; 5596 container.width = this.width - vsb.width; 5597 container.height = this.height - hsb.height; 5598 container.recomputeChildLayout(); 5599 5600 scrollX_ = 0; 5601 scrollY_ = 0; 5602 5603 hsb.setPosition(0); 5604 vsb.setPosition(0); 5605 5606 int mw, mh; 5607 Widget c = container; 5608 // FIXME: hack here to handle a layout inside... 5609 if(c.children.length == 1 && cast(Layout) c.children[0]) 5610 c = c.children[0]; 5611 foreach(child; c.children) { 5612 auto w = child.x + child.width; 5613 auto h = child.y + child.height; 5614 5615 if(w > mw) mw = w; 5616 if(h > mh) mh = h; 5617 } 5618 5619 setTotalArea(mw, mh); 5620 setViewableArea(width, height); 5621 } 5622 5623 override int minHeight() { return scaleWithDpi(64); } 5624 5625 HorizontalScrollbar hsb; 5626 VerticalScrollbar vsb; 5627 ContainerWidget container; 5628 } 5629 5630 5631 version(custom_widgets) 5632 private class InternalScrollableContainerWidget : Widget { 5633 5634 ScrollableWidget sw; 5635 5636 VerticalScrollbar verticalScrollBar; 5637 HorizontalScrollbar horizontalScrollBar; 5638 5639 this(ScrollableWidget sw, Widget parent) { 5640 this.sw = sw; 5641 5642 this.tabStop = false; 5643 5644 super(parent); 5645 5646 horizontalScrollBar = new HorizontalScrollbar(this); 5647 verticalScrollBar = new VerticalScrollbar(this); 5648 5649 horizontalScrollBar.showing_ = false; 5650 verticalScrollBar.showing_ = false; 5651 5652 horizontalScrollBar.addEventListener("scrolltonextline", { 5653 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 5654 sw.horizontalScrollTo(horizontalScrollBar.position); 5655 }); 5656 horizontalScrollBar.addEventListener("scrolltopreviousline", { 5657 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 5658 sw.horizontalScrollTo(horizontalScrollBar.position); 5659 }); 5660 verticalScrollBar.addEventListener("scrolltonextline", { 5661 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 5662 sw.verticalScrollTo(verticalScrollBar.position); 5663 }); 5664 verticalScrollBar.addEventListener("scrolltopreviousline", { 5665 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 5666 sw.verticalScrollTo(verticalScrollBar.position); 5667 }); 5668 horizontalScrollBar.addEventListener("scrolltonextpage", { 5669 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 5670 sw.horizontalScrollTo(horizontalScrollBar.position); 5671 }); 5672 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 5673 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 5674 sw.horizontalScrollTo(horizontalScrollBar.position); 5675 }); 5676 verticalScrollBar.addEventListener("scrolltonextpage", { 5677 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 5678 sw.verticalScrollTo(verticalScrollBar.position); 5679 }); 5680 verticalScrollBar.addEventListener("scrolltopreviouspage", { 5681 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 5682 sw.verticalScrollTo(verticalScrollBar.position); 5683 }); 5684 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 5685 horizontalScrollBar.setPosition(event.intValue); 5686 sw.horizontalScrollTo(horizontalScrollBar.position); 5687 }); 5688 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 5689 verticalScrollBar.setPosition(event.intValue); 5690 sw.verticalScrollTo(verticalScrollBar.position); 5691 }); 5692 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 5693 horizontalScrollBar.setPosition(event.intValue); 5694 sw.horizontalScrollTo(horizontalScrollBar.position); 5695 }); 5696 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 5697 verticalScrollBar.setPosition(event.intValue); 5698 }); 5699 } 5700 5701 // this is supposed to be basically invisible... 5702 override int minWidth() { return sw.minWidth; } 5703 override int minHeight() { return sw.minHeight; } 5704 override int maxWidth() { return sw.maxWidth; } 5705 override int maxHeight() { return sw.maxHeight; } 5706 override int widthStretchiness() { return sw.widthStretchiness; } 5707 override int heightStretchiness() { return sw.heightStretchiness; } 5708 override int marginLeft() { return sw.marginLeft; } 5709 override int marginRight() { return sw.marginRight; } 5710 override int marginTop() { return sw.marginTop; } 5711 override int marginBottom() { return sw.marginBottom; } 5712 override int paddingLeft() { return sw.paddingLeft; } 5713 override int paddingRight() { return sw.paddingRight; } 5714 override int paddingTop() { return sw.paddingTop; } 5715 override int paddingBottom() { return sw.paddingBottom; } 5716 override void focus() { sw.focus(); } 5717 5718 5719 override void recomputeChildLayout() { 5720 // The stupid thing needs to calculate if a scroll bar is needed... 5721 recomputeChildLayoutHelper(); 5722 // then running it again will position things correctly if the bar is NOT needed 5723 recomputeChildLayoutHelper(); 5724 5725 // this sucks but meh it barely works 5726 } 5727 5728 private void recomputeChildLayoutHelper() { 5729 if(sw is null) return; 5730 5731 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 5732 if(horizontalScrollBar && verticalScrollBar) { 5733 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 5734 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 5735 horizontalScrollBar.x = 0; 5736 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 5737 5738 verticalScrollBar.width = verticalScrollBar.minWidth(); 5739 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 5740 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 5741 verticalScrollBar.y = 0 + 2; 5742 5743 sw.x = 0; 5744 sw.y = 0; 5745 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 5746 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 5747 5748 if(sw.contentWidth_ <= this.width) 5749 sw.scrollOrigin_.x = 0; 5750 if(sw.contentHeight_ <= this.height) 5751 sw.scrollOrigin_.y = 0; 5752 5753 horizontalScrollBar.recomputeChildLayout(); 5754 verticalScrollBar.recomputeChildLayout(); 5755 sw.recomputeChildLayout(); 5756 } 5757 5758 if(sw.contentWidth_ <= this.width) 5759 sw.scrollOrigin_.x = 0; 5760 if(sw.contentHeight_ <= this.height) 5761 sw.scrollOrigin_.y = 0; 5762 5763 if(sw.showingHorizontalScroll()) 5764 horizontalScrollBar.showing(true, false); 5765 else 5766 horizontalScrollBar.showing(false, false); 5767 if(sw.showingVerticalScroll()) 5768 verticalScrollBar.showing(true, false); 5769 else 5770 verticalScrollBar.showing(false, false); 5771 5772 verticalScrollBar.setViewableArea(sw.viewportHeight()); 5773 verticalScrollBar.setMax(sw.contentHeight); 5774 verticalScrollBar.setPosition(sw.scrollOrigin.y); 5775 5776 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 5777 horizontalScrollBar.setMax(sw.contentWidth); 5778 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 5779 } 5780 } 5781 5782 /* 5783 class ScrollableClientWidget : Widget { 5784 this(Widget parent) { 5785 super(parent); 5786 } 5787 override void paint(WidgetPainter p) { 5788 parent.paint(p); 5789 } 5790 } 5791 */ 5792 5793 /++ 5794 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. 5795 +/ 5796 abstract class Slider : Widget { 5797 this(int min, int max, int step, Widget parent) { 5798 min_ = min; 5799 max_ = max; 5800 step_ = step; 5801 page_ = step; 5802 super(parent); 5803 } 5804 5805 private int min_; 5806 private int max_; 5807 private int step_; 5808 private int position_; 5809 private int page_; 5810 5811 // selection start and selection end 5812 // tics 5813 // tooltip? 5814 // some way to see and just type the value 5815 // win32 buddy controls are labels 5816 5817 /// 5818 void setMin(int a) { 5819 min_ = a; 5820 version(custom_widgets) 5821 redraw(); 5822 version(win32_widgets) 5823 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 5824 } 5825 /// 5826 int min() { 5827 return min_; 5828 } 5829 /// 5830 void setMax(int a) { 5831 max_ = a; 5832 version(custom_widgets) 5833 redraw(); 5834 version(win32_widgets) 5835 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 5836 } 5837 /// 5838 int max() { 5839 return max_; 5840 } 5841 /// 5842 void setPosition(int a) { 5843 if(a > max) 5844 a = max; 5845 if(a < min) 5846 a = min; 5847 position_ = a; 5848 version(custom_widgets) 5849 setPositionCustom(a); 5850 5851 version(win32_widgets) 5852 setPositionWindows(a); 5853 } 5854 version(win32_widgets) { 5855 protected abstract void setPositionWindows(int a); 5856 } 5857 5858 protected abstract int win32direction(); 5859 5860 /++ 5861 Alias for [position] for better compatibility with generic code. 5862 5863 History: 5864 Added October 5, 2021 5865 +/ 5866 @property int value() { 5867 return position; 5868 } 5869 5870 /// 5871 int position() { 5872 return position_; 5873 } 5874 /// 5875 void setStep(int a) { 5876 step_ = a; 5877 version(win32_widgets) 5878 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 5879 } 5880 /// 5881 int step() { 5882 return step_; 5883 } 5884 /// 5885 void setPageSize(int a) { 5886 page_ = a; 5887 version(win32_widgets) 5888 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 5889 } 5890 /// 5891 int pageSize() { 5892 return page_; 5893 } 5894 5895 private void notify() { 5896 auto event = new ChangeEvent!int(this, &this.position); 5897 event.dispatch(); 5898 } 5899 5900 version(win32_widgets) 5901 void win32Setup(int style) { 5902 createWin32Window(this, TRACKBAR_CLASS, "", 5903 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 5904 5905 // the trackbar sends the same messages as scroll, which 5906 // our other layer sends as these... just gonna translate 5907 // here 5908 this.addDirectEventListener("scrolltoposition", (Event event) { 5909 event.stopPropagation(); 5910 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 5911 notify(); 5912 }); 5913 this.addDirectEventListener("scrolltonextline", (Event event) { 5914 event.stopPropagation(); 5915 this.setPosition(this.position + this.step_ * this.win32direction); 5916 notify(); 5917 }); 5918 this.addDirectEventListener("scrolltopreviousline", (Event event) { 5919 event.stopPropagation(); 5920 this.setPosition(this.position - this.step_ * this.win32direction); 5921 notify(); 5922 }); 5923 this.addDirectEventListener("scrolltonextpage", (Event event) { 5924 event.stopPropagation(); 5925 this.setPosition(this.position + this.page_ * this.win32direction); 5926 notify(); 5927 }); 5928 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 5929 event.stopPropagation(); 5930 this.setPosition(this.position - this.page_ * this.win32direction); 5931 notify(); 5932 }); 5933 5934 setMin(min_); 5935 setMax(max_); 5936 setStep(step_); 5937 setPageSize(page_); 5938 } 5939 5940 version(custom_widgets) { 5941 protected MouseTrackingWidget thumb; 5942 5943 protected abstract void setPositionCustom(int a); 5944 5945 override void defaultEventHandler_keydown(KeyDownEvent event) { 5946 switch(event.key) { 5947 case Key.Up: 5948 case Key.Right: 5949 setPosition(position() - step() * win32direction); 5950 changed(); 5951 break; 5952 case Key.Down: 5953 case Key.Left: 5954 setPosition(position() + step() * win32direction); 5955 changed(); 5956 break; 5957 case Key.Home: 5958 setPosition(win32direction > 0 ? min() : max()); 5959 changed(); 5960 break; 5961 case Key.End: 5962 setPosition(win32direction > 0 ? max() : min()); 5963 changed(); 5964 break; 5965 case Key.PageUp: 5966 setPosition(position() - pageSize() * win32direction); 5967 changed(); 5968 break; 5969 case Key.PageDown: 5970 setPosition(position() + pageSize() * win32direction); 5971 changed(); 5972 break; 5973 default: 5974 } 5975 super.defaultEventHandler_keydown(event); 5976 } 5977 5978 protected void changed() { 5979 auto ev = new ChangeEvent!int(this, &position); 5980 ev.dispatch(); 5981 } 5982 } 5983 } 5984 5985 /++ 5986 5987 +/ 5988 class VerticalSlider : Slider { 5989 this(int min, int max, int step, Widget parent) { 5990 version(custom_widgets) 5991 initialize(); 5992 5993 super(min, max, step, parent); 5994 5995 version(win32_widgets) 5996 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 5997 } 5998 5999 protected override int win32direction() { 6000 return -1; 6001 } 6002 6003 version(win32_widgets) 6004 protected override void setPositionWindows(int a) { 6005 // the windows thing makes the top 0 and i don't like that. 6006 SendMessage(hwnd, TBM_SETPOS, true, max - a); 6007 } 6008 6009 version(custom_widgets) 6010 private void initialize() { 6011 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 6012 6013 thumb.tabStop = false; 6014 6015 thumb.thumbWidth = width; 6016 thumb.thumbHeight = scaleWithDpi(16); 6017 6018 thumb.addEventListener(EventType.change, () { 6019 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 6020 sx = max - sx; 6021 //informProgramThatUserChangedPosition(sx); 6022 6023 position_ = sx; 6024 6025 changed(); 6026 }); 6027 } 6028 6029 version(custom_widgets) 6030 override void recomputeChildLayout() { 6031 thumb.thumbWidth = this.width; 6032 super.recomputeChildLayout(); 6033 setPositionCustom(position_); 6034 } 6035 6036 version(custom_widgets) 6037 protected override void setPositionCustom(int a) { 6038 if(max()) 6039 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 6040 redraw(); 6041 } 6042 } 6043 6044 /++ 6045 6046 +/ 6047 class HorizontalSlider : Slider { 6048 this(int min, int max, int step, Widget parent) { 6049 version(custom_widgets) 6050 initialize(); 6051 6052 super(min, max, step, parent); 6053 6054 version(win32_widgets) 6055 win32Setup(TBS_HORZ); 6056 } 6057 6058 version(win32_widgets) 6059 protected override void setPositionWindows(int a) { 6060 SendMessage(hwnd, TBM_SETPOS, true, a); 6061 } 6062 6063 protected override int win32direction() { 6064 return 1; 6065 } 6066 6067 version(custom_widgets) 6068 private void initialize() { 6069 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 6070 6071 thumb.tabStop = false; 6072 6073 thumb.thumbWidth = scaleWithDpi(16); 6074 thumb.thumbHeight = height; 6075 6076 thumb.addEventListener(EventType.change, () { 6077 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 6078 //informProgramThatUserChangedPosition(sx); 6079 6080 position_ = sx; 6081 6082 changed(); 6083 }); 6084 } 6085 6086 version(custom_widgets) 6087 override void recomputeChildLayout() { 6088 thumb.thumbHeight = this.height; 6089 super.recomputeChildLayout(); 6090 setPositionCustom(position_); 6091 } 6092 6093 version(custom_widgets) 6094 protected override void setPositionCustom(int a) { 6095 if(max()) 6096 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 6097 redraw(); 6098 } 6099 } 6100 6101 6102 /// 6103 abstract class ScrollbarBase : Widget { 6104 /// 6105 this(Widget parent) { 6106 super(parent); 6107 tabStop = false; 6108 step_ = scaleWithDpi(16); 6109 } 6110 6111 private int viewableArea_; 6112 private int max_; 6113 private int step_;// = 16; 6114 private int position_; 6115 6116 /// 6117 bool atEnd() { 6118 return position_ + viewableArea_ >= max_; 6119 } 6120 6121 /// 6122 bool atStart() { 6123 return position_ == 0; 6124 } 6125 6126 /// 6127 void setViewableArea(int a) { 6128 viewableArea_ = a; 6129 version(custom_widgets) 6130 redraw(); 6131 } 6132 /// 6133 void setMax(int a) { 6134 max_ = a; 6135 version(custom_widgets) 6136 redraw(); 6137 } 6138 /// 6139 int max() { 6140 return max_; 6141 } 6142 /// 6143 void setPosition(int a) { 6144 auto logicalMax = max_ - viewableArea_; 6145 if(a == int.max) 6146 a = logicalMax; 6147 6148 if(a > logicalMax) 6149 a = logicalMax; 6150 if(a < 0) 6151 a = 0; 6152 6153 position_ = a; 6154 6155 version(custom_widgets) 6156 redraw(); 6157 } 6158 /// 6159 int position() { 6160 return position_; 6161 } 6162 /// 6163 void setStep(int a) { 6164 step_ = a; 6165 } 6166 /// 6167 int step() { 6168 return step_; 6169 } 6170 6171 // FIXME: remove this.... maybe 6172 /+ 6173 protected void informProgramThatUserChangedPosition(int n) { 6174 position_ = n; 6175 auto evt = new Event(EventType.change, this); 6176 evt.intValue = n; 6177 evt.dispatch(); 6178 } 6179 +/ 6180 6181 version(custom_widgets) { 6182 enum MIN_THUMB_SIZE = 8; 6183 6184 abstract protected int getBarDim(); 6185 int thumbSize() { 6186 if(viewableArea_ >= max_ || max_ == 0) 6187 return getBarDim(); 6188 6189 int res = viewableArea_ * getBarDim() / max_; 6190 6191 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 6192 res = scaleWithDpi(MIN_THUMB_SIZE); 6193 6194 return res; 6195 } 6196 6197 int thumbPosition() { 6198 /* 6199 viewableArea_ is the viewport height/width 6200 position_ is where we are 6201 */ 6202 //if(position_ + viewableArea_ >= max_) 6203 //return getBarDim - thumbSize; 6204 6205 auto maximumPossibleValue = getBarDim() - thumbSize; 6206 auto maximiumLogicalValue = max_ - viewableArea_; 6207 6208 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 6209 6210 return p; 6211 } 6212 } 6213 } 6214 6215 //public import mgt; 6216 6217 /++ 6218 A mouse tracking widget is one that follows the mouse when dragged inside it. 6219 6220 Concrete subclasses may include a scrollbar thumb and a volume control. 6221 +/ 6222 //version(custom_widgets) 6223 class MouseTrackingWidget : Widget { 6224 6225 /// 6226 int positionX() { return positionX_; } 6227 /// 6228 int positionY() { return positionY_; } 6229 6230 /// 6231 void positionX(int p) { positionX_ = p; } 6232 /// 6233 void positionY(int p) { positionY_ = p; } 6234 6235 private int positionX_; 6236 private int positionY_; 6237 6238 /// 6239 enum Orientation { 6240 horizontal, /// 6241 vertical, /// 6242 twoDimensional, /// 6243 } 6244 6245 private int thumbWidth_; 6246 private int thumbHeight_; 6247 6248 /// 6249 int thumbWidth() { return thumbWidth_; } 6250 /// 6251 int thumbHeight() { return thumbHeight_; } 6252 /// 6253 int thumbWidth(int a) { return thumbWidth_ = a; } 6254 /// 6255 int thumbHeight(int a) { return thumbHeight_ = a; } 6256 6257 private bool dragging; 6258 private bool hovering; 6259 private int startMouseX, startMouseY; 6260 6261 /// 6262 this(Orientation orientation, Widget parent) { 6263 super(parent); 6264 6265 //assert(parentWindow !is null); 6266 6267 addEventListener((MouseDownEvent event) { 6268 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6269 dragging = true; 6270 startMouseX = event.clientX - positionX; 6271 startMouseY = event.clientY - positionY; 6272 parentWindow.captureMouse(this); 6273 } else { 6274 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6275 positionX = event.clientX - thumbWidth / 2; 6276 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6277 positionY = event.clientY - thumbHeight / 2; 6278 6279 if(positionX + thumbWidth > this.width) 6280 positionX = this.width - thumbWidth; 6281 if(positionY + thumbHeight > this.height) 6282 positionY = this.height - thumbHeight; 6283 6284 if(positionX < 0) 6285 positionX = 0; 6286 if(positionY < 0) 6287 positionY = 0; 6288 6289 6290 // this.emit!(ChangeEvent!void)(); 6291 auto evt = new Event(EventType.change, this); 6292 evt.sendDirectly(); 6293 6294 redraw(); 6295 6296 } 6297 }); 6298 6299 addEventListener(EventType.mouseup, (Event event) { 6300 dragging = false; 6301 parentWindow.releaseMouseCapture(); 6302 }); 6303 6304 addEventListener(EventType.mouseout, (Event event) { 6305 if(!hovering) 6306 return; 6307 hovering = false; 6308 redraw(); 6309 }); 6310 6311 int lpx, lpy; 6312 6313 addEventListener((MouseMoveEvent event) { 6314 auto oh = hovering; 6315 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6316 hovering = true; 6317 } else { 6318 hovering = false; 6319 } 6320 if(!dragging) { 6321 if(hovering != oh) 6322 redraw(); 6323 return; 6324 } 6325 6326 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6327 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 6328 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6329 positionY = event.clientY - startMouseY; 6330 6331 if(positionX + thumbWidth > this.width) 6332 positionX = this.width - thumbWidth; 6333 if(positionY + thumbHeight > this.height) 6334 positionY = this.height - thumbHeight; 6335 6336 if(positionX < 0) 6337 positionX = 0; 6338 if(positionY < 0) 6339 positionY = 0; 6340 6341 if(positionX != lpx || positionY != lpy) { 6342 lpx = positionX; 6343 lpy = positionY; 6344 6345 auto evt = new Event(EventType.change, this); 6346 evt.sendDirectly(); 6347 } 6348 6349 redraw(); 6350 }); 6351 } 6352 6353 version(custom_widgets) 6354 override void paint(WidgetPainter painter) { 6355 auto cs = getComputedStyle(); 6356 auto c = darken(cs.windowBackgroundColor, 0.2); 6357 painter.outlineColor = c; 6358 painter.fillColor = c; 6359 painter.drawRectangle(Point(0, 0), this.width, this.height); 6360 6361 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 6362 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 6363 } 6364 } 6365 6366 //version(custom_widgets) 6367 //private 6368 class HorizontalScrollbar : ScrollbarBase { 6369 6370 version(custom_widgets) { 6371 private MouseTrackingWidget thumb; 6372 6373 override int getBarDim() { 6374 return thumb.width; 6375 } 6376 } 6377 6378 override void setViewableArea(int a) { 6379 super.setViewableArea(a); 6380 6381 version(win32_widgets) { 6382 SCROLLINFO info; 6383 info.cbSize = info.sizeof; 6384 info.nPage = a + 1; 6385 info.fMask = SIF_PAGE; 6386 SetScrollInfo(hwnd, SB_CTL, &info, true); 6387 } else version(custom_widgets) { 6388 thumb.positionX = thumbPosition; 6389 thumb.thumbWidth = thumbSize; 6390 thumb.redraw(); 6391 } else static assert(0); 6392 6393 } 6394 6395 override void setMax(int a) { 6396 super.setMax(a); 6397 version(win32_widgets) { 6398 SCROLLINFO info; 6399 info.cbSize = info.sizeof; 6400 info.nMin = 0; 6401 info.nMax = max; 6402 info.fMask = SIF_RANGE; 6403 SetScrollInfo(hwnd, SB_CTL, &info, true); 6404 } else version(custom_widgets) { 6405 thumb.positionX = thumbPosition; 6406 thumb.thumbWidth = thumbSize; 6407 thumb.redraw(); 6408 } 6409 } 6410 6411 override void setPosition(int a) { 6412 super.setPosition(a); 6413 version(win32_widgets) { 6414 SCROLLINFO info; 6415 info.cbSize = info.sizeof; 6416 info.fMask = SIF_POS; 6417 info.nPos = position; 6418 SetScrollInfo(hwnd, SB_CTL, &info, true); 6419 } else version(custom_widgets) { 6420 thumb.positionX = thumbPosition(); 6421 thumb.thumbWidth = thumbSize; 6422 thumb.redraw(); 6423 } else static assert(0); 6424 } 6425 6426 this(Widget parent) { 6427 super(parent); 6428 6429 version(win32_widgets) { 6430 createWin32Window(this, "Scrollbar"w, "", 6431 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 6432 } else version(custom_widgets) { 6433 auto vl = new HorizontalLayout(this); 6434 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 6435 leftButton.setClickRepeat(scrollClickRepeatInterval); 6436 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 6437 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 6438 rightButton.setClickRepeat(scrollClickRepeatInterval); 6439 6440 leftButton.tabStop = false; 6441 rightButton.tabStop = false; 6442 thumb.tabStop = false; 6443 6444 leftButton.addEventListener(EventType.triggered, () { 6445 this.emitCommand!"scrolltopreviousline"(); 6446 //informProgramThatUserChangedPosition(position - step()); 6447 }); 6448 rightButton.addEventListener(EventType.triggered, () { 6449 this.emitCommand!"scrolltonextline"(); 6450 //informProgramThatUserChangedPosition(position + step()); 6451 }); 6452 6453 thumb.thumbWidth = this.minWidth; 6454 thumb.thumbHeight = scaleWithDpi(16); 6455 6456 thumb.addEventListener(EventType.change, () { 6457 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 6458 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 6459 6460 //informProgramThatUserChangedPosition(sx); 6461 6462 auto ev = new ScrollToPositionEvent(this, sx); 6463 ev.dispatch(); 6464 }); 6465 } 6466 } 6467 6468 override int minHeight() { return scaleWithDpi(16); } 6469 override int maxHeight() { return scaleWithDpi(16); } 6470 override int minWidth() { return scaleWithDpi(48); } 6471 } 6472 6473 class ScrollToPositionEvent : Event { 6474 enum EventString = "scrolltoposition"; 6475 6476 this(Widget target, int value) { 6477 this.value = value; 6478 super(EventString, target); 6479 } 6480 6481 immutable int value; 6482 6483 override @property int intValue() { 6484 return value; 6485 } 6486 } 6487 6488 //version(custom_widgets) 6489 //private 6490 class VerticalScrollbar : ScrollbarBase { 6491 6492 version(custom_widgets) { 6493 override int getBarDim() { 6494 return thumb.height; 6495 } 6496 6497 private MouseTrackingWidget thumb; 6498 } 6499 6500 override void setViewableArea(int a) { 6501 super.setViewableArea(a); 6502 6503 version(win32_widgets) { 6504 SCROLLINFO info; 6505 info.cbSize = info.sizeof; 6506 info.nPage = a + 1; 6507 info.fMask = SIF_PAGE; 6508 SetScrollInfo(hwnd, SB_CTL, &info, true); 6509 } else version(custom_widgets) { 6510 thumb.positionY = thumbPosition; 6511 thumb.thumbHeight = thumbSize; 6512 thumb.redraw(); 6513 } else static assert(0); 6514 6515 } 6516 6517 override void setMax(int a) { 6518 super.setMax(a); 6519 version(win32_widgets) { 6520 SCROLLINFO info; 6521 info.cbSize = info.sizeof; 6522 info.nMin = 0; 6523 info.nMax = max; 6524 info.fMask = SIF_RANGE; 6525 SetScrollInfo(hwnd, SB_CTL, &info, true); 6526 } else version(custom_widgets) { 6527 thumb.positionY = thumbPosition; 6528 thumb.thumbHeight = thumbSize; 6529 thumb.redraw(); 6530 } 6531 } 6532 6533 override void setPosition(int a) { 6534 super.setPosition(a); 6535 version(win32_widgets) { 6536 SCROLLINFO info; 6537 info.cbSize = info.sizeof; 6538 info.fMask = SIF_POS; 6539 info.nPos = position; 6540 SetScrollInfo(hwnd, SB_CTL, &info, true); 6541 } else version(custom_widgets) { 6542 thumb.positionY = thumbPosition; 6543 thumb.thumbHeight = thumbSize; 6544 thumb.redraw(); 6545 } else static assert(0); 6546 } 6547 6548 this(Widget parent) { 6549 super(parent); 6550 6551 version(win32_widgets) { 6552 createWin32Window(this, "Scrollbar"w, "", 6553 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 6554 } else version(custom_widgets) { 6555 auto vl = new VerticalLayout(this); 6556 auto upButton = new ArrowButton(ArrowDirection.up, vl); 6557 upButton.setClickRepeat(scrollClickRepeatInterval); 6558 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 6559 auto downButton = new ArrowButton(ArrowDirection.down, vl); 6560 downButton.setClickRepeat(scrollClickRepeatInterval); 6561 6562 upButton.addEventListener(EventType.triggered, () { 6563 this.emitCommand!"scrolltopreviousline"(); 6564 //informProgramThatUserChangedPosition(position - step()); 6565 }); 6566 downButton.addEventListener(EventType.triggered, () { 6567 this.emitCommand!"scrolltonextline"(); 6568 //informProgramThatUserChangedPosition(position + step()); 6569 }); 6570 6571 thumb.thumbWidth = this.minWidth; 6572 thumb.thumbHeight = scaleWithDpi(16); 6573 6574 thumb.addEventListener(EventType.change, () { 6575 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 6576 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 6577 6578 auto ev = new ScrollToPositionEvent(this, sy); 6579 ev.dispatch(); 6580 6581 //informProgramThatUserChangedPosition(sy); 6582 }); 6583 6584 upButton.tabStop = false; 6585 downButton.tabStop = false; 6586 thumb.tabStop = false; 6587 } 6588 } 6589 6590 override int minWidth() { return scaleWithDpi(16); } 6591 override int maxWidth() { return scaleWithDpi(16); } 6592 override int minHeight() { return scaleWithDpi(48); } 6593 } 6594 6595 6596 /++ 6597 EXPERIMENTAL 6598 6599 A widget specialized for being a container for other widgets. 6600 6601 History: 6602 Added May 29, 2021. Not stabilized at this time. 6603 +/ 6604 class WidgetContainer : Widget { 6605 this(Widget parent) { 6606 tabStop = false; 6607 super(parent); 6608 } 6609 6610 override int maxHeight() { 6611 if(this.children.length == 1) { 6612 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 6613 } else { 6614 return int.max; 6615 } 6616 } 6617 6618 override int maxWidth() { 6619 if(this.children.length == 1) { 6620 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 6621 } else { 6622 return int.max; 6623 } 6624 } 6625 6626 /+ 6627 6628 override int minHeight() { 6629 int largest = 0; 6630 int margins = 0; 6631 int lastMargin = 0; 6632 foreach(child; children) { 6633 auto mh = child.minHeight(); 6634 if(mh > largest) 6635 largest = mh; 6636 margins += mymax(lastMargin, child.marginTop()); 6637 lastMargin = child.marginBottom(); 6638 } 6639 return largest + margins; 6640 } 6641 6642 override int maxHeight() { 6643 int largest = 0; 6644 int margins = 0; 6645 int lastMargin = 0; 6646 foreach(child; children) { 6647 auto mh = child.maxHeight(); 6648 if(mh == int.max) 6649 return int.max; 6650 if(mh > largest) 6651 largest = mh; 6652 margins += mymax(lastMargin, child.marginTop()); 6653 lastMargin = child.marginBottom(); 6654 } 6655 return largest + margins; 6656 } 6657 6658 override int minWidth() { 6659 int min; 6660 foreach(child; children) { 6661 auto cm = child.minWidth; 6662 if(cm > min) 6663 min = cm; 6664 } 6665 return min + paddingLeft + paddingRight; 6666 } 6667 6668 override int minHeight() { 6669 int min; 6670 foreach(child; children) { 6671 auto cm = child.minHeight; 6672 if(cm > min) 6673 min = cm; 6674 } 6675 return min + paddingTop + paddingBottom; 6676 } 6677 6678 override int maxHeight() { 6679 int largest = 0; 6680 int margins = 0; 6681 int lastMargin = 0; 6682 foreach(child; children) { 6683 auto mh = child.maxHeight(); 6684 if(mh == int.max) 6685 return int.max; 6686 if(mh > largest) 6687 largest = mh; 6688 margins += mymax(lastMargin, child.marginTop()); 6689 lastMargin = child.marginBottom(); 6690 } 6691 return largest + margins; 6692 } 6693 6694 override int heightStretchiness() { 6695 int max; 6696 foreach(child; children) { 6697 auto c = child.heightStretchiness; 6698 if(c > max) 6699 max = c; 6700 } 6701 return max; 6702 } 6703 6704 override int marginTop() { 6705 if(this.children.length) 6706 return this.children[0].marginTop; 6707 return 0; 6708 } 6709 +/ 6710 } 6711 6712 /// 6713 abstract class Layout : Widget { 6714 this(Widget parent) { 6715 tabStop = false; 6716 super(parent); 6717 } 6718 } 6719 6720 /++ 6721 Makes all children minimum width and height, placing them down 6722 left to right, top to bottom. 6723 6724 Useful if you want to make a list of buttons that automatically 6725 wrap to a new line when necessary. 6726 +/ 6727 class InlineBlockLayout : Layout { 6728 /// 6729 this(Widget parent) { super(parent); } 6730 6731 override void recomputeChildLayout() { 6732 registerMovement(); 6733 6734 int x = this.paddingLeft, y = this.paddingTop; 6735 6736 int lineHeight; 6737 int previousMargin = 0; 6738 int previousMarginBottom = 0; 6739 6740 foreach(child; children) { 6741 if(child.hidden) 6742 continue; 6743 if(cast(FixedPosition) child) { 6744 child.recomputeChildLayout(); 6745 continue; 6746 } 6747 child.width = child.flexBasisWidth(); 6748 if(child.width == 0) 6749 child.width = child.minWidth(); 6750 if(child.width == 0) 6751 child.width = 32; 6752 6753 child.height = child.flexBasisHeight(); 6754 if(child.height == 0) 6755 child.height = child.minHeight(); 6756 if(child.height == 0) 6757 child.height = 32; 6758 6759 if(x + child.width + paddingRight > this.width) { 6760 x = this.paddingLeft; 6761 y += lineHeight; 6762 lineHeight = 0; 6763 previousMargin = 0; 6764 previousMarginBottom = 0; 6765 } 6766 6767 auto margin = child.marginLeft; 6768 if(previousMargin > margin) 6769 margin = previousMargin; 6770 6771 x += margin; 6772 6773 child.x = x; 6774 child.y = y; 6775 6776 int marginTopApplied; 6777 if(child.marginTop > previousMarginBottom) { 6778 child.y += child.marginTop; 6779 marginTopApplied = child.marginTop; 6780 } 6781 6782 x += child.width; 6783 previousMargin = child.marginRight; 6784 6785 if(child.marginBottom > previousMarginBottom) 6786 previousMarginBottom = child.marginBottom; 6787 6788 auto h = child.height + previousMarginBottom + marginTopApplied; 6789 if(h > lineHeight) 6790 lineHeight = h; 6791 6792 child.recomputeChildLayout(); 6793 } 6794 6795 } 6796 6797 override int minWidth() { 6798 int min; 6799 foreach(child; children) { 6800 auto cm = child.minWidth; 6801 if(cm > min) 6802 min = cm; 6803 } 6804 return min + paddingLeft + paddingRight; 6805 } 6806 6807 override int minHeight() { 6808 int min; 6809 foreach(child; children) { 6810 auto cm = child.minHeight; 6811 if(cm > min) 6812 min = cm; 6813 } 6814 return min + paddingTop + paddingBottom; 6815 } 6816 } 6817 6818 /++ 6819 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 6820 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 6821 the [TabWidget] will automatically change pages of child widgets. 6822 6823 This allows you to react to it however you see fit rather than having to 6824 be tied to just the new sets of child widgets. 6825 6826 It sends the message in the form of `this.emitCommand!"changetab"();`. 6827 6828 History: 6829 Added December 24, 2021 (dub v10.5) 6830 +/ 6831 class TabMessageWidget : Widget { 6832 6833 protected void tabIndexClicked(int item) { 6834 this.emitCommand!"changetab"(); 6835 } 6836 6837 /++ 6838 Adds the a new tab to the control with the given title. 6839 6840 Returns: 6841 The index of the newly added tab. You will need to know 6842 this index to refer to it later and to know which tab to 6843 change to when you get a changetab message. 6844 +/ 6845 int addTab(string title, int pos = int.max) { 6846 version(win32_widgets) { 6847 TCITEM item; 6848 item.mask = TCIF_TEXT; 6849 WCharzBuffer buf = WCharzBuffer(title); 6850 item.pszText = buf.ptr; 6851 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 6852 } else version(custom_widgets) { 6853 if(pos >= tabs.length) { 6854 tabs ~= title; 6855 redraw(); 6856 return cast(int) tabs.length - 1; 6857 } else if(pos <= 0) { 6858 tabs = title ~ tabs; 6859 redraw(); 6860 return 0; 6861 } else { 6862 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 6863 redraw(); 6864 return pos; 6865 } 6866 } 6867 } 6868 6869 override void addChild(Widget child, int pos = int.max) { 6870 if(container) 6871 container.addChild(child, pos); 6872 else 6873 super.addChild(child, pos); 6874 } 6875 6876 protected Widget makeContainer() { 6877 return new Widget(this); 6878 } 6879 6880 private Widget container; 6881 6882 override void recomputeChildLayout() { 6883 version(win32_widgets) { 6884 this.registerMovement(); 6885 6886 RECT rect; 6887 GetWindowRect(hwnd, &rect); 6888 6889 auto left = rect.left; 6890 auto top = rect.top; 6891 6892 TabCtrl_AdjustRect(hwnd, false, &rect); 6893 foreach(child; children) { 6894 if(!child.showing) continue; 6895 child.x = rect.left - left; 6896 child.y = rect.top - top; 6897 child.width = rect.right - rect.left; 6898 child.height = rect.bottom - rect.top; 6899 child.recomputeChildLayout(); 6900 } 6901 } else version(custom_widgets) { 6902 this.registerMovement(); 6903 foreach(child; children) { 6904 if(!child.showing) continue; 6905 child.x = 2; 6906 child.y = tabBarHeight + 2; // for the border 6907 child.width = width - 4; // for the border 6908 child.height = height - tabBarHeight - 2 - 2; // for the border 6909 child.recomputeChildLayout(); 6910 } 6911 } else static assert(0); 6912 } 6913 6914 version(custom_widgets) 6915 string[] tabs; 6916 6917 this(Widget parent) { 6918 super(parent); 6919 6920 tabStop = false; 6921 6922 version(win32_widgets) { 6923 createWin32Window(this, WC_TABCONTROL, "", 0); 6924 } else version(custom_widgets) { 6925 addEventListener((ClickEvent event) { 6926 if(event.target !is this) 6927 return; 6928 if(event.clientY >= 0 && event.clientY < tabBarHeight) { 6929 auto t = (event.clientX / tabWidth); 6930 if(t >= 0 && t < tabs.length) { 6931 currentTab_ = t; 6932 tabIndexClicked(t); 6933 redraw(); 6934 } 6935 } 6936 }); 6937 } else static assert(0); 6938 6939 this.container = makeContainer(); 6940 } 6941 6942 override int marginTop() { return 4; } 6943 override int paddingBottom() { return 4; } 6944 6945 override int minHeight() { 6946 int max = 0; 6947 foreach(child; children) 6948 max = mymax(child.minHeight, max); 6949 6950 6951 version(win32_widgets) { 6952 RECT rect; 6953 rect.right = this.width; 6954 rect.bottom = max; 6955 TabCtrl_AdjustRect(hwnd, true, &rect); 6956 6957 max = rect.bottom; 6958 } else { 6959 max += defaultLineHeight + 4; 6960 } 6961 6962 6963 return max; 6964 } 6965 6966 version(win32_widgets) 6967 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 6968 switch(code) { 6969 case TCN_SELCHANGE: 6970 auto sel = TabCtrl_GetCurSel(hwnd); 6971 tabIndexClicked(sel); 6972 break; 6973 default: 6974 } 6975 return 0; 6976 } 6977 6978 version(custom_widgets) { 6979 private int currentTab_; 6980 private int tabBarHeight() { return defaultLineHeight; } 6981 int tabWidth() { return scaleWithDpi(80); } 6982 } 6983 6984 version(win32_widgets) 6985 override void paint(WidgetPainter painter) {} 6986 6987 version(custom_widgets) 6988 override void paint(WidgetPainter painter) { 6989 auto cs = getComputedStyle(); 6990 6991 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 6992 6993 int posX = 0; 6994 foreach(idx, title; tabs) { 6995 auto isCurrent = idx == getCurrentTab(); 6996 6997 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 6998 6999 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 7000 painter.outlineColor = cs.foregroundColor; 7001 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 7002 7003 if(isCurrent) { 7004 painter.outlineColor = cs.windowBackgroundColor; 7005 painter.fillColor = Color.transparent; 7006 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 7007 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 7008 7009 painter.outlineColor = Color.white; 7010 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 7011 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 7012 painter.outlineColor = cs.activeTabColor; 7013 painter.drawPixel(Point(posX, tabBarHeight - 1)); 7014 } 7015 7016 posX += tabWidth - 2; 7017 } 7018 } 7019 7020 /// 7021 @scriptable 7022 void setCurrentTab(int item) { 7023 version(win32_widgets) 7024 TabCtrl_SetCurSel(hwnd, item); 7025 else version(custom_widgets) 7026 currentTab_ = item; 7027 else static assert(0); 7028 7029 tabIndexClicked(item); 7030 } 7031 7032 /// 7033 @scriptable 7034 int getCurrentTab() { 7035 version(win32_widgets) 7036 return TabCtrl_GetCurSel(hwnd); 7037 else version(custom_widgets) 7038 return currentTab_; // FIXME 7039 else static assert(0); 7040 } 7041 7042 /// 7043 @scriptable 7044 void removeTab(int item) { 7045 if(item && item == getCurrentTab()) 7046 setCurrentTab(item - 1); 7047 7048 version(win32_widgets) { 7049 TabCtrl_DeleteItem(hwnd, item); 7050 } 7051 7052 for(int a = item; a < children.length - 1; a++) 7053 this._children[a] = this._children[a + 1]; 7054 this._children = this._children[0 .. $-1]; 7055 } 7056 7057 } 7058 7059 7060 /++ 7061 A tab widget is a set of clickable tab buttons followed by a content area. 7062 7063 7064 Tabs can change existing content or can be new pages. 7065 7066 When the user picks a different tab, a `change` message is generated. 7067 +/ 7068 class TabWidget : TabMessageWidget { 7069 this(Widget parent) { 7070 super(parent); 7071 } 7072 7073 override protected Widget makeContainer() { 7074 return null; 7075 } 7076 7077 override void addChild(Widget child, int pos = int.max) { 7078 if(auto twp = cast(TabWidgetPage) child) { 7079 Widget.addChild(child, pos); 7080 if(pos == int.max) 7081 pos = cast(int) this.children.length - 1; 7082 7083 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 7084 7085 if(pos != getCurrentTab) { 7086 child.showing = false; 7087 } 7088 } else { 7089 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 7090 } 7091 } 7092 7093 // FIXME: add tab icons at some point, Windows supports them 7094 /++ 7095 Adds a page and its associated tab with the given label to the widget. 7096 7097 Returns: 7098 The added page object, to which you can add other widgets. 7099 +/ 7100 @scriptable 7101 TabWidgetPage addPage(string title) { 7102 return new TabWidgetPage(title, this); 7103 } 7104 7105 /++ 7106 Gets the page at the given tab index, or `null` if the index is bad. 7107 7108 History: 7109 Added December 24, 2021. 7110 +/ 7111 TabWidgetPage getPage(int index) { 7112 if(index < this.children.length) 7113 return null; 7114 return cast(TabWidgetPage) this.children[index]; 7115 } 7116 7117 /++ 7118 While you can still use the addTab from the parent class, 7119 *strongly* recommend you use [addPage] insteaad. 7120 7121 History: 7122 Added December 24, 2021 to fulful the interface 7123 requirement that came from adding [TabMessageWidget]. 7124 7125 You should not use it though since the [addPage] function 7126 is much easier to use here. 7127 +/ 7128 override int addTab(string title, int pos = int.max) { 7129 auto p = addPage(title); 7130 foreach(idx, child; this.children) 7131 if(child is p) 7132 return cast(int) idx; 7133 return -1; 7134 } 7135 7136 protected override void tabIndexClicked(int item) { 7137 foreach(idx, child; children) { 7138 child.showing(false, false); // batch the recalculates for the end 7139 } 7140 7141 foreach(idx, child; children) { 7142 if(idx == item) { 7143 child.showing(true, false); 7144 if(parentWindow) { 7145 auto f = parentWindow.getFirstFocusable(child); 7146 if(f) 7147 f.focus(); 7148 } 7149 recomputeChildLayout(); 7150 } 7151 } 7152 7153 version(win32_widgets) { 7154 InvalidateRect(hwnd, null, true); 7155 } else version(custom_widgets) { 7156 this.redraw(); 7157 } 7158 } 7159 7160 } 7161 7162 /++ 7163 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 7164 7165 You add [TabWidgetPage]s to it. 7166 +/ 7167 class PageWidget : Widget { 7168 this(Widget parent) { 7169 super(parent); 7170 } 7171 7172 override int minHeight() { 7173 int max = 0; 7174 foreach(child; children) 7175 max = mymax(child.minHeight, max); 7176 7177 return max; 7178 } 7179 7180 7181 override void addChild(Widget child, int pos = int.max) { 7182 if(auto twp = cast(TabWidgetPage) child) { 7183 super.addChild(child, pos); 7184 if(pos == int.max) 7185 pos = cast(int) this.children.length - 1; 7186 7187 if(pos != getCurrentTab) { 7188 child.showing = false; 7189 } 7190 } else { 7191 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 7192 } 7193 } 7194 7195 override void recomputeChildLayout() { 7196 this.registerMovement(); 7197 foreach(child; children) { 7198 child.x = 0; 7199 child.y = 0; 7200 child.width = width; 7201 child.height = height; 7202 child.recomputeChildLayout(); 7203 } 7204 } 7205 7206 private int currentTab_; 7207 7208 /// 7209 @scriptable 7210 void setCurrentTab(int item) { 7211 currentTab_ = item; 7212 7213 showOnly(item); 7214 } 7215 7216 /// 7217 @scriptable 7218 int getCurrentTab() { 7219 return currentTab_; 7220 } 7221 7222 /// 7223 @scriptable 7224 void removeTab(int item) { 7225 if(item && item == getCurrentTab()) 7226 setCurrentTab(item - 1); 7227 7228 for(int a = item; a < children.length - 1; a++) 7229 this._children[a] = this._children[a + 1]; 7230 this._children = this._children[0 .. $-1]; 7231 } 7232 7233 /// 7234 @scriptable 7235 TabWidgetPage addPage(string title) { 7236 return new TabWidgetPage(title, this); 7237 } 7238 7239 private void showOnly(int item) { 7240 foreach(idx, child; children) 7241 if(idx == item) { 7242 child.show(); 7243 child.queueRecomputeChildLayout(); 7244 } else { 7245 child.hide(); 7246 } 7247 } 7248 } 7249 7250 /++ 7251 7252 +/ 7253 class TabWidgetPage : Widget { 7254 string title; 7255 this(string title, Widget parent) { 7256 this.title = title; 7257 this.tabStop = false; 7258 super(parent); 7259 7260 ///* 7261 version(win32_widgets) { 7262 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 7263 } 7264 //*/ 7265 } 7266 7267 override int minHeight() { 7268 int sum = 0; 7269 foreach(child; children) 7270 sum += child.minHeight(); 7271 return sum; 7272 } 7273 } 7274 7275 version(none) 7276 /++ 7277 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 7278 7279 I think I need to modify the layout algorithms to support this. 7280 +/ 7281 class CollapsableSidebar : Widget { 7282 7283 } 7284 7285 /// Stacks the widgets vertically, taking all the available width for each child. 7286 class VerticalLayout : Layout { 7287 // most of this is intentionally blank - widget's default is vertical layout right now 7288 /// 7289 this(Widget parent) { super(parent); } 7290 7291 /++ 7292 Sets a max width for the layout so you don't have to subclass. The max width 7293 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7294 7295 History: 7296 Added November 29, 2021 (dub v10.5) 7297 +/ 7298 this(int maxWidth, Widget parent) { 7299 this.mw = maxWidth; 7300 super(parent); 7301 } 7302 7303 private int mw = int.max; 7304 7305 override int maxWidth() { return scaleWithDpi(mw); } 7306 } 7307 7308 /// Stacks the widgets horizontally, taking all the available height for each child. 7309 class HorizontalLayout : Layout { 7310 /// 7311 this(Widget parent) { super(parent); } 7312 7313 /++ 7314 Sets a max height for the layout so you don't have to subclass. The max height 7315 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7316 7317 History: 7318 Added November 29, 2021 (dub v10.5) 7319 +/ 7320 this(int maxHeight, Widget parent) { 7321 this.mh = maxHeight; 7322 super(parent); 7323 } 7324 7325 private int mh = 0; 7326 7327 7328 7329 override void recomputeChildLayout() { 7330 .recomputeChildLayout!"width"(this); 7331 } 7332 7333 override int minHeight() { 7334 int largest = 0; 7335 int margins = 0; 7336 int lastMargin = 0; 7337 foreach(child; children) { 7338 auto mh = child.minHeight(); 7339 if(mh > largest) 7340 largest = mh; 7341 margins += mymax(lastMargin, child.marginTop()); 7342 lastMargin = child.marginBottom(); 7343 } 7344 return largest + margins; 7345 } 7346 7347 override int maxHeight() { 7348 if(mh != 0) 7349 return mymax(minHeight, scaleWithDpi(mh)); 7350 7351 int largest = 0; 7352 int margins = 0; 7353 int lastMargin = 0; 7354 foreach(child; children) { 7355 auto mh = child.maxHeight(); 7356 if(mh == int.max) 7357 return int.max; 7358 if(mh > largest) 7359 largest = mh; 7360 margins += mymax(lastMargin, child.marginTop()); 7361 lastMargin = child.marginBottom(); 7362 } 7363 return largest + margins; 7364 } 7365 7366 override int heightStretchiness() { 7367 int max; 7368 foreach(child; children) { 7369 auto c = child.heightStretchiness; 7370 if(c > max) 7371 max = c; 7372 } 7373 return max; 7374 } 7375 7376 } 7377 7378 version(win32_widgets) 7379 private 7380 extern(Windows) 7381 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 7382 Widget* pwin = hwnd in Widget.nativeMapping; 7383 if(pwin is null) 7384 return DefWindowProc(hwnd, message, wparam, lparam); 7385 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 7386 if(win is null) 7387 return DefWindowProc(hwnd, message, wparam, lparam); 7388 7389 switch(message) { 7390 case WM_SIZE: 7391 auto width = LOWORD(lparam); 7392 auto height = HIWORD(lparam); 7393 7394 auto hdc = GetDC(hwnd); 7395 auto hdcBmp = CreateCompatibleDC(hdc); 7396 7397 // FIXME: could this be more efficient? it never relinquishes a large bitmap 7398 if(width > win.bmpWidth || height > win.bmpHeight) { 7399 auto oldBuffer = win.buffer; 7400 win.buffer = CreateCompatibleBitmap(hdc, width, height); 7401 7402 if(oldBuffer) 7403 DeleteObject(oldBuffer); 7404 7405 win.bmpWidth = width; 7406 win.bmpHeight = height; 7407 } 7408 7409 // just always erase it upon resizing so minigui can draw over with a clean slate 7410 auto oldBmp = SelectObject(hdcBmp, win.buffer); 7411 7412 auto brush = GetSysColorBrush(COLOR_3DFACE); 7413 RECT r; 7414 r.left = 0; 7415 r.top = 0; 7416 r.right = width; 7417 r.bottom = height; 7418 FillRect(hdcBmp, &r, brush); 7419 7420 SelectObject(hdcBmp, oldBmp); 7421 DeleteDC(hdcBmp); 7422 ReleaseDC(hwnd, hdc); 7423 break; 7424 case WM_PAINT: 7425 if(win.buffer is null) 7426 goto default; 7427 7428 BITMAP bm; 7429 PAINTSTRUCT ps; 7430 7431 HDC hdc = BeginPaint(hwnd, &ps); 7432 7433 HDC hdcMem = CreateCompatibleDC(hdc); 7434 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 7435 7436 GetObject(win.buffer, bm.sizeof, &bm); 7437 7438 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 7439 7440 SelectObject(hdcMem, hbmOld); 7441 DeleteDC(hdcMem); 7442 EndPaint(hwnd, &ps); 7443 break; 7444 default: 7445 return DefWindowProc(hwnd, message, wparam, lparam); 7446 } 7447 7448 return 0; 7449 } 7450 7451 private wstring Win32Class(wstring name)() { 7452 static bool classRegistered; 7453 if(!classRegistered) { 7454 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 7455 WNDCLASSEX wc; 7456 wc.cbSize = wc.sizeof; 7457 wc.hInstance = hInstance; 7458 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 7459 wc.lpfnWndProc = &DoubleBufferWndProc; 7460 wc.lpszClassName = name.ptr; 7461 if(!RegisterClassExW(&wc)) 7462 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 7463 classRegistered = true; 7464 } 7465 7466 return name; 7467 } 7468 7469 /+ 7470 version(win32_widgets) 7471 extern(Windows) 7472 private 7473 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 7474 switch(iMessage) { 7475 case WM_PAINT: 7476 if(auto te = hWnd in Widget.nativeMapping) { 7477 try { 7478 //te.redraw(); 7479 writeln(te, " drawing"); 7480 } catch(Exception) {} 7481 } 7482 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7483 default: 7484 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7485 } 7486 } 7487 +/ 7488 7489 7490 /++ 7491 A widget specifically designed to hold other widgets. 7492 7493 History: 7494 Added July 1, 2021 7495 +/ 7496 class ContainerWidget : Widget { 7497 this(Widget parent) { 7498 super(parent); 7499 this.tabStop = false; 7500 7501 version(win32_widgets) { 7502 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 7503 } 7504 } 7505 } 7506 7507 /++ 7508 A widget that takes your widget, puts scroll bars around it, and sends 7509 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 7510 no effort to automatically scroll or clip its child widgets - it just sends 7511 the messages. 7512 7513 7514 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 7515 The scroll coordinates are all given in a unit you interpret as you wish. One 7516 of these units is moved on each press of the arrow buttons and represents the 7517 smallest amount the user can scroll. The intention is for this to be one line, 7518 one item in a list, one row in a table, etc. Whatever makes sense for your widget 7519 in each direction that the user might be interested in. 7520 7521 You can set a "page size" with the [step] property. (Yes, I regret the name...) 7522 This is the amount it jumps when the user pressed page up and page down, or clicks 7523 in the exposed part of the scroll bar. 7524 7525 You should add child content to the ScrollMessageWidget. However, it is important to 7526 note that the coordinates are always independent of the scroll position! It is YOUR 7527 responsibility to do any necessary transforms, clipping, etc., while drawing the 7528 content and interpreting mouse events if they are supposed to change with the scroll. 7529 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 7530 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 7531 you more control (which can be considerably more efficient and adapted to your actual data) 7532 at the expense of you also needing to be aware of its reality. 7533 7534 Please note that it does NOT react to mouse wheel events or various keyboard events as of 7535 version 10.3. Maybe this will change in the future.... but for now you must call 7536 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 7537 +/ 7538 class ScrollMessageWidget : Widget { 7539 this(Widget parent) { 7540 super(parent); 7541 7542 container = new Widget(this); 7543 hsb = new HorizontalScrollbar(this); 7544 vsb = new VerticalScrollbar(this); 7545 7546 hsb.addEventListener("scrolltonextline", { 7547 hsb.setPosition(hsb.position + movementPerButtonClickH_); 7548 notify(); 7549 }); 7550 hsb.addEventListener("scrolltopreviousline", { 7551 hsb.setPosition(hsb.position - movementPerButtonClickH_); 7552 notify(); 7553 }); 7554 vsb.addEventListener("scrolltonextline", { 7555 vsb.setPosition(vsb.position + movementPerButtonClickV_); 7556 notify(); 7557 }); 7558 vsb.addEventListener("scrolltopreviousline", { 7559 vsb.setPosition(vsb.position - movementPerButtonClickV_); 7560 notify(); 7561 }); 7562 hsb.addEventListener("scrolltonextpage", { 7563 hsb.setPosition(hsb.position + hsb.step_); 7564 notify(); 7565 }); 7566 hsb.addEventListener("scrolltopreviouspage", { 7567 hsb.setPosition(hsb.position - hsb.step_); 7568 notify(); 7569 }); 7570 vsb.addEventListener("scrolltonextpage", { 7571 vsb.setPosition(vsb.position + vsb.step_); 7572 notify(); 7573 }); 7574 vsb.addEventListener("scrolltopreviouspage", { 7575 vsb.setPosition(vsb.position - vsb.step_); 7576 notify(); 7577 }); 7578 hsb.addEventListener("scrolltoposition", (Event event) { 7579 hsb.setPosition(event.intValue); 7580 notify(); 7581 }); 7582 vsb.addEventListener("scrolltoposition", (Event event) { 7583 vsb.setPosition(event.intValue); 7584 notify(); 7585 }); 7586 7587 7588 tabStop = false; 7589 container.tabStop = false; 7590 magic = true; 7591 } 7592 7593 private int movementPerButtonClickH_ = 1; 7594 private int movementPerButtonClickV_ = 1; 7595 public void movementPerButtonClick(int h, int v) { 7596 movementPerButtonClickH_ = h; 7597 movementPerButtonClickV_ = v; 7598 } 7599 7600 /++ 7601 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 7602 7603 7604 The defaults for [addDefaultWheelListeners] are: 7605 7606 $(LIST 7607 * Mouse wheel scrolls vertically 7608 * Alt key + mouse wheel scrolls horiontally 7609 * Shift + mouse wheel scrolls faster. 7610 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 7611 ) 7612 7613 The defaults for [addDefaultKeyboardListeners] are: 7614 7615 $(LIST 7616 * Arrow keys scroll by the given amounts 7617 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 7618 * Page up and down scroll by the vertical viewable area 7619 * Home and end scroll to the start and end of the verticle viewable area. 7620 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 7621 ) 7622 7623 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 7624 7625 Params: 7626 horizontalArrowScrollAmount = 7627 verticalArrowScrollAmount = 7628 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 7629 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 7630 shiftMultiplier = multiplies the scroll amount by this when shift is held 7631 +/ 7632 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 7633 auto _this = this; 7634 7635 container.addEventListener((scope KeyDownEvent ke) { 7636 switch(ke.key) { 7637 case Key.Left: 7638 _this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7639 break; 7640 case Key.Right: 7641 _this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7642 break; 7643 case Key.Up: 7644 _this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7645 break; 7646 case Key.Down: 7647 _this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7648 break; 7649 case Key.PageUp: 7650 if(ke.altKey) 7651 _this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7652 else 7653 _this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7654 break; 7655 case Key.PageDown: 7656 if(ke.altKey) 7657 _this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7658 else 7659 _this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7660 break; 7661 case Key.Home: 7662 if(ke.altKey) 7663 _this.scrollLeft(short.max * 16); 7664 else 7665 _this.scrollUp(short.max * 16); 7666 break; 7667 case Key.End: 7668 if(ke.altKey) 7669 _this.scrollRight(short.max * 16); 7670 else 7671 _this.scrollDown(short.max * 16); 7672 break; 7673 7674 default: 7675 // ignore, not for us. 7676 } 7677 7678 }); 7679 } 7680 7681 /// ditto 7682 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 7683 auto _this = this; 7684 container.addEventListener((scope ClickEvent ce) { 7685 7686 //if(ce.target && ce.target.tabStop) 7687 //ce.target.focus(); 7688 7689 // ctrl is reserved for the application 7690 if(ce.ctrlKey) 7691 return; 7692 7693 if(horizontalWheelScrollAmount == 0 && ce.altKey) 7694 return; 7695 7696 if(shiftMultiplier == 0 && ce.shiftKey) 7697 return; 7698 7699 if(ce.button == MouseButton.wheelDown) { 7700 if(ce.altKey) 7701 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7702 else 7703 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7704 } else if(ce.button == MouseButton.wheelUp) { 7705 if(ce.altKey) 7706 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7707 else 7708 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7709 } 7710 }); 7711 } 7712 7713 /++ 7714 Scrolls the given amount. 7715 7716 History: 7717 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. 7718 +/ 7719 void scrollUp(int amount = 1) { 7720 vsb.setPosition(vsb.position - amount); 7721 notify(); 7722 } 7723 /// ditto 7724 void scrollDown(int amount = 1) { 7725 vsb.setPosition(vsb.position + amount); 7726 notify(); 7727 } 7728 /// ditto 7729 void scrollLeft(int amount = 1) { 7730 hsb.setPosition(hsb.position - amount); 7731 notify(); 7732 } 7733 /// ditto 7734 void scrollRight(int amount = 1) { 7735 hsb.setPosition(hsb.position + amount); 7736 notify(); 7737 } 7738 7739 /// 7740 VerticalScrollbar verticalScrollBar() { return vsb; } 7741 /// 7742 HorizontalScrollbar horizontalScrollBar() { return hsb; } 7743 7744 void notify() { 7745 static bool insideNotify; 7746 7747 if(insideNotify) 7748 return; // avoid the recursive call, even if it isn't strictly correct 7749 7750 insideNotify = true; 7751 scope(exit) insideNotify = false; 7752 7753 this.emit!ScrollEvent(); 7754 } 7755 7756 mixin Emits!ScrollEvent; 7757 7758 /// 7759 Point position() { 7760 return Point(hsb.position, vsb.position); 7761 } 7762 7763 /// 7764 void setPosition(int x, int y) { 7765 hsb.setPosition(x); 7766 vsb.setPosition(y); 7767 } 7768 7769 /// 7770 void setPageSize(int unitsX, int unitsY) { 7771 hsb.setStep(unitsX); 7772 vsb.setStep(unitsY); 7773 } 7774 7775 /// Always call this BEFORE setViewableArea 7776 void setTotalArea(int width, int height) { 7777 hsb.setMax(width); 7778 vsb.setMax(height); 7779 } 7780 7781 /++ 7782 Always set the viewable area AFTER setitng the total area if you are going to change both. 7783 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 7784 If you need to do that, use [queueRecomputeChildLayout]. 7785 +/ 7786 void setViewableArea(int width, int height) { 7787 7788 // actually there IS A need to dothis cuz the max might have changed since then 7789 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 7790 //return; // no need to do what is already done 7791 hsb.setViewableArea(width); 7792 vsb.setViewableArea(height); 7793 7794 bool needsNotify = false; 7795 7796 // FIXME: if at any point the rhs is outside the scrollbar, we need 7797 // to reset to 0. but it should remember the old position in case the 7798 // window resizes again, so it can kinda return ot where it was. 7799 // 7800 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 7801 if(width >= hsb.max) { 7802 // there's plenty of room to display it all so we need to reset to zero 7803 // FIXME: adjust so it matches the note above 7804 hsb.setPosition(0); 7805 needsNotify = true; 7806 } 7807 if(height >= vsb.max) { 7808 // there's plenty of room to display it all so we need to reset to zero 7809 // FIXME: adjust so it matches the note above 7810 vsb.setPosition(0); 7811 needsNotify = true; 7812 } 7813 if(needsNotify) 7814 notify(); 7815 } 7816 7817 private bool magic; 7818 override void addChild(Widget w, int position = int.max) { 7819 if(magic) 7820 container.addChild(w, position); 7821 else 7822 super.addChild(w, position); 7823 } 7824 7825 override void recomputeChildLayout() { 7826 if(hsb is null || vsb is null || container is null) return; 7827 7828 registerMovement(); 7829 7830 enum BUTTON_SIZE = 16; 7831 7832 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 7833 hsb.x = 0; 7834 hsb.y = this.height - hsb.height; 7835 7836 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 7837 vsb.x = this.width - vsb.width; 7838 vsb.y = 0; 7839 7840 auto vsb_width = vsb.showing ? vsb.width : 0; 7841 auto hsb_height = hsb.showing ? hsb.height : 0; 7842 7843 hsb.width = this.width - vsb_width; 7844 vsb.height = this.height - hsb_height; 7845 7846 hsb.recomputeChildLayout(); 7847 vsb.recomputeChildLayout(); 7848 7849 if(this.header is null) { 7850 container.x = 0; 7851 container.y = 0; 7852 container.width = this.width - vsb_width; 7853 container.height = this.height - hsb_height; 7854 container.recomputeChildLayout(); 7855 } else { 7856 header.x = 0; 7857 header.y = 0; 7858 header.width = this.width - vsb_width; 7859 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 7860 header.recomputeChildLayout(); 7861 7862 container.x = 0; 7863 container.y = scaleWithDpi(BUTTON_SIZE); 7864 container.width = this.width - vsb_width; 7865 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 7866 container.recomputeChildLayout(); 7867 } 7868 } 7869 7870 private HorizontalScrollbar hsb; 7871 private VerticalScrollbar vsb; 7872 Widget container; 7873 private Widget header; 7874 7875 /++ 7876 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 7877 7878 History: 7879 Added September 27, 2021 (dub v10.3) 7880 +/ 7881 Widget getHeader() { 7882 if(this.header is null) { 7883 magic = false; 7884 scope(exit) magic = true; 7885 this.header = new Widget(this); 7886 queueRecomputeChildLayout(); 7887 } 7888 return this.header; 7889 } 7890 7891 /++ 7892 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 7893 7894 History: 7895 Added January 3, 2023 (dub v11.0) 7896 +/ 7897 void scrollIntoView(Rectangle rect) { 7898 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 7899 7900 // import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1))); 7901 7902 // the lower right is exclusive normally 7903 auto test = rect.lowerRight; 7904 if(test.x > 0) test.x--; 7905 if(test.y > 0) test.y--; 7906 7907 if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) { 7908 // try to scroll only one dimension at a time if we can 7909 if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y))) 7910 setPosition(rect.upperLeft.x, position.y); 7911 if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y))) 7912 setPosition(position.x, rect.upperLeft.y); 7913 } 7914 7915 } 7916 7917 override int minHeight() { 7918 int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0)); 7919 if(header !is null) 7920 min += header.minHeight; 7921 if(horizontalScrollBar.showing) 7922 min += horizontalScrollBar.minHeight; 7923 return min; 7924 } 7925 7926 override int maxHeight() { 7927 int max = container ? container.maxHeight : int.max; 7928 if(max == int.max) 7929 return max; 7930 if(horizontalScrollBar.showing) 7931 max += horizontalScrollBar.minHeight; 7932 return max; 7933 } 7934 } 7935 7936 /++ 7937 $(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") 7938 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 7939 +/ 7940 version(minigui_screenshots) 7941 @Screenshot("ScrollMessageWidget") 7942 unittest { 7943 auto window = new Window("ScrollMessageWidget"); 7944 7945 auto smw = new ScrollMessageWidget(window); 7946 smw.addDefaultKeyboardListeners(); 7947 smw.addDefaultWheelListeners(); 7948 7949 window.loop(); 7950 } 7951 7952 /++ 7953 Bypasses automatic layout for its children, using manual positioning and sizing only. 7954 While you need to manually position them, you must ensure they are inside the StaticLayout's 7955 bounding box to avoid undefined behavior. 7956 7957 You should almost never use this. 7958 +/ 7959 class StaticLayout : Layout { 7960 /// 7961 this(Widget parent) { super(parent); } 7962 override void recomputeChildLayout() { 7963 registerMovement(); 7964 foreach(child; children) 7965 child.recomputeChildLayout(); 7966 } 7967 } 7968 7969 /++ 7970 Bypasses automatic positioning when being laid out. It is your responsibility to make 7971 room for this widget in the parent layout. 7972 7973 Its children are laid out normally, unless there is exactly one, in which case it takes 7974 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 7975 can do that with `padding`). 7976 +/ 7977 class StaticPosition : Layout { 7978 /// 7979 this(Widget parent) { super(parent); } 7980 7981 override void recomputeChildLayout() { 7982 registerMovement(); 7983 if(this.children.length == 1) { 7984 auto child = children[0]; 7985 child.x = 0; 7986 child.y = 0; 7987 child.width = this.width; 7988 child.height = this.height; 7989 child.recomputeChildLayout(); 7990 } else 7991 foreach(child; children) 7992 child.recomputeChildLayout(); 7993 } 7994 7995 alias width = typeof(super).width; 7996 alias height = typeof(super).height; 7997 7998 @property int width(int w) @nogc pure @safe nothrow { 7999 return this._width = w; 8000 } 8001 8002 @property int height(int w) @nogc pure @safe nothrow { 8003 return this._height = w; 8004 } 8005 8006 } 8007 8008 /++ 8009 FixedPosition is like [StaticPosition], but its coordinates 8010 are always relative to the viewport, meaning they do not scroll with 8011 the parent content. 8012 +/ 8013 class FixedPosition : StaticPosition { 8014 /// 8015 this(Widget parent) { super(parent); } 8016 } 8017 8018 version(win32_widgets) 8019 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 8020 if(true) { 8021 // cmd == 0 = menu, cmd == 1 = accelerator 8022 if(auto item = idm in Action.mapping) { 8023 foreach(handler; (*item).triggered) 8024 handler(); 8025 /* 8026 auto event = new Event("triggered", *item); 8027 event.button = idm; 8028 event.dispatch(); 8029 */ 8030 return 0; 8031 } 8032 } 8033 if(handle) 8034 if(auto widgetp = handle in Widget.nativeMapping) { 8035 (*widgetp).handleWmCommand(cmd, idm); 8036 return 0; 8037 } 8038 return 1; 8039 } 8040 8041 8042 /// 8043 class Window : Widget { 8044 int mouseCaptureCount = 0; 8045 Widget mouseCapturedBy; 8046 void captureMouse(Widget byWhom) { 8047 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 8048 mouseCaptureCount++; 8049 mouseCapturedBy = byWhom; 8050 win.grabInput(false, true, false); 8051 //void grabInput(bool keyboard = true, bool mouse = true, bool confine = false) { 8052 } 8053 void releaseMouseCapture() { 8054 mouseCaptureCount--; 8055 mouseCapturedBy = null; 8056 win.releaseInputGrab(); 8057 } 8058 8059 8060 /++ 8061 8062 +/ 8063 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 8064 return .messageBox(this, title, message, style, icon); 8065 } 8066 8067 /// ditto 8068 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 8069 return messageBox(null, message, style, icon); 8070 } 8071 8072 8073 /++ 8074 Sets the window icon which is often seen in title bars and taskbars. 8075 8076 History: 8077 Added April 5, 2022 (dub v10.8) 8078 +/ 8079 @property void icon(MemoryImage icon) { 8080 if(win && icon) 8081 win.icon = icon; 8082 } 8083 8084 // forwarder to the top-level icon thing so this doesn't conflict too much with the UDAs seen inside the class ins ome older examples 8085 // this does NOT change the icon on the window! That's what the other overload is for 8086 static @property .icon icon(GenericIcons i) { 8087 return .icon(i); 8088 } 8089 8090 /// 8091 @scriptable 8092 @property bool focused() { 8093 return win.focused; 8094 } 8095 8096 static class Style : Widget.Style { 8097 override WidgetBackground background() { 8098 version(custom_widgets) 8099 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 8100 else version(win32_widgets) 8101 return WidgetBackground(Color.transparent); 8102 else static assert(0); 8103 } 8104 } 8105 mixin OverrideStyle!Style; 8106 8107 /++ 8108 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. 8109 +/ 8110 deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() { 8111 return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback(); 8112 } 8113 8114 private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() { 8115 OperatingSystemFont font; 8116 if(auto vt = WidgetPainter.visualTheme) { 8117 font = vt.defaultFontCached(96); // FIXME 8118 } 8119 8120 if(font is null) { 8121 static int defaultHeightCache; 8122 if(defaultHeightCache == 0) { 8123 font = new OperatingSystemFont; 8124 font.loadDefault; 8125 defaultHeightCache = font.height();// * 5 / 4; 8126 } 8127 return defaultHeightCache; 8128 } 8129 8130 return font.height();// * 5 / 4; 8131 } 8132 8133 Widget focusedWidget; 8134 8135 private SimpleWindow win_; 8136 8137 @property { 8138 /++ 8139 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 8140 8141 History: 8142 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 8143 +/ 8144 public SimpleWindow win() { 8145 return win_; 8146 } 8147 /// 8148 protected void win(SimpleWindow w) { 8149 win_ = w; 8150 } 8151 } 8152 8153 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 8154 this(Widget p) { 8155 tabStop = false; 8156 super(p); 8157 } 8158 8159 private void actualRedraw() { 8160 if(recomputeChildLayoutRequired) 8161 recomputeChildLayoutEntry(); 8162 if(!showing) return; 8163 8164 assert(parentWindow !is null); 8165 8166 auto w = drawableWindow; 8167 if(w is null) 8168 w = parentWindow.win; 8169 8170 if(w.closed()) 8171 return; 8172 8173 auto ugh = this.parent; 8174 int lox, loy; 8175 while(ugh) { 8176 lox += ugh.x; 8177 loy += ugh.y; 8178 ugh = ugh.parent; 8179 } 8180 auto painter = w.draw(true); 8181 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 8182 } 8183 8184 8185 private bool skipNextChar = false; 8186 8187 /++ 8188 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 8189 8190 This constructor is intended primarily for internal use and may be changed to `protected` later. 8191 +/ 8192 this(SimpleWindow win) { 8193 8194 static if(UsingSimpledisplayX11) { 8195 win.discardAdditionalConnectionState = &discardXConnectionState; 8196 win.recreateAdditionalConnectionState = &recreateXConnectionState; 8197 } 8198 8199 tabStop = false; 8200 super(null); 8201 this.win = win; 8202 8203 win.addEventListener((Widget.RedrawEvent) { 8204 if(win.eventQueued!RecomputeEvent) { 8205 // writeln("skipping"); 8206 return; // let the recompute event do the actual redraw 8207 } 8208 this.actualRedraw(); 8209 }); 8210 8211 win.addEventListener((Widget.RecomputeEvent) { 8212 recomputeChildLayoutEntry(); 8213 if(win.eventQueued!RedrawEvent) 8214 return; // let the queued one do it 8215 else { 8216 // writeln("drawing"); 8217 this.actualRedraw(); // if not queued, it needs to be done now anyway 8218 } 8219 }); 8220 8221 this.width = win.width; 8222 this.height = win.height; 8223 this.parentWindow = this; 8224 8225 win.closeQuery = () { 8226 if(this.emit!ClosingEvent()) 8227 win.close(); 8228 }; 8229 win.onClosing = () { 8230 this.emit!ClosedEvent(); 8231 }; 8232 8233 win.windowResized = (int w, int h) { 8234 this.width = w; 8235 this.height = h; 8236 queueRecomputeChildLayout(); 8237 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 8238 //version(win32_widgets) 8239 //InvalidateRect(hwnd, null, true); 8240 redraw(); 8241 }; 8242 8243 win.onFocusChange = (bool getting) { 8244 if(this.focusedWidget) { 8245 if(getting) { 8246 this.focusedWidget.emit!FocusEvent(); 8247 this.focusedWidget.emit!FocusInEvent(); 8248 } else { 8249 this.focusedWidget.emit!BlurEvent(); 8250 this.focusedWidget.emit!FocusOutEvent(); 8251 } 8252 } 8253 8254 if(getting) { 8255 this.emit!FocusEvent(); 8256 this.emit!FocusInEvent(); 8257 } else { 8258 this.emit!BlurEvent(); 8259 this.emit!FocusOutEvent(); 8260 } 8261 }; 8262 8263 win.onDpiChanged = { 8264 this.queueRecomputeChildLayout(); 8265 auto event = new DpiChangedEvent(this); 8266 event.sendDirectly(); 8267 8268 privateDpiChanged(); 8269 }; 8270 8271 win.setEventHandlers( 8272 (MouseEvent e) { 8273 dispatchMouseEvent(e); 8274 }, 8275 (KeyEvent e) { 8276 //writefln("%x %s", cast(uint) e.key, e.key); 8277 dispatchKeyEvent(e); 8278 }, 8279 (dchar e) { 8280 if(e == 13) e = 10; // hack? 8281 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8282 dispatchCharEvent(e); 8283 }, 8284 ); 8285 8286 addEventListener("char", (Widget, Event ev) { 8287 if(skipNextChar) { 8288 ev.preventDefault(); 8289 skipNextChar = false; 8290 } 8291 }); 8292 8293 version(win32_widgets) 8294 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 8295 if(hwnd !is this.win.impl.hwnd) 8296 return 1; // we don't care... pass it on 8297 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 8298 if(mustReturn) 8299 return ret; 8300 return 1; // pass it on 8301 }; 8302 8303 if(Window.newWindowCreated) 8304 Window.newWindowCreated(this); 8305 } 8306 8307 version(custom_widgets) 8308 override void defaultEventHandler_click(ClickEvent event) { 8309 if(event.button != MouseButton.wheelDown && event.button != MouseButton.wheelUp) { 8310 if(event.target && event.target.tabStop) 8311 event.target.focus(); 8312 } 8313 } 8314 8315 private static void delegate(Window) newWindowCreated; 8316 8317 version(win32_widgets) 8318 override void paint(WidgetPainter painter) { 8319 /* 8320 RECT rect; 8321 rect.right = this.width; 8322 rect.bottom = this.height; 8323 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 8324 */ 8325 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 8326 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 8327 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 8328 // since the pen is null, to fill the whole space, we need the +1 on both. 8329 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 8330 SelectObject(painter.impl.hdc, p); 8331 SelectObject(painter.impl.hdc, b); 8332 } 8333 version(custom_widgets) 8334 override void paint(WidgetPainter painter) { 8335 auto cs = getComputedStyle(); 8336 painter.fillColor = cs.windowBackgroundColor; 8337 painter.outlineColor = cs.windowBackgroundColor; 8338 painter.drawRectangle(Point(0, 0), this.width, this.height); 8339 } 8340 8341 8342 override void defaultEventHandler_keydown(KeyDownEvent event) { 8343 Widget _this = event.target; 8344 8345 if(event.key == Key.Tab) { 8346 /* Window tab ordering is a recursive thingy with each group */ 8347 8348 // FIXME inefficient 8349 Widget[] helper(Widget p) { 8350 if(p.hidden) 8351 return null; 8352 Widget[] childOrdering; 8353 8354 auto children = p.children.dup; 8355 8356 while(true) { 8357 // UIs should be generally small, so gonna brute force it a little 8358 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 8359 8360 Widget smallestTab; 8361 foreach(ref c; children) { 8362 if(c is null) continue; 8363 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 8364 smallestTab = c; 8365 c = null; 8366 } 8367 } 8368 if(smallestTab !is null) { 8369 if(smallestTab.tabStop && !smallestTab.hidden) 8370 childOrdering ~= smallestTab; 8371 if(!smallestTab.hidden) 8372 childOrdering ~= helper(smallestTab); 8373 } else 8374 break; 8375 8376 } 8377 8378 return childOrdering; 8379 } 8380 8381 Widget[] tabOrdering = helper(this); 8382 8383 Widget recipient; 8384 8385 if(tabOrdering.length) { 8386 bool seenThis = false; 8387 Widget previous; 8388 foreach(idx, child; tabOrdering) { 8389 if(child is focusedWidget) { 8390 8391 if(event.shiftKey) { 8392 if(idx == 0) 8393 recipient = tabOrdering[$-1]; 8394 else 8395 recipient = tabOrdering[idx - 1]; 8396 break; 8397 } 8398 8399 seenThis = true; 8400 if(idx + 1 == tabOrdering.length) { 8401 // we're at the end, either move to the next group 8402 // or start back over 8403 recipient = tabOrdering[0]; 8404 } 8405 continue; 8406 } 8407 if(seenThis) { 8408 recipient = child; 8409 break; 8410 } 8411 previous = child; 8412 } 8413 } 8414 8415 if(recipient !is null) { 8416 // writeln(typeid(recipient)); 8417 recipient.focus(); 8418 8419 skipNextChar = true; 8420 } 8421 } 8422 8423 debug if(event.key == Key.F12) { 8424 if(devTools) { 8425 devTools.close(); 8426 devTools = null; 8427 } else { 8428 devTools = new DevToolWindow(this); 8429 devTools.show(); 8430 } 8431 } 8432 } 8433 8434 debug DevToolWindow devTools; 8435 8436 8437 /++ 8438 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 8439 8440 History: 8441 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 8442 8443 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 8444 +/ 8445 this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) { 8446 if(title is null) { 8447 import core.runtime; 8448 if(Runtime.args.length) 8449 title = Runtime.args[0]; 8450 } 8451 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, windowType, windowFlags, parent); 8452 8453 static if(UsingSimpledisplayX11) 8454 if(windowFlags & WindowFlags.managesChildWindowFocus) { 8455 ///+ 8456 // for input proxy 8457 auto display = XDisplayConnection.get; 8458 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 8459 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 8460 XMapWindow(display, inputProxy); 8461 // writefln("input proxy: 0x%0x", inputProxy); 8462 this.inputProxy = new SimpleWindow(inputProxy); 8463 8464 XEvent lastEvent; 8465 this.inputProxy.handleNativeEvent = (XEvent ev) { 8466 lastEvent = ev; 8467 return 1; 8468 }; 8469 this.inputProxy.setEventHandlers( 8470 (MouseEvent e) { 8471 dispatchMouseEvent(e); 8472 }, 8473 (KeyEvent e) { 8474 //writefln("%x %s", cast(uint) e.key, e.key); 8475 if(dispatchKeyEvent(e)) { 8476 // FIXME: i should trap error 8477 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 8478 auto thing = nw.focusableWindow(); 8479 if(thing && thing.window) { 8480 lastEvent.xkey.window = thing.window; 8481 // writeln("sending event ", lastEvent.xkey); 8482 trapXErrors( { 8483 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 8484 }); 8485 } 8486 } 8487 } 8488 }, 8489 (dchar e) { 8490 if(e == 13) e = 10; // hack? 8491 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8492 dispatchCharEvent(e); 8493 }, 8494 ); 8495 8496 this.inputProxy.populateXic(); 8497 // done 8498 //+/ 8499 } 8500 8501 8502 8503 win.setRequestedInputFocus = &this.setRequestedInputFocus; 8504 8505 this(win); 8506 } 8507 8508 SimpleWindow inputProxy; 8509 8510 private SimpleWindow setRequestedInputFocus() { 8511 return inputProxy; 8512 } 8513 8514 /// ditto 8515 this(string title, int width = 500, int height = 500) { 8516 this(width, height, title); 8517 } 8518 8519 /// 8520 @property string title() { return parentWindow.win.title; } 8521 /// 8522 @property void title(string title) { parentWindow.win.title = title; } 8523 8524 /// 8525 @scriptable 8526 void close() { 8527 win.close(); 8528 // I synchronize here upon window closing to ensure all child windows 8529 // get updated too before the event loop. This avoids some random X errors. 8530 static if(UsingSimpledisplayX11) { 8531 runInGuiThread( { 8532 XSync(XDisplayConnection.get, false); 8533 }); 8534 } 8535 } 8536 8537 bool dispatchKeyEvent(KeyEvent ev) { 8538 auto wid = focusedWidget; 8539 if(wid is null) 8540 wid = this; 8541 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 8542 event.originalKeyEvent = ev; 8543 event.key = ev.key; 8544 event.state = ev.modifierState; 8545 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8546 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8547 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8548 event.dispatch(); 8549 8550 return !event.propagationStopped; 8551 } 8552 8553 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 8554 bool dispatchCharEvent(dchar ch) { 8555 if(focusedWidget) { 8556 auto event = new CharEvent(focusedWidget, ch); 8557 event.dispatch(); 8558 return !event.propagationStopped; 8559 } 8560 return true; 8561 } 8562 8563 Widget mouseLastOver; 8564 Widget mouseLastDownOn; 8565 bool lastWasDoubleClick; 8566 bool dispatchMouseEvent(MouseEvent ev) { 8567 auto eleR = widgetAtPoint(this, ev.x, ev.y); 8568 auto ele = eleR.widget; 8569 8570 auto captureEle = ele; 8571 8572 if(mouseCapturedBy !is null) { 8573 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 8574 captureEle = mouseCapturedBy; 8575 } 8576 8577 // a hack to get it relative to the widget. 8578 eleR.x = ev.x; 8579 eleR.y = ev.y; 8580 auto pain = captureEle; 8581 while(pain) { 8582 eleR.x -= pain.x; 8583 eleR.y -= pain.y; 8584 pain.addScrollPosition(eleR.x, eleR.y); 8585 pain = pain.parent; 8586 } 8587 8588 void populateMouseEventBase(MouseEventBase event) { 8589 event.button = ev.button; 8590 event.buttonLinear = ev.buttonLinear; 8591 event.state = ev.modifierState; 8592 event.clientX = eleR.x; 8593 event.clientY = eleR.y; 8594 8595 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8596 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8597 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8598 } 8599 8600 if(ev.type == MouseEventType.buttonPressed) { 8601 { 8602 auto event = new MouseDownEvent(captureEle); 8603 populateMouseEventBase(event); 8604 event.dispatch(); 8605 } 8606 8607 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 8608 auto event = new DoubleClickEvent(captureEle); 8609 populateMouseEventBase(event); 8610 event.dispatch(); 8611 lastWasDoubleClick = ev.doubleClick; 8612 } else { 8613 lastWasDoubleClick = false; 8614 } 8615 8616 mouseLastDownOn = ele; 8617 } else if(ev.type == MouseEventType.buttonReleased) { 8618 { 8619 auto event = new MouseUpEvent(captureEle); 8620 populateMouseEventBase(event); 8621 event.dispatch(); 8622 } 8623 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 8624 auto event = new ClickEvent(captureEle); 8625 populateMouseEventBase(event); 8626 event.dispatch(); 8627 } 8628 } else if(ev.type == MouseEventType.motion) { 8629 // motion 8630 { 8631 auto event = new MouseMoveEvent(captureEle); 8632 populateMouseEventBase(event); // fills in button which is meaningless but meh 8633 event.dispatch(); 8634 } 8635 8636 if(mouseLastOver !is ele) { 8637 if(ele !is null) { 8638 if(!isAParentOf(ele, mouseLastOver)) { 8639 ele.setDynamicState(DynamicState.hover, true); 8640 auto event = new MouseEnterEvent(ele); 8641 event.relatedTarget = mouseLastOver; 8642 event.sendDirectly(); 8643 8644 ele.useStyleProperties((scope Widget.Style s) { 8645 ele.parentWindow.win.cursor = s.cursor; 8646 }); 8647 } 8648 } 8649 8650 if(mouseLastOver !is null) { 8651 if(!isAParentOf(mouseLastOver, ele)) { 8652 mouseLastOver.setDynamicState(DynamicState.hover, false); 8653 auto event = new MouseLeaveEvent(mouseLastOver); 8654 event.relatedTarget = ele; 8655 event.sendDirectly(); 8656 } 8657 } 8658 8659 if(ele !is null) { 8660 auto event = new MouseOverEvent(ele); 8661 event.relatedTarget = mouseLastOver; 8662 event.dispatch(); 8663 } 8664 8665 if(mouseLastOver !is null) { 8666 auto event = new MouseOutEvent(mouseLastOver); 8667 event.relatedTarget = ele; 8668 event.dispatch(); 8669 } 8670 8671 mouseLastOver = ele; 8672 } 8673 } 8674 8675 return true; // FIXME: the event default prevented? 8676 } 8677 8678 /++ 8679 Shows the window and runs the application event loop. 8680 8681 Blocks until this window is closed. 8682 8683 Bugs: 8684 8685 $(PITFALL 8686 You should always have one event loop live for your application. 8687 If you make two windows in sequence, the second call to loop (or 8688 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 8689 might fail: 8690 8691 --- 8692 // don't do this! 8693 auto window = new Window(); 8694 window.loop(); 8695 8696 // or new Window or new MainWindow, all the same 8697 auto window2 = new SimpleWindow(); 8698 window2.eventLoop(0); // problematic! might crash 8699 --- 8700 8701 simpledisplay's current implementation assumes that final cleanup is 8702 done when the event loop refcount reaches zero. So after the first 8703 eventLoop returns, when there isn't already another one active, it assumes 8704 the program will exit soon and cleans up. 8705 8706 This is arguably a bug that it doesn't reinitialize, and I'll probably change 8707 it eventually, but in the mean time, there's an easy solution: 8708 8709 --- 8710 // do this 8711 EventLoop mainEventLoop = EventLoop.get; // just add this line 8712 8713 auto window = new Window(); 8714 window.loop(); 8715 8716 // or any other type of Window etc. 8717 auto window2 = new Window(); 8718 window2.loop(); // perfectly fine since mainEventLoop still alive 8719 --- 8720 8721 By adding a top-level reference to the event loop, it ensures the final cleanup 8722 is not performed until it goes out of scope too, letting the individual window loops 8723 work without trouble despite the bug. 8724 ) 8725 8726 History: 8727 The [BlockingMode] parameter was added on December 8, 2021. 8728 The default behavior is to block until the application quits 8729 (so all windows have been closed), unless another minigui or 8730 simpledisplay event loop is already running, in which case it 8731 will block until this window closes specifically. 8732 +/ 8733 @scriptable 8734 void loop(BlockingMode bm = BlockingMode.automatic) { 8735 if(win.closed) 8736 return; // otherwise show will throw 8737 show(); 8738 win.eventLoopWithBlockingMode(bm, 0); 8739 } 8740 8741 private bool firstShow = true; 8742 8743 @scriptable 8744 override void show() { 8745 bool rd = false; 8746 if(firstShow) { 8747 firstShow = false; 8748 queueRecomputeChildLayout(); 8749 auto f = getFirstFocusable(this); // FIXME: autofocus? 8750 if(f) 8751 f.focus(); 8752 redraw(); 8753 } 8754 win.show(); 8755 super.show(); 8756 } 8757 @scriptable 8758 override void hide() { 8759 win.hide(); 8760 super.hide(); 8761 } 8762 8763 static Widget getFirstFocusable(Widget start) { 8764 if(start is null) 8765 return null; 8766 8767 foreach(widget; &start.focusableWidgets) { 8768 return widget; 8769 } 8770 8771 return null; 8772 } 8773 8774 static Widget getLastFocusable(Widget start) { 8775 if(start is null) 8776 return null; 8777 8778 Widget last; 8779 foreach(widget; &start.focusableWidgets) { 8780 last = widget; 8781 } 8782 8783 return last; 8784 } 8785 8786 8787 mixin Emits!ClosingEvent; 8788 mixin Emits!ClosedEvent; 8789 } 8790 8791 /++ 8792 History: 8793 Added January 12, 2022 8794 +/ 8795 class DpiChangedEvent : Event { 8796 enum EventString = "dpichanged"; 8797 8798 this(Widget target) { 8799 super(EventString, target); 8800 } 8801 } 8802 8803 debug private class DevToolWindow : Window { 8804 Window p; 8805 8806 TextEdit parentList; 8807 TextEdit logWindow; 8808 TextLabel clickX, clickY; 8809 8810 this(Window p) { 8811 this.p = p; 8812 super(400, 300, "Developer Toolbox"); 8813 8814 logWindow = new TextEdit(this); 8815 parentList = new TextEdit(this); 8816 8817 auto hl = new HorizontalLayout(this); 8818 clickX = new TextLabel("", TextAlignment.Right, hl); 8819 clickY = new TextLabel("", TextAlignment.Right, hl); 8820 8821 parentListeners ~= p.addEventListener("*", (Event ev) { 8822 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 8823 }); 8824 8825 parentListeners ~= p.addEventListener((ClickEvent ev) { 8826 auto s = ev.srcElement; 8827 8828 string list; 8829 8830 void addInfo(Widget s) { 8831 list ~= s.toString(); 8832 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 8833 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 8834 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 8835 list ~= "\n\theight: " ~ toInternal!string(s.height); 8836 list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth); 8837 list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth); 8838 list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness); 8839 list ~= "\n\twidth: " ~ toInternal!string(s.width); 8840 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 8841 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 8842 } 8843 8844 addInfo(s); 8845 8846 s = s.parent; 8847 while(s) { 8848 list ~= "\n"; 8849 addInfo(s); 8850 s = s.parent; 8851 } 8852 parentList.content = list; 8853 8854 clickX.label = toInternal!string(ev.clientX); 8855 clickY.label = toInternal!string(ev.clientY); 8856 }); 8857 } 8858 8859 EventListener[] parentListeners; 8860 8861 override void close() { 8862 assert(p !is null); 8863 foreach(p; parentListeners) 8864 p.disconnect(); 8865 parentListeners = null; 8866 p.devTools = null; 8867 p = null; 8868 super.close(); 8869 } 8870 8871 override void defaultEventHandler_keydown(KeyDownEvent ev) { 8872 if(ev.key == Key.F12) { 8873 this.close(); 8874 if(p) 8875 p.devTools = null; 8876 } else { 8877 super.defaultEventHandler_keydown(ev); 8878 } 8879 } 8880 8881 void log(T...)(T t) { 8882 string str; 8883 import std.conv; 8884 foreach(i; t) 8885 str ~= to!string(i); 8886 str ~= "\n"; 8887 logWindow.addText(str); 8888 8889 //version(custom_widgets) 8890 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 8891 } 8892 } 8893 8894 /++ 8895 A dialog is a transient window that intends to get information from 8896 the user before being dismissed. 8897 +/ 8898 class Dialog : Window { 8899 /// 8900 this(Window parent, int width, int height, string title = null) { 8901 super(width, height, title, WindowTypes.dialog, WindowFlags.dontAutoShow | WindowFlags.transient, parent is null ? null : parent.win); 8902 8903 // this(int width = 500, int height = 500, string title = null, WindowTypes windowType = WindowTypes.normal, WindowFlags windowFlags = WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus, SimpleWindow parent = null) { 8904 } 8905 8906 /// 8907 this(Window parent, string title, int width, int height) { 8908 this(parent, width, height, title); 8909 } 8910 8911 deprecated("Pass an explicit parent window, even if it is `null`") 8912 this(int width, int height, string title = null) { 8913 this(null, width, height, title); 8914 } 8915 8916 /// 8917 void OK() { 8918 8919 } 8920 8921 /// 8922 void Cancel() { 8923 this.close(); 8924 } 8925 } 8926 8927 /++ 8928 A custom widget similar to the HTML5 <details> tag. 8929 +/ 8930 version(none) 8931 class DetailsView : Widget { 8932 8933 } 8934 8935 // FIXME: maybe i should expose the other list views Windows offers too 8936 8937 /++ 8938 A TableView is a widget made to display a table of data strings. 8939 8940 8941 Future_Directions: 8942 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 8943 8944 I will add a selection changed event at some point, as well as item clicked events. 8945 History: 8946 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 8947 See_Also: 8948 [ListWidget] which displays a list of strings without additional columns. 8949 +/ 8950 class TableView : Widget { 8951 /++ 8952 8953 +/ 8954 this(Widget parent) { 8955 super(parent); 8956 8957 version(win32_widgets) { 8958 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 8959 } else version(custom_widgets) { 8960 auto smw = new ScrollMessageWidget(this); 8961 smw.addDefaultKeyboardListeners(); 8962 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 8963 tvwi = new TableViewWidgetInner(this, smw); 8964 } 8965 } 8966 8967 // FIXME: auto-size columns on double click of header thing like in Windows 8968 // it need only make the currently displayed things fit well. 8969 8970 8971 private ColumnInfo[] columns; 8972 private int itemCount; 8973 8974 version(custom_widgets) private { 8975 TableViewWidgetInner tvwi; 8976 } 8977 8978 /// Passed to [setColumnInfo] 8979 static struct ColumnInfo { 8980 const(char)[] name; /// the name displayed in the header 8981 /++ 8982 The default width, in pixels. As a special case, you can set this to -1 8983 if you want the system to try to automatically size the width to fit visible 8984 content. If it can't, it will try to pick a sensible default size. 8985 8986 Any other negative value is not allowed and may lead to unpredictable results. 8987 8988 History: 8989 The -1 behavior was specified on December 3, 2021. It actually worked before 8990 anyway on Win32 but now it is a formal feature with partial Linux support. 8991 8992 Bugs: 8993 It doesn't actually attempt to calculate a best-fit width on Linux as of 8994 December 3, 2021. I do plan to fix this in the future, but Windows is the 8995 priority right now. At least it doesn't break things when you use it now. 8996 +/ 8997 int width; 8998 8999 /++ 9000 Alignment of the text in the cell. Applies to the header as well as all data in this 9001 column. 9002 9003 Bugs: 9004 On Windows, the first column ignores this member and is always left aligned. 9005 You can work around this by inserting a dummy first column with width = 0 9006 then putting your actual data in the second column, which does respect the 9007 alignment. 9008 9009 This is a quirk of the operating system's implementation going back a very 9010 long time and is unlikely to ever be fixed. 9011 +/ 9012 TextAlignment alignment; 9013 9014 /++ 9015 After all the pixel widths have been assigned, any left over 9016 space is divided up among all columns and distributed to according 9017 to the widthPercent field. 9018 9019 9020 For example, if you have two fields, both with width 50 and one with 9021 widthPercent of 25 and the other with widthPercent of 75, and the 9022 container is 200 pixels wide, first both get their width of 50. 9023 then the 100 remaining pixels are split up, so the one gets a total 9024 of 75 pixels and the other gets a total of 125. 9025 9026 This is automatically applied as the window is resized. 9027 9028 If there is not enough space - that is, when a horizontal scrollbar 9029 needs to appear - there are 0 pixels divided up, and thus everyone 9030 gets 0. This can cause a column to shrink out of proportion when 9031 passing the scroll threshold. 9032 9033 It is important to still set a fixed width (that is, to populate the 9034 `width` field) even if you use the percents because that will be the 9035 default minimum in the event of a scroll bar appearing. 9036 9037 The percents total in the column can never exceed 100 or be less than 0. 9038 Doing this will trigger an assert error. 9039 9040 Implementation note: 9041 9042 Please note that percentages are only recalculated 1) upon original 9043 construction and 2) upon resizing the control. If the user adjusts the 9044 width of a column, the percentage items will not be updated. 9045 9046 On the other hand, if the user adjusts the width of a percentage column 9047 then resizes the window, it is recalculated, meaning their hand adjustment 9048 is discarded. This specific behavior may change in the future as it is 9049 arguably a bug, but I'm not certain yet. 9050 9051 History: 9052 Added November 10, 2021 (dub v10.4) 9053 +/ 9054 int widthPercent; 9055 9056 9057 private int calculatedWidth; 9058 } 9059 /++ 9060 Sets the number of columns along with information about the headers. 9061 9062 Please note: on Windows, the first column ignores your alignment preference 9063 and is always left aligned. 9064 +/ 9065 void setColumnInfo(ColumnInfo[] columns...) { 9066 9067 foreach(ref c; columns) { 9068 c.name = c.name.idup; 9069 } 9070 this.columns = columns.dup; 9071 9072 updateCalculatedWidth(false); 9073 9074 version(custom_widgets) { 9075 tvwi.header.updateHeaders(); 9076 tvwi.updateScrolls(); 9077 } else version(win32_widgets) 9078 foreach(i, column; this.columns) { 9079 LVCOLUMN lvColumn; 9080 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 9081 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 9082 9083 auto bfr = WCharzBuffer(column.name); 9084 lvColumn.pszText = bfr.ptr; 9085 9086 if(column.alignment & TextAlignment.Center) 9087 lvColumn.fmt = LVCFMT_CENTER; 9088 else if(column.alignment & TextAlignment.Right) 9089 lvColumn.fmt = LVCFMT_RIGHT; 9090 else 9091 lvColumn.fmt = LVCFMT_LEFT; 9092 9093 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 9094 throw new WindowsApiException("Insert Column Fail", GetLastError()); 9095 } 9096 } 9097 9098 private int getActualSetSize(size_t i, bool askWindows) { 9099 version(win32_widgets) 9100 if(askWindows) 9101 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 9102 auto w = columns[i].width; 9103 if(w == -1) 9104 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 9105 return w; 9106 } 9107 9108 private void updateCalculatedWidth(bool informWindows) { 9109 int padding; 9110 version(win32_widgets) 9111 padding = 4; 9112 int remaining = this.width; 9113 foreach(i, column; columns) 9114 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 9115 remaining -= padding; 9116 if(remaining < 0) 9117 remaining = 0; 9118 9119 int percentTotal; 9120 foreach(i, ref column; columns) { 9121 percentTotal += column.widthPercent; 9122 9123 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 9124 9125 column.calculatedWidth = c; 9126 9127 version(win32_widgets) 9128 if(informWindows) 9129 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 9130 } 9131 9132 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 9133 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)."); 9134 9135 9136 } 9137 9138 override void registerMovement() { 9139 super.registerMovement(); 9140 9141 updateCalculatedWidth(true); 9142 } 9143 9144 /++ 9145 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. 9146 +/ 9147 void setItemCount(int count) { 9148 this.itemCount = count; 9149 version(custom_widgets) { 9150 tvwi.updateScrolls(); 9151 redraw(); 9152 } else version(win32_widgets) { 9153 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 9154 } 9155 } 9156 9157 /++ 9158 Clears all items; 9159 +/ 9160 void clear() { 9161 this.itemCount = 0; 9162 this.columns = null; 9163 version(custom_widgets) { 9164 tvwi.header.updateHeaders(); 9165 tvwi.updateScrolls(); 9166 redraw(); 9167 } else version(win32_widgets) { 9168 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 9169 } 9170 } 9171 9172 /+ 9173 version(win32_widgets) 9174 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 9175 auto itemId = dis.itemID; 9176 auto hdc = dis.hDC; 9177 auto rect = dis.rcItem; 9178 switch(dis.itemAction) { 9179 case ODA_DRAWENTIRE: 9180 9181 // FIXME: do other items 9182 // FIXME: do the focus rectangle i guess 9183 // FIXME: alignment 9184 // FIXME: column width 9185 // FIXME: padding left 9186 // FIXME: check dpi scaling 9187 // FIXME: don't owner draw unless it is necessary. 9188 9189 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 9190 RECT itemRect; 9191 itemRect.top = 1; // subitem idx, 1-based 9192 itemRect.left = LVIR_BOUNDS; 9193 9194 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 9195 itemRect.left += padding; 9196 9197 getData(itemId, 0, (in char[] data) { 9198 auto wdata = WCharzBuffer(data); 9199 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 9200 9201 }); 9202 goto case; 9203 case ODA_FOCUS: 9204 if(dis.itemState & ODS_FOCUS) 9205 DrawFocusRect(hdc, &rect); 9206 break; 9207 case ODA_SELECT: 9208 // itemState & ODS_SELECTED 9209 break; 9210 default: 9211 } 9212 return 1; 9213 } 9214 +/ 9215 9216 version(win32_widgets) { 9217 CellStyle last; 9218 COLORREF defaultColor; 9219 COLORREF defaultBackground; 9220 } 9221 9222 version(win32_widgets) 9223 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 9224 switch(code) { 9225 case NM_CUSTOMDRAW: 9226 auto s = cast(NMLVCUSTOMDRAW*) hdr; 9227 switch(s.nmcd.dwDrawStage) { 9228 case CDDS_PREPAINT: 9229 if(getCellStyle is null) 9230 return 0; 9231 9232 mustReturn = true; 9233 return CDRF_NOTIFYITEMDRAW; 9234 case CDDS_ITEMPREPAINT: 9235 mustReturn = true; 9236 return CDRF_NOTIFYSUBITEMDRAW; 9237 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 9238 mustReturn = true; 9239 9240 if(getCellStyle is null) // this SHOULD never happen... 9241 return 0; 9242 9243 if(s.iSubItem == 0) { 9244 // Windows resets it per row so we'll use item 0 as a chance 9245 // to capture these for later 9246 defaultColor = s.clrText; 9247 defaultBackground = s.clrTextBk; 9248 } 9249 9250 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 9251 // if no special style and no reset needed... 9252 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 9253 return 0; // allow default processing to continue 9254 9255 last = style; 9256 9257 // might still need to reset or use the preference. 9258 9259 if(style.flags & CellStyle.Flags.textColorSet) 9260 s.clrText = style.textColor.asWindowsColorRef; 9261 else 9262 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 9263 if(style.flags & CellStyle.Flags.backgroundColorSet) 9264 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 9265 else 9266 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 9267 9268 return CDRF_NEWFONT; 9269 default: 9270 return 0; 9271 9272 } 9273 case NM_RETURN: // no need since i subclass keydown 9274 break; 9275 case LVN_COLUMNCLICK: 9276 auto info = cast(LPNMLISTVIEW) hdr; 9277 this.emit!HeaderClickedEvent(info.iSubItem); 9278 break; 9279 case NM_CLICK: 9280 case NM_DBLCLK: 9281 case NM_RCLICK: 9282 case NM_RDBLCLK: 9283 // the item/subitem is set here and that can be a useful notification 9284 // even beyond the normal click notification 9285 break; 9286 case LVN_GETDISPINFO: 9287 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 9288 if(info.item.mask & LVIF_TEXT) { 9289 if(getData) { 9290 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 9291 auto bfr = WCharzBuffer(dataReceived); 9292 auto len = info.item.cchTextMax; 9293 if(bfr.length < len) 9294 len = cast(typeof(len)) bfr.length; 9295 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 9296 info.item.pszText[len] = 0; 9297 }); 9298 } else { 9299 info.item.pszText[0] = 0; 9300 } 9301 //info.item.iItem 9302 //if(info.item.iSubItem) 9303 } 9304 break; 9305 default: 9306 } 9307 return 0; 9308 } 9309 9310 override bool encapsulatedChildren() { 9311 return true; 9312 } 9313 9314 /++ 9315 Informs the control that content has changed. 9316 9317 History: 9318 Added November 10, 2021 (dub v10.4) 9319 +/ 9320 void update() { 9321 version(custom_widgets) 9322 redraw(); 9323 else { 9324 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 9325 UpdateWindow(hwnd); 9326 } 9327 9328 9329 } 9330 9331 /++ 9332 Called by the system to request the text content of an individual cell. You 9333 should pass the text into the provided `sink` delegate. This function will be 9334 called for each visible cell as-needed when drawing. 9335 +/ 9336 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 9337 9338 /++ 9339 Available per-cell style customization options. Use one of the constructors 9340 provided to set the values conveniently, or default construct it and set individual 9341 values yourself. Just remember to set the `flags` so your values are actually used. 9342 If the flag isn't set, the field is ignored and the system default is used instead. 9343 9344 This is returned by the [getCellStyle] delegate. 9345 9346 Examples: 9347 --- 9348 // assumes you have a variables called `my_data` which is an array of arrays of numbers 9349 auto table = new TableView(window); 9350 // snip: you would set up columns here 9351 9352 // this is how you provide data to the table view class 9353 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 9354 import std.conv; 9355 sink(to!string(my_data[row][column])); 9356 }; 9357 9358 // and this is how you customize the colors 9359 table.getCellStyle = delegate(int row, int column) { 9360 return (my_data[row][column] < 0) ? 9361 TableView.CellStyle(Color.red); // make negative numbers red 9362 : TableView.CellStyle.init; // leave the rest alone 9363 }; 9364 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 9365 --- 9366 9367 History: 9368 Added November 27, 2021 (dub v10.4) 9369 +/ 9370 struct CellStyle { 9371 /// 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. 9372 this(Color textColor) { 9373 this.textColor = textColor; 9374 this.flags |= Flags.textColorSet; 9375 } 9376 /// Sets a custom text and background color. 9377 this(Color textColor, Color backgroundColor) { 9378 this.textColor = textColor; 9379 this.backgroundColor = backgroundColor; 9380 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 9381 } 9382 9383 Color textColor; 9384 Color backgroundColor; 9385 int flags; /// bitmask of [Flags] 9386 /// available options to combine into [flags] 9387 enum Flags { 9388 textColorSet = 1 << 0, 9389 backgroundColorSet = 1 << 1, 9390 } 9391 } 9392 /++ 9393 Companion delegate to [getData] that allows you to custom style each 9394 cell of the table. 9395 9396 Returns: 9397 A [CellStyle] structure that describes the desired style for the 9398 given cell. `return CellStyle.init` if you want the default style. 9399 9400 History: 9401 Added November 27, 2021 (dub v10.4) 9402 +/ 9403 CellStyle delegate(int row, int column) getCellStyle; 9404 9405 // i want to be able to do things like draw little colored things to show red for negative numbers 9406 // or background color indicators or even in-cell charts 9407 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 9408 9409 /++ 9410 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 9411 +/ 9412 mixin Emits!HeaderClickedEvent; 9413 } 9414 9415 /++ 9416 This is emitted by the [TableView] when a user clicks on a column header. 9417 9418 Its member `columnIndex` has the zero-based index of the column that was clicked. 9419 9420 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 9421 9422 History: 9423 Added November 27, 2021 (dub v10.4) 9424 +/ 9425 class HeaderClickedEvent : Event { 9426 enum EventString = "HeaderClicked"; 9427 this(Widget target, int columnIndex) { 9428 this.columnIndex = columnIndex; 9429 super(EventString, target); 9430 } 9431 9432 /// The index of the column 9433 int columnIndex; 9434 9435 /// 9436 override @property int intValue() { 9437 return columnIndex; 9438 } 9439 } 9440 9441 version(custom_widgets) 9442 private class TableViewWidgetInner : Widget { 9443 9444 // wrap this thing in a ScrollMessageWidget 9445 9446 TableView tvw; 9447 ScrollMessageWidget smw; 9448 HeaderWidget header; 9449 9450 this(TableView tvw, ScrollMessageWidget smw) { 9451 this.tvw = tvw; 9452 this.smw = smw; 9453 super(smw); 9454 9455 this.tabStop = true; 9456 9457 header = new HeaderWidget(this, smw.getHeader()); 9458 9459 smw.addEventListener("scroll", () { 9460 this.redraw(); 9461 header.redraw(); 9462 }); 9463 9464 9465 // I need headers outside the scroll area but rendered on the same line as the up arrow 9466 // FIXME: add a fixed header to the SMW 9467 } 9468 9469 enum padding = 3; 9470 9471 void updateScrolls() { 9472 int w; 9473 foreach(idx, column; tvw.columns) { 9474 if(column.width == 0) continue; 9475 w += tvw.getActualSetSize(idx, false);// + padding; 9476 } 9477 smw.setTotalArea(w, tvw.itemCount); 9478 columnsWidth = w; 9479 } 9480 9481 private int columnsWidth; 9482 9483 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 9484 9485 override void registerMovement() { 9486 super.registerMovement(); 9487 // FIXME: actual column width. it might need to be done per-pixel instead of per-column 9488 smw.setViewableArea(this.width, this.height / lh); 9489 } 9490 9491 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 9492 int x; 9493 int y; 9494 9495 int row = smw.position.y; 9496 9497 foreach(lol; 0 .. this.height / lh) { 9498 if(row >= tvw.itemCount) 9499 break; 9500 x = 0; 9501 foreach(columnNumber, column; tvw.columns) { 9502 auto x2 = x + column.calculatedWidth; 9503 auto smwx = smw.position.x; 9504 9505 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 9506 auto startX = x; 9507 auto endX = x + column.calculatedWidth; 9508 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 9509 case TextAlignment.Left: startX += padding; break; 9510 case TextAlignment.Center: startX += padding; endX -= padding; break; 9511 case TextAlignment.Right: endX -= padding; break; 9512 default: /* broken */ break; 9513 } 9514 if(column.width != 0) // no point drawing an invisible column 9515 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 9516 auto clip = painter.setClipRectangle(Rectangle(Point(startX - smw.position.x, y), Point(endX - smw.position.x, y + lh))); 9517 9518 void dotext(WidgetPainter painter) { 9519 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 9520 } 9521 9522 if(tvw.getCellStyle !is null) { 9523 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 9524 9525 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 9526 auto tempPainter = painter; 9527 tempPainter.fillColor = style.backgroundColor; 9528 tempPainter.outlineColor = style.backgroundColor; 9529 9530 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 9531 Point(endX - smw.position.x, y + lh)); 9532 } 9533 auto tempPainter = painter; 9534 if(style.flags & TableView.CellStyle.Flags.textColorSet) 9535 tempPainter.outlineColor = style.textColor; 9536 9537 dotext(tempPainter); 9538 } else { 9539 dotext(painter); 9540 } 9541 }); 9542 } 9543 9544 x += column.calculatedWidth; 9545 } 9546 row++; 9547 y += lh; 9548 } 9549 return bounds; 9550 } 9551 9552 static class Style : Widget.Style { 9553 override WidgetBackground background() { 9554 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 9555 } 9556 } 9557 mixin OverrideStyle!Style; 9558 9559 private static class HeaderWidget : Widget { 9560 /+ 9561 maybe i should do a splitter thing on top of the other widgets 9562 so the splitter itself isn't really drawn but still replies to mouse events? 9563 +/ 9564 this(TableViewWidgetInner tvw, Widget parent) { 9565 super(parent); 9566 this.tvw = tvw; 9567 9568 this.remainder = new Button("", this); 9569 9570 this.addEventListener((scope ClickEvent ev) { 9571 int header = -1; 9572 foreach(idx, child; this.children[1 .. $]) { 9573 if(child is ev.target) { 9574 header = cast(int) idx; 9575 break; 9576 } 9577 } 9578 9579 if(header != -1) { 9580 auto hce = new HeaderClickedEvent(tvw.tvw, header); 9581 hce.dispatch(); 9582 } 9583 9584 }); 9585 } 9586 9587 void updateHeaders() { 9588 foreach(child; children[1 .. $]) 9589 child.removeWidget(); 9590 9591 foreach(column; tvw.tvw.columns) { 9592 // the cast is ok because I dup it above, just the type is never changed. 9593 // all this is private so it should never get messed up. 9594 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 9595 } 9596 } 9597 9598 Button remainder; 9599 TableViewWidgetInner tvw; 9600 9601 override void recomputeChildLayout() { 9602 registerMovement(); 9603 int pos; 9604 foreach(idx, child; children[1 .. $]) { 9605 if(idx >= tvw.tvw.columns.length) 9606 continue; 9607 child.x = pos; 9608 child.y = 0; 9609 child.width = tvw.tvw.columns[idx].calculatedWidth; 9610 child.height = scaleWithDpi(16);// this.height; 9611 pos += child.width; 9612 9613 child.recomputeChildLayout(); 9614 } 9615 9616 if(remainder is null) 9617 return; 9618 9619 remainder.x = pos; 9620 remainder.y = 0; 9621 if(pos < this.width) 9622 remainder.width = this.width - pos;// + 4; 9623 else 9624 remainder.width = 0; 9625 remainder.height = scaleWithDpi(16); 9626 9627 remainder.recomputeChildLayout(); 9628 } 9629 9630 // for the scrollable children mixin 9631 Point scrollOrigin() { 9632 return Point(tvw.smw.position.x, 0); 9633 } 9634 void paintFrameAndBackground(WidgetPainter painter) { } 9635 9636 mixin ScrollableChildren; 9637 } 9638 } 9639 9640 /+ 9641 9642 // given struct / array / number / string / etc, make it viewable and editable 9643 class DataViewerWidget : Widget { 9644 9645 } 9646 +/ 9647 9648 /++ 9649 A line edit box with an associated label. 9650 9651 History: 9652 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 9653 9654 ``` 9655 Old: ________ 9656 9657 New: 9658 ____________ 9659 ``` 9660 9661 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 9662 9663 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 9664 horizontal label but left aligned. You may also consider a [GridLayout]. 9665 +/ 9666 alias LabeledLineEdit = Labeled!LineEdit; 9667 9668 private int widthThatWouldFitChildLabels(Widget w) { 9669 if(w is null) 9670 return 0; 9671 9672 int max; 9673 9674 if(auto label = cast(TextLabel) w) { 9675 return label.TextLabel.flexBasisWidth() + label.paddingLeft() + label.paddingRight(); 9676 } else { 9677 foreach(child; w.children) { 9678 max = mymax(max, widthThatWouldFitChildLabels(child)); 9679 } 9680 } 9681 9682 return max; 9683 } 9684 9685 /++ 9686 History: 9687 Added May 19, 2021 9688 +/ 9689 class Labeled(T) : Widget { 9690 /// 9691 this(string label, Widget parent) { 9692 super(parent); 9693 initialize!VerticalLayout(label, TextAlignment.Left, parent); 9694 } 9695 9696 /++ 9697 History: 9698 The alignment parameter was added May 17, 2021 9699 +/ 9700 this(string label, TextAlignment alignment, Widget parent) { 9701 super(parent); 9702 initialize!HorizontalLayout(label, alignment, parent); 9703 } 9704 9705 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 9706 tabStop = false; 9707 horizontal = is(L == HorizontalLayout); 9708 auto hl = new L(this); 9709 if(horizontal) { 9710 static class SpecialTextLabel : TextLabel { 9711 Widget outerParent; 9712 9713 this(string label, TextAlignment alignment, Widget outerParent, Widget parent) { 9714 this.outerParent = outerParent; 9715 super(label, alignment, parent); 9716 } 9717 9718 override int flexBasisWidth() { 9719 return widthThatWouldFitChildLabels(outerParent); 9720 } 9721 /+ 9722 override int widthShrinkiness() { return 0; } 9723 override int widthStretchiness() { return 1; } 9724 +/ 9725 9726 override int paddingRight() { return 6; } 9727 override int paddingLeft() { return 9; } 9728 9729 override int paddingTop() { return 3; } 9730 } 9731 this.label = new SpecialTextLabel(label, alignment, parent, hl); 9732 } else 9733 this.label = new TextLabel(label, alignment, hl); 9734 this.lineEdit = new T(hl); 9735 9736 this.label.labelFor = this.lineEdit; 9737 } 9738 9739 private bool horizontal; 9740 9741 TextLabel label; /// 9742 T lineEdit; /// 9743 9744 override int flexBasisWidth() { return 250; } 9745 override int widthShrinkiness() { return 1; } 9746 9747 override int minHeight() { 9748 return this.children[0].minHeight; 9749 } 9750 override int maxHeight() { return minHeight(); } 9751 override int marginTop() { return 4; } 9752 override int marginBottom() { return 4; } 9753 9754 // FIXME: i should prolly call it value as well as content tbh 9755 9756 /// 9757 @property string content() { 9758 return lineEdit.content; 9759 } 9760 /// 9761 @property void content(string c) { 9762 return lineEdit.content(c); 9763 } 9764 9765 /// 9766 void selectAll() { 9767 lineEdit.selectAll(); 9768 } 9769 9770 override void focus() { 9771 lineEdit.focus(); 9772 } 9773 } 9774 9775 /++ 9776 A labeled password edit. 9777 9778 History: 9779 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 9780 9781 The default parameters for the constructors were also removed on May 19, 2021 9782 +/ 9783 alias LabeledPasswordEdit = Labeled!PasswordEdit; 9784 9785 private string toMenuLabel(string s) { 9786 string n; 9787 n.reserve(s.length); 9788 foreach(c; s) 9789 if(c == '_') 9790 n ~= ' '; 9791 else 9792 n ~= c; 9793 return n; 9794 } 9795 9796 private void autoExceptionHandler(Exception e) { 9797 messageBox(e.msg); 9798 } 9799 9800 private void delegate() makeAutomaticHandler(alias fn, T)(Window window, T t) { 9801 static if(is(T : void delegate())) { 9802 return () { 9803 try 9804 t(); 9805 catch(Exception e) 9806 autoExceptionHandler(e); 9807 }; 9808 } else static if(is(typeof(fn) Params == __parameters)) { 9809 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 9810 return () { 9811 void onOK(string s) { 9812 member = s; 9813 try 9814 t(Params[0](s)); 9815 catch(Exception e) 9816 autoExceptionHandler(e); 9817 } 9818 9819 if( 9820 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 9821 || type == FileDialogType.Save) 9822 { 9823 getSaveFileName(window, &onOK, member, filters, null); 9824 } else 9825 getOpenFileName(window, &onOK, member, filters, null); 9826 }; 9827 } else { 9828 struct S { 9829 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 9830 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 9831 } else mixin(q{ 9832 static foreach(idx, ignore; Params) { 9833 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 9834 } 9835 }); 9836 } 9837 return () { 9838 dialog(window, (S s) { 9839 try { 9840 static if(is(typeof(t) Ret == return)) { 9841 static if(is(Ret == void)) { 9842 t(s.tupleof); 9843 } else { 9844 auto ret = t(s.tupleof); 9845 import std.conv; 9846 messageBox(to!string(ret), "Returned Value"); 9847 } 9848 } 9849 } catch(Exception e) 9850 autoExceptionHandler(e); 9851 }, null, __traits(identifier, fn)); 9852 }; 9853 } 9854 } 9855 } 9856 9857 private template hasAnyRelevantAnnotations(a...) { 9858 bool helper() { 9859 bool any; 9860 foreach(attr; a) { 9861 static if(is(typeof(attr) == .menu)) 9862 any = true; 9863 else static if(is(typeof(attr) == .toolbar)) 9864 any = true; 9865 else static if(is(attr == .separator)) 9866 any = true; 9867 else static if(is(typeof(attr) == .accelerator)) 9868 any = true; 9869 else static if(is(typeof(attr) == .hotkey)) 9870 any = true; 9871 else static if(is(typeof(attr) == .icon)) 9872 any = true; 9873 else static if(is(typeof(attr) == .label)) 9874 any = true; 9875 else static if(is(typeof(attr) == .tip)) 9876 any = true; 9877 } 9878 return any; 9879 } 9880 9881 enum bool hasAnyRelevantAnnotations = helper(); 9882 } 9883 9884 /++ 9885 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. 9886 +/ 9887 class MainWindow : Window { 9888 /// 9889 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 9890 super(initialWidth, initialHeight, title); 9891 9892 _clientArea = new ClientAreaWidget(); 9893 _clientArea.x = 0; 9894 _clientArea.y = 0; 9895 _clientArea.width = this.width; 9896 _clientArea.height = this.height; 9897 _clientArea.tabStop = false; 9898 9899 super.addChild(_clientArea); 9900 9901 statusBar = new StatusBar(this); 9902 } 9903 9904 /++ 9905 Adds a menu and toolbar from annotated functions. It uses the top-level annotations from this module, so it is better to put the commands in a separate struct instad of in your window subclass, to avoid potential conflicts with method names (if you do hit one though, you can use `@(.icon(...))` instead of plain `@icon(...)` to disambiguate, though). 9906 9907 --- 9908 struct Commands { 9909 @menu("File") { 9910 @toolbar("") // adds it to a generic toolbar 9911 void New() {} 9912 void Open() {} 9913 void Save() {} 9914 @separator 9915 void Exit() @accelerator("Alt+F4") @hotkey('x') { 9916 window.close(); 9917 } 9918 } 9919 9920 @menu("Edit") { 9921 @icon(GenericIcons.Undo) 9922 void Undo() { 9923 undo(); 9924 } 9925 @separator 9926 void Cut() {} 9927 void Copy() {} 9928 void Paste() {} 9929 } 9930 9931 @menu("Help") { 9932 void About() {} 9933 } 9934 } 9935 9936 Commands commands; 9937 9938 window.setMenuAndToolbarFromAnnotatedCode(commands); 9939 --- 9940 9941 Note that you can call this function multiple times and it will add the items in order to the given items. 9942 9943 +/ 9944 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 9945 setMenuAndToolbarFromAnnotatedCode_internal(t); 9946 } 9947 /// ditto 9948 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 9949 setMenuAndToolbarFromAnnotatedCode_internal(t); 9950 } 9951 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 9952 Action[] toolbarActions; 9953 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 9954 Menu[string] mcs; 9955 9956 foreach(menu; menuBar.subMenus) { 9957 mcs[menu.label] = menu; 9958 } 9959 9960 foreach(memberName; __traits(derivedMembers, T)) { 9961 static if(memberName != "this") 9962 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 9963 .menu menu; 9964 .toolbar toolbar; 9965 bool separator; 9966 .accelerator accelerator; 9967 .hotkey hotkey; 9968 .icon icon; 9969 string label; 9970 string tip; 9971 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 9972 static if(is(typeof(attr) == .menu)) 9973 menu = attr; 9974 else static if(is(typeof(attr) == .toolbar)) 9975 toolbar = attr; 9976 else static if(is(attr == .separator)) 9977 separator = true; 9978 else static if(is(typeof(attr) == .accelerator)) 9979 accelerator = attr; 9980 else static if(is(typeof(attr) == .hotkey)) 9981 hotkey = attr; 9982 else static if(is(typeof(attr) == .icon)) 9983 icon = attr; 9984 else static if(is(typeof(attr) == .label)) 9985 label = attr.label; 9986 else static if(is(typeof(attr) == .tip)) 9987 tip = attr.tip; 9988 } 9989 9990 if(menu !is .menu.init || toolbar !is .toolbar.init) { 9991 ushort correctIcon = icon.id; // FIXME 9992 if(label.length == 0) 9993 label = memberName.toMenuLabel; 9994 9995 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(this.parentWindow, &__traits(getMember, t, memberName)); 9996 9997 auto action = new Action(label, correctIcon, handler); 9998 9999 if(accelerator.keyString.length) { 10000 auto ke = KeyEvent.parse(accelerator.keyString); 10001 action.accelerator = ke; 10002 accelerators[ke.toStr] = handler; 10003 } 10004 10005 if(toolbar !is .toolbar.init) 10006 toolbarActions ~= action; 10007 if(menu !is .menu.init) { 10008 Menu mc; 10009 if(menu.name in mcs) { 10010 mc = mcs[menu.name]; 10011 } else { 10012 mc = new Menu(menu.name, this); 10013 menuBar.addItem(mc); 10014 mcs[menu.name] = mc; 10015 } 10016 10017 if(separator) 10018 mc.addSeparator(); 10019 mc.addItem(new MenuItem(action)); 10020 } 10021 } 10022 } 10023 } 10024 10025 this.menuBar = menuBar; 10026 10027 if(toolbarActions.length) { 10028 auto tb = new ToolBar(toolbarActions, this); 10029 } 10030 } 10031 10032 void delegate()[string] accelerators; 10033 10034 override void defaultEventHandler_keydown(KeyDownEvent event) { 10035 auto str = event.originalKeyEvent.toStr; 10036 if(auto acl = str in accelerators) 10037 (*acl)(); 10038 super.defaultEventHandler_keydown(event); 10039 } 10040 10041 override void defaultEventHandler_mouseover(MouseOverEvent event) { 10042 super.defaultEventHandler_mouseover(event); 10043 if(this.statusBar !is null && event.target.statusTip.length) 10044 this.statusBar.parts[0].content = event.target.statusTip; 10045 else if(this.statusBar !is null && this.statusTip.length) 10046 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 10047 } 10048 10049 override void addChild(Widget c, int position = int.max) { 10050 if(auto tb = cast(ToolBar) c) 10051 version(win32_widgets) 10052 super.addChild(c, 0); 10053 else version(custom_widgets) 10054 super.addChild(c, menuBar ? 1 : 0); 10055 else static assert(0); 10056 else 10057 clientArea.addChild(c, position); 10058 } 10059 10060 ToolBar _toolBar; 10061 /// 10062 ToolBar toolBar() { return _toolBar; } 10063 /// 10064 ToolBar toolBar(ToolBar t) { 10065 _toolBar = t; 10066 foreach(child; this.children) 10067 if(child is t) 10068 return t; 10069 version(win32_widgets) 10070 super.addChild(t, 0); 10071 else version(custom_widgets) 10072 super.addChild(t, menuBar ? 1 : 0); 10073 else static assert(0); 10074 return t; 10075 } 10076 10077 MenuBar _menu; 10078 /// 10079 MenuBar menuBar() { return _menu; } 10080 /// 10081 MenuBar menuBar(MenuBar m) { 10082 if(m is _menu) { 10083 version(custom_widgets) 10084 queueRecomputeChildLayout(); 10085 return m; 10086 } 10087 10088 if(_menu !is null) { 10089 // make sure it is sanely removed 10090 // FIXME 10091 } 10092 10093 _menu = m; 10094 10095 version(win32_widgets) { 10096 SetMenu(parentWindow.win.impl.hwnd, m.handle); 10097 } else version(custom_widgets) { 10098 super.addChild(m, 0); 10099 10100 // clientArea.y = menu.height; 10101 // clientArea.height = this.height - menu.height; 10102 10103 queueRecomputeChildLayout(); 10104 } else static assert(false); 10105 10106 return _menu; 10107 } 10108 private Widget _clientArea; 10109 /// 10110 @property Widget clientArea() { return _clientArea; } 10111 protected @property void clientArea(Widget wid) { 10112 _clientArea = wid; 10113 } 10114 10115 private StatusBar _statusBar; 10116 /++ 10117 Returns the window's [StatusBar]. Be warned it may be `null`. 10118 +/ 10119 @property StatusBar statusBar() { return _statusBar; } 10120 /// ditto 10121 @property void statusBar(StatusBar bar) { 10122 if(_statusBar !is null) 10123 _statusBar.removeWidget(); 10124 _statusBar = bar; 10125 if(bar !is null) 10126 super.addChild(_statusBar); 10127 } 10128 } 10129 10130 /+ 10131 This is really an implementation detail of [MainWindow] 10132 +/ 10133 private class ClientAreaWidget : Widget { 10134 this() { 10135 this.tabStop = false; 10136 super(null); 10137 //sa = new ScrollableWidget(this); 10138 } 10139 /* 10140 ScrollableWidget sa; 10141 override void addChild(Widget w, int position) { 10142 if(sa is null) 10143 super.addChild(w, position); 10144 else { 10145 sa.addChild(w, position); 10146 sa.setContentSize(this.minWidth + 1, this.minHeight); 10147 writeln(sa.contentWidth, "x", sa.contentHeight); 10148 } 10149 } 10150 */ 10151 } 10152 10153 /** 10154 Toolbars are lists of buttons (typically icons) that appear under the menu. 10155 Each button ought to correspond to a menu item, represented by [Action] objects. 10156 */ 10157 class ToolBar : Widget { 10158 version(win32_widgets) { 10159 private int idealHeight; 10160 override int minHeight() { return idealHeight; } 10161 override int maxHeight() { return idealHeight; } 10162 } else version(custom_widgets) { 10163 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 10164 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 10165 } else static assert(false); 10166 override int heightStretchiness() { return 0; } 10167 10168 version(win32_widgets) { 10169 HIMAGELIST imageListSmall; 10170 HIMAGELIST imageListLarge; 10171 } 10172 10173 this(Widget parent) { 10174 this(null, parent); 10175 } 10176 10177 version(win32_widgets) 10178 void changeIconSize(bool useLarge) { 10179 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall)); 10180 10181 /+ 10182 SIZE size; 10183 import core.sys.windows.commctrl; 10184 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 10185 idealHeight = size.cy + 4; // the plus 4 is a hack 10186 +/ 10187 10188 idealHeight = useLarge ? 34 : 26; 10189 10190 if(parent) { 10191 parent.queueRecomputeChildLayout(); 10192 parent.redraw(); 10193 } 10194 10195 SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4)); 10196 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 10197 } 10198 10199 /// 10200 this(Action[] actions, Widget parent) { 10201 super(parent); 10202 10203 tabStop = false; 10204 10205 version(win32_widgets) { 10206 // so i like how the flat thing looks on windows, but not on wine 10207 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 10208 // leave it commented 10209 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 10210 10211 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 10212 10213 imageListSmall = ImageList_Create( 10214 // width, height 10215 16, 16, 10216 ILC_COLOR16 | ILC_MASK, 10217 16 /*numberOfButtons*/, 0); 10218 10219 imageListLarge = ImageList_Create( 10220 // width, height 10221 24, 24, 10222 ILC_COLOR16 | ILC_MASK, 10223 16 /*numberOfButtons*/, 0); 10224 10225 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall); 10226 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 10227 10228 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge); 10229 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL); 10230 10231 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 10232 10233 TBBUTTON[] buttons; 10234 10235 // FIXME: I_IMAGENONE is if here is no icon 10236 foreach(action; actions) 10237 buttons ~= TBBUTTON( 10238 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 10239 action.id, 10240 TBSTATE_ENABLED, // state 10241 0, // style 10242 0, // reserved array, just zero it out 10243 0, // dwData 10244 cast(size_t) toWstringzInternal(action.label) // INT_PTR 10245 ); 10246 10247 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 10248 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 10249 10250 /* 10251 RECT rect; 10252 GetWindowRect(hwnd, &rect); 10253 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 10254 */ 10255 10256 dpiChanged(); // to load the things calling changeIconSize the first time 10257 10258 assert(idealHeight); 10259 } else version(custom_widgets) { 10260 foreach(action; actions) 10261 new ToolButton(action, this); 10262 } else static assert(false); 10263 } 10264 10265 override void recomputeChildLayout() { 10266 .recomputeChildLayout!"width"(this); 10267 } 10268 10269 10270 version(win32_widgets) 10271 override protected void dpiChanged() { 10272 auto sz = scaleWithDpi(16); 10273 if(sz >= 20) 10274 changeIconSize(true); 10275 else 10276 changeIconSize(false); 10277 } 10278 } 10279 10280 enum toolbarIconSize = 24; 10281 10282 /// 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. 10283 class ToolButton : Button { 10284 /// 10285 this(string label, Widget parent) { 10286 super(label, parent); 10287 tabStop = false; 10288 } 10289 /// 10290 this(Action action, Widget parent) { 10291 super(action.label, parent); 10292 tabStop = false; 10293 this.action = action; 10294 } 10295 10296 version(custom_widgets) 10297 override void defaultEventHandler_click(ClickEvent event) { 10298 foreach(handler; action.triggered) 10299 handler(); 10300 } 10301 10302 Action action; 10303 10304 override int maxWidth() { return toolbarIconSize; } 10305 override int minWidth() { return toolbarIconSize; } 10306 override int maxHeight() { return toolbarIconSize; } 10307 override int minHeight() { return toolbarIconSize; } 10308 10309 version(custom_widgets) 10310 override void paint(WidgetPainter painter) { 10311 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 10312 painter.outlineColor = Color.black; 10313 10314 // I want to get from 16 to 24. that's * 3 / 2 10315 static assert(toolbarIconSize >= 16); 10316 enum multiplier = toolbarIconSize / 8; 10317 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 10318 switch(action.iconId) { 10319 case GenericIcons.New: 10320 painter.fillColor = Color.white; 10321 painter.drawPolygon( 10322 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 10323 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 10324 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 10325 ); 10326 break; 10327 case GenericIcons.Save: 10328 painter.fillColor = Color.white; 10329 painter.outlineColor = Color.black; 10330 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10331 10332 // the label 10333 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 10334 10335 // the slider 10336 painter.fillColor = Color.black; 10337 painter.outlineColor = Color.black; 10338 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 10339 10340 painter.fillColor = Color.white; 10341 painter.outlineColor = Color.white; 10342 // the disc window 10343 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 10344 break; 10345 case GenericIcons.Open: 10346 painter.fillColor = Color.white; 10347 painter.drawPolygon( 10348 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 10349 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 10350 painter.drawPolygon( 10351 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 10352 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 10353 Point(2, 6) * multiplier / divisor); 10354 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 10355 break; 10356 case GenericIcons.Copy: 10357 painter.fillColor = Color.white; 10358 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 10359 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 10360 break; 10361 case GenericIcons.Cut: 10362 painter.fillColor = Color.transparent; 10363 painter.outlineColor = getComputedStyle.foregroundColor(); 10364 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 10365 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 10366 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 10367 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 10368 break; 10369 case GenericIcons.Paste: 10370 painter.fillColor = Color.white; 10371 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 10372 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10373 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 10374 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 10375 painter.fillColor = Color.black; 10376 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 10377 break; 10378 case GenericIcons.Help: 10379 painter.outlineColor = getComputedStyle.foregroundColor(); 10380 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10381 break; 10382 case GenericIcons.Undo: 10383 painter.fillColor = Color.transparent; 10384 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10385 painter.outlineColor = Color.black; 10386 painter.fillColor = Color.black; 10387 painter.drawPolygon( 10388 Point(4, 4) * multiplier / divisor, 10389 Point(8, 2) * multiplier / divisor, 10390 Point(8, 6) * multiplier / divisor, 10391 Point(4, 4) * multiplier / divisor, 10392 ); 10393 break; 10394 case GenericIcons.Redo: 10395 painter.fillColor = Color.transparent; 10396 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10397 painter.outlineColor = Color.black; 10398 painter.fillColor = Color.black; 10399 painter.drawPolygon( 10400 Point(10, 4) * multiplier / divisor, 10401 Point(6, 2) * multiplier / divisor, 10402 Point(6, 6) * multiplier / divisor, 10403 Point(10, 4) * multiplier / divisor, 10404 ); 10405 break; 10406 default: 10407 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10408 } 10409 return bounds; 10410 }); 10411 } 10412 10413 } 10414 10415 10416 /++ 10417 You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode]. 10418 +/ 10419 class MenuBar : Widget { 10420 MenuItem[] items; 10421 Menu[] subMenus; 10422 10423 version(win32_widgets) { 10424 HMENU handle; 10425 /// 10426 this(Widget parent = null) { 10427 super(parent); 10428 10429 handle = CreateMenu(); 10430 tabStop = false; 10431 } 10432 } else version(custom_widgets) { 10433 /// 10434 this(Widget parent = null) { 10435 tabStop = false; // these are selected some other way 10436 super(parent); 10437 } 10438 10439 mixin Padding!q{2}; 10440 } else static assert(false); 10441 10442 version(custom_widgets) 10443 override void paint(WidgetPainter painter) { 10444 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 10445 } 10446 10447 /// 10448 MenuItem addItem(MenuItem item) { 10449 this.addChild(item); 10450 items ~= item; 10451 version(win32_widgets) { 10452 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10453 } 10454 return item; 10455 } 10456 10457 10458 /// 10459 Menu addItem(Menu item) { 10460 10461 subMenus ~= item; 10462 10463 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 10464 10465 addChild(mbItem); 10466 items ~= mbItem; 10467 10468 version(win32_widgets) { 10469 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 10470 } else version(custom_widgets) { 10471 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 10472 item.popup(mbItem); 10473 }; 10474 } else static assert(false); 10475 10476 return item; 10477 } 10478 10479 override void recomputeChildLayout() { 10480 .recomputeChildLayout!"width"(this); 10481 } 10482 10483 override int maxHeight() { return defaultLineHeight + 4; } 10484 override int minHeight() { return defaultLineHeight + 4; } 10485 } 10486 10487 10488 /** 10489 Status bars appear at the bottom of a MainWindow. 10490 They are made out of Parts, with a width and content. 10491 10492 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 10493 10494 10495 sb.parts[0].content = "Status bar text!"; 10496 */ 10497 class StatusBar : Widget { 10498 private Part[] partsArray; 10499 /// 10500 struct Parts { 10501 @disable this(); 10502 this(StatusBar owner) { this.owner = owner; } 10503 //@disable this(this); 10504 /// 10505 @property int length() { return cast(int) owner.partsArray.length; } 10506 private StatusBar owner; 10507 private this(StatusBar owner, Part[] parts) { 10508 this.owner.partsArray = parts; 10509 this.owner = owner; 10510 } 10511 /// 10512 Part opIndex(int p) { 10513 if(owner.partsArray.length == 0) 10514 this ~= new StatusBar.Part(0); 10515 return owner.partsArray[p]; 10516 } 10517 10518 /// 10519 Part opOpAssign(string op : "~" )(Part p) { 10520 assert(owner.partsArray.length < 255); 10521 p.owner = this.owner; 10522 p.idx = cast(int) owner.partsArray.length; 10523 owner.partsArray ~= p; 10524 10525 owner.queueRecomputeChildLayout(); 10526 10527 version(win32_widgets) { 10528 int[256] pos; 10529 int cpos; 10530 foreach(idx, part; owner.partsArray) { 10531 if(idx + 1 == owner.partsArray.length) 10532 pos[idx] = -1; 10533 else { 10534 cpos += part.currentlyAssignedWidth; 10535 pos[idx] = cpos; 10536 } 10537 } 10538 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 10539 } else version(custom_widgets) { 10540 owner.redraw(); 10541 } else static assert(false); 10542 10543 return p; 10544 } 10545 } 10546 10547 private Parts _parts; 10548 /// 10549 final @property Parts parts() { 10550 return _parts; 10551 } 10552 10553 /++ 10554 10555 +/ 10556 static class Part { 10557 /++ 10558 History: 10559 Added September 1, 2023 (dub v11.1) 10560 +/ 10561 enum WidthUnits { 10562 /++ 10563 Unscaled pixels as they appear on screen. 10564 10565 If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui. 10566 +/ 10567 DeviceDependentPixels, 10568 /++ 10569 Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui. 10570 +/ 10571 DeviceIndependentPixels, 10572 /++ 10573 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`). 10574 +/ 10575 ApproximateCharacters, 10576 /++ 10577 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. 10578 10579 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. 10580 +/ 10581 Proportional 10582 } 10583 private WidthUnits units; 10584 private int width; 10585 private StatusBar owner; 10586 10587 private int currentlyAssignedWidth; 10588 10589 /++ 10590 History: 10591 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. 10592 10593 It now allows you to provide your own value for [WidthUnits]. 10594 10595 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`. 10596 +/ 10597 this(int w, WidthUnits units = WidthUnits.Proportional) { 10598 this.units = units; 10599 this.width = w; 10600 } 10601 10602 /// ditto 10603 this(int w = 0) { 10604 if(w == 0) 10605 this(w, WidthUnits.Proportional); 10606 else 10607 this(w, WidthUnits.DeviceDependentPixels); 10608 } 10609 10610 private int idx; 10611 private string _content; 10612 /// 10613 @property string content() { return _content; } 10614 /// 10615 @property void content(string s) { 10616 version(win32_widgets) { 10617 _content = s; 10618 WCharzBuffer bfr = WCharzBuffer(s); 10619 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 10620 } else version(custom_widgets) { 10621 if(_content != s) { 10622 _content = s; 10623 owner.redraw(); 10624 } 10625 } else static assert(false); 10626 } 10627 } 10628 string simpleModeContent; 10629 bool inSimpleMode; 10630 10631 10632 /// 10633 this(Widget parent) { 10634 super(null); // FIXME 10635 _parts = Parts(this); 10636 tabStop = false; 10637 version(win32_widgets) { 10638 parentWindow = parent.parentWindow; 10639 createWin32Window(this, "msctls_statusbar32"w, "", 0); 10640 10641 RECT rect; 10642 GetWindowRect(hwnd, &rect); 10643 idealHeight = rect.bottom - rect.top; 10644 assert(idealHeight); 10645 } else version(custom_widgets) { 10646 } else static assert(false); 10647 } 10648 10649 override void recomputeChildLayout() { 10650 int remainingLength = this.width; 10651 10652 int proportionalSum; 10653 int proportionalCount; 10654 foreach(idx, part; this.partsArray) { 10655 with(Part.WidthUnits) 10656 final switch(part.units) { 10657 case DeviceDependentPixels: 10658 part.currentlyAssignedWidth = part.width; 10659 remainingLength -= part.currentlyAssignedWidth; 10660 break; 10661 case DeviceIndependentPixels: 10662 part.currentlyAssignedWidth = scaleWithDpi(part.width); 10663 remainingLength -= part.currentlyAssignedWidth; 10664 break; 10665 case ApproximateCharacters: 10666 auto cs = getComputedStyle(); 10667 auto font = cs.font; 10668 10669 part.currentlyAssignedWidth = font.averageWidth * this.width; 10670 remainingLength -= part.currentlyAssignedWidth; 10671 break; 10672 case Proportional: 10673 proportionalSum += part.width; 10674 proportionalCount ++; 10675 break; 10676 } 10677 } 10678 10679 foreach(part; this.partsArray) { 10680 if(part.units == Part.WidthUnits.Proportional) { 10681 auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width; 10682 if(proportion == 0) 10683 proportion = 1; 10684 10685 if(proportionalSum == 0) 10686 proportionalSum = proportionalCount; 10687 10688 part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum; 10689 } 10690 } 10691 10692 super.recomputeChildLayout(); 10693 } 10694 10695 version(win32_widgets) 10696 override protected void dpiChanged() { 10697 RECT rect; 10698 GetWindowRect(hwnd, &rect); 10699 idealHeight = rect.bottom - rect.top; 10700 assert(idealHeight); 10701 } 10702 10703 version(custom_widgets) 10704 override void paint(WidgetPainter painter) { 10705 auto cs = getComputedStyle(); 10706 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10707 int cpos = 0; 10708 foreach(idx, part; this.partsArray) { 10709 auto partWidth = part.currentlyAssignedWidth; 10710 // part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 10711 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 10712 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 10713 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 10714 10715 painter.outlineColor = cs.foregroundColor(); 10716 painter.fillColor = cs.foregroundColor(); 10717 10718 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 10719 cpos += partWidth; 10720 } 10721 } 10722 10723 10724 version(win32_widgets) { 10725 private int idealHeight; 10726 override int maxHeight() { return idealHeight; } 10727 override int minHeight() { return idealHeight; } 10728 } else version(custom_widgets) { 10729 override int maxHeight() { return defaultLineHeight + 4; } 10730 override int minHeight() { return defaultLineHeight + 4; } 10731 } else static assert(false); 10732 } 10733 10734 /// Displays an in-progress indicator without known values 10735 version(none) 10736 class IndefiniteProgressBar : Widget { 10737 version(win32_widgets) 10738 this(Widget parent) { 10739 super(parent); 10740 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 10741 tabStop = false; 10742 } 10743 override int minHeight() { return 10; } 10744 } 10745 10746 /// A progress bar with a known endpoint and completion amount 10747 class ProgressBar : Widget { 10748 /++ 10749 History: 10750 Added March 16, 2022 (dub v10.7) 10751 +/ 10752 this(int min, int max, Widget parent) { 10753 this(parent); 10754 setRange(cast(ushort) min, cast(ushort) max); // FIXME 10755 } 10756 this(Widget parent) { 10757 version(win32_widgets) { 10758 super(parent); 10759 createWin32Window(this, "msctls_progress32"w, "", 0); 10760 tabStop = false; 10761 } else version(custom_widgets) { 10762 super(parent); 10763 max = 100; 10764 step = 10; 10765 tabStop = false; 10766 } else static assert(0); 10767 } 10768 10769 version(custom_widgets) 10770 override void paint(WidgetPainter painter) { 10771 auto cs = getComputedStyle(); 10772 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10773 painter.fillColor = cs.progressBarColor; 10774 painter.drawRectangle(Point(0, 0), width * current / max, height); 10775 } 10776 10777 10778 version(custom_widgets) { 10779 int current; 10780 int max; 10781 int step; 10782 } 10783 10784 /// 10785 void advanceOneStep() { 10786 version(win32_widgets) 10787 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 10788 else version(custom_widgets) 10789 addToPosition(step); 10790 else static assert(false); 10791 } 10792 10793 /// 10794 void setStepIncrement(int increment) { 10795 version(win32_widgets) 10796 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 10797 else version(custom_widgets) 10798 step = increment; 10799 else static assert(false); 10800 } 10801 10802 /// 10803 void addToPosition(int amount) { 10804 version(win32_widgets) 10805 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 10806 else version(custom_widgets) 10807 setPosition(current + amount); 10808 else static assert(false); 10809 } 10810 10811 /// 10812 void setPosition(int pos) { 10813 version(win32_widgets) 10814 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 10815 else version(custom_widgets) { 10816 current = pos; 10817 if(current > max) 10818 current = max; 10819 redraw(); 10820 } 10821 else static assert(false); 10822 } 10823 10824 /// 10825 void setRange(ushort min, ushort max) { 10826 version(win32_widgets) 10827 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 10828 else version(custom_widgets) { 10829 this.max = max; 10830 } 10831 else static assert(false); 10832 } 10833 10834 override int minHeight() { return 10; } 10835 } 10836 10837 version(custom_widgets) 10838 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 10839 thisLabel.reserve(label.length); 10840 bool justSawAmpersand; 10841 foreach(ch; label) { 10842 if(justSawAmpersand) { 10843 justSawAmpersand = false; 10844 if(ch == '&') { 10845 goto plain; 10846 } 10847 thisAccelerator = ch; 10848 } else { 10849 if(ch == '&') { 10850 justSawAmpersand = true; 10851 continue; 10852 } 10853 plain: 10854 thisLabel ~= ch; 10855 } 10856 } 10857 } 10858 10859 /++ 10860 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. 10861 10862 10863 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 10864 10865 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10866 10867 History: 10868 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. 10869 +/ 10870 class Fieldset : Widget { 10871 // FIXME: on Windows,it doesn't draw the background on the label 10872 // on X, it doesn't fix the clipping rectangle for it 10873 version(win32_widgets) 10874 override int paddingTop() { return defaultLineHeight; } 10875 else version(custom_widgets) 10876 override int paddingTop() { return defaultLineHeight + 2; } 10877 else static assert(false); 10878 override int paddingBottom() { return 6; } 10879 override int paddingLeft() { return 6; } 10880 override int paddingRight() { return 6; } 10881 10882 override int marginLeft() { return 6; } 10883 override int marginRight() { return 6; } 10884 override int marginTop() { return 2; } 10885 override int marginBottom() { return 2; } 10886 10887 string legend; 10888 10889 version(custom_widgets) private dchar accelerator; 10890 10891 this(string legend, Widget parent) { 10892 version(win32_widgets) { 10893 super(parent); 10894 this.legend = legend; 10895 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 10896 tabStop = false; 10897 } else version(custom_widgets) { 10898 super(parent); 10899 tabStop = false; 10900 10901 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 10902 } else static assert(0); 10903 } 10904 10905 version(custom_widgets) 10906 override void paint(WidgetPainter painter) { 10907 auto dlh = defaultLineHeight; 10908 10909 painter.fillColor = Color.transparent; 10910 auto cs = getComputedStyle(); 10911 painter.pen = Pen(cs.foregroundColor, 1); 10912 painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2); 10913 10914 auto tx = painter.textSize(legend); 10915 painter.outlineColor = Color.transparent; 10916 10917 version(Windows) { 10918 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 10919 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 10920 SelectObject(painter.impl.hdc, b); 10921 } else static if(UsingSimpledisplayX11) { 10922 painter.fillColor = getComputedStyle().windowBackgroundColor; 10923 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 10924 } 10925 painter.outlineColor = cs.foregroundColor; 10926 painter.drawText(Point(8, 0), legend); 10927 } 10928 10929 override int maxHeight() { 10930 auto m = paddingTop() + paddingBottom(); 10931 foreach(child; children) { 10932 auto mh = child.maxHeight(); 10933 if(mh == int.max) 10934 return int.max; 10935 m += mh; 10936 m += child.marginBottom(); 10937 m += child.marginTop(); 10938 } 10939 m += 6; 10940 if(m < minHeight) 10941 return minHeight; 10942 return m; 10943 } 10944 10945 override int minHeight() { 10946 auto m = paddingTop() + paddingBottom(); 10947 foreach(child; children) { 10948 m += child.minHeight(); 10949 m += child.marginBottom(); 10950 m += child.marginTop(); 10951 } 10952 return m + 6; 10953 } 10954 10955 override int minWidth() { 10956 return 6 + cast(int) this.legend.length * 7; 10957 } 10958 } 10959 10960 /++ 10961 $(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") 10962 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 10963 +/ 10964 version(minigui_screenshots) 10965 @Screenshot("Fieldset") 10966 unittest { 10967 auto window = new Window(200, 100); 10968 auto set = new Fieldset("Baby will", window); 10969 auto option1 = new Radiobox("Eat", set); 10970 auto option2 = new Radiobox("Cry", set); 10971 auto option3 = new Radiobox("Sleep", set); 10972 window.loop(); 10973 } 10974 10975 /// Draws a line 10976 class HorizontalRule : Widget { 10977 mixin Margin!q{ 2 }; 10978 override int minHeight() { return 2; } 10979 override int maxHeight() { return 2; } 10980 10981 /// 10982 this(Widget parent) { 10983 super(parent); 10984 } 10985 10986 override void paint(WidgetPainter painter) { 10987 auto cs = getComputedStyle(); 10988 painter.outlineColor = cs.darkAccentColor; 10989 painter.drawLine(Point(0, 0), Point(width, 0)); 10990 painter.outlineColor = cs.lightAccentColor; 10991 painter.drawLine(Point(0, 1), Point(width, 1)); 10992 } 10993 } 10994 10995 version(minigui_screenshots) 10996 @Screenshot("HorizontalRule") 10997 /++ 10998 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 10999 11000 +/ 11001 unittest { 11002 auto window = new Window(200, 100); 11003 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 11004 new HorizontalRule(window); 11005 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 11006 window.loop(); 11007 } 11008 11009 /// ditto 11010 class VerticalRule : Widget { 11011 mixin Margin!q{ 2 }; 11012 override int minWidth() { return 2; } 11013 override int maxWidth() { return 2; } 11014 11015 /// 11016 this(Widget parent) { 11017 super(parent); 11018 } 11019 11020 override void paint(WidgetPainter painter) { 11021 auto cs = getComputedStyle(); 11022 painter.outlineColor = cs.darkAccentColor; 11023 painter.drawLine(Point(0, 0), Point(0, height)); 11024 painter.outlineColor = cs.lightAccentColor; 11025 painter.drawLine(Point(1, 0), Point(1, height)); 11026 } 11027 } 11028 11029 11030 /// 11031 class Menu : Window { 11032 void remove() { 11033 foreach(i, child; parentWindow.children) 11034 if(child is this) { 11035 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 11036 break; 11037 } 11038 parentWindow.redraw(); 11039 11040 parentWindow.releaseMouseCapture(); 11041 } 11042 11043 /// 11044 void addSeparator() { 11045 version(win32_widgets) 11046 AppendMenu(handle, MF_SEPARATOR, 0, null); 11047 else version(custom_widgets) 11048 auto hr = new HorizontalRule(this); 11049 else static assert(0); 11050 } 11051 11052 override int paddingTop() { return 4; } 11053 override int paddingBottom() { return 4; } 11054 override int paddingLeft() { return 2; } 11055 override int paddingRight() { return 2; } 11056 11057 version(win32_widgets) {} 11058 else version(custom_widgets) { 11059 SimpleWindow dropDown; 11060 Widget menuParent; 11061 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 11062 this.menuParent = parent; 11063 11064 int w = 150; 11065 int h = paddingTop + paddingBottom; 11066 if(this.children.length) { 11067 // hacking it to get the ideal height out of recomputeChildLayout 11068 this.width = w; 11069 this.height = h; 11070 this.recomputeChildLayoutEntry(); 11071 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 11072 h += paddingBottom; 11073 11074 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 11075 } 11076 11077 if(offsetY == int.min) 11078 offsetY = parent.defaultLineHeight; 11079 11080 auto coord = parent.globalCoordinates(); 11081 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 11082 this.x = 0; 11083 this.y = 0; 11084 this.width = dropDown.width; 11085 this.height = dropDown.height; 11086 this.drawableWindow = dropDown; 11087 this.recomputeChildLayoutEntry(); 11088 11089 static if(UsingSimpledisplayX11) 11090 XSync(XDisplayConnection.get, 0); 11091 11092 dropDown.visibilityChanged = (bool visible) { 11093 if(visible) { 11094 this.redraw(); 11095 dropDown.grabInput(); 11096 } else { 11097 dropDown.releaseInputGrab(); 11098 } 11099 }; 11100 11101 dropDown.show(); 11102 11103 clickListener = this.addEventListener((scope ClickEvent ev) { 11104 unpopup(); 11105 // need to unlock asap just in case other user handlers block... 11106 static if(UsingSimpledisplayX11) 11107 flushGui(); 11108 }, true /* again for asap action */); 11109 } 11110 11111 EventListener clickListener; 11112 } 11113 else static assert(false); 11114 11115 version(custom_widgets) 11116 void unpopup() { 11117 mouseLastOver = mouseLastDownOn = null; 11118 dropDown.hide(); 11119 if(!menuParent.parentWindow.win.closed) { 11120 if(auto maw = cast(MouseActivatedWidget) menuParent) { 11121 maw.setDynamicState(DynamicState.depressed, false); 11122 maw.setDynamicState(DynamicState.hover, false); 11123 maw.redraw(); 11124 } 11125 // menuParent.parentWindow.win.focus(); 11126 } 11127 clickListener.disconnect(); 11128 } 11129 11130 MenuItem[] items; 11131 11132 /// 11133 MenuItem addItem(MenuItem item) { 11134 addChild(item); 11135 items ~= item; 11136 version(win32_widgets) { 11137 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 11138 } 11139 return item; 11140 } 11141 11142 string label; 11143 11144 version(win32_widgets) { 11145 HMENU handle; 11146 /// 11147 this(string label, Widget parent) { 11148 // not actually passing the parent since it effs up the drawing 11149 super(cast(Widget) null);// parent); 11150 this.label = label; 11151 handle = CreatePopupMenu(); 11152 } 11153 } else version(custom_widgets) { 11154 /// 11155 this(string label, Widget parent) { 11156 11157 if(dropDown) { 11158 dropDown.close(); 11159 } 11160 dropDown = new SimpleWindow( 11161 150, 4, 11162 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 11163 11164 this.label = label; 11165 11166 super(dropDown); 11167 } 11168 } else static assert(false); 11169 11170 override int maxHeight() { return defaultLineHeight; } 11171 override int minHeight() { return defaultLineHeight; } 11172 11173 version(custom_widgets) 11174 override void paint(WidgetPainter painter) { 11175 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 11176 } 11177 } 11178 11179 /++ 11180 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 11181 +/ 11182 class MenuItem : MouseActivatedWidget { 11183 Menu submenu; 11184 11185 Action action; 11186 string label; 11187 11188 override int paddingLeft() { return 4; } 11189 11190 override int maxHeight() { return defaultLineHeight + 4; } 11191 override int minHeight() { return defaultLineHeight + 4; } 11192 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 11193 override int maxWidth() { 11194 if(cast(MenuBar) parent) { 11195 return minWidth(); 11196 } 11197 return int.max; 11198 } 11199 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 11200 this(string lbl, Widget parent = null) { 11201 super(parent); 11202 //label = lbl; // FIXME 11203 foreach(char ch; lbl) // FIXME 11204 if(ch != '&') // FIXME 11205 label ~= ch; // FIXME 11206 tabStop = false; // these are selected some other way 11207 } 11208 11209 /// 11210 this(Action action, Widget parent = null) { 11211 assert(action !is null); 11212 this(action.label, parent); 11213 this.action = action; 11214 tabStop = false; // these are selected some other way 11215 } 11216 11217 version(custom_widgets) 11218 override void paint(WidgetPainter painter) { 11219 auto cs = getComputedStyle(); 11220 if(dynamicState & DynamicState.depressed) 11221 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 11222 if(dynamicState & DynamicState.hover) 11223 painter.outlineColor = cs.activeMenuItemColor; 11224 else 11225 painter.outlineColor = cs.foregroundColor; 11226 painter.fillColor = Color.transparent; 11227 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11228 if(action && action.accelerator !is KeyEvent.init) { 11229 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 11230 11231 } 11232 } 11233 11234 static class Style : Widget.Style { 11235 override bool variesWithState(ulong dynamicStateFlags) { 11236 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11237 } 11238 } 11239 mixin OverrideStyle!Style; 11240 11241 override void defaultEventHandler_triggered(Event event) { 11242 if(action) 11243 foreach(handler; action.triggered) 11244 handler(); 11245 11246 if(auto pmenu = cast(Menu) this.parent) 11247 pmenu.remove(); 11248 11249 super.defaultEventHandler_triggered(event); 11250 } 11251 } 11252 11253 version(win32_widgets) 11254 /// A "mouse activiated widget" is really just an abstract variant of button. 11255 class MouseActivatedWidget : Widget { 11256 @property bool isChecked() { 11257 assert(hwnd); 11258 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 11259 11260 } 11261 @property void isChecked(bool state) { 11262 assert(hwnd); 11263 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 11264 11265 } 11266 11267 override void handleWmCommand(ushort cmd, ushort id) { 11268 if(cmd == 0) { 11269 auto event = new Event(EventType.triggered, this); 11270 event.dispatch(); 11271 } 11272 } 11273 11274 this(Widget parent) { 11275 super(parent); 11276 } 11277 } 11278 else version(custom_widgets) 11279 /// ditto 11280 class MouseActivatedWidget : Widget { 11281 @property bool isChecked() { return isChecked_; } 11282 @property bool isChecked(bool b) { isChecked_ = b; this.redraw(); return isChecked_;} 11283 11284 private bool isChecked_; 11285 11286 this(Widget parent) { 11287 super(parent); 11288 11289 addEventListener((MouseDownEvent ev) { 11290 if(ev.button == MouseButton.left) { 11291 setDynamicState(DynamicState.depressed, true); 11292 setDynamicState(DynamicState.hover, true); 11293 redraw(); 11294 } 11295 }); 11296 11297 addEventListener((MouseUpEvent ev) { 11298 if(ev.button == MouseButton.left) { 11299 setDynamicState(DynamicState.depressed, false); 11300 setDynamicState(DynamicState.hover, false); 11301 redraw(); 11302 } 11303 }); 11304 11305 addEventListener((MouseMoveEvent mme) { 11306 if(!(mme.state & ModifierState.leftButtonDown)) { 11307 if(dynamicState_ & DynamicState.depressed) { 11308 setDynamicState(DynamicState.depressed, false); 11309 redraw(); 11310 } 11311 } 11312 }); 11313 } 11314 11315 override void defaultEventHandler_focus(Event ev) { 11316 super.defaultEventHandler_focus(ev); 11317 this.redraw(); 11318 } 11319 override void defaultEventHandler_blur(Event ev) { 11320 super.defaultEventHandler_blur(ev); 11321 setDynamicState(DynamicState.depressed, false); 11322 this.redraw(); 11323 } 11324 override void defaultEventHandler_keydown(KeyDownEvent ev) { 11325 super.defaultEventHandler_keydown(ev); 11326 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 11327 setDynamicState(DynamicState.depressed, true); 11328 setDynamicState(DynamicState.hover, true); 11329 this.redraw(); 11330 } 11331 } 11332 override void defaultEventHandler_keyup(KeyUpEvent ev) { 11333 super.defaultEventHandler_keyup(ev); 11334 if(!(dynamicState & DynamicState.depressed)) 11335 return; 11336 setDynamicState(DynamicState.depressed, false); 11337 setDynamicState(DynamicState.hover, false); 11338 this.redraw(); 11339 11340 auto event = new Event(EventType.triggered, this); 11341 event.sendDirectly(); 11342 } 11343 override void defaultEventHandler_click(ClickEvent ev) { 11344 super.defaultEventHandler_click(ev); 11345 if(ev.button == MouseButton.left) { 11346 auto event = new Event(EventType.triggered, this); 11347 event.sendDirectly(); 11348 } 11349 } 11350 11351 } 11352 else static assert(false); 11353 11354 /* 11355 /++ 11356 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 11357 11358 Basically the same as a checkbox. 11359 +/ 11360 class OnOffSwitch : MouseActivatedWidget { 11361 11362 } 11363 */ 11364 11365 /++ 11366 History: 11367 Added June 15, 2021 (dub v10.1) 11368 +/ 11369 struct ImageLabel { 11370 /++ 11371 Defines a label+image combo used by some widgets. 11372 11373 If you provide just a text label, that is all the widget will try to 11374 display. Or just an image will display just that. If you provide both, 11375 it may display both text and image side by side or display the image 11376 and offer text on an input event depending on the widget. 11377 11378 History: 11379 The `alignment` parameter was added on September 27, 2021 11380 +/ 11381 this(string label, TextAlignment alignment = TextAlignment.Center) { 11382 this.label = label; 11383 this.displayFlags = DisplayFlags.displayText; 11384 this.alignment = alignment; 11385 } 11386 11387 /// ditto 11388 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11389 this.label = label; 11390 this.image = image; 11391 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11392 this.alignment = alignment; 11393 } 11394 11395 /// ditto 11396 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11397 this.image = image; 11398 this.displayFlags = DisplayFlags.displayImage; 11399 this.alignment = alignment; 11400 } 11401 11402 /// ditto 11403 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 11404 this.label = label; 11405 this.image = image; 11406 this.alignment = alignment; 11407 this.displayFlags = displayFlags; 11408 } 11409 11410 string label; 11411 MemoryImage image; 11412 11413 enum DisplayFlags { 11414 displayText = 1 << 0, 11415 displayImage = 1 << 1, 11416 } 11417 11418 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11419 11420 TextAlignment alignment; 11421 } 11422 11423 /++ 11424 A basic checked or not checked box with an attached label. 11425 11426 11427 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 11428 11429 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11430 11431 History: 11432 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. 11433 +/ 11434 class Checkbox : MouseActivatedWidget { 11435 version(win32_widgets) { 11436 override int maxHeight() { return scaleWithDpi(16); } 11437 override int minHeight() { return scaleWithDpi(16); } 11438 } else version(custom_widgets) { 11439 private enum buttonSize = 16; 11440 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 11441 override int minHeight() { return maxHeight(); } 11442 } else static assert(0); 11443 11444 override int marginLeft() { return 4; } 11445 11446 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 11447 11448 /++ 11449 Just an alias because I keep typing checked out of web habit. 11450 11451 History: 11452 Added May 31, 2021 11453 +/ 11454 alias checked = isChecked; 11455 11456 private string label; 11457 private dchar accelerator; 11458 11459 /++ 11460 +/ 11461 this(string label, Widget parent) { 11462 this(ImageLabel(label), Appearance.checkbox, parent); 11463 } 11464 11465 /// ditto 11466 this(string label, Appearance appearance, Widget parent) { 11467 this(ImageLabel(label), appearance, parent); 11468 } 11469 11470 /++ 11471 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 11472 11473 History: 11474 Added June 29, 2021 (dub v10.2) 11475 +/ 11476 enum Appearance { 11477 checkbox, /// a normal checkbox 11478 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 11479 //sliderswitch, 11480 } 11481 private Appearance appearance; 11482 11483 /// ditto 11484 private this(ImageLabel label, Appearance appearance, Widget parent) { 11485 super(parent); 11486 version(win32_widgets) { 11487 this.label = label.label; 11488 11489 uint extraStyle; 11490 final switch(appearance) { 11491 case Appearance.checkbox: 11492 break; 11493 case Appearance.pushbutton: 11494 extraStyle |= BS_PUSHLIKE; 11495 break; 11496 } 11497 11498 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 11499 } else version(custom_widgets) { 11500 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 11501 } else static assert(0); 11502 } 11503 11504 version(custom_widgets) 11505 override void paint(WidgetPainter painter) { 11506 auto cs = getComputedStyle(); 11507 if(isFocused()) { 11508 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11509 painter.fillColor = cs.windowBackgroundColor; 11510 painter.drawRectangle(Point(0, 0), width, height); 11511 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11512 } else { 11513 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 11514 painter.fillColor = cs.windowBackgroundColor; 11515 painter.drawRectangle(Point(0, 0), width, height); 11516 } 11517 11518 11519 painter.outlineColor = Color.black; 11520 painter.fillColor = Color.white; 11521 enum rectOffset = 2; 11522 painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset)); 11523 11524 if(isChecked) { 11525 auto size = scaleWithDpi(2); 11526 painter.pen = Pen(Color.black, size); 11527 // I'm using height so the checkbox is square 11528 enum padding = 3; 11529 painter.drawLine( 11530 scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)), 11531 scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2) 11532 ); 11533 painter.drawLine( 11534 scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0), 11535 scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 - size % 2) 11536 ); 11537 11538 painter.pen = Pen(Color.black, 1); 11539 } 11540 11541 if(label !is null) { 11542 painter.outlineColor = cs.foregroundColor(); 11543 painter.fillColor = cs.foregroundColor(); 11544 11545 // i want the centerline of the text to be aligned with the centerline of the checkbox 11546 /+ 11547 auto font = cs.font(); 11548 auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2; 11549 painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label); 11550 +/ 11551 painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter); 11552 } 11553 } 11554 11555 override void defaultEventHandler_triggered(Event ev) { 11556 isChecked = !isChecked; 11557 11558 this.emit!(ChangeEvent!bool)(&isChecked); 11559 11560 redraw(); 11561 } 11562 11563 /// Emits a change event with the checked state 11564 mixin Emits!(ChangeEvent!bool); 11565 } 11566 11567 /// Adds empty space to a layout. 11568 class VerticalSpacer : Widget { 11569 /// 11570 this(Widget parent) { 11571 super(parent); 11572 } 11573 } 11574 11575 /// ditto 11576 class HorizontalSpacer : Widget { 11577 /// 11578 this(Widget parent) { 11579 super(parent); 11580 this.tabStop = false; 11581 } 11582 } 11583 11584 11585 /++ 11586 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 11587 11588 11589 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 11590 11591 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11592 11593 History: 11594 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. 11595 +/ 11596 class Radiobox : MouseActivatedWidget { 11597 11598 version(win32_widgets) { 11599 override int maxHeight() { return scaleWithDpi(16); } 11600 override int minHeight() { return scaleWithDpi(16); } 11601 } else version(custom_widgets) { 11602 private enum buttonSize = 16; 11603 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 11604 override int minHeight() { return maxHeight(); } 11605 } else static assert(0); 11606 11607 override int marginLeft() { return 4; } 11608 11609 // FIXME: make a label getter 11610 private string label; 11611 private dchar accelerator; 11612 11613 /++ 11614 11615 +/ 11616 this(string label, Widget parent) { 11617 super(parent); 11618 version(win32_widgets) { 11619 this.label = label; 11620 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 11621 } else version(custom_widgets) { 11622 label.extractWindowsStyleLabel(this.label, this.accelerator); 11623 height = 16; 11624 width = height + 4 + cast(int) label.length * 16; 11625 } 11626 } 11627 11628 version(custom_widgets) 11629 override void paint(WidgetPainter painter) { 11630 auto cs = getComputedStyle(); 11631 11632 if(isFocused) { 11633 painter.fillColor = cs.windowBackgroundColor; 11634 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11635 } else { 11636 painter.fillColor = cs.windowBackgroundColor; 11637 painter.outlineColor = cs.windowBackgroundColor; 11638 } 11639 painter.drawRectangle(Point(0, 0), width, height); 11640 11641 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11642 11643 painter.outlineColor = Color.black; 11644 painter.fillColor = Color.white; 11645 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 11646 if(isChecked) { 11647 painter.outlineColor = Color.black; 11648 painter.fillColor = Color.black; 11649 // I'm using height so the checkbox is square 11650 auto size = scaleWithDpi(2); 11651 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2)); 11652 } 11653 11654 painter.outlineColor = cs.foregroundColor(); 11655 painter.fillColor = cs.foregroundColor(); 11656 11657 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11658 } 11659 11660 11661 override void defaultEventHandler_triggered(Event ev) { 11662 isChecked = true; 11663 11664 if(this.parent) { 11665 foreach(child; this.parent.children) { 11666 if(child is this) continue; 11667 if(auto rb = cast(Radiobox) child) { 11668 rb.isChecked = false; 11669 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 11670 rb.redraw(); 11671 } 11672 } 11673 } 11674 11675 this.emit!(ChangeEvent!bool)(&this.isChecked); 11676 11677 redraw(); 11678 } 11679 11680 /// 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. 11681 mixin Emits!(ChangeEvent!bool); 11682 } 11683 11684 11685 /++ 11686 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 11687 11688 11689 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 11690 11691 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11692 11693 History: 11694 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. 11695 +/ 11696 class Button : MouseActivatedWidget { 11697 override int heightStretchiness() { return 3; } 11698 override int widthStretchiness() { return 3; } 11699 11700 /++ 11701 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. 11702 11703 History: 11704 Added July 2, 2021 11705 +/ 11706 public bool triggersOnMultiClick; 11707 11708 private string label_; 11709 private TextAlignment alignment; 11710 private dchar accelerator; 11711 11712 /// 11713 string label() { return label_; } 11714 /// 11715 void label(string l) { 11716 label_ = l; 11717 version(win32_widgets) { 11718 WCharzBuffer bfr = WCharzBuffer(l); 11719 SetWindowTextW(hwnd, bfr.ptr); 11720 } else version(custom_widgets) { 11721 redraw(); 11722 } 11723 } 11724 11725 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 11726 super.defaultEventHandler_dblclick(ev); 11727 if(triggersOnMultiClick) { 11728 if(ev.button == MouseButton.left) { 11729 auto event = new Event(EventType.triggered, this); 11730 event.sendDirectly(); 11731 } 11732 } 11733 } 11734 11735 private Sprite sprite; 11736 private int displayFlags; 11737 11738 /++ 11739 Creates a push button with the given label, which may be an image or some text. 11740 11741 Bugs: 11742 If the image is bigger than the button, it may not be displayed in the right position on Linux. 11743 11744 History: 11745 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 11746 11747 The button with label and image will respect requests to show both on Windows as 11748 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 11749 +/ 11750 this(ImageLabel label, Widget parent) { 11751 version(win32_widgets) { 11752 // FIXME: use ideal button size instead 11753 width = 50; 11754 height = 30; 11755 super(parent); 11756 11757 // BS_BITMAP is set when we want image only, so checking for exactly that combination 11758 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 11759 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 11760 11761 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 11762 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 11763 11764 if(label.image) { 11765 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 11766 11767 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 11768 } 11769 11770 this.label = label.label; 11771 } else version(custom_widgets) { 11772 width = 50; 11773 height = 30; 11774 super(parent); 11775 11776 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 11777 11778 if(label.image) { 11779 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 11780 this.displayFlags = label.displayFlags; 11781 } 11782 11783 this.alignment = label.alignment; 11784 } 11785 } 11786 11787 /// 11788 this(string label, Widget parent) { 11789 this(ImageLabel(label), parent); 11790 } 11791 11792 override int minHeight() { return defaultLineHeight + 4; } 11793 11794 static class Style : Widget.Style { 11795 override WidgetBackground background() { 11796 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 11797 11798 auto pressed = DynamicState.depressed | DynamicState.hover; 11799 if((widget.dynamicState & pressed) == pressed) { 11800 return WidgetBackground(cs.depressedButtonColor()); 11801 } else if(widget.dynamicState & DynamicState.hover) { 11802 return WidgetBackground(cs.hoveringColor()); 11803 } else { 11804 return WidgetBackground(cs.buttonColor()); 11805 } 11806 } 11807 11808 override FrameStyle borderStyle() { 11809 auto pressed = DynamicState.depressed | DynamicState.hover; 11810 if((widget.dynamicState & pressed) == pressed) { 11811 return FrameStyle.sunk; 11812 } else { 11813 return FrameStyle.risen; 11814 } 11815 11816 } 11817 11818 override bool variesWithState(ulong dynamicStateFlags) { 11819 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11820 } 11821 } 11822 mixin OverrideStyle!Style; 11823 11824 version(custom_widgets) 11825 override void paint(WidgetPainter painter) { 11826 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 11827 if(sprite) { 11828 sprite.drawAt( 11829 painter, 11830 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 11831 Point(0, 0) 11832 ); 11833 } else { 11834 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 11835 } 11836 return bounds; 11837 }); 11838 } 11839 11840 override int flexBasisWidth() { 11841 version(win32_widgets) { 11842 SIZE size; 11843 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11844 if(size.cx == 0) 11845 goto fallback; 11846 return size.cx + scaleWithDpi(16); 11847 } 11848 fallback: 11849 return scaleWithDpi(cast(int) label.length * 8 + 16); 11850 } 11851 11852 override int flexBasisHeight() { 11853 version(win32_widgets) { 11854 SIZE size; 11855 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11856 if(size.cy == 0) 11857 goto fallback; 11858 return size.cy + scaleWithDpi(6); 11859 } 11860 fallback: 11861 return defaultLineHeight + 4; 11862 } 11863 } 11864 11865 /++ 11866 A button with a consistent size, suitable for user commands like OK and CANCEL. 11867 +/ 11868 class CommandButton : Button { 11869 this(string label, Widget parent) { 11870 super(label, parent); 11871 } 11872 11873 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 11874 11875 override int maxHeight() { 11876 return defaultLineHeight + 4; 11877 } 11878 11879 override int maxWidth() { 11880 return defaultLineHeight * 4; 11881 } 11882 11883 override int marginLeft() { return 12; } 11884 override int marginRight() { return 12; } 11885 override int marginTop() { return 12; } 11886 override int marginBottom() { return 12; } 11887 } 11888 11889 /// 11890 enum ArrowDirection { 11891 left, /// 11892 right, /// 11893 up, /// 11894 down /// 11895 } 11896 11897 /// 11898 version(custom_widgets) 11899 class ArrowButton : Button { 11900 /// 11901 this(ArrowDirection direction, Widget parent) { 11902 super("", parent); 11903 this.direction = direction; 11904 triggersOnMultiClick = true; 11905 } 11906 11907 private ArrowDirection direction; 11908 11909 override int minHeight() { return scaleWithDpi(16); } 11910 override int maxHeight() { return scaleWithDpi(16); } 11911 override int minWidth() { return scaleWithDpi(16); } 11912 override int maxWidth() { return scaleWithDpi(16); } 11913 11914 override void paint(WidgetPainter painter) { 11915 super.paint(painter); 11916 11917 auto cs = getComputedStyle(); 11918 11919 painter.outlineColor = cs.foregroundColor; 11920 painter.fillColor = cs.foregroundColor; 11921 11922 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 11923 11924 final switch(direction) { 11925 case ArrowDirection.up: 11926 painter.drawPolygon( 11927 scaleWithDpi(Point(2, 10) + offset), 11928 scaleWithDpi(Point(7, 5) + offset), 11929 scaleWithDpi(Point(12, 10) + offset), 11930 scaleWithDpi(Point(2, 10) + offset) 11931 ); 11932 break; 11933 case ArrowDirection.down: 11934 painter.drawPolygon( 11935 scaleWithDpi(Point(2, 6) + offset), 11936 scaleWithDpi(Point(7, 11) + offset), 11937 scaleWithDpi(Point(12, 6) + offset), 11938 scaleWithDpi(Point(2, 6) + offset) 11939 ); 11940 break; 11941 case ArrowDirection.left: 11942 painter.drawPolygon( 11943 scaleWithDpi(Point(10, 2) + offset), 11944 scaleWithDpi(Point(5, 7) + offset), 11945 scaleWithDpi(Point(10, 12) + offset), 11946 scaleWithDpi(Point(10, 2) + offset) 11947 ); 11948 break; 11949 case ArrowDirection.right: 11950 painter.drawPolygon( 11951 scaleWithDpi(Point(6, 2) + offset), 11952 scaleWithDpi(Point(11, 7) + offset), 11953 scaleWithDpi(Point(6, 12) + offset), 11954 scaleWithDpi(Point(6, 2) + offset) 11955 ); 11956 break; 11957 } 11958 } 11959 } 11960 11961 private 11962 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 11963 int x, y; 11964 Widget par = c; 11965 while(par) { 11966 x += par.x; 11967 y += par.y; 11968 par = par.parent; 11969 } 11970 return [x, y]; 11971 } 11972 11973 version(win32_widgets) 11974 private 11975 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 11976 // MapWindowPoints? 11977 int x, y; 11978 Widget par = c; 11979 while(par) { 11980 x += par.x; 11981 y += par.y; 11982 par = par.parent; 11983 if(par !is null && par.useNativeDrawing()) 11984 break; 11985 } 11986 return [x, y]; 11987 } 11988 11989 /// 11990 class ImageBox : Widget { 11991 private MemoryImage image_; 11992 11993 override int widthStretchiness() { return 1; } 11994 override int heightStretchiness() { return 1; } 11995 override int widthShrinkiness() { return 1; } 11996 override int heightShrinkiness() { return 1; } 11997 11998 override int flexBasisHeight() { 11999 return image_.height; 12000 } 12001 12002 override int flexBasisWidth() { 12003 return image_.width; 12004 } 12005 12006 /// 12007 public void setImage(MemoryImage image){ 12008 this.image_ = image; 12009 if(this.parentWindow && this.parentWindow.win) { 12010 if(sprite) 12011 sprite.dispose(); 12012 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 12013 } 12014 redraw(); 12015 } 12016 12017 /// How to fit the image in the box if they aren't an exact match in size? 12018 enum HowToFit { 12019 center, /// centers the image, cropping around all the edges as needed 12020 crop, /// always draws the image in the upper left, cropping the lower right if needed 12021 // stretch, /// not implemented 12022 } 12023 12024 private Sprite sprite; 12025 private HowToFit howToFit_; 12026 12027 private Color backgroundColor_; 12028 12029 /// 12030 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 12031 this.image_ = image; 12032 this.tabStop = false; 12033 this.howToFit_ = howToFit; 12034 this.backgroundColor_ = backgroundColor; 12035 super(parent); 12036 updateSprite(); 12037 } 12038 12039 /// ditto 12040 this(MemoryImage image, HowToFit howToFit, Widget parent) { 12041 this(image, howToFit, Color.transparent, parent); 12042 } 12043 12044 private void updateSprite() { 12045 if(sprite is null && this.parentWindow && this.parentWindow.win) { 12046 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 12047 } 12048 } 12049 12050 override void paint(WidgetPainter painter) { 12051 updateSprite(); 12052 if(backgroundColor_.a) { 12053 painter.fillColor = backgroundColor_; 12054 painter.drawRectangle(Point(0, 0), width, height); 12055 } 12056 if(howToFit_ == HowToFit.crop) 12057 sprite.drawAt(painter, Point(0, 0)); 12058 else if(howToFit_ == HowToFit.center) { 12059 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 12060 } 12061 } 12062 } 12063 12064 /// 12065 class TextLabel : Widget { 12066 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; } 12067 override int maxHeight() { return minHeight; } 12068 override int minWidth() { return 32; } 12069 12070 override int flexBasisHeight() { return minHeight(); } 12071 override int flexBasisWidth() { return defaultTextWidth(label); } 12072 12073 string label_; 12074 12075 /++ 12076 Indicates which other control this label is here for. Similar to HTML `for` attribute. 12077 12078 In practice this means a click on the label will focus the `labelFor`. In future versions 12079 it will also set screen reader hints but that is not yet implemented. 12080 12081 History: 12082 Added October 3, 2021 (dub v10.4) 12083 +/ 12084 Widget labelFor; 12085 12086 /// 12087 @scriptable 12088 string label() { return label_; } 12089 12090 /// 12091 @scriptable 12092 void label(string l) { 12093 label_ = l; 12094 version(win32_widgets) { 12095 WCharzBuffer bfr = WCharzBuffer(l); 12096 SetWindowTextW(hwnd, bfr.ptr); 12097 } else version(custom_widgets) 12098 redraw(); 12099 } 12100 12101 override void defaultEventHandler_click(scope ClickEvent ce) { 12102 if(this.labelFor !is null) 12103 this.labelFor.focus(); 12104 } 12105 12106 /++ 12107 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 12108 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 12109 +/ 12110 this(string label, TextAlignment alignment, Widget parent) { 12111 this.label_ = label; 12112 this.alignment = alignment; 12113 this.tabStop = false; 12114 super(parent); 12115 12116 version(win32_widgets) 12117 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 12118 } 12119 12120 /// ditto 12121 this(string label, Widget parent) { 12122 this(label, TextAlignment.Right, parent); 12123 } 12124 12125 TextAlignment alignment; 12126 12127 version(custom_widgets) 12128 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 12129 painter.outlineColor = getComputedStyle().foregroundColor; 12130 painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment); 12131 return bounds; 12132 } 12133 12134 } 12135 12136 version(custom_widgets) 12137 private struct etc { 12138 mixin ExperimentalTextComponent; 12139 } 12140 12141 version(win32_widgets) { 12142 alias EditableTextWidgetParent = Widget; /// 12143 version=use_new_text_system; 12144 import arsd.textlayouter; 12145 } else version(custom_widgets) { 12146 version(trash_text) { 12147 alias EditableTextWidgetParent = ScrollableWidget; /// 12148 } else { 12149 alias EditableTextWidgetParent = Widget; 12150 version=use_new_text_system; 12151 import arsd.textlayouter; 12152 } 12153 } else static assert(0); 12154 12155 version(use_new_text_system) 12156 class TextDisplayHelper : Widget { 12157 protected TextLayouter l; 12158 protected ScrollMessageWidget smw; 12159 12160 private const(TextLayouter.State)*[] undoStack; 12161 private const(TextLayouter.State)*[] redoStack; 12162 12163 private string preservedPrimaryText; 12164 protected void selectionChanged() { 12165 // sdpyPrintDebugString("selectionChanged"); try throw new Exception("e"); catch(Exception e) sdpyPrintDebugString(e.toString()); 12166 static if(UsingSimpledisplayX11) 12167 with(l.selection()) { 12168 if(!isEmpty()) { 12169 //sdpyPrintDebugString("!isEmpty"); 12170 12171 getPrimarySelection(parentWindow.win, (in char[] txt) { 12172 // sdpyPrintDebugString("getPrimarySelection: " ~ getContentString() ~ " (old " ~ txt ~ ")"); 12173 // import std.stdio; writeln("txt: ", txt, " sel: ", getContentString); 12174 if(txt.length) { 12175 preservedPrimaryText = txt.idup; 12176 // writeln(preservedPrimaryText); 12177 } 12178 12179 setPrimarySelection(parentWindow.win, getContentString()); 12180 }); 12181 } 12182 } 12183 } 12184 12185 final TextLayouter layouter() { 12186 return l; 12187 } 12188 12189 bool readonly; 12190 bool caretNavigation; // scroll lock can flip this 12191 bool singleLine; 12192 bool acceptsTabInput; 12193 12194 private Menu ctx; 12195 override Menu contextMenu(int x, int y) { 12196 if(ctx is null) { 12197 ctx = new Menu("Actions", this); 12198 if(!readonly) { 12199 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 12200 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 12201 ctx.addSeparator(); 12202 } 12203 if(!readonly) 12204 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 12205 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 12206 if(!readonly) 12207 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 12208 if(!readonly) 12209 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 12210 ctx.addSeparator(); 12211 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 12212 } 12213 return ctx; 12214 } 12215 12216 override void defaultEventHandler_blur(Event ev) { 12217 super.defaultEventHandler_blur(ev); 12218 if(l.wasMutated()) { 12219 auto evt = new ChangeEvent!string(this, &this.content); 12220 evt.dispatch(); 12221 l.clearWasMutatedFlag(); 12222 } 12223 } 12224 12225 private string content() { 12226 return l.getTextString(); 12227 } 12228 12229 void undo() { 12230 if(readonly) return; 12231 if(undoStack.length) { 12232 auto state = undoStack[$-1]; 12233 undoStack = undoStack[0 .. $-1]; 12234 undoStack.assumeSafeAppend(); 12235 redoStack ~= l.saveState(); 12236 l.restoreState(state); 12237 adjustScrollbarSizes(); 12238 scrollForCaret(); 12239 redraw(); 12240 stateCheckpoint = true; 12241 } 12242 } 12243 12244 void redo() { 12245 if(readonly) return; 12246 if(redoStack.length) { 12247 doStateCheckpoint(); 12248 auto state = redoStack[$-1]; 12249 redoStack = redoStack[0 .. $-1]; 12250 redoStack.assumeSafeAppend(); 12251 l.restoreState(state); 12252 adjustScrollbarSizes(); 12253 scrollForCaret(); 12254 redraw(); 12255 stateCheckpoint = true; 12256 } 12257 } 12258 12259 void cut() { 12260 if(readonly) return; 12261 with(l.selection()) { 12262 if(!isEmpty()) { 12263 setClipboardText(parentWindow.win, getContentString()); 12264 doStateCheckpoint(); 12265 replaceContent(""); 12266 adjustScrollbarSizes(); 12267 scrollForCaret(); 12268 this.redraw(); 12269 } 12270 } 12271 12272 } 12273 12274 void copy() { 12275 with(l.selection()) { 12276 if(!isEmpty()) { 12277 setClipboardText(parentWindow.win, getContentString()); 12278 this.redraw(); 12279 } 12280 } 12281 } 12282 12283 void paste() { 12284 if(readonly) return; 12285 getClipboardText(parentWindow.win, (txt) { 12286 doStateCheckpoint(); 12287 if(singleLine) 12288 l.selection.replaceContent(txt.stripInternal()); 12289 else 12290 l.selection.replaceContent(txt); 12291 adjustScrollbarSizes(); 12292 scrollForCaret(); 12293 this.redraw(); 12294 }); 12295 } 12296 12297 void deleteContentOfSelection() { 12298 if(readonly) return; 12299 doStateCheckpoint(); 12300 l.selection.replaceContent(""); 12301 l.selection.setUserXCoordinate(); 12302 adjustScrollbarSizes(); 12303 scrollForCaret(); 12304 redraw(); 12305 } 12306 12307 void selectAll() { 12308 with(l.selection) { 12309 moveToStartOfDocument(); 12310 setAnchor(); 12311 moveToEndOfDocument(); 12312 setFocus(); 12313 12314 selectionChanged(); 12315 } 12316 redraw(); 12317 } 12318 12319 protected bool stateCheckpoint = true; 12320 12321 protected void doStateCheckpoint() { 12322 if(stateCheckpoint) { 12323 undoStack ~= l.saveState(); 12324 stateCheckpoint = false; 12325 } 12326 } 12327 12328 protected void adjustScrollbarSizes() { 12329 // FIXME: will want a content area helper function instead of doing all these subtractions myself 12330 auto borderWidth = 2; 12331 this.smw.setTotalArea(l.width, l.height); 12332 this.smw.setViewableArea( 12333 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 12334 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 12335 } 12336 12337 protected void scrollForCaret() { 12338 // writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 12339 smw.scrollIntoView(l.selection.focusBoundingBox()); 12340 } 12341 12342 // FIXME: this should be a theme changed event listener instead 12343 private BaseVisualTheme currentTheme; 12344 override void recomputeChildLayout() { 12345 if(currentTheme is null) 12346 currentTheme = WidgetPainter.visualTheme; 12347 if(WidgetPainter.visualTheme !is currentTheme) { 12348 currentTheme = WidgetPainter.visualTheme; 12349 auto ds = this.l.defaultStyle; 12350 if(auto ms = cast(MyTextStyle) ds) { 12351 auto cs = getComputedStyle(); 12352 auto font = cs.font(); 12353 if(font !is null) 12354 ms.font_ = font; 12355 else { 12356 auto osc = new OperatingSystemFont(); 12357 osc.loadDefault; 12358 ms.font_ = osc; 12359 } 12360 } 12361 } 12362 super.recomputeChildLayout(); 12363 } 12364 12365 private Point adjustForSingleLine(Point p) { 12366 if(singleLine) 12367 return Point(p.x, this.height / 2); 12368 else 12369 return p; 12370 } 12371 12372 private bool wordWrapEnabled_; 12373 12374 this(TextLayouter l, ScrollMessageWidget parent) { 12375 this.smw = parent; 12376 12377 smw.addDefaultWheelListeners(16, 16, 8); 12378 smw.movementPerButtonClick(16, 16); 12379 12380 this.defaultPadding = Rectangle(2, 2, 2, 2); 12381 12382 this.l = l; 12383 super(parent); 12384 12385 smw.addEventListener((scope ScrollEvent se) { 12386 this.redraw(); 12387 }); 12388 12389 bool mouseDown; 12390 bool mouseActuallyMoved; 12391 12392 this.addEventListener((scope ResizeEvent re) { 12393 // FIXME: I should add a method to give this client area width thing 12394 if(wordWrapEnabled_) 12395 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 12396 12397 adjustScrollbarSizes(); 12398 scrollForCaret(); 12399 12400 this.redraw(); 12401 }); 12402 12403 this.addEventListener((scope KeyDownEvent kde) { 12404 switch(kde.key) { 12405 case Key.Up, Key.Down, Key.Left, Key.Right: 12406 case Key.Home, Key.End: 12407 stateCheckpoint = true; 12408 bool setPosition = false; 12409 switch(kde.key) { 12410 case Key.Up: l.selection.moveUp(); break; 12411 case Key.Down: l.selection.moveDown(); break; 12412 case Key.Left: l.selection.moveLeft(); setPosition = true; break; 12413 case Key.Right: l.selection.moveRight(); setPosition = true; break; 12414 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 12415 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 12416 default: assert(0); 12417 } 12418 12419 if(kde.shiftKey) 12420 l.selection.setFocus(); 12421 else 12422 l.selection.setAnchor(); 12423 12424 selectionChanged(); 12425 12426 if(setPosition) 12427 l.selection.setUserXCoordinate(); 12428 scrollForCaret(); 12429 redraw(); 12430 break; 12431 case Key.PageUp, Key.PageDown: 12432 // FIXME 12433 scrollForCaret(); 12434 break; 12435 case Key.Delete: 12436 if(l.selection.isEmpty()) { 12437 l.selection.setAnchor(); 12438 l.selection.moveRight(); 12439 l.selection.setFocus(); 12440 } 12441 deleteContentOfSelection(); 12442 adjustScrollbarSizes(); 12443 scrollForCaret(); 12444 break; 12445 case Key.Insert: 12446 break; 12447 case Key.A: 12448 if(kde.ctrlKey) 12449 selectAll(); 12450 break; 12451 case Key.F: 12452 // find 12453 break; 12454 case Key.Z: 12455 if(kde.ctrlKey) 12456 undo(); 12457 break; 12458 case Key.R: 12459 if(kde.ctrlKey) 12460 redo(); 12461 break; 12462 case Key.X: 12463 if(kde.ctrlKey) 12464 cut(); 12465 break; 12466 case Key.C: 12467 if(kde.ctrlKey) 12468 copy(); 12469 break; 12470 case Key.V: 12471 if(kde.ctrlKey) 12472 paste(); 12473 break; 12474 case Key.F1: 12475 with(l.selection()) { 12476 moveToStartOfLine(); 12477 setAnchor(); 12478 moveToEndOfLine(); 12479 moveToIncludeAdjacentEndOfLineMarker(); 12480 setFocus(); 12481 replaceContent(""); 12482 } 12483 12484 redraw(); 12485 break; 12486 /* 12487 case Key.F2: 12488 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 12489 //(cast(MyTextStyle) old).font, 12490 font2, 12491 Color.red))); 12492 redraw(); 12493 break; 12494 */ 12495 case Key.Tab: 12496 // we process the char event, so don't want to change focus on it 12497 if(acceptsTabInput) 12498 kde.preventDefault(); 12499 break; 12500 default: 12501 } 12502 }); 12503 12504 Point downAt; 12505 12506 static if(UsingSimpledisplayX11) 12507 this.addEventListener((scope ClickEvent ce) { 12508 if(ce.button == MouseButton.middle) { 12509 parentWindow.win.getPrimarySelection((txt) { 12510 doStateCheckpoint(); 12511 12512 // import arsd.core; writeln(txt);writeln(l.selection.getContentString);writeln(preservedPrimaryText); 12513 12514 if(txt == l.selection.getContentString && preservedPrimaryText.length) 12515 l.selection.replaceContent(preservedPrimaryText); 12516 else 12517 l.selection.replaceContent(txt); 12518 redraw(); 12519 }); 12520 } 12521 }); 12522 12523 this.addEventListener((scope DoubleClickEvent dce) { 12524 if(dce.button == MouseButton.left) { 12525 with(l.selection()) { 12526 scope dg = delegate const(char)[] (scope return const(char)[] ch) { 12527 if(ch == " " || ch == "\t" || ch == "\n" || ch == "\r") 12528 return ch; 12529 return null; 12530 }; 12531 find(dg, 1, true).moveToEnd.setAnchor; 12532 find(dg, 1, false).moveTo.setFocus; 12533 selectionChanged(); 12534 redraw(); 12535 } 12536 } 12537 }); 12538 12539 this.addEventListener((scope MouseDownEvent ce) { 12540 if(ce.button == MouseButton.left) { 12541 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12542 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 12543 l.selection.setAnchor(); 12544 mouseDown = true; 12545 mouseActuallyMoved = false; 12546 parentWindow.captureMouse(this); 12547 this.redraw(); 12548 } else if(ce.button == MouseButton.right) { 12549 this.showContextMenu(ce.clientX, ce.clientY); 12550 } 12551 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12552 }); 12553 12554 Timer autoscrollTimer; 12555 int autoscrollDirection; 12556 int autoscrollAmount; 12557 12558 void autoscroll() { 12559 switch(autoscrollDirection) { 12560 case 0: smw.scrollUp(autoscrollAmount); break; 12561 case 1: smw.scrollDown(autoscrollAmount); break; 12562 case 2: smw.scrollLeft(autoscrollAmount); break; 12563 case 3: smw.scrollRight(autoscrollAmount); break; 12564 default: assert(0); 12565 } 12566 12567 this.redraw(); 12568 } 12569 12570 void setAutoscrollTimer(int direction, int amount) { 12571 if(autoscrollTimer is null) { 12572 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 12573 } 12574 12575 autoscrollDirection = direction; 12576 autoscrollAmount = amount; 12577 } 12578 12579 void stopAutoscrollTimer() { 12580 if(autoscrollTimer !is null) { 12581 autoscrollTimer.dispose(); 12582 autoscrollTimer = null; 12583 } 12584 autoscrollAmount = 0; 12585 autoscrollDirection = 0; 12586 } 12587 12588 this.addEventListener((scope MouseMoveEvent ce) { 12589 if(mouseDown) { 12590 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12591 12592 // FIXME: when scrolling i actually do want a timer. 12593 // i also want a zone near the sides of the window where i can auto scroll 12594 12595 auto scrollMultiplier = scaleWithDpi(16); 12596 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 12597 12598 if(!singleLine && movedTo.y < 4) { 12599 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 12600 } else 12601 if(!singleLine && (movedTo.y + 6) > this.height) { 12602 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 12603 } else 12604 if(movedTo.x < 4) { 12605 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 12606 } else 12607 if((movedTo.x + 6) > this.width) { 12608 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 12609 } else 12610 stopAutoscrollTimer(); 12611 12612 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 12613 l.selection.setFocus(); 12614 mouseActuallyMoved = true; 12615 this.redraw(); 12616 } 12617 }); 12618 12619 this.addEventListener((scope MouseUpEvent ce) { 12620 // FIXME: assert primary selection 12621 if(mouseDown && ce.button == MouseButton.left) { 12622 stateCheckpoint = true; 12623 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 12624 //l.selection.setFocus(); 12625 mouseDown = false; 12626 parentWindow.releaseMouseCapture(); 12627 stopAutoscrollTimer(); 12628 this.redraw(); 12629 12630 if(mouseActuallyMoved) 12631 selectionChanged(); 12632 } 12633 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12634 }); 12635 12636 this.addEventListener((scope CharEvent ce) { 12637 if(readonly) 12638 return; 12639 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 12640 return; // skip the ctrl+x characters we don't care about as plain text 12641 12642 if(singleLine && ce.character == '\n') 12643 return; 12644 if(!acceptsTabInput && ce.character == '\t') 12645 return; 12646 12647 doStateCheckpoint(); 12648 12649 char[4] buffer; 12650 import arsd.core; 12651 auto stride = encodeUtf8(buffer, ce.character); 12652 l.selection.replaceContent(buffer[0 .. stride]); 12653 l.selection.setUserXCoordinate(); 12654 adjustScrollbarSizes(); 12655 scrollForCaret(); 12656 redraw(); 12657 }); 12658 } 12659 12660 // we want to delegate all the Widget.Style stuff up to the other class that the user can see 12661 override void useStyleProperties(scope void delegate(scope .Widget.Style props) dg) { 12662 // this should be the upper container - first parent is a ScrollMessageWidget content area container, then ScrollMessageWidget itself, next parent is finally the EditableTextWidgetParent 12663 if(parent && parent.parent && parent.parent.parent) 12664 parent.parent.parent.useStyleProperties(dg); 12665 else 12666 super.useStyleProperties(dg); 12667 } 12668 12669 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; } 12670 override int maxHeight() { 12671 if(singleLine) 12672 return minHeight; 12673 else 12674 return super.maxHeight(); 12675 } 12676 12677 void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 12678 painter.drawText(upperLeft, text); 12679 } 12680 12681 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 12682 //painter.setFont(font); 12683 12684 auto cs = getComputedStyle(); 12685 auto defaultColor = cs.foregroundColor; 12686 12687 auto old = painter.setClipRectangle(bounds); 12688 scope(exit) painter.setClipRectangle(old); 12689 12690 l.getDrawableText(delegate bool(txt, style, info, carets...) { 12691 //writeln("Segment: ", txt); 12692 assert(style !is null); 12693 12694 auto myStyle = cast(MyTextStyle) style; 12695 assert(myStyle !is null); 12696 12697 painter.setFont(myStyle.font); 12698 // defaultColor = myStyle.color; // FIXME: so wrong 12699 12700 if(info.selections && info.boundingBox.width > 0) { 12701 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 12702 painter.fillColor = color; 12703 painter.outlineColor = color; 12704 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 12705 painter.outlineColor = cs.selectionForegroundColor; 12706 //painter.fillColor = Color.white; 12707 } else { 12708 painter.outlineColor = defaultColor; 12709 } 12710 12711 if(this.isFocused) 12712 foreach(idx, caret; carets) { 12713 if(idx == 0) 12714 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 12715 painter.drawLine( 12716 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 12717 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 12718 ); 12719 } 12720 12721 if(txt.stripInternal.length) { 12722 drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 12723 } 12724 12725 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) { 12726 return false; 12727 } else { 12728 return true; 12729 } 12730 }, Rectangle(smw.position(), bounds.size)); 12731 12732 /+ 12733 int place = 0; 12734 int y = 75; 12735 foreach(width; widths) { 12736 painter.fillColor = Color.red; 12737 painter.drawRectangle(Point(place, y), Size(width, 75)); 12738 //y += 15; 12739 place += width; 12740 } 12741 +/ 12742 12743 return bounds; 12744 } 12745 12746 static class MyTextStyle : TextStyle { 12747 OperatingSystemFont font_; 12748 this(OperatingSystemFont font, bool passwordMode = false) { 12749 this.font_ = font; 12750 } 12751 12752 override OperatingSystemFont font() { 12753 return font_; 12754 } 12755 } 12756 } 12757 12758 /+ 12759 version(use_new_text_system) 12760 class TextWidget : Widget { 12761 TextLayouter l; 12762 ScrollMessageWidget smw; 12763 TextDisplayHelper helper; 12764 this(TextLayouter l, Widget parent) { 12765 this.l = l; 12766 super(parent); 12767 12768 smw = new ScrollMessageWidget(this); 12769 //smw.horizontalScrollBar.hide; 12770 //smw.verticalScrollBar.hide; 12771 smw.addDefaultWheelListeners(16, 16, 8); 12772 smw.movementPerButtonClick(16, 16); 12773 helper = new TextDisplayHelper(l, smw); 12774 12775 // no need to do this here since there's gonna be a resize 12776 // event immediately before any drawing 12777 // smw.setTotalArea(l.width, l.height); 12778 smw.setViewableArea( 12779 this.width - this.paddingLeft - this.paddingRight, 12780 this.height - this.paddingTop - this.paddingBottom); 12781 12782 /+ 12783 writeln(l.width, "x", l.height); 12784 +/ 12785 } 12786 } 12787 +/ 12788 12789 12790 12791 12792 /+ 12793 This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too 12794 +/ 12795 12796 /// Contains the implementation of text editing 12797 abstract class EditableTextWidget : EditableTextWidgetParent { 12798 this(Widget parent) { 12799 version(custom_widgets) 12800 this(true, parent); 12801 else 12802 this(false, parent); 12803 } 12804 12805 private bool useCustomWidget; 12806 12807 this(bool useCustomWidget, Widget parent) { 12808 this.useCustomWidget = useCustomWidget; 12809 12810 super(parent); 12811 12812 if(useCustomWidget) 12813 setupCustomTextEditing(); 12814 } 12815 12816 private bool wordWrapEnabled_; 12817 void wordWrapEnabled(bool enabled) { 12818 if(useCustomWidget) { 12819 wordWrapEnabled_ = enabled; 12820 version(use_new_text_system) 12821 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 12822 } else version(win32_widgets) { 12823 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 12824 } 12825 } 12826 12827 override int minWidth() { return scaleWithDpi(16); } 12828 override int widthStretchiness() { return 7; } 12829 override int widthShrinkiness() { return 1; } 12830 12831 version(use_new_text_system) 12832 override int maxHeight() { 12833 if(useCustomWidget) 12834 return tdh.maxHeight; 12835 else 12836 return super.maxHeight(); 12837 } 12838 12839 version(use_new_text_system) 12840 override void focus() { 12841 if(useCustomWidget && tdh) 12842 tdh.focus(); 12843 else 12844 super.focus(); 12845 } 12846 12847 void selectAll() { 12848 if(useCustomWidget) { 12849 version(use_new_text_system) 12850 tdh.selectAll(); 12851 else version(trash_text) 12852 textLayout.selectAll(); 12853 redraw(); 12854 } else version(win32_widgets) { 12855 SendMessage(hwnd, EM_SETSEL, 0, -1); 12856 } 12857 } 12858 12859 version(use_new_text_system) 12860 TextDisplayHelper tdh; 12861 12862 @property string content() { 12863 if(useCustomWidget) { 12864 version(use_new_text_system) { 12865 return textLayout.getTextString(); 12866 } else version(trash_text) { 12867 return textLayout.getPlainText(); 12868 } 12869 } else version(win32_widgets) { 12870 wchar[4096] bufferstack; 12871 wchar[] buffer; 12872 auto len = GetWindowTextLength(hwnd); 12873 if(len < bufferstack.length) 12874 buffer = bufferstack[0 .. len + 1]; 12875 else 12876 buffer = new wchar[](len + 1); 12877 12878 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 12879 if(l >= 0) 12880 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 12881 else 12882 return null; 12883 } 12884 12885 assert(0); 12886 } 12887 @property void content(string s) { 12888 if(useCustomWidget) { 12889 version(use_new_text_system) { 12890 with(textLayout.selection) { 12891 moveToStartOfDocument(); 12892 setAnchor(); 12893 moveToEndOfDocument(); 12894 setFocus(); 12895 replaceContent(s); 12896 } 12897 12898 tdh.adjustScrollbarSizes(); 12899 // these don't seem to help 12900 // tdh.smw.setPosition(0, 0); 12901 // tdh.scrollForCaret(); 12902 12903 redraw(); 12904 } else version(trash_text) { 12905 textLayout.clear(); 12906 textLayout.addText(s); 12907 12908 { 12909 // FIXME: it should be able to get this info easier 12910 auto painter = draw(); 12911 textLayout.redoLayout(painter); 12912 } 12913 auto cbb = textLayout.contentBoundingBox(); 12914 setContentSize(cbb.width, cbb.height); 12915 /* 12916 textLayout.addText(ForegroundColor.red, s); 12917 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 12918 textLayout.addText(" is the best!"); 12919 */ 12920 redraw(); 12921 } 12922 } else version(win32_widgets) { 12923 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 12924 SetWindowTextW(hwnd, bfr.ptr); 12925 } 12926 } 12927 12928 void addText(string txt) { 12929 if(useCustomWidget) { 12930 version(use_new_text_system) { 12931 textLayout.appendText(txt); 12932 tdh.adjustScrollbarSizes(); 12933 redraw(); 12934 } else if(trash_text) { 12935 textLayout.addText(txt); 12936 12937 { 12938 // FIXME: it should be able to get this info easier 12939 auto painter = draw(); 12940 textLayout.redoLayout(painter); 12941 } 12942 auto cbb = textLayout.contentBoundingBox(); 12943 setContentSize(cbb.width, cbb.height); 12944 } 12945 } else version(win32_widgets) { 12946 // get the current selection 12947 DWORD StartPos, EndPos; 12948 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 12949 12950 // move the caret to the end of the text 12951 int outLength = GetWindowTextLengthW(hwnd); 12952 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 12953 12954 // insert the text at the new caret position 12955 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 12956 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 12957 12958 // restore the previous selection 12959 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 12960 } 12961 } 12962 12963 version(custom_widgets) 12964 version(trash_text) 12965 override void paintFrameAndBackground(WidgetPainter painter) { 12966 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 12967 } 12968 12969 version(use_new_text_system) 12970 TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 12971 return new TextDisplayHelper(textLayout, smw); 12972 } 12973 12974 version(use_new_text_system) 12975 TextStyle defaultTextStyle() { 12976 return new TextDisplayHelper.MyTextStyle(getUsedFont()); 12977 } 12978 12979 version(use_new_text_system) 12980 private OperatingSystemFont getUsedFont() { 12981 auto cs = getComputedStyle(); 12982 auto font = cs.font; 12983 if(font is null) { 12984 font = new OperatingSystemFont; 12985 font.loadDefault(); 12986 } 12987 return font; 12988 } 12989 12990 version(use_new_text_system) { 12991 TextLayouter textLayout; 12992 12993 void setupCustomTextEditing() { 12994 textLayout = new TextLayouter(defaultTextStyle()); 12995 12996 auto smw = new ScrollMessageWidget(this); 12997 if(!showingHorizontalScroll) 12998 smw.horizontalScrollBar.hide(); 12999 if(!showingVerticalScroll) 13000 smw.verticalScrollBar.hide(); 13001 this.tabStop = false; 13002 smw.tabStop = false; 13003 tdh = textDisplayHelperFactory(textLayout, smw); 13004 } 13005 13006 override void newParentWindow(Window old, Window n) { 13007 if(n is null) return; 13008 this.parentWindow.addEventListener((scope DpiChangedEvent dce) { 13009 if(textLayout) { 13010 if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) { 13011 // the dpi change can change the font, so this informs the layouter that it has changed too 13012 style.font_ = getUsedFont(); 13013 13014 // arsd.core.writeln(this.parentWindow.win.actualDpi); 13015 } 13016 } 13017 }); 13018 } 13019 13020 } else version(trash_text) { 13021 static if(SimpledisplayTimerAvailable) 13022 Timer caretTimer; 13023 etc.TextLayout textLayout; 13024 13025 void setupCustomTextEditing() { 13026 textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4)); 13027 textLayout.selectionXorColor = getComputedStyle().activeListXorColor; 13028 } 13029 13030 override void paint(WidgetPainter painter) { 13031 if(parentWindow.win.closed) return; 13032 13033 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 13034 13035 /* 13036 painter.outlineColor = Color.white; 13037 painter.fillColor = Color.white; 13038 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 13039 */ 13040 13041 painter.outlineColor = Color.black; 13042 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 13043 13044 textLayout.caretShowingOnScreen = false; 13045 13046 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 13047 } 13048 } 13049 13050 static class Style : Widget.Style { 13051 override WidgetBackground background() { 13052 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 13053 } 13054 13055 override Color foregroundColor() { 13056 return WidgetPainter.visualTheme.foregroundColor; 13057 } 13058 13059 override FrameStyle borderStyle() { 13060 return FrameStyle.sunk; 13061 } 13062 13063 override MouseCursor cursor() { 13064 return GenericCursor.Text; 13065 } 13066 } 13067 mixin OverrideStyle!Style; 13068 13069 version(trash_text) 13070 version(custom_widgets) 13071 override void defaultEventHandler_mousedown(MouseDownEvent ev) { 13072 super.defaultEventHandler_mousedown(ev); 13073 if(parentWindow.win.closed) return; 13074 if(ev.button == MouseButton.left) { 13075 if(textLayout.selectNone()) 13076 redraw(); 13077 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 13078 this.focus(); 13079 //this.parentWindow.win.grabInput(); 13080 } else if(ev.button == MouseButton.middle) { 13081 static if(UsingSimpledisplayX11) { 13082 getPrimarySelection(parentWindow.win, (in char[] txt) { 13083 textLayout.insert(txt); 13084 redraw(); 13085 13086 auto cbb = textLayout.contentBoundingBox(); 13087 setContentSize(cbb.width, cbb.height); 13088 }); 13089 } 13090 } 13091 } 13092 13093 version(trash_text) 13094 version(custom_widgets) 13095 override void defaultEventHandler_mouseup(MouseUpEvent ev) { 13096 //this.parentWindow.win.releaseInputGrab(); 13097 super.defaultEventHandler_mouseup(ev); 13098 } 13099 13100 version(trash_text) 13101 version(custom_widgets) 13102 override void defaultEventHandler_mousemove(MouseMoveEvent ev) { 13103 super.defaultEventHandler_mousemove(ev); 13104 if(ev.state & ModifierState.leftButtonDown) { 13105 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 13106 redraw(); 13107 } 13108 } 13109 13110 version(trash_text) 13111 version(custom_widgets) 13112 override void defaultEventHandler_focus(Event ev) { 13113 super.defaultEventHandler_focus(ev); 13114 if(parentWindow.win.closed) return; 13115 auto painter = this.draw(); 13116 textLayout.drawCaret(painter); 13117 13118 static if(SimpledisplayTimerAvailable) 13119 if(caretTimer) { 13120 caretTimer.destroy(); 13121 caretTimer = null; 13122 } 13123 13124 bool blinkingCaret = true; 13125 static if(UsingSimpledisplayX11) 13126 if(!Image.impl.xshmAvailable) 13127 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 13128 13129 if(blinkingCaret) 13130 static if(SimpledisplayTimerAvailable) 13131 caretTimer = new Timer(500, { 13132 if(parentWindow.win.closed) { 13133 caretTimer.destroy(); 13134 return; 13135 } 13136 if(isFocused()) { 13137 auto painter = this.draw(); 13138 textLayout.drawCaret(painter); 13139 } else if(textLayout.caretShowingOnScreen) { 13140 auto painter = this.draw(); 13141 textLayout.eraseCaret(painter); 13142 } 13143 }); 13144 } 13145 13146 version(trash_text) { 13147 private string lastContentBlur; 13148 13149 override void defaultEventHandler_blur(Event ev) { 13150 super.defaultEventHandler_blur(ev); 13151 if(parentWindow.win.closed) return; 13152 version(custom_widgets) { 13153 auto painter = this.draw(); 13154 textLayout.eraseCaret(painter); 13155 static if(SimpledisplayTimerAvailable) 13156 if(caretTimer) { 13157 caretTimer.destroy(); 13158 caretTimer = null; 13159 } 13160 } 13161 13162 if(this.content != lastContentBlur) { 13163 auto evt = new ChangeEvent!string(this, &this.content); 13164 evt.dispatch(); 13165 lastContentBlur = this.content; 13166 } 13167 } 13168 } 13169 13170 version(win32_widgets) { 13171 private string lastContentBlur; 13172 13173 override void defaultEventHandler_blur(Event ev) { 13174 super.defaultEventHandler_blur(ev); 13175 13176 if(!useCustomWidget) 13177 if(this.content != lastContentBlur) { 13178 auto evt = new ChangeEvent!string(this, &this.content); 13179 evt.dispatch(); 13180 lastContentBlur = this.content; 13181 } 13182 } 13183 } 13184 13185 13186 version(trash_text) 13187 version(custom_widgets) 13188 override void defaultEventHandler_char(CharEvent ev) { 13189 super.defaultEventHandler_char(ev); 13190 textLayout.insert(ev.character); 13191 redraw(); 13192 13193 // FIXME: too inefficient 13194 auto cbb = textLayout.contentBoundingBox(); 13195 setContentSize(cbb.width, cbb.height); 13196 } 13197 version(trash_text) 13198 version(custom_widgets) 13199 override void defaultEventHandler_keydown(KeyDownEvent ev) { 13200 //super.defaultEventHandler_keydown(ev); 13201 switch(ev.key) { 13202 case Key.Delete: 13203 textLayout.delete_(); 13204 redraw(); 13205 break; 13206 case Key.Left: 13207 textLayout.moveLeft(); 13208 redraw(); 13209 break; 13210 case Key.Right: 13211 textLayout.moveRight(); 13212 redraw(); 13213 break; 13214 case Key.Up: 13215 textLayout.moveUp(); 13216 redraw(); 13217 break; 13218 case Key.Down: 13219 textLayout.moveDown(); 13220 redraw(); 13221 break; 13222 case Key.Home: 13223 textLayout.moveHome(); 13224 redraw(); 13225 break; 13226 case Key.End: 13227 textLayout.moveEnd(); 13228 redraw(); 13229 break; 13230 case Key.PageUp: 13231 foreach(i; 0 .. 32) 13232 textLayout.moveUp(); 13233 redraw(); 13234 break; 13235 case Key.PageDown: 13236 foreach(i; 0 .. 32) 13237 textLayout.moveDown(); 13238 redraw(); 13239 break; 13240 13241 default: 13242 {} // intentionally blank, let "char" handle it 13243 } 13244 /* 13245 if(ev.key == Key.Backspace) { 13246 textLayout.backspace(); 13247 redraw(); 13248 } 13249 */ 13250 ensureVisibleInScroll(textLayout.caretBoundingBox()); 13251 } 13252 13253 version(use_new_text_system) { 13254 bool showingVerticalScroll() { return true; } 13255 bool showingHorizontalScroll() { return true; } 13256 } 13257 } 13258 13259 /// 13260 class LineEdit : EditableTextWidget { 13261 override bool showingVerticalScroll() { return false; } 13262 override bool showingHorizontalScroll() { return false; } 13263 13264 override int flexBasisWidth() { return 250; } 13265 override int widthShrinkiness() { return 10; } 13266 13267 /// 13268 this(Widget parent) { 13269 super(parent); 13270 version(win32_widgets) { 13271 createWin32Window(this, "edit"w, "", 13272 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 13273 } else version(custom_widgets) { 13274 version(trash_text) { 13275 setupCustomTextEditing(); 13276 addEventListener(delegate(CharEvent ev) { 13277 if(ev.character == '\n') 13278 ev.preventDefault(); 13279 }); 13280 } 13281 } else static assert(false); 13282 } 13283 13284 private this(bool useCustomWidget, Widget parent) { 13285 if(!useCustomWidget) 13286 this(parent); 13287 else 13288 super(true, parent); 13289 } 13290 13291 version(use_new_text_system) 13292 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13293 auto tdh = new TextDisplayHelper(textLayout, smw); 13294 tdh.singleLine = true; 13295 return tdh; 13296 } 13297 13298 version(win32_widgets) { 13299 mixin Padding!q{0}; 13300 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 13301 override int maxHeight() { return minHeight; } 13302 } 13303 13304 /+ 13305 @property void passwordMode(bool p) { 13306 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 13307 } 13308 +/ 13309 } 13310 13311 /// ditto 13312 class CustomLineEdit : LineEdit { 13313 this(Widget parent) { 13314 super(true, parent); 13315 } 13316 } 13317 13318 /++ 13319 A [LineEdit] that displays `*` in place of the actual characters. 13320 13321 Alas, Windows requires the window to be created differently to use this style, 13322 so it had to be a new class instead of a toggle on and off on an existing object. 13323 13324 FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now. 13325 13326 History: 13327 Added January 24, 2021 13328 +/ 13329 class PasswordEdit : EditableTextWidget { 13330 override bool showingVerticalScroll() { return false; } 13331 override bool showingHorizontalScroll() { return false; } 13332 13333 override int flexBasisWidth() { return 250; } 13334 13335 version(use_new_text_system) 13336 override TextStyle defaultTextStyle() { 13337 auto cs = getComputedStyle(); 13338 13339 auto osf = new class OperatingSystemFont { 13340 this() { 13341 super(cs.font); 13342 } 13343 override int stringWidth(scope const(char)[] text, SimpleWindow window = null) { 13344 int count = 0; 13345 foreach(dchar ch; text) 13346 count++; 13347 return count * super.stringWidth("*", window); 13348 } 13349 }; 13350 13351 return new TextDisplayHelper.MyTextStyle(osf); 13352 } 13353 13354 version(use_new_text_system) 13355 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13356 static class TDH : TextDisplayHelper { 13357 this(TextLayouter textLayout, ScrollMessageWidget smw) { 13358 singleLine = true; 13359 super(textLayout, smw); 13360 } 13361 13362 override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 13363 char[256] buffer = void; 13364 int bufferLength = 0; 13365 foreach(dchar ch; text) 13366 buffer[bufferLength++] = '*'; 13367 painter.drawText(upperLeft, buffer[0..bufferLength]); 13368 } 13369 } 13370 13371 return new TDH(textLayout, smw); 13372 } 13373 13374 /// 13375 this(Widget parent) { 13376 super(parent); 13377 version(win32_widgets) { 13378 createWin32Window(this, "edit"w, "", 13379 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 13380 } else version(custom_widgets) { 13381 version(trash_text) { 13382 setupCustomTextEditing(); 13383 13384 // should this be under trash text? i think so. 13385 addEventListener(delegate(CharEvent ev) { 13386 if(ev.character == '\n') 13387 ev.preventDefault(); 13388 }); 13389 } 13390 } else static assert(false); 13391 } 13392 13393 private this(bool useCustomWidget, Widget parent) { 13394 if(!useCustomWidget) 13395 this(parent); 13396 else 13397 super(true, parent); 13398 } 13399 13400 version(win32_widgets) { 13401 mixin Padding!q{2}; 13402 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 13403 override int maxHeight() { return minHeight; } 13404 } 13405 } 13406 13407 /// ditto 13408 class CustomPasswordEdit : PasswordEdit { 13409 this(Widget parent) { 13410 super(true, parent); 13411 } 13412 } 13413 13414 13415 /// 13416 class TextEdit : EditableTextWidget { 13417 /// 13418 this(Widget parent) { 13419 super(parent); 13420 version(win32_widgets) { 13421 createWin32Window(this, "edit"w, "", 13422 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 13423 } else version(custom_widgets) { 13424 version(trash_text) 13425 setupCustomTextEditing(); 13426 } else static assert(false); 13427 } 13428 13429 private this(bool useCustomWidget, Widget parent) { 13430 if(!useCustomWidget) 13431 this(parent); 13432 else 13433 super(true, parent); 13434 } 13435 13436 override int maxHeight() { return int.max; } 13437 override int heightStretchiness() { return 7; } 13438 13439 override int flexBasisWidth() { return 250; } 13440 override int flexBasisHeight() { return 25; } 13441 } 13442 13443 /// ditto 13444 class CustomTextEdit : TextEdit { 13445 this(Widget parent) { 13446 super(true, parent); 13447 } 13448 } 13449 13450 /+ 13451 /++ 13452 13453 +/ 13454 version(none) 13455 class RichTextDisplay : Widget { 13456 @property void content(string c) {} 13457 void appendContent(string c) {} 13458 } 13459 +/ 13460 13461 /++ 13462 A read-only text display 13463 13464 History: 13465 Added October 31, 2023 (dub v11.3) 13466 +/ 13467 class TextDisplay : EditableTextWidget { 13468 this(string text, Widget parent) { 13469 super(true, parent); 13470 this.content = text; 13471 } 13472 13473 override int maxHeight() { return int.max; } 13474 override int minHeight() { return Window.defaultLineHeight; } 13475 override int heightStretchiness() { return 7; } 13476 override int heightShrinkiness() { return 2; } 13477 13478 override int flexBasisWidth() { 13479 return scaleWithDpi(250); 13480 } 13481 override int flexBasisHeight() { 13482 if(textLayout is null || this.tdh is null) 13483 return Window.defaultLineHeight; 13484 13485 auto textHeight = borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textLayout.height))).height; 13486 return this.tdh.borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, textHeight))).height; 13487 } 13488 13489 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13490 return new MyTextDisplayHelper(textLayout, smw); 13491 } 13492 13493 override void registerMovement() { 13494 super.registerMovement(); 13495 this.wordWrapEnabled = true; // FIXME: hack it should do this movement recalc internally 13496 } 13497 13498 static class MyTextDisplayHelper : TextDisplayHelper { 13499 this(TextLayouter textLayout, ScrollMessageWidget smw) { 13500 smw.verticalScrollBar.hide(); 13501 smw.horizontalScrollBar.hide(); 13502 super(textLayout, smw); 13503 this.readonly = true; 13504 } 13505 13506 override void registerMovement() { 13507 super.registerMovement(); 13508 13509 // FIXME: do the horizontal one too as needed and make sure that it does 13510 // wordwrapping again 13511 if(l.height + smw.horizontalScrollBar.height > this.height) 13512 smw.verticalScrollBar.show(); 13513 else 13514 smw.verticalScrollBar.hide(); 13515 13516 l.wordWrapWidth = this.width; 13517 13518 smw.verticalScrollBar.setPosition = 0; 13519 } 13520 } 13521 13522 class Style : Widget.Style { 13523 // just want the generic look for these 13524 } 13525 13526 mixin OverrideStyle!Style; 13527 } 13528 13529 // FIXME: if a item currently has keyboard focus, even if it is scrolled away, we could keep that item active 13530 /++ 13531 A scrollable viewer for an array of widgets. The widgets inside a list item can be whatever you want, and you can have any number of total items you want because only the visible widgets need to actually exist and load their data at a time, giving constantly predictable performance. 13532 13533 13534 When you use this, you must subclass it and implement minimally `itemFactory` and `itemSize`, optionally also `layoutMode`. 13535 13536 Your `itemFactory` must return a subclass of `GenericListViewItem` that implements the abstract method to load item from your list on-demand. 13537 13538 Note that some state in reused widget objects may either be preserved or reset when the user isn't expecting it. It is your responsibility to handle this when you load an item (try to save it when it is unloaded, then set it when reloaded), but my recommendation would be to have minimal extra state. For example, avoid having a scrollable widget inside a list, since the scroll state might change as it goes out and into view. Instead, I'd suggest making the list be a loader for a details pane on the side. 13539 13540 History: 13541 Added August 12, 2024 (dub v11.6) 13542 +/ 13543 abstract class GenericListViewWidget : Widget { 13544 /++ 13545 13546 +/ 13547 this(Widget parent) { 13548 super(parent); 13549 13550 smw = new ScrollMessageWidget(this); 13551 smw.addDefaultKeyboardListeners(); 13552 smw.addDefaultWheelListeners(itemSize.height, itemSize.width); 13553 13554 inner = new GenericListViewWidgetInner(this, smw); 13555 } 13556 13557 private ScrollMessageWidget smw; 13558 private GenericListViewWidgetInner inner; 13559 13560 /++ 13561 13562 +/ 13563 abstract GenericListViewItem itemFactory(Widget parent); 13564 // in device-dependent pixels 13565 /++ 13566 13567 +/ 13568 abstract Size itemSize(); // use 0 to indicate it can stretch? 13569 13570 enum LayoutMode { 13571 rows, 13572 columns, 13573 gridRowsFirst, 13574 gridColumnsFirst 13575 } 13576 LayoutMode layoutMode() { 13577 return LayoutMode.rows; 13578 } 13579 13580 private int itemCount_; 13581 13582 /++ 13583 Sets the count of available items in the list. This will not allocate any items, but it will adjust the scroll bars and try to load items up to this count on-demand as they appear visible. 13584 +/ 13585 void setItemCount(int count) { 13586 smw.setTotalArea(inner.width, count * itemSize().height); 13587 smw.setViewableArea(inner.width, inner.height); 13588 this.itemCount_ = count; 13589 } 13590 13591 /++ 13592 Returns the current count of items expected to available in the list. 13593 +/ 13594 int itemCount() { 13595 return this.itemCount_; 13596 } 13597 13598 /++ 13599 Call these when the watched data changes. It will cause any visible widgets affected by the change to reload and redraw their data. 13600 13601 Note you must $(I also) call [setItemCount] if the total item count has changed. 13602 +/ 13603 void notifyItemsChanged(int index, int count = 1) { 13604 } 13605 /// ditto 13606 void notifyItemsInserted(int index, int count = 1) { 13607 } 13608 /// ditto 13609 void notifyItemsRemoved(int index, int count = 1) { 13610 } 13611 /// ditto 13612 void notifyItemsMoved(int movedFromIndex, int movedToIndex, int count = 1) { 13613 } 13614 13615 private GenericListViewItem[] items; 13616 } 13617 13618 /// ditto 13619 abstract class GenericListViewItem : Widget { 13620 /++ 13621 +/ 13622 this(Widget parent) { 13623 super(parent); 13624 } 13625 13626 private int _currentIndex = -1; 13627 13628 private void showItemPrivate(int idx) { 13629 showItem(idx); 13630 _currentIndex = idx; 13631 } 13632 13633 /++ 13634 Implement this to show an item from your data backing to the list. 13635 13636 Note that even if you are showing the requested index already, you should still try to reload it because it is possible the index now points to a different item (e.g. an item was added so all the indexes have changed) or if data has changed in this index and it is requesting you to update it prior to a repaint. 13637 +/ 13638 abstract void showItem(int idx); 13639 13640 /++ 13641 Maintained by the library after calling [showItem] so the object knows which data index it currently has. 13642 13643 It may be -1, indicating nothing is currently loaded (or a load failed, and the current data is potentially inconsistent). 13644 13645 Inside the call to `showItem`, `currentIndexLoaded` is the old index, and the argument to `showItem` is the new index. You might use that to save state to the right place as needed before you overwrite it with the new item. 13646 +/ 13647 final int currentIndexLoaded() { 13648 return _currentIndex; 13649 } 13650 } 13651 13652 /// 13653 unittest { 13654 import arsd.minigui; 13655 13656 import std.conv; 13657 13658 void main() { 13659 auto mw = new MainWindow(); 13660 13661 static class MyListViewItem : GenericListViewItem { 13662 this(Widget parent) { 13663 super(parent); 13664 13665 label = new TextLabel("unloaded", TextAlignment.Left, this); 13666 button = new Button("Click", this); 13667 13668 button.addEventListener("triggered", (){ 13669 messageBox(text("clicked ", currentIndexLoaded())); 13670 }); 13671 } 13672 override void showItem(int idx) { 13673 label.label = "Item " ~ to!string(idx); 13674 } 13675 13676 TextLabel label; 13677 Button button; 13678 } 13679 13680 auto widget = new class GenericListViewWidget { 13681 this() { 13682 super(mw); 13683 } 13684 override GenericListViewItem itemFactory(Widget parent) { 13685 return new MyListViewItem(parent); 13686 } 13687 override Size itemSize() { 13688 return Size(0, scaleWithDpi(80)); 13689 } 13690 }; 13691 13692 widget.setItemCount(5000); 13693 13694 mw.loop(); 13695 } 13696 } 13697 13698 private class GenericListViewWidgetInner : Widget { 13699 this(GenericListViewWidget glvw, ScrollMessageWidget smw) { 13700 super(smw); 13701 this.glvw = glvw; 13702 this.tabStop = false; 13703 13704 reloadVisible(); 13705 13706 smw.addEventListener("scroll", () { 13707 reloadVisible(); 13708 }); 13709 } 13710 13711 override void registerMovement() { 13712 super.registerMovement(); 13713 if(glvw && glvw.smw) 13714 glvw.smw.setViewableArea(this.width, this.height); 13715 } 13716 13717 void reloadVisible() { 13718 auto y = glvw.smw.position.y / glvw.itemSize.height; 13719 int offset = glvw.smw.position.y % glvw.itemSize.height; 13720 13721 if(offset || y >= glvw.itemCount()) 13722 y--; 13723 if(y < 0) 13724 y = 0; 13725 13726 recomputeChildLayout(); 13727 13728 foreach(item; glvw.items) { 13729 if(y < glvw.itemCount()) { 13730 item.showItemPrivate(y); 13731 item.show(); 13732 } else { 13733 item.hide(); 13734 } 13735 y++; 13736 } 13737 13738 this.redraw(); 13739 } 13740 13741 private GenericListViewWidget glvw; 13742 13743 private bool inRcl; 13744 override void recomputeChildLayout() { 13745 if(inRcl) 13746 return; 13747 inRcl = true; 13748 scope(exit) 13749 inRcl = false; 13750 13751 auto ih = glvw.itemSize().height; 13752 13753 auto itemCount = this.height / ih + 2; // extra for partial display before and after 13754 bool hadNew; 13755 while(glvw.items.length < itemCount) { 13756 // FIXME: free the old items? maybe just set length 13757 glvw.items ~= glvw.itemFactory(this); 13758 hadNew = true; 13759 } 13760 13761 if(hadNew) 13762 reloadVisible(); 13763 13764 int y = -(glvw.smw.position.y % ih); 13765 foreach(child; children) { 13766 child.x = 0; 13767 child.y = y; 13768 y += glvw.itemSize().height; 13769 child.width = this.width; 13770 child.height = ih; 13771 13772 child.recomputeChildLayout(); 13773 } 13774 } 13775 } 13776 13777 13778 13779 /++ 13780 History: 13781 It was a child of Window before, but as of September 29, 2024, it is now a child of `Dialog`. 13782 +/ 13783 class MessageBox : Dialog { 13784 private string message; 13785 MessageBoxButton buttonPressed = MessageBoxButton.None; 13786 /++ 13787 13788 History: 13789 The overload that takes `Window originator` was added on September 29, 2024. 13790 +/ 13791 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 13792 this(null, message, buttons, buttonIds); 13793 } 13794 /// ditto 13795 this(Window originator, string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 13796 message = message.stripRightInternal; 13797 int mainWidth; 13798 13799 // estimate longest line 13800 int count; 13801 foreach(ch; message) { 13802 if(ch == '\n') { 13803 if(count > mainWidth) 13804 mainWidth = count; 13805 count = 0; 13806 } else { 13807 count++; 13808 } 13809 } 13810 mainWidth *= 8; 13811 if(mainWidth < 300) 13812 mainWidth = 300; 13813 if(mainWidth > 600) 13814 mainWidth = 600; 13815 13816 super(originator, mainWidth, 100); 13817 13818 assert(buttons.length); 13819 assert(buttons.length == buttonIds.length); 13820 13821 this.message = message; 13822 13823 auto label = new TextDisplay(message, this); 13824 13825 auto hl = new HorizontalLayout(this); 13826 auto spacer = new HorizontalSpacer(hl); // to right align 13827 13828 foreach(idx, buttonText; buttons) { 13829 auto button = new CommandButton(buttonText, hl); 13830 13831 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 13832 this.buttonPressed = buttonIds[idx]; 13833 win.close(); 13834 }; })(idx)); 13835 13836 if(idx == 0) 13837 button.focus(); 13838 } 13839 13840 if(buttons.length == 1) 13841 auto spacer2 = new HorizontalSpacer(hl); // to center it 13842 13843 auto size = label.flexBasisHeight() + hl.minHeight() + this.paddingTop + this.paddingBottom; 13844 auto max = scaleWithDpi(600); // random max height 13845 if(size > max) 13846 size = max; 13847 13848 win.resize(scaleWithDpi(mainWidth), size); 13849 13850 win.show(); 13851 redraw(); 13852 } 13853 13854 override void OK() { 13855 this.win.close(); 13856 } 13857 13858 mixin Padding!q{16}; 13859 } 13860 13861 /// 13862 enum MessageBoxStyle { 13863 OK, /// 13864 OKCancel, /// 13865 RetryCancel, /// 13866 YesNo, /// 13867 YesNoCancel, /// 13868 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. 13869 } 13870 13871 /// 13872 enum MessageBoxIcon { 13873 None, /// 13874 Info, /// 13875 Warning, /// 13876 Error /// 13877 } 13878 13879 /// Identifies the button the user pressed on a message box. 13880 enum MessageBoxButton { 13881 None, /// The user closed the message box without clicking any of the buttons. 13882 OK, /// 13883 Cancel, /// 13884 Retry, /// 13885 Yes, /// 13886 No, /// 13887 Continue /// 13888 } 13889 13890 13891 /++ 13892 Displays a modal message box, blocking until the user dismisses it. 13893 13894 Returns: the button pressed. 13895 +/ 13896 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13897 return messageBox(null, title, message, style, icon); 13898 } 13899 13900 /// ditto 13901 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13902 return messageBox(null, null, message, style, icon); 13903 } 13904 13905 /++ 13906 13907 +/ 13908 MessageBoxButton messageBox(Window originator, string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13909 version(win32_widgets) { 13910 WCharzBuffer t = WCharzBuffer(title); 13911 WCharzBuffer m = WCharzBuffer(message); 13912 UINT type; 13913 with(MessageBoxStyle) 13914 final switch(style) { 13915 case OK: type |= MB_OK; break; 13916 case OKCancel: type |= MB_OKCANCEL; break; 13917 case RetryCancel: type |= MB_RETRYCANCEL; break; 13918 case YesNo: type |= MB_YESNO; break; 13919 case YesNoCancel: type |= MB_YESNOCANCEL; break; 13920 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 13921 } 13922 with(MessageBoxIcon) 13923 final switch(icon) { 13924 case None: break; 13925 case Info: type |= MB_ICONINFORMATION; break; 13926 case Warning: type |= MB_ICONWARNING; break; 13927 case Error: type |= MB_ICONERROR; break; 13928 } 13929 switch(MessageBoxW(originator is null ? null : originator.win.hwnd, m.ptr, t.ptr, type)) { 13930 case IDOK: return MessageBoxButton.OK; 13931 case IDCANCEL: return MessageBoxButton.Cancel; 13932 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 13933 case IDYES: return MessageBoxButton.Yes; 13934 case IDNO: return MessageBoxButton.No; 13935 case IDCONTINUE: return MessageBoxButton.Continue; 13936 default: return MessageBoxButton.None; 13937 } 13938 } else { 13939 string[] buttons; 13940 MessageBoxButton[] buttonIds; 13941 with(MessageBoxStyle) 13942 final switch(style) { 13943 case OK: 13944 buttons = ["OK"]; 13945 buttonIds = [MessageBoxButton.OK]; 13946 break; 13947 case OKCancel: 13948 buttons = ["OK", "Cancel"]; 13949 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 13950 break; 13951 case RetryCancel: 13952 buttons = ["Retry", "Cancel"]; 13953 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 13954 break; 13955 case YesNo: 13956 buttons = ["Yes", "No"]; 13957 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 13958 break; 13959 case YesNoCancel: 13960 buttons = ["Yes", "No", "Cancel"]; 13961 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 13962 break; 13963 case RetryCancelContinue: 13964 buttons = ["Try Again", "Cancel", "Continue"]; 13965 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 13966 break; 13967 } 13968 auto mb = new MessageBox(originator, message, buttons, buttonIds); 13969 EventLoop el = EventLoop.get; 13970 el.run(() { return !mb.win.closed; }); 13971 return mb.buttonPressed; 13972 } 13973 13974 } 13975 13976 /// ditto 13977 int messageBox(Window originator, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13978 return messageBox(originator, message, style, icon); 13979 } 13980 13981 13982 /// 13983 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 13984 13985 /++ 13986 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 13987 13988 History: 13989 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. 13990 +/ 13991 struct EventListener { 13992 private Widget widget; 13993 private string event; 13994 private EventHandler handler; 13995 private bool useCapture; 13996 13997 /// 13998 void disconnect() { 13999 widget.removeEventListener(this); 14000 } 14001 } 14002 14003 /++ 14004 The purpose of this enum was to give a compile-time checked version of various standard event strings. 14005 14006 Now, I recommend you use a statically typed event object instead. 14007 14008 See_Also: [Event] 14009 +/ 14010 enum EventType : string { 14011 click = "click", /// 14012 14013 mouseenter = "mouseenter", /// 14014 mouseleave = "mouseleave", /// 14015 mousein = "mousein", /// 14016 mouseout = "mouseout", /// 14017 mouseup = "mouseup", /// 14018 mousedown = "mousedown", /// 14019 mousemove = "mousemove", /// 14020 14021 keydown = "keydown", /// 14022 keyup = "keyup", /// 14023 char_ = "char", /// 14024 14025 focus = "focus", /// 14026 blur = "blur", /// 14027 14028 triggered = "triggered", /// 14029 14030 change = "change", /// 14031 } 14032 14033 /++ 14034 Represents an event that is currently being processed. 14035 14036 14037 Minigui's event model is based on the web browser. An event has a name, a target, 14038 and an associated data object. It starts from the window and works its way down through 14039 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 14040 then goes back up again all the way back to the window, triggering bubble phase handlers. At 14041 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 14042 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 14043 was called; that just stops it from calling other handlers in the widget tree, but the default happens 14044 whenever propagation is done, not only if it gets to the end of the chain). 14045 14046 This model has several nice points: 14047 14048 $(LIST 14049 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 14050 with event handlers set, then add/remove children as much as you want without needing 14051 to manage the event handlers on them - the parent alone can manage everything. 14052 14053 * It is easy to create new custom events in your application. 14054 14055 * It is familiar to many web developers. 14056 ) 14057 14058 There's a few downsides though: 14059 14060 $(LIST 14061 * There's not a lot of type safety. 14062 14063 * You don't get a static list of what events a widget can emit. 14064 14065 * Tracing where an event got cancelled along the chain can get difficult; the downside of 14066 the central delegation benefit is it can be lead to debugging of action at a distance. 14067 ) 14068 14069 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 14070 while keeping the benefits - and most compatibility with - the existing model. The main idea is 14071 to simply use a D object type which provides a static interface as well as a built-in event name. 14072 Then, a new static interface allows you to see what an event can emit and attach handlers to it 14073 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 14074 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 14075 to having a little more help from the D compiler and documentation generator. 14076 14077 Your code would change like this: 14078 14079 --- 14080 // old 14081 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 14082 14083 // new 14084 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 14085 --- 14086 14087 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. 14088 14089 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 14090 14091 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 14092 14093 Thus the family of functions are: 14094 14095 [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. 14096 14097 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 14098 14099 [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. 14100 14101 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 14102 14103 --- 14104 class MyCheckbox : Widget { 14105 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 14106 /// It is NOT actually required but should be used whenever possible. 14107 mixin Emits!(ChangeEvent!bool); 14108 14109 this(Widget parent) { 14110 super(parent); 14111 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 14112 } 14113 14114 private bool _checked; 14115 @property bool checked() { return _checked; } 14116 @property void checked(bool set) { 14117 _checked = set; 14118 emit!(ChangeEvent!bool)(&checked); 14119 } 14120 } 14121 --- 14122 14123 ## Creating Your Own Events 14124 14125 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. 14126 14127 --- 14128 class MyEvent : Event { 14129 this(Widget target) { super(EventString, target); } 14130 mixin Register; // adds EventString and other reflection information 14131 } 14132 --- 14133 14134 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 14135 14136 History: 14137 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. 14138 14139 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. 14140 +/ 14141 /+ 14142 14143 ## General Conventions 14144 14145 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. 14146 14147 14148 ## Qt-style signals and slots 14149 14150 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. 14151 14152 The intention is for events to be used when 14153 14154 --- 14155 class Demo : Widget { 14156 this() { 14157 myPropertyChanged = Signal!int(this); 14158 } 14159 @property myProperty(int v) { 14160 myPropertyChanged.emit(v); 14161 } 14162 14163 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 14164 // but it can just genuinely not care about `this` since that's not really passed. 14165 } 14166 14167 class Foo : Widget { 14168 // the slot uda is not necessary, but it helps the script and ui builder find it. 14169 @slot void setValue(int v) { ... } 14170 } 14171 14172 demo.myPropertyChanged.connect(&foo.setValue); 14173 --- 14174 14175 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 14176 14177 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 14178 14179 class StringChangeEvent : ChangeEvent, Signal!string { 14180 mixin SignalImpl 14181 } 14182 14183 +/ 14184 class Event : ReflectableProperties { 14185 /// Creates an event without populating any members and without sending it. See [dispatch] 14186 this(string eventName, Widget emittedBy) { 14187 this.eventName = eventName; 14188 this.srcElement = emittedBy; 14189 } 14190 14191 14192 /// Implementations for the [ReflectableProperties] interface/ 14193 void getPropertiesList(scope void delegate(string name) sink) const {} 14194 /// ditto 14195 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 14196 /// ditto 14197 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 14198 return SetPropertyResult.notPermitted; 14199 } 14200 14201 14202 /+ 14203 /++ 14204 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 14205 14206 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. 14207 +/ 14208 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 14209 if(value.length == 0) { 14210 finalSink(memberName, `""`); 14211 return; 14212 } 14213 14214 char[1024] bufferBacking; 14215 char[] buffer = bufferBacking; 14216 int bufferPosition; 14217 14218 void sink(char ch) { 14219 if(bufferPosition >= buffer.length) 14220 buffer.length = buffer.length + 1024; 14221 buffer[bufferPosition++] = ch; 14222 } 14223 14224 sink('"'); 14225 14226 foreach(ch; value) { 14227 switch(ch) { 14228 case '\\': 14229 sink('\\'); sink('\\'); 14230 break; 14231 case '"': 14232 sink('\\'); sink('"'); 14233 break; 14234 case '\n': 14235 sink('\\'); sink('n'); 14236 break; 14237 case '\r': 14238 sink('\\'); sink('r'); 14239 break; 14240 case '\t': 14241 sink('\\'); sink('t'); 14242 break; 14243 default: 14244 sink(ch); 14245 } 14246 } 14247 14248 sink('"'); 14249 14250 finalSink(memberName, buffer[0 .. bufferPosition]); 14251 } 14252 +/ 14253 14254 /+ 14255 enum EventInitiator { 14256 system, 14257 minigui, 14258 user 14259 } 14260 14261 immutable EventInitiator; initiatedBy; 14262 +/ 14263 14264 /++ 14265 Events should generally follow the propagation model, but there's some exceptions 14266 to that rule. If so, they should override this to return false. In that case, only 14267 bubbling event handlers on the target itself and capturing event handlers on the containing 14268 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 14269 capture -> target -> bubble process.) 14270 14271 History: 14272 Added May 12, 2021 14273 +/ 14274 bool propagates() const pure nothrow @nogc @safe { 14275 return true; 14276 } 14277 14278 /++ 14279 hints as to whether preventDefault will actually do anything. not entirely reliable. 14280 14281 History: 14282 Added May 14, 2021 14283 +/ 14284 bool cancelable() const pure nothrow @nogc @safe { 14285 return true; 14286 } 14287 14288 /++ 14289 You can mix this into child class to register some boilerplate. It includes the `EventString` 14290 member, a constructor, and implementations of the dynamic get data interfaces. 14291 14292 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 14293 14294 14295 You can override the default EventString by simply providing your own in the form of 14296 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 14297 which provides some namespace protection against conflicts in other libraries while still being fairly 14298 easy to use. 14299 14300 If you provide your own constructor, it will override the default constructor provided here. A constructor 14301 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 14302 first argument to your constructor. 14303 14304 History: 14305 Added May 13, 2021. 14306 +/ 14307 protected static mixin template Register() { 14308 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 14309 this(Widget target) { super(EventString, target); } 14310 14311 mixin ReflectableProperties.RegisterGetters; 14312 } 14313 14314 /++ 14315 This is the widget that emitted the event. 14316 14317 14318 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 14319 14320 History: 14321 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 14322 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 14323 so I don't intend to remove these aliases. 14324 +/ 14325 Widget source; 14326 /// ditto 14327 alias source target; 14328 /// ditto 14329 alias source srcElement; 14330 14331 Widget relatedTarget; /// Note: likely to be deprecated at some point. 14332 14333 /// Prevents the default event handler (if there is one) from being called 14334 void preventDefault() { 14335 lastDefaultPrevented = true; 14336 defaultPrevented = true; 14337 } 14338 14339 /// Stops the event propagation immediately. 14340 void stopPropagation() { 14341 propagationStopped = true; 14342 } 14343 14344 private bool defaultPrevented; 14345 private bool propagationStopped; 14346 private string eventName; 14347 14348 private bool isBubbling; 14349 14350 /// 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. 14351 protected void adjustScrolling() { } 14352 /// ditto 14353 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 14354 14355 /++ 14356 this sends it only to the target. If you want propagation, use dispatch() instead. 14357 14358 This should be made private!!! 14359 14360 +/ 14361 void sendDirectly() { 14362 if(srcElement is null) 14363 return; 14364 14365 // 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. 14366 14367 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 14368 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 14369 14370 adjustScrolling(); 14371 14372 if(auto e = target.parentWindow) { 14373 if(auto handlers = "*" in e.capturingEventHandlers) 14374 foreach(handler; *handlers) 14375 if(handler) handler(e, this); 14376 if(auto handlers = eventName in e.capturingEventHandlers) 14377 foreach(handler; *handlers) 14378 if(handler) handler(e, this); 14379 } 14380 14381 auto e = srcElement; 14382 14383 if(auto handlers = eventName in e.bubblingEventHandlers) 14384 foreach(handler; *handlers) 14385 if(handler) handler(e, this); 14386 14387 if(auto handlers = "*" in e.bubblingEventHandlers) 14388 foreach(handler; *handlers) 14389 if(handler) handler(e, this); 14390 14391 // there's never a default for a catch-all event 14392 if(!defaultPrevented) 14393 if(eventName in e.defaultEventHandlers) 14394 e.defaultEventHandlers[eventName](e, this); 14395 } 14396 14397 /// this dispatches the element using the capture -> target -> bubble process 14398 void dispatch() { 14399 if(srcElement is null) 14400 return; 14401 14402 if(!propagates) { 14403 sendDirectly; 14404 return; 14405 } 14406 14407 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 14408 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 14409 14410 adjustScrolling(); 14411 // first capture, then bubble 14412 14413 Widget[] chain; 14414 Widget curr = srcElement; 14415 while(curr) { 14416 auto l = curr; 14417 chain ~= l; 14418 curr = curr.parent; 14419 } 14420 14421 isBubbling = false; 14422 14423 foreach_reverse(e; chain) { 14424 if(auto handlers = "*" in e.capturingEventHandlers) 14425 foreach(handler; *handlers) if(handler !is null) handler(e, this); 14426 14427 if(propagationStopped) 14428 break; 14429 14430 if(auto handlers = eventName in e.capturingEventHandlers) 14431 foreach(handler; *handlers) if(handler !is null) handler(e, this); 14432 14433 // the default on capture should really be to always do nothing 14434 14435 //if(!defaultPrevented) 14436 // if(eventName in e.defaultEventHandlers) 14437 // e.defaultEventHandlers[eventName](e.element, this); 14438 14439 if(propagationStopped) 14440 break; 14441 } 14442 14443 int adjustX; 14444 int adjustY; 14445 14446 isBubbling = true; 14447 if(!propagationStopped) 14448 foreach(e; chain) { 14449 if(auto handlers = eventName in e.bubblingEventHandlers) 14450 foreach(handler; *handlers) if(handler !is null) handler(e, this); 14451 14452 if(propagationStopped) 14453 break; 14454 14455 if(auto handlers = "*" in e.bubblingEventHandlers) 14456 foreach(handler; *handlers) if(handler !is null) handler(e, this); 14457 14458 if(propagationStopped) 14459 break; 14460 14461 if(e.encapsulatedChildren()) { 14462 adjustClientCoordinates(adjustX, adjustY); 14463 target = e; 14464 } else { 14465 adjustX += e.x; 14466 adjustY += e.y; 14467 } 14468 } 14469 14470 if(!defaultPrevented) 14471 foreach(e; chain) { 14472 if(eventName in e.defaultEventHandlers) 14473 e.defaultEventHandlers[eventName](e, this); 14474 } 14475 } 14476 14477 14478 /* old compatibility things */ 14479 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 14480 final @property { 14481 Key key() { return (cast(KeyEventBase) this).key; } 14482 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 14483 14484 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 14485 bool altKey() { return (cast(KeyEventBase) this).altKey; } 14486 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 14487 } 14488 14489 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 14490 final @property { 14491 int clientX() { return (cast(MouseEventBase) this).clientX; } 14492 int clientY() { return (cast(MouseEventBase) this).clientY; } 14493 14494 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 14495 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 14496 14497 int button() { return (cast(MouseEventBase) this).button; } 14498 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 14499 } 14500 14501 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 14502 final @property { 14503 int state() { 14504 if(auto meb = cast(MouseEventBase) this) 14505 return meb.state; 14506 if(auto keb = cast(KeyEventBase) this) 14507 return keb.state; 14508 assert(0); 14509 } 14510 } 14511 14512 deprecated("Use a CharEvent instead of Event in your handler going forward") 14513 final @property { 14514 dchar character() { 14515 if(auto ce = cast(CharEvent) this) 14516 return ce.character; 14517 return dchar.init; 14518 } 14519 } 14520 14521 // for change events 14522 @property { 14523 /// 14524 int intValue() { return 0; } 14525 /// 14526 string stringValue() { return null; } 14527 } 14528 } 14529 14530 /++ 14531 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 14532 14533 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 14534 dynamic and custom events, but the static list helps ensure you get them right. 14535 14536 If this is declared, you can use [Widget.emit] to send the event. 14537 14538 All events work the same way though, following the capture->widget->bubble model described under [Event]. 14539 14540 History: 14541 Added May 4, 2021 14542 +/ 14543 mixin template Emits(EventType) { 14544 import arsd.minigui : EventString; 14545 static if(is(EventType : Event) && !is(EventType == Event)) 14546 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 14547 else 14548 static assert(0, "You can only emit subclasses of Event"); 14549 } 14550 14551 /// ditto 14552 mixin template Emits(string eventString) { 14553 mixin("private Event[0] emits_" ~ eventString ~";"); 14554 } 14555 14556 /* 14557 class SignalEvent(string name) : Event { 14558 14559 } 14560 */ 14561 14562 /++ 14563 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". 14564 14565 14566 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. 14567 14568 History: 14569 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 14570 +/ 14571 class CommandEvent : Event { 14572 enum EventString = "command"; 14573 this(Widget source, string CommandString = EventString) { 14574 super(CommandString, source); 14575 } 14576 } 14577 14578 /++ 14579 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 14580 +/ 14581 class CommandEventWithArgs(Args...) : CommandEvent { 14582 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 14583 Args args; 14584 } 14585 14586 /++ 14587 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. 14588 14589 See [CommandEvent] for more information. 14590 14591 Returns: 14592 The [EventListener] you can use to remove the handler. 14593 +/ 14594 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 14595 return w.addEventListener(CommandString, (Event ev) { 14596 if(ev.target is w) 14597 return; // it does not consume its own commands! 14598 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 14599 handler(cev.args); 14600 ev.stopPropagation(); 14601 } 14602 }); 14603 } 14604 14605 /++ 14606 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. 14607 +/ 14608 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 14609 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 14610 event.dispatch(); 14611 } 14612 14613 class ResizeEvent : Event { 14614 enum EventString = "resize"; 14615 14616 this(Widget target) { super(EventString, target); } 14617 14618 override bool propagates() const { return false; } 14619 } 14620 14621 /++ 14622 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 14623 14624 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. 14625 14626 History: 14627 Added June 21, 2021 (dub v10.1) 14628 +/ 14629 class ClosingEvent : Event { 14630 enum EventString = "closing"; 14631 14632 this(Widget target) { super(EventString, target); } 14633 14634 override bool propagates() const { return false; } 14635 override bool cancelable() const { return true; } 14636 } 14637 14638 /// ditto 14639 class ClosedEvent : Event { 14640 enum EventString = "closed"; 14641 14642 this(Widget target) { super(EventString, target); } 14643 14644 override bool propagates() const { return false; } 14645 override bool cancelable() const { return false; } 14646 } 14647 14648 /// 14649 class BlurEvent : Event { 14650 enum EventString = "blur"; 14651 14652 // FIXME: related target? 14653 this(Widget target) { super(EventString, target); } 14654 14655 override bool propagates() const { return false; } 14656 } 14657 14658 /// 14659 class FocusEvent : Event { 14660 enum EventString = "focus"; 14661 14662 // FIXME: related target? 14663 this(Widget target) { super(EventString, target); } 14664 14665 override bool propagates() const { return false; } 14666 } 14667 14668 /++ 14669 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 14670 14671 History: 14672 Added July 3, 2021 14673 +/ 14674 class FocusInEvent : Event { 14675 enum EventString = "focusin"; 14676 14677 // FIXME: related target? 14678 this(Widget target) { super(EventString, target); } 14679 14680 override bool cancelable() const { return false; } 14681 } 14682 14683 /// ditto 14684 class FocusOutEvent : Event { 14685 enum EventString = "focusout"; 14686 14687 // FIXME: related target? 14688 this(Widget target) { super(EventString, target); } 14689 14690 override bool cancelable() const { return false; } 14691 } 14692 14693 /// 14694 class ScrollEvent : Event { 14695 enum EventString = "scroll"; 14696 this(Widget target) { super(EventString, target); } 14697 14698 override bool cancelable() const { return false; } 14699 } 14700 14701 /++ 14702 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 14703 14704 History: 14705 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 14706 +/ 14707 class CharEvent : Event { 14708 enum EventString = "char"; 14709 this(Widget target, dchar ch) { 14710 character = ch; 14711 super(EventString, target); 14712 } 14713 14714 immutable dchar character; 14715 } 14716 14717 /++ 14718 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 14719 +/ 14720 abstract class ChangeEventBase : Event { 14721 enum EventString = "change"; 14722 this(Widget target) { 14723 super(EventString, target); 14724 } 14725 14726 /+ 14727 // idk where or how exactly i want to do this. 14728 // i might come back to it later. 14729 14730 // If a widget itself broadcasts one of theses itself, it stops propagation going down 14731 // this way the source doesn't get too confused (think of a nested scroll widget) 14732 // 14733 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 14734 // then you consume that command and change you scroll x position to whatever. then you do 14735 // some kind of change event that is broadcast back to the children and any horizontal scroll 14736 // listeners are now able to update, without having an explicit connection between them. 14737 void broadcastToChildren(string fieldName) { 14738 14739 } 14740 +/ 14741 } 14742 14743 /++ 14744 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. 14745 14746 14747 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). 14748 14749 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);` 14750 14751 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 14752 14753 History: 14754 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. 14755 +/ 14756 class ChangeEvent(T) : ChangeEventBase { 14757 this(Widget target, T delegate() getNewValue) { 14758 assert(getNewValue !is null); 14759 this.getNewValue = getNewValue; 14760 super(target); 14761 } 14762 14763 private T delegate() getNewValue; 14764 14765 /++ 14766 Gets the new value that just changed. 14767 +/ 14768 @property T value() { 14769 return getNewValue(); 14770 } 14771 14772 /// compatibility method for old generic Events 14773 static if(is(immutable T == immutable int)) 14774 override int intValue() { return value; } 14775 /// ditto 14776 static if(is(immutable T == immutable string)) 14777 override string stringValue() { return value; } 14778 } 14779 14780 /++ 14781 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 14782 14783 14784 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14785 14786 History: 14787 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14788 +/ 14789 abstract class KeyEventBase : Event { 14790 this(string name, Widget target) { 14791 super(name, target); 14792 } 14793 14794 // for key events 14795 Key key; /// 14796 14797 KeyEvent originalKeyEvent; 14798 14799 /++ 14800 Indicates the current state of the given keyboard modifier keys. 14801 14802 History: 14803 Added to events on April 15, 2020. 14804 +/ 14805 bool ctrlKey; 14806 14807 /// ditto 14808 bool altKey; 14809 14810 /// ditto 14811 bool shiftKey; 14812 14813 /++ 14814 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 14815 14816 See [arsd.simpledisplay.ModifierState] for other possible flags. 14817 +/ 14818 int state; 14819 14820 mixin Register; 14821 } 14822 14823 /++ 14824 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]. 14825 14826 14827 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14828 14829 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. 14830 14831 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. 14832 14833 See_Also: [KeyUpEvent], [CharEvent] 14834 14835 History: 14836 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 14837 +/ 14838 class KeyDownEvent : KeyEventBase { 14839 enum EventString = "keydown"; 14840 this(Widget target) { super(EventString, target); } 14841 } 14842 14843 /++ 14844 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 14845 14846 14847 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14848 14849 See_Also: [KeyDownEvent], [CharEvent] 14850 14851 History: 14852 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 14853 +/ 14854 class KeyUpEvent : KeyEventBase { 14855 enum EventString = "keyup"; 14856 this(Widget target) { super(EventString, target); } 14857 } 14858 14859 /++ 14860 Contains shared properties for various mouse events; 14861 14862 14863 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14864 14865 History: 14866 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14867 +/ 14868 abstract class MouseEventBase : Event { 14869 this(string name, Widget target) { 14870 super(name, target); 14871 } 14872 14873 // for mouse events 14874 int clientX; /// The mouse event location relative to the target widget 14875 int clientY; /// ditto 14876 14877 int viewportX; /// The mouse event location relative to the window origin 14878 int viewportY; /// ditto 14879 14880 int button; /// See: [MouseEvent.button] 14881 int buttonLinear; /// See: [MouseEvent.buttonLinear] 14882 14883 /++ 14884 Indicates the current state of the given keyboard modifier keys. 14885 14886 History: 14887 Added to mouse events on September 28, 2010. 14888 +/ 14889 bool ctrlKey; 14890 14891 /// ditto 14892 bool altKey; 14893 14894 /// ditto 14895 bool shiftKey; 14896 14897 14898 14899 int state; /// 14900 14901 /++ 14902 for consistent names with key event. 14903 14904 History: 14905 Added September 28, 2021 (dub v10.3) 14906 +/ 14907 alias modifierState = state; 14908 14909 /++ 14910 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 14911 14912 History: 14913 Added May 15, 2021 14914 +/ 14915 bool isMouseWheel() { 14916 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 14917 } 14918 14919 // private 14920 override void adjustClientCoordinates(int deltaX, int deltaY) { 14921 clientX += deltaX; 14922 clientY += deltaY; 14923 } 14924 14925 override void adjustScrolling() { 14926 version(custom_widgets) { // TEMP 14927 viewportX = clientX; 14928 viewportY = clientY; 14929 if(auto se = cast(ScrollableWidget) srcElement) { 14930 clientX += se.scrollOrigin.x; 14931 clientY += se.scrollOrigin.y; 14932 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 14933 //clientX += se.scrollX_; 14934 //clientY += se.scrollY_; 14935 } 14936 } 14937 } 14938 14939 mixin Register; 14940 } 14941 14942 /++ 14943 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 14944 14945 14946 $(WARNING 14947 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 14948 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 14949 behavior. 14950 ) 14951 14952 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 14953 14954 [MouseUpEvent] is sent when the user releases a mouse button. 14955 14956 [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.) 14957 14958 [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. 14959 14960 [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. 14961 14962 [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. 14963 14964 [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. 14965 14966 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 14967 14968 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 14969 14970 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14971 14972 Rationale: 14973 14974 If you only want to do drag, mousedown/up works just fine being consistently sent. 14975 14976 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). 14977 14978 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. 14979 14980 History: 14981 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. 14982 +/ 14983 class MouseUpEvent : MouseEventBase { 14984 enum EventString = "mouseup"; /// 14985 this(Widget target) { super(EventString, target); } 14986 } 14987 /// ditto 14988 class MouseDownEvent : MouseEventBase { 14989 enum EventString = "mousedown"; /// 14990 this(Widget target) { super(EventString, target); } 14991 } 14992 /// ditto 14993 class MouseMoveEvent : MouseEventBase { 14994 enum EventString = "mousemove"; /// 14995 this(Widget target) { super(EventString, target); } 14996 } 14997 /// ditto 14998 class ClickEvent : MouseEventBase { 14999 enum EventString = "click"; /// 15000 this(Widget target) { super(EventString, target); } 15001 } 15002 /// ditto 15003 class DoubleClickEvent : MouseEventBase { 15004 enum EventString = "dblclick"; /// 15005 this(Widget target) { super(EventString, target); } 15006 } 15007 /// ditto 15008 class MouseOverEvent : Event { 15009 enum EventString = "mouseover"; /// 15010 this(Widget target) { super(EventString, target); } 15011 } 15012 /// ditto 15013 class MouseOutEvent : Event { 15014 enum EventString = "mouseout"; /// 15015 this(Widget target) { super(EventString, target); } 15016 } 15017 /// ditto 15018 class MouseEnterEvent : Event { 15019 enum EventString = "mouseenter"; /// 15020 this(Widget target) { super(EventString, target); } 15021 15022 override bool propagates() const { return false; } 15023 } 15024 /// ditto 15025 class MouseLeaveEvent : Event { 15026 enum EventString = "mouseleave"; /// 15027 this(Widget target) { super(EventString, target); } 15028 15029 override bool propagates() const { return false; } 15030 } 15031 15032 private bool isAParentOf(Widget a, Widget b) { 15033 if(a is null || b is null) 15034 return false; 15035 15036 while(b !is null) { 15037 if(a is b) 15038 return true; 15039 b = b.parent; 15040 } 15041 15042 return false; 15043 } 15044 15045 private struct WidgetAtPointResponse { 15046 Widget widget; 15047 15048 // x, y relative to the widget in the response. 15049 int x; 15050 int y; 15051 } 15052 15053 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 15054 assert(starting !is null); 15055 15056 starting.addScrollPosition(x, y); 15057 15058 auto child = starting.getChildAtPosition(x, y); 15059 while(child) { 15060 if(child.hidden) 15061 continue; 15062 starting = child; 15063 x -= child.x; 15064 y -= child.y; 15065 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 15066 child = r.widget; 15067 if(child is starting) 15068 break; 15069 } 15070 return WidgetAtPointResponse(starting, x, y); 15071 } 15072 15073 version(win32_widgets) { 15074 private: 15075 import core.sys.windows.commctrl; 15076 15077 pragma(lib, "comctl32"); 15078 shared static this() { 15079 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 15080 INITCOMMONCONTROLSEX ic; 15081 ic.dwSize = cast(DWORD) ic.sizeof; 15082 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 15083 if(!InitCommonControlsEx(&ic)) { 15084 //writeln("ICC failed"); 15085 } 15086 } 15087 15088 15089 // everything from here is just win32 headers copy pasta 15090 private: 15091 extern(Windows): 15092 15093 alias HANDLE HMENU; 15094 HMENU CreateMenu(); 15095 bool SetMenu(HWND, HMENU); 15096 HMENU CreatePopupMenu(); 15097 enum MF_POPUP = 0x10; 15098 enum MF_STRING = 0; 15099 15100 15101 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 15102 struct INITCOMMONCONTROLSEX { 15103 DWORD dwSize; 15104 DWORD dwICC; 15105 } 15106 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 15107 enum { 15108 IDB_STD_SMALL_COLOR, 15109 IDB_STD_LARGE_COLOR, 15110 IDB_VIEW_SMALL_COLOR = 4, 15111 IDB_VIEW_LARGE_COLOR = 5 15112 } 15113 enum { 15114 STD_CUT, 15115 STD_COPY, 15116 STD_PASTE, 15117 STD_UNDO, 15118 STD_REDOW, 15119 STD_DELETE, 15120 STD_FILENEW, 15121 STD_FILEOPEN, 15122 STD_FILESAVE, 15123 STD_PRINTPRE, 15124 STD_PROPERTIES, 15125 STD_HELP, 15126 STD_FIND, 15127 STD_REPLACE, 15128 STD_PRINT // = 14 15129 } 15130 15131 alias HANDLE HIMAGELIST; 15132 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 15133 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 15134 BOOL ImageList_Destroy(HIMAGELIST); 15135 15136 uint MAKELONG(ushort a, ushort b) { 15137 return cast(uint) ((b << 16) | a); 15138 } 15139 15140 15141 struct TBBUTTON { 15142 int iBitmap; 15143 int idCommand; 15144 BYTE fsState; 15145 BYTE fsStyle; 15146 version(Win64) 15147 BYTE[6] bReserved; 15148 else 15149 BYTE[2] bReserved; 15150 DWORD dwData; 15151 INT_PTR iString; 15152 } 15153 15154 enum { 15155 TB_ADDBUTTONSA = WM_USER + 20, 15156 TB_INSERTBUTTONA = WM_USER + 21, 15157 TB_GETIDEALSIZE = WM_USER + 99, 15158 } 15159 15160 struct SIZE { 15161 LONG cx; 15162 LONG cy; 15163 } 15164 15165 15166 enum { 15167 TBSTATE_CHECKED = 1, 15168 TBSTATE_PRESSED = 2, 15169 TBSTATE_ENABLED = 4, 15170 TBSTATE_HIDDEN = 8, 15171 TBSTATE_INDETERMINATE = 16, 15172 TBSTATE_WRAP = 32 15173 } 15174 15175 15176 15177 enum { 15178 ILC_COLOR = 0, 15179 ILC_COLOR4 = 4, 15180 ILC_COLOR8 = 8, 15181 ILC_COLOR16 = 16, 15182 ILC_COLOR24 = 24, 15183 ILC_COLOR32 = 32, 15184 ILC_COLORDDB = 254, 15185 ILC_MASK = 1, 15186 ILC_PALETTE = 2048 15187 } 15188 15189 15190 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 15191 15192 15193 enum { 15194 TB_ENABLEBUTTON = WM_USER + 1, 15195 TB_CHECKBUTTON, 15196 TB_PRESSBUTTON, 15197 TB_HIDEBUTTON, 15198 TB_INDETERMINATE, // = WM_USER + 5, 15199 TB_ISBUTTONENABLED = WM_USER + 9, 15200 TB_ISBUTTONCHECKED, 15201 TB_ISBUTTONPRESSED, 15202 TB_ISBUTTONHIDDEN, 15203 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 15204 TB_SETSTATE = WM_USER + 17, 15205 TB_GETSTATE = WM_USER + 18, 15206 TB_ADDBITMAP = WM_USER + 19, 15207 TB_DELETEBUTTON = WM_USER + 22, 15208 TB_GETBUTTON, 15209 TB_BUTTONCOUNT, 15210 TB_COMMANDTOINDEX, 15211 TB_SAVERESTOREA, 15212 TB_CUSTOMIZE, 15213 TB_ADDSTRINGA, 15214 TB_GETITEMRECT, 15215 TB_BUTTONSTRUCTSIZE, 15216 TB_SETBUTTONSIZE, 15217 TB_SETBITMAPSIZE, 15218 TB_AUTOSIZE, // = WM_USER + 33, 15219 TB_GETTOOLTIPS = WM_USER + 35, 15220 TB_SETTOOLTIPS = WM_USER + 36, 15221 TB_SETPARENT = WM_USER + 37, 15222 TB_SETROWS = WM_USER + 39, 15223 TB_GETROWS, 15224 TB_GETBITMAPFLAGS, 15225 TB_SETCMDID, 15226 TB_CHANGEBITMAP, 15227 TB_GETBITMAP, 15228 TB_GETBUTTONTEXTA, 15229 TB_REPLACEBITMAP, // = WM_USER + 46, 15230 TB_GETBUTTONSIZE = WM_USER + 58, 15231 TB_SETBUTTONWIDTH = WM_USER + 59, 15232 TB_GETBUTTONTEXTW = WM_USER + 75, 15233 TB_SAVERESTOREW = WM_USER + 76, 15234 TB_ADDSTRINGW = WM_USER + 77, 15235 } 15236 15237 extern(Windows) 15238 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 15239 15240 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 15241 15242 15243 enum { 15244 TB_SETINDENT = WM_USER + 47, 15245 TB_SETIMAGELIST, 15246 TB_GETIMAGELIST, 15247 TB_LOADIMAGES, 15248 TB_GETRECT, 15249 TB_SETHOTIMAGELIST, 15250 TB_GETHOTIMAGELIST, 15251 TB_SETDISABLEDIMAGELIST, 15252 TB_GETDISABLEDIMAGELIST, 15253 TB_SETSTYLE, 15254 TB_GETSTYLE, 15255 //TB_GETBUTTONSIZE, 15256 //TB_SETBUTTONWIDTH, 15257 TB_SETMAXTEXTROWS, 15258 TB_GETTEXTROWS // = WM_USER + 61 15259 } 15260 15261 enum { 15262 CCM_FIRST = 0x2000, 15263 CCM_LAST = CCM_FIRST + 0x200, 15264 CCM_SETBKCOLOR = 8193, 15265 CCM_SETCOLORSCHEME = 8194, 15266 CCM_GETCOLORSCHEME = 8195, 15267 CCM_GETDROPTARGET = 8196, 15268 CCM_SETUNICODEFORMAT = 8197, 15269 CCM_GETUNICODEFORMAT = 8198, 15270 CCM_SETVERSION = 0x2007, 15271 CCM_GETVERSION = 0x2008, 15272 CCM_SETNOTIFYWINDOW = 0x2009 15273 } 15274 15275 15276 enum { 15277 PBM_SETRANGE = WM_USER + 1, 15278 PBM_SETPOS, 15279 PBM_DELTAPOS, 15280 PBM_SETSTEP, 15281 PBM_STEPIT, // = WM_USER + 5 15282 PBM_SETRANGE32 = 1030, 15283 PBM_GETRANGE, 15284 PBM_GETPOS, 15285 PBM_SETBARCOLOR, // = 1033 15286 PBM_SETBKCOLOR = CCM_SETBKCOLOR 15287 } 15288 15289 enum { 15290 PBS_SMOOTH = 1, 15291 PBS_VERTICAL = 4 15292 } 15293 15294 enum { 15295 ICC_LISTVIEW_CLASSES = 1, 15296 ICC_TREEVIEW_CLASSES = 2, 15297 ICC_BAR_CLASSES = 4, 15298 ICC_TAB_CLASSES = 8, 15299 ICC_UPDOWN_CLASS = 16, 15300 ICC_PROGRESS_CLASS = 32, 15301 ICC_HOTKEY_CLASS = 64, 15302 ICC_ANIMATE_CLASS = 128, 15303 ICC_WIN95_CLASSES = 255, 15304 ICC_DATE_CLASSES = 256, 15305 ICC_USEREX_CLASSES = 512, 15306 ICC_COOL_CLASSES = 1024, 15307 ICC_STANDARD_CLASSES = 0x00004000, 15308 } 15309 15310 enum WM_USER = 1024; 15311 } 15312 15313 version(win32_widgets) 15314 pragma(lib, "comdlg32"); 15315 15316 15317 /// 15318 enum GenericIcons : ushort { 15319 None, /// 15320 // these happen to match the win32 std icons numerically if you just subtract one from the value 15321 Cut, /// 15322 Copy, /// 15323 Paste, /// 15324 Undo, /// 15325 Redo, /// 15326 Delete, /// 15327 New, /// 15328 Open, /// 15329 Save, /// 15330 PrintPreview, /// 15331 Properties, /// 15332 Help, /// 15333 Find, /// 15334 Replace, /// 15335 Print, /// 15336 } 15337 15338 enum FileDialogType { 15339 Automatic, 15340 Open, 15341 Save 15342 } 15343 string previousFileReferenced; 15344 15345 /++ 15346 Used in automatic menu functions to indicate that the user should be able to browse for a file. 15347 15348 Params: 15349 storage = an alias to a `static string` variable that stores the last file referenced. It will 15350 use this to pre-fill the dialog with a suggestion. 15351 15352 Please note that it MUST be `static` or you will get compile errors. 15353 15354 filters = the filters param to [getFileName] 15355 15356 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 15357 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 15358 a save dialog box. Otherwise, it will show an open dialog box. 15359 +/ 15360 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 15361 string name; 15362 alias name this; 15363 } 15364 15365 /++ 15366 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. 15367 15368 History: 15369 onCancel was added November 6, 2021. 15370 15371 The dialog itself on Linux was modified on December 2, 2021 to include 15372 a directory picker in addition to the command line completion view. 15373 15374 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 15375 15376 The `owner` argument was added September 29, 2024. The overloads without this argument are likely to be deprecated in the next major version. 15377 Future_directions: 15378 I want to add some kind of custom preview and maybe thumbnail thing in the future, 15379 at least on Linux, maybe on Windows too. 15380 +/ 15381 void getOpenFileName( 15382 Window owner, 15383 void delegate(string) onOK, 15384 string prefilledName = null, 15385 string[] filters = null, 15386 void delegate() onCancel = null, 15387 string initialDirectory = null, 15388 ) 15389 { 15390 return getFileName(owner, true, onOK, prefilledName, filters, onCancel, initialDirectory); 15391 } 15392 15393 /// ditto 15394 void getSaveFileName( 15395 Window owner, 15396 void delegate(string) onOK, 15397 string prefilledName = null, 15398 string[] filters = null, 15399 void delegate() onCancel = null, 15400 string initialDirectory = null, 15401 ) 15402 { 15403 return getFileName(owner, false, onOK, prefilledName, filters, onCancel, initialDirectory); 15404 } 15405 15406 // deprecated("Pass an explicit owner window as the first argument, even if `null`. You can usually pass the `parentWindow` member of the widget that prompted this interaction.") 15407 /// ditto 15408 void getOpenFileName( 15409 void delegate(string) onOK, 15410 string prefilledName = null, 15411 string[] filters = null, 15412 void delegate() onCancel = null, 15413 string initialDirectory = null, 15414 ) 15415 { 15416 return getFileName(null, true, onOK, prefilledName, filters, onCancel, initialDirectory); 15417 } 15418 15419 /// ditto 15420 void getSaveFileName( 15421 void delegate(string) onOK, 15422 string prefilledName = null, 15423 string[] filters = null, 15424 void delegate() onCancel = null, 15425 string initialDirectory = null, 15426 ) 15427 { 15428 return getFileName(null, false, onOK, prefilledName, filters, onCancel, initialDirectory); 15429 } 15430 15431 void getFileName( 15432 Window owner, 15433 bool openOrSave, 15434 void delegate(string) onOK, 15435 string prefilledName = null, 15436 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 15437 void delegate() onCancel = null, 15438 string initialDirectory = null, 15439 ) 15440 { 15441 15442 version(win32_widgets) { 15443 import core.sys.windows.commdlg; 15444 /* 15445 Ofn.lStructSize = sizeof(OPENFILENAME); 15446 Ofn.hwndOwner = hWnd; 15447 Ofn.lpstrFilter = szFilter; 15448 Ofn.lpstrFile= szFile; 15449 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 15450 Ofn.lpstrFileTitle = szFileTitle; 15451 Ofn.nMaxFileTitle = sizeof(szFileTitle); 15452 Ofn.lpstrInitialDir = (LPSTR)NULL; 15453 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 15454 Ofn.lpstrTitle = szTitle; 15455 */ 15456 15457 15458 wchar[1024] file = 0; 15459 wchar[1024] filterBuffer = 0; 15460 makeWindowsString(prefilledName, file[]); 15461 OPENFILENAME ofn; 15462 ofn.lStructSize = ofn.sizeof; 15463 ofn.hwndOwner = owner is null ? null : owner.win.hwnd; 15464 if(filters.length) { 15465 string filter; 15466 foreach(i, f; filters) { 15467 filter ~= f; 15468 filter ~= "\0"; 15469 } 15470 filter ~= "\0"; 15471 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 15472 } 15473 ofn.lpstrFile = file.ptr; 15474 ofn.nMaxFile = file.length; 15475 15476 wchar[1024] initialDir = 0; 15477 if(initialDirectory !is null) { 15478 makeWindowsString(initialDirectory, initialDir[]); 15479 ofn.lpstrInitialDir = file.ptr; 15480 } 15481 15482 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 15483 { 15484 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 15485 if(okString.length && okString[$-1] == '\0') 15486 okString = okString[0..$-1]; 15487 onOK(okString); 15488 } else { 15489 if(onCancel) 15490 onCancel(); 15491 } 15492 } else version(custom_widgets) { 15493 if(filters.length == 0) 15494 filters = ["All Files\0*.*"]; 15495 auto picker = new FilePicker(prefilledName, filters, initialDirectory, owner); 15496 picker.onOK = onOK; 15497 picker.onCancel = onCancel; 15498 picker.show(); 15499 } 15500 } 15501 15502 version(custom_widgets) 15503 private 15504 class FilePicker : Dialog { 15505 void delegate(string) onOK; 15506 void delegate() onCancel; 15507 LineEdit lineEdit; 15508 15509 // returns common prefix 15510 string loadFiles(string cwd, string[] filters...) { 15511 string[] files; 15512 string[] dirs; 15513 15514 string commonPrefix; 15515 15516 getFiles(cwd, (string name, bool isDirectory) { 15517 if(name == ".") 15518 return; // skip this as unnecessary 15519 if(isDirectory) 15520 dirs ~= name; 15521 else { 15522 foreach(filter; filters) 15523 if( 15524 filter.length <= 1 || 15525 filter == "*.*" || 15526 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 15527 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 15528 ) 15529 { 15530 files ~= name; 15531 15532 if(filter.length > 0 && filter[$-1] == '*') { 15533 if(commonPrefix is null) { 15534 commonPrefix = name; 15535 } else { 15536 foreach(idx, char i; name) { 15537 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 15538 commonPrefix = commonPrefix[0 .. idx]; 15539 break; 15540 } 15541 } 15542 } 15543 } 15544 15545 break; 15546 } 15547 } 15548 }); 15549 15550 extern(C) static int comparator(scope const void* a, scope const void* b) { 15551 // FIXME: make it a natural sort for numbers 15552 // maybe put dot files at the end too. 15553 auto sa = *cast(string*) a; 15554 auto sb = *cast(string*) b; 15555 15556 for(int i = 0; i < sa.length; i++) { 15557 if(i == sb.length) 15558 return 1; 15559 auto diff = sa[i] - sb[i]; 15560 if(diff) 15561 return diff; 15562 } 15563 15564 return 0; 15565 } 15566 15567 nonPhobosSort(files, &comparator); 15568 nonPhobosSort(dirs, &comparator); 15569 15570 listWidget.clear(); 15571 dirWidget.clear(); 15572 foreach(name; dirs) 15573 dirWidget.addOption(name); 15574 foreach(name; files) 15575 listWidget.addOption(name); 15576 15577 return commonPrefix; 15578 } 15579 15580 ListWidget listWidget; 15581 ListWidget dirWidget; 15582 15583 string currentDirectory; 15584 string[] processedFilters; 15585 15586 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"] 15587 this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) { 15588 super(owner, 500, 400, "Choose File..."); // owner); 15589 15590 foreach(filter; filters) { 15591 while(filter.length && filter[0] != 0) { 15592 filter = filter[1 .. $]; 15593 } 15594 if(filter.length) 15595 filter = filter[1 .. $]; // trim off the 0 15596 15597 while(filter.length) { 15598 int idx = 0; 15599 while(idx < filter.length && filter[idx] != ';') { 15600 idx++; 15601 } 15602 15603 processedFilters ~= filter[0 .. idx]; 15604 if(idx < filter.length) 15605 idx++; // skip the ; 15606 filter = filter[idx .. $]; 15607 } 15608 } 15609 15610 currentDirectory = initialDirectory is null ? "." : initialDirectory; 15611 15612 { 15613 auto hl = new HorizontalLayout(this); 15614 dirWidget = new ListWidget(hl); 15615 listWidget = new ListWidget(hl); 15616 15617 // double click events normally trigger something else but 15618 // here user might be clicking kinda fast and we'd rather just 15619 // keep it 15620 dirWidget.addEventListener((scope DoubleClickEvent dev) { 15621 auto ce = new ChangeEvent!void(dirWidget, () {}); 15622 ce.dispatch(); 15623 }); 15624 15625 dirWidget.addEventListener((scope ChangeEvent!void sce) { 15626 string v; 15627 foreach(o; dirWidget.options) 15628 if(o.selected) { 15629 v = o.label; 15630 break; 15631 } 15632 if(v.length) { 15633 currentDirectory ~= "/" ~ v; 15634 loadFiles(currentDirectory, processedFilters); 15635 } 15636 }); 15637 15638 // double click here, on the other hand, selects the file 15639 // and moves on 15640 listWidget.addEventListener((scope DoubleClickEvent dev) { 15641 OK(); 15642 }); 15643 } 15644 15645 lineEdit = new LineEdit(this); 15646 lineEdit.focus(); 15647 lineEdit.addEventListener(delegate(CharEvent event) { 15648 if(event.character == '\t' || event.character == '\n') 15649 event.preventDefault(); 15650 }); 15651 15652 listWidget.addEventListener(EventType.change, () { 15653 foreach(o; listWidget.options) 15654 if(o.selected) 15655 lineEdit.content = o.label; 15656 }); 15657 15658 loadFiles(currentDirectory, processedFilters); 15659 15660 lineEdit.addEventListener((KeyDownEvent event) { 15661 if(event.key == Key.Tab) { 15662 15663 auto current = lineEdit.content; 15664 if(current.length >= 2 && current[0 ..2] == "./") 15665 current = current[2 .. $]; 15666 15667 auto commonPrefix = loadFiles(currentDirectory, current ~ "*"); 15668 15669 if(commonPrefix.length) 15670 lineEdit.content = commonPrefix; 15671 15672 // FIXME: if that is a directory, add the slash? or even go inside? 15673 15674 event.preventDefault(); 15675 } 15676 }); 15677 15678 lineEdit.content = prefilledName; 15679 15680 auto hl = new HorizontalLayout(60, this); 15681 auto cancelButton = new Button("Cancel", hl); 15682 auto okButton = new Button("OK", hl); 15683 15684 cancelButton.addEventListener(EventType.triggered, &Cancel); 15685 okButton.addEventListener(EventType.triggered, &OK); 15686 15687 this.addEventListener((KeyDownEvent event) { 15688 if(event.key == Key.Enter || event.key == Key.PadEnter) { 15689 event.preventDefault(); 15690 OK(); 15691 } 15692 if(event.key == Key.Escape) 15693 Cancel(); 15694 }); 15695 15696 } 15697 15698 override void OK() { 15699 if(lineEdit.content.length) { 15700 string accepted; 15701 auto c = lineEdit.content; 15702 if(c.length && c[0] == '/') 15703 accepted = c; 15704 else 15705 accepted = currentDirectory ~ "/" ~ lineEdit.content; 15706 15707 if(isDir(accepted)) { 15708 // FIXME: would be kinda nice to support ~ and collapse these paths too 15709 // FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later. 15710 currentDirectory = accepted; 15711 loadFiles(currentDirectory, processedFilters); 15712 lineEdit.content = ""; 15713 return; 15714 } 15715 15716 if(onOK) 15717 onOK(accepted); 15718 } 15719 close(); 15720 } 15721 15722 override void Cancel() { 15723 if(onCancel) 15724 onCancel(); 15725 close(); 15726 } 15727 } 15728 15729 private bool isDir(string name) { 15730 version(Windows) { 15731 auto ws = WCharzBuffer(name); 15732 auto ret = GetFileAttributesW(ws.ptr); 15733 if(ret == INVALID_FILE_ATTRIBUTES) 15734 return false; 15735 return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0; 15736 } else version(Posix) { 15737 import core.sys.posix.sys.stat; 15738 stat_t buf; 15739 auto ret = stat((name ~ '\0').ptr, &buf); 15740 if(ret == -1) 15741 return false; // I could probably check more specific errors tbh 15742 return (buf.st_mode & S_IFMT) == S_IFDIR; 15743 } else return false; 15744 } 15745 15746 /* 15747 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 15748 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 15749 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 15750 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 15751 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 15752 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 15753 http://www.sbin.org/doc/Xlib/chapt_03.html 15754 15755 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 15756 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 15757 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 15758 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 15759 */ 15760 15761 15762 // These are all for setMenuAndToolbarFromAnnotatedCode 15763 /// This item in the menu will be preceded by a separator line 15764 /// Group: generating_from_code 15765 struct separator {} 15766 deprecated("It was misspelled, use separator instead") alias seperator = separator; 15767 /// Program-wide keyboard shortcut to trigger the action 15768 /// Group: generating_from_code 15769 struct accelerator { string keyString; } 15770 /// tells which menu the action will be on 15771 /// Group: generating_from_code 15772 struct menu { string name; } 15773 /// Describes which toolbar section the action appears on 15774 /// Group: generating_from_code 15775 struct toolbar { string groupName; } 15776 /// 15777 /// Group: generating_from_code 15778 struct icon { ushort id; } 15779 /// 15780 /// Group: generating_from_code 15781 struct label { string label; } 15782 /// 15783 /// Group: generating_from_code 15784 struct hotkey { dchar ch; } 15785 /// 15786 /// Group: generating_from_code 15787 struct tip { string tip; } 15788 15789 15790 /++ 15791 Observes and allows inspection of an object via automatic gui 15792 +/ 15793 /// Group: generating_from_code 15794 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 15795 return new ObjectInspectionWindowImpl!(T)(t); 15796 } 15797 15798 class ObjectInspectionWindow : Window { 15799 this(int a, int b, string c) { 15800 super(a, b, c); 15801 } 15802 15803 abstract void readUpdatesFromObject(); 15804 } 15805 15806 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 15807 T t; 15808 this(T t) { 15809 this.t = t; 15810 15811 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 15812 15813 foreach(memberName; __traits(derivedMembers, T)) {{ 15814 alias member = I!(__traits(getMember, t, memberName))[0]; 15815 alias type = typeof(member); 15816 static if(is(type == int)) { 15817 auto le = new LabeledLineEdit(memberName ~ ": ", this); 15818 //le.addEventListener("char", (Event ev) { 15819 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 15820 //ev.preventDefault(); 15821 //}); 15822 le.addEventListener(EventType.change, (Event ev) { 15823 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 15824 }); 15825 15826 updateMemberDelegates[memberName] = () { 15827 le.content = toInternal!string(__traits(getMember, t, memberName)); 15828 }; 15829 } 15830 }} 15831 } 15832 15833 void delegate()[string] updateMemberDelegates; 15834 15835 override void readUpdatesFromObject() { 15836 foreach(k, v; updateMemberDelegates) 15837 v(); 15838 } 15839 } 15840 15841 /++ 15842 Creates a dialog based on a data structure. 15843 15844 --- 15845 dialog(window, (YourStructure value) { 15846 // the user filled in the struct and clicked OK, 15847 // you can check the members now 15848 }); 15849 --- 15850 15851 Params: 15852 initialData = the initial value to show in the dialog. It will not modify this unless 15853 it is a class then it might, no promises. 15854 15855 History: 15856 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 15857 15858 The overloads with `parent` were added September 29, 2024. The ones without it are likely to 15859 be deprecated soon. 15860 +/ 15861 /// Group: generating_from_code 15862 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15863 dialog(null, T.init, onOK, onCancel, title); 15864 } 15865 /// ditto 15866 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15867 dialog(null, T.init, onOK, onCancel, title); 15868 } 15869 /// ditto 15870 void dialog(T)(Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15871 dialog(parent, T.init, onOK, onCancel, title); 15872 } 15873 /// ditto 15874 void dialog(T)(T initialData, Window parent, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15875 dialog(parent, initialData, onOK, onCancel, title); 15876 } 15877 /// ditto 15878 void dialog(T)(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15879 auto dg = new AutomaticDialog!T(parent, initialData, onOK, onCancel, title); 15880 dg.show(); 15881 } 15882 15883 private static template I(T...) { alias I = T; } 15884 15885 15886 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 15887 if(name == "id") 15888 return allLowerCase ? name : "ID"; 15889 15890 char[160] buffer; 15891 int bufferIndex = 0; 15892 bool shouldCap = true; 15893 bool shouldSpace; 15894 bool lastWasCap; 15895 foreach(idx, char ch; name) { 15896 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15897 15898 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 15899 if(lastWasCap) { 15900 // two caps in a row, don't change. Prolly acronym. 15901 } else { 15902 if(idx) 15903 shouldSpace = true; // new word, add space 15904 } 15905 15906 lastWasCap = true; 15907 } else { 15908 lastWasCap = false; 15909 } 15910 15911 if(shouldSpace) { 15912 buffer[bufferIndex++] = space; 15913 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15914 shouldSpace = false; 15915 } 15916 if(shouldCap) { 15917 if(ch >= 'a' && ch <= 'z') 15918 ch -= 32; 15919 shouldCap = false; 15920 } 15921 if(allLowerCase && ch >= 'A' && ch <= 'Z') 15922 ch += 32; 15923 buffer[bufferIndex++] = ch; 15924 } 15925 return buffer[0 .. bufferIndex].idup; 15926 } 15927 15928 /++ 15929 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. 15930 +/ 15931 class AutomaticDialog(T) : Dialog { 15932 T t; 15933 15934 void delegate(T) onOK; 15935 void delegate() onCancel; 15936 15937 override int paddingTop() { return defaultLineHeight; } 15938 override int paddingBottom() { return defaultLineHeight; } 15939 override int paddingRight() { return defaultLineHeight; } 15940 override int paddingLeft() { return defaultLineHeight; } 15941 15942 this(Window parent, T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 15943 assert(onOK !is null); 15944 15945 t = initialData; 15946 15947 static if(is(T == class)) { 15948 if(t is null) 15949 t = new T(); 15950 } 15951 this.onOK = onOK; 15952 this.onCancel = onCancel; 15953 super(parent, 400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title); 15954 15955 static if(is(T == class)) 15956 this.addDataControllerWidget(t); 15957 else 15958 this.addDataControllerWidget(&t); 15959 15960 auto hl = new HorizontalLayout(this); 15961 auto stretch = new HorizontalSpacer(hl); // to right align 15962 auto ok = new CommandButton("OK", hl); 15963 auto cancel = new CommandButton("Cancel", hl); 15964 ok.addEventListener(EventType.triggered, &OK); 15965 cancel.addEventListener(EventType.triggered, &Cancel); 15966 15967 this.addEventListener((KeyDownEvent ev) { 15968 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 15969 ok.focus(); 15970 OK(); 15971 ev.preventDefault(); 15972 } 15973 if(ev.key == Key.Escape) { 15974 Cancel(); 15975 ev.preventDefault(); 15976 } 15977 }); 15978 15979 this.addEventListener((scope ClosedEvent ce) { 15980 if(onCancel) 15981 onCancel(); 15982 }); 15983 15984 //this.children[0].focus(); 15985 } 15986 15987 override void OK() { 15988 onOK(t); 15989 close(); 15990 } 15991 15992 override void Cancel() { 15993 if(onCancel) 15994 onCancel(); 15995 close(); 15996 } 15997 } 15998 15999 private template baseClassCount(Class) { 16000 private int helper() { 16001 int count = 0; 16002 static if(is(Class bases == super)) { 16003 foreach(base; bases) 16004 static if(is(base == class)) 16005 count += 1 + baseClassCount!base; 16006 } 16007 return count; 16008 } 16009 16010 enum int baseClassCount = helper(); 16011 } 16012 16013 private long stringToLong(string s) { 16014 long ret; 16015 if(s.length == 0) 16016 return ret; 16017 bool negative = s[0] == '-'; 16018 if(negative) 16019 s = s[1 .. $]; 16020 foreach(ch; s) { 16021 if(ch >= '0' && ch <= '9') { 16022 ret *= 10; 16023 ret += ch - '0'; 16024 } 16025 } 16026 if(negative) 16027 ret = -ret; 16028 return ret; 16029 } 16030 16031 16032 interface ReflectableProperties { 16033 /++ 16034 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 16035 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 16036 json in the current implementation. 16037 16038 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 16039 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 16040 as of the June 2, 2021 release. 16041 16042 History: 16043 Added June 2, 2021. 16044 16045 See_Also: [getPropertyAsString], [setPropertyFromString] 16046 +/ 16047 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 16048 /++ 16049 Requests a property to be delivered to you as a string, through your `sink` delegate. 16050 16051 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 16052 be interpreted as json, otherwise, it is just a plain string. 16053 16054 The sink should always be called exactly once for each call (it is basically a return value, but it might 16055 use a local buffer it maintains instead of allocating a return value). 16056 16057 History: 16058 Added June 2, 2021. 16059 16060 See_Also: [getPropertiesList], [setPropertyFromString] 16061 +/ 16062 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 16063 /++ 16064 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. 16065 16066 History: 16067 Added June 2, 2021. 16068 16069 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 16070 +/ 16071 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 16072 16073 /// [setPropertyFromString] possible return values 16074 enum SetPropertyResult { 16075 success = 0, /// the property has been successfully set to the request value 16076 notPermitted = -1, /// the property exists but it cannot be changed at this time 16077 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 16078 noSuchProperty = -3, /// there is no property by that name 16079 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 16080 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) 16081 } 16082 16083 /++ 16084 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 16085 16086 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 16087 16088 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 16089 rarely need to use these building blocks directly. 16090 +/ 16091 mixin template RegisterSetters() { 16092 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 16093 switch(name) { 16094 foreach(memberName; __traits(derivedMembers, typeof(this))) { 16095 case memberName: 16096 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 16097 if(value != "true" && value != "false") 16098 return SetPropertyResult.wrongFormat; 16099 __traits(getMember, this, memberName) = value == "true" ? true : false; 16100 return SetPropertyResult.success; 16101 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 16102 import core.stdc.stdlib; 16103 char[128] zero = 0; 16104 if(buffer.length + 1 >= zero.length) 16105 return SetPropertyResult.wrongFormat; 16106 zero[0 .. buffer.length] = buffer[]; 16107 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 16108 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 16109 import core.stdc.stdlib; 16110 char[128] zero = 0; 16111 if(buffer.length + 1 >= zero.length) 16112 return SetPropertyResult.wrongFormat; 16113 zero[0 .. buffer.length] = buffer[]; 16114 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 16115 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 16116 __traits(getMember, this, memberName) = value.idup; 16117 } else { 16118 return SetPropertyResult.notImplemented; 16119 } 16120 16121 } 16122 default: 16123 return super.setPropertyFromString(name, value, valueIsJson); 16124 } 16125 } 16126 } 16127 16128 /++ 16129 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 16130 16131 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 16132 16133 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 16134 rarely need to use these building blocks directly. 16135 +/ 16136 mixin template RegisterGetters() { 16137 override void getPropertiesList(scope void delegate(string name) sink) const { 16138 super.getPropertiesList(sink); 16139 16140 foreach(memberName; __traits(derivedMembers, typeof(this))) { 16141 sink(memberName); 16142 } 16143 } 16144 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 16145 switch(name) { 16146 foreach(memberName; __traits(derivedMembers, typeof(this))) { 16147 case memberName: 16148 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 16149 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 16150 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 16151 import core.stdc.stdio; 16152 char[32] buffer; 16153 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 16154 sink(name, buffer[0 .. len], true); 16155 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 16156 import core.stdc.stdio; 16157 char[32] buffer; 16158 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 16159 sink(name, buffer[0 .. len], true); 16160 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 16161 sink(name, __traits(getMember, this, memberName), false); 16162 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 16163 } else { 16164 sink(name, null, true); 16165 } 16166 16167 return; 16168 } 16169 default: 16170 return super.getPropertyAsString(name, sink); 16171 } 16172 } 16173 } 16174 } 16175 16176 private struct Stack(T) { 16177 this(int maxSize) { 16178 internalLength = 0; 16179 arr = initialBuffer[]; 16180 } 16181 16182 ///. 16183 void push(T t) { 16184 if(internalLength >= arr.length) { 16185 auto oldarr = arr; 16186 if(arr.length < 4096) 16187 arr = new T[arr.length * 2]; 16188 else 16189 arr = new T[arr.length + 4096]; 16190 arr[0 .. oldarr.length] = oldarr[]; 16191 } 16192 16193 arr[internalLength] = t; 16194 internalLength++; 16195 } 16196 16197 ///. 16198 T pop() { 16199 assert(internalLength); 16200 internalLength--; 16201 return arr[internalLength]; 16202 } 16203 16204 ///. 16205 T peek() { 16206 assert(internalLength); 16207 return arr[internalLength - 1]; 16208 } 16209 16210 ///. 16211 @property bool empty() { 16212 return internalLength ? false : true; 16213 } 16214 16215 ///. 16216 private T[] arr; 16217 private size_t internalLength; 16218 private T[64] initialBuffer; 16219 // 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), 16220 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 16221 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 16222 } 16223 16224 /// 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. 16225 private struct WidgetStream { 16226 16227 ///. 16228 @property Widget front() { 16229 return current.widget; 16230 } 16231 16232 /// Use Widget.tree instead. 16233 this(Widget start) { 16234 current.widget = start; 16235 current.childPosition = -1; 16236 isEmpty = false; 16237 stack = typeof(stack)(0); 16238 } 16239 16240 /* 16241 Handle it 16242 handle its children 16243 16244 */ 16245 16246 ///. 16247 void popFront() { 16248 more: 16249 if(isEmpty) return; 16250 16251 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 16252 16253 current.childPosition++; 16254 if(current.childPosition >= current.widget.children.length) { 16255 if(stack.empty()) 16256 isEmpty = true; 16257 else { 16258 current = stack.pop(); 16259 goto more; 16260 } 16261 } else { 16262 stack.push(current); 16263 current.widget = current.widget.children[current.childPosition]; 16264 current.childPosition = -1; 16265 } 16266 } 16267 16268 ///. 16269 @property bool empty() { 16270 return isEmpty; 16271 } 16272 16273 private: 16274 16275 struct Current { 16276 Widget widget; 16277 int childPosition; 16278 } 16279 16280 Current current; 16281 16282 Stack!(Current) stack; 16283 16284 bool isEmpty; 16285 } 16286 16287 16288 /+ 16289 16290 I could fix up the hierarchy kinda like this 16291 16292 class Widget { 16293 Widget[] children() { return null; } 16294 } 16295 interface WidgetContainer { 16296 Widget asWidget(); 16297 void addChild(Widget w); 16298 16299 // alias asWidget this; // but meh 16300 } 16301 16302 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 16303 16304 class Layout : Widget, WidgetContainer {} 16305 16306 class Window : WidgetContainer {} 16307 16308 16309 All constructors that previously took Widgets should now take WidgetContainers instead 16310 16311 16312 16313 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". 16314 +/ 16315 16316 /+ 16317 LAYOUTS 2.0 16318 16319 can just be assigned as a function. assigning a new one will cause it to be immediately called. 16320 16321 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 16322 16323 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 16324 16325 and even Paint can just use computedStyle... 16326 16327 background color 16328 font 16329 border color and style 16330 16331 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 16332 please note that many widgets and in some modes will completely ignore properties as they will. 16333 they are just hints you set, not promises. 16334 16335 16336 16337 16338 16339 So generally the existing virtual functions are just the default for the class. But individual objects 16340 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 16341 +/ 16342 16343 /++ 16344 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. 16345 16346 History: 16347 Added May 24, 2021. 16348 +/ 16349 struct WidgetBackground { 16350 /++ 16351 A background with the given solid color. 16352 +/ 16353 this(Color color) { 16354 this.color = color; 16355 } 16356 16357 this(WidgetBackground bg) { 16358 this = bg; 16359 } 16360 16361 /++ 16362 Creates a widget from the string. 16363 16364 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 16365 +/ 16366 static WidgetBackground fromString(string s) { 16367 return WidgetBackground(Color.fromString(s)); 16368 } 16369 16370 /++ 16371 The background is not necessarily a solid color, but you can always specify a color as a fallback. 16372 16373 History: 16374 Made `public` on December 18, 2022 (dub v10.10). 16375 +/ 16376 Color color; 16377 } 16378 16379 /++ 16380 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!) 16381 16382 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. 16383 16384 You should not inherit from this directly, but instead use [VisualTheme]. 16385 16386 History: 16387 Added May 8, 2021 16388 +/ 16389 abstract class BaseVisualTheme { 16390 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 16391 abstract void doPaint(Widget widget, WidgetPainter painter); 16392 16393 /+ 16394 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 16395 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 16396 +/ 16397 16398 /++ 16399 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 16400 where the interpretation of the string varies for each property and may include things like measurement units. 16401 +/ 16402 abstract string getPropertyString(Widget widget, string propertyName); 16403 16404 /++ 16405 Default background color of the window. Widgets also use this to simulate transparency. 16406 16407 Probably some shade of grey. 16408 +/ 16409 abstract Color windowBackgroundColor(); 16410 abstract Color widgetBackgroundColor(); 16411 abstract Color foregroundColor(); 16412 abstract Color lightAccentColor(); 16413 abstract Color darkAccentColor(); 16414 16415 /++ 16416 Colors used to indicate active selections in lists and text boxes, etc. 16417 +/ 16418 abstract Color selectionForegroundColor(); 16419 /// ditto 16420 abstract Color selectionBackgroundColor(); 16421 16422 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 16423 16424 /++ 16425 If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally. 16426 +/ 16427 abstract OperatingSystemFont defaultFont(int dpi); 16428 16429 private OperatingSystemFont[int] defaultFontCache_; 16430 private OperatingSystemFont defaultFontCached(int dpi) { 16431 if(dpi !in defaultFontCache_) { 16432 // FIXME: set this to false if X disconnect or if visual theme changes 16433 defaultFontCache_[dpi] = defaultFont(dpi); 16434 } 16435 return defaultFontCache_[dpi]; 16436 } 16437 } 16438 16439 /+ 16440 A widget should have: 16441 classList 16442 dataset 16443 attributes 16444 computedStyles 16445 state (persistent) 16446 dynamic state (focused, hover, etc) 16447 +/ 16448 16449 // visualTheme.computedStyle(this).paddingLeft 16450 16451 16452 /++ 16453 This is your entry point to create your own visual theme for custom widgets. 16454 16455 You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods. 16456 16457 Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating. 16458 +/ 16459 abstract class VisualTheme(CRTP) : BaseVisualTheme { 16460 override string getPropertyString(Widget widget, string propertyName) { 16461 return null; 16462 } 16463 16464 /+ 16465 mixin StyleOverride!Widget 16466 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 16467 w.useStyleProperties(dg); 16468 } 16469 +/ 16470 16471 final override void doPaint(Widget widget, WidgetPainter painter) { 16472 auto derived = cast(CRTP) cast(void*) this; 16473 16474 scope void delegate(Widget, WidgetPainter) bestMatch; 16475 int bestMatchScore; 16476 16477 static if(__traits(hasMember, CRTP, "paint")) 16478 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 16479 static if(is(typeof(overload) Params == __parameters)) { 16480 static assert(Params.length == 2); 16481 static assert(is(Params[0] : Widget)); 16482 static assert(is(Params[1] == WidgetPainter)); 16483 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); 16484 16485 alias type = Params[0]; 16486 if(cast(type) widget) { 16487 auto score = baseClassCount!type; 16488 16489 if(score > bestMatchScore) { 16490 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 16491 bestMatchScore = score; 16492 } 16493 } 16494 } else static assert(0, "paint should be a method."); 16495 } 16496 16497 if(bestMatch) 16498 bestMatch(widget, painter); 16499 else 16500 widget.paint(painter); 16501 } 16502 16503 deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; } 16504 16505 // 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 16506 // mixin Beautiful95Theme; 16507 mixin DefaultLightTheme; 16508 16509 private static struct Cached { 16510 // i prolly want to do this 16511 } 16512 } 16513 16514 /// ditto 16515 mixin template Beautiful95Theme() { 16516 override Color windowBackgroundColor() { return Color(212, 212, 212); } 16517 override Color widgetBackgroundColor() { return Color.white; } 16518 override Color foregroundColor() { return Color.black; } 16519 override Color darkAccentColor() { return Color(172, 172, 172); } 16520 override Color lightAccentColor() { return Color(223, 223, 223); } 16521 override Color selectionForegroundColor() { return Color.white; } 16522 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 16523 override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr 16524 } 16525 16526 /// ditto 16527 mixin template DefaultLightTheme() { 16528 override Color windowBackgroundColor() { return Color(232, 232, 232); } 16529 override Color widgetBackgroundColor() { return Color.white; } 16530 override Color foregroundColor() { return Color.black; } 16531 override Color darkAccentColor() { return Color(172, 172, 172); } 16532 override Color lightAccentColor() { return Color(223, 223, 223); } 16533 override Color selectionForegroundColor() { return Color.white; } 16534 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 16535 override OperatingSystemFont defaultFont(int dpi) { 16536 version(Windows) 16537 return new OperatingSystemFont("Segoe UI"); 16538 else static if(UsingSimpledisplayCocoa) { 16539 return (new OperatingSystemFont()).loadDefault; 16540 } else { 16541 // FIXME: undo xft's scaling so we don't end up double scaled 16542 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 16543 } 16544 } 16545 } 16546 16547 /// ditto 16548 mixin template DefaultDarkTheme() { 16549 override Color windowBackgroundColor() { return Color(64, 64, 64); } 16550 override Color widgetBackgroundColor() { return Color.black; } 16551 override Color foregroundColor() { return Color.white; } 16552 override Color darkAccentColor() { return Color(20, 20, 20); } 16553 override Color lightAccentColor() { return Color(80, 80, 80); } 16554 override Color selectionForegroundColor() { return Color.white; } 16555 override Color selectionBackgroundColor() { return Color(128, 0, 128); } 16556 override OperatingSystemFont defaultFont(int dpi) { 16557 version(Windows) 16558 return new OperatingSystemFont("Segoe UI", 12); 16559 else static if(UsingSimpledisplayCocoa) { 16560 return (new OperatingSystemFont()).loadDefault; 16561 } else { 16562 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 16563 } 16564 } 16565 } 16566 16567 /// ditto 16568 alias DefaultTheme = DefaultLightTheme; 16569 16570 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 16571 /+ 16572 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 16573 Color windowBackgroundColor() { return Color(242, 242, 242); } 16574 Color darkAccentColor() { return windowBackgroundColor; } 16575 Color lightAccentColor() { return windowBackgroundColor; } 16576 +/ 16577 } 16578 16579 /++ 16580 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 16581 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 16582 16583 History: 16584 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 16585 +/ 16586 class StateChanged(alias field) : Event { 16587 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 16588 override bool cancelable() const { return false; } 16589 this(Widget target, typeof(field) newValue) { 16590 this.newValue = newValue; 16591 super(EventString, target); 16592 } 16593 16594 typeof(field) newValue; 16595 } 16596 16597 /++ 16598 Convenience function to add a `triggered` event listener. 16599 16600 Its implementation is simply `w.addEventListener("triggered", dg);` 16601 16602 History: 16603 Added November 27, 2021 (dub v10.4) 16604 +/ 16605 void addWhenTriggered(Widget w, void delegate() dg) { 16606 w.addEventListener("triggered", dg); 16607 } 16608 16609 /++ 16610 Observable varables can be added to widgets and when they are changed, it fires 16611 off a [StateChanged] event so you can react to it. 16612 16613 It is implemented as a getter and setter property, along with another helper you 16614 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 16615 event through the usual means. Just give the name of the variable. See [StateChanged] for an 16616 example. 16617 16618 History: 16619 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 16620 +/ 16621 mixin template Observable(T, string name) { 16622 private T backing; 16623 16624 mixin(q{ 16625 void } ~ name ~ q{_changed (void delegate(T) dg) { 16626 this.addEventListener((StateChanged!this_thing ev) { 16627 dg(ev.newValue); 16628 }); 16629 } 16630 16631 @property T } ~ name ~ q{ () { 16632 return backing; 16633 } 16634 16635 @property void } ~ name ~ q{ (T t) { 16636 backing = t; 16637 auto event = new StateChanged!this_thing(this, t); 16638 event.dispatch(); 16639 } 16640 }); 16641 16642 mixin("private alias this_thing = " ~ name ~ ";"); 16643 } 16644 16645 16646 private bool startsWith(string test, string thing) { 16647 if(test.length < thing.length) 16648 return false; 16649 return test[0 .. thing.length] == thing; 16650 } 16651 16652 private bool endsWith(string test, string thing) { 16653 if(test.length < thing.length) 16654 return false; 16655 return test[$ - thing.length .. $] == thing; 16656 } 16657 16658 // still do layout delegation 16659 // and... split off Window from Widget. 16660 16661 version(minigui_screenshots) 16662 struct Screenshot { 16663 string name; 16664 } 16665 16666 version(minigui_screenshots) 16667 static if(__VERSION__ > 2092) 16668 mixin(q{ 16669 shared static this() { 16670 import core.runtime; 16671 16672 static UnitTestResult screenshotMagic() { 16673 string name; 16674 16675 import arsd.png; 16676 16677 auto results = new Window(); 16678 auto button = new Button("do it", results); 16679 16680 Window.newWindowCreated = delegate(Window w) { 16681 Timer timer; 16682 timer = new Timer(250, { 16683 auto img = w.win.takeScreenshot(); 16684 timer.destroy(); 16685 16686 version(Windows) 16687 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 16688 else 16689 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 16690 16691 w.close(); 16692 }); 16693 }; 16694 16695 button.addWhenTriggered( { 16696 16697 foreach(test; __traits(getUnitTests, mixin(__MODULE__))) { 16698 name = null; 16699 static foreach(attr; __traits(getAttributes, test)) { 16700 static if(is(typeof(attr) == Screenshot)) 16701 name = attr.name; 16702 } 16703 if(name.length) { 16704 test(); 16705 } 16706 } 16707 16708 }); 16709 16710 results.loop(); 16711 16712 return UnitTestResult(0, 0, false, false); 16713 } 16714 16715 16716 Runtime.extendedModuleUnitTester = &screenshotMagic; 16717 } 16718 }); 16719 version(minigui_screenshots) { 16720 version(unittest) 16721 void main() {} 16722 else static assert(0, "dont forget the -unittest flag to dmd"); 16723 } 16724 16725 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 16726 // FIXME: make multiple accelerators disambiguate based ona rgs 16727 // FIXME: MainWindow ctor should have same arg order as Window 16728 // FIXME: mainwindow ctor w/ client area size instead of total size. 16729 // 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. 16730 // FIXME: tri-state checkbox 16731 // FIXME: subordinate controls grouping...