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