1 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx 2 3 // if doing nested menus, make sure the straight line from where it pops up to any destination on the new popup is not going to disappear the menu until at least a delay 4 5 // me@arsd:~/.kde/share/config$ vim kdeglobals 6 7 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too. 8 9 // https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/ 10 11 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever. 12 13 // responsive minigui, menu search, and file open with a preview hook on the side. 14 15 // FIXME: add menu checkbox and menu icon eventually 16 17 /* 18 19 im tempted to add some css kind of thing to minigui. i've not done in the past cuz i have a lot of virtual functins i use but i think i have an evil plan 20 21 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 22 */ 23 24 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places 25 26 // FIXME: text label must be copyable to the clipboard, at least as a full chunk. 27 28 // FIXME: opt-in file picker widget with image support 29 30 // FIXME: number widget 31 32 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm 33 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles 34 35 // osx style menu search. 36 37 // would be cool for a scroll bar to have marking capabilities 38 // kinda like vim's marks just on clicks etc and visual representation 39 // generically. may be cool to add an up arrow to the bottom too 40 // 41 // leave a shadow of where you last were for going back easily 42 43 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 44 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 45 // the window. 46 47 // so what about context menus? 48 49 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 50 51 // FIXME: make the scroll thing go to bottom when the content changes. 52 53 // add a knob slider view... you click and go up and down so basically same as a vertical slider, just presented as a round image 54 55 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 56 57 58 // FIXME: add a command search thingy built in and implement tip. 59 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 60 61 // On Windows: 62 // FIXME: various labels look broken in high contrast mode 63 // FIXME: changing themes while the program is upen doesn't trigger a redraw 64 65 // add note about manifest to documentation. also icons. 66 67 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 68 // FIXME: clear the corner of scrollbars if they pop up 69 70 // minigui needs to have a stdout redirection for gui mode on windows writeln 71 72 // I kinda wanna do state reacting. sort of. idk tho 73 74 // need a viewer widget that works like a web page - arrows scroll down consistently 75 76 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 77 78 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 79 // and help info about menu items. 80 // and search in menus? 81 82 // FIXME: a scroll area event signaling when a thing comes into view might be good 83 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 84 85 // FIXME: unify Windows style line endings 86 87 /* 88 TODO: 89 90 pie menu 91 92 class Form with submit behavior -- see AutomaticDialog 93 94 disabled widgets and menu items 95 96 event cleanup 97 tooltips. 98 api improvements 99 100 margins are kinda broken, they don't collapse like they should. at least. 101 102 a table form btw would be a horizontal layout of vertical layouts holding each column 103 that would give the same width things 104 */ 105 106 /* 107 108 1(15:19:48) NotSpooky: Menus, text entry, label, notebook, box, frame, file dialogs and layout (this one is very useful because I can draw lines between its child widgets 109 */ 110 111 /++ 112 minigui is a smallish GUI widget library, aiming to be on par with at least 113 HTML4 forms and a few other expected gui components. It uses native controls 114 on Windows and does its own thing on Linux (Mac is not currently supported but 115 may be later, and should use native controls) to keep size down. The Linux 116 appearance is similar to Windows 95 and avoids using images to maintain network 117 efficiency on remote X connections, though you can customize that. 118 119 120 minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color], 121 on which it is built. simpledisplay provides the low-level interfaces and minigui 122 builds the concept of widgets inside the windows on top of it. 123 124 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 125 It isn't hugely concerned with appearance - on Windows, it just uses the native 126 controls and native theme, and on Linux, it keeps it simple and I may change that 127 at any time, though after May 2021, you can customize some things with css-inspired 128 [Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`, 129 you can use the custom implementation there too, but... you shouldn't.) 130 131 The event model is similar to what you use in the browser with Javascript and the 132 layout engine tries to automatically fit things in, similar to a css flexbox. 133 134 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 135 `-L/SUBSYSTEM:WINDOWS:5.0`, for example, because otherwise you'll get a 136 console and other visual bugs. 137 138 HTML_To_Classes: 139 $(SMALL_TABLE 140 HTML Code | Minigui Class 141 142 `<input type="text">` | [LineEdit] 143 `<textarea>` | [TextEdit] 144 `<select>` | [DropDownSelection] 145 `<input type="checkbox">` | [Checkbox] 146 `<input type="radio">` | [Radiobox] 147 `<button>` | [Button] 148 ) 149 150 151 Stretchiness: 152 The default is 4. You can use larger numbers for things that should 153 consume a lot of space, and lower numbers for ones that are better at 154 smaller sizes. 155 156 Overlapped_input: 157 COMING EVENTUALLY: 158 minigui will include a little bit of I/O functionality that just works 159 with the event loop. If you want to get fancy, I suggest spinning up 160 another thread and posting events back and forth. 161 162 $(H2 Add ons) 163 See the `minigui_addons` directory in the arsd repo for some add on widgets 164 you can import separately too. 165 166 $(H3 XML definitions) 167 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 168 169 $(H3 Scriptability) 170 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 171 in this documentation, it means you can call it from the script language. 172 173 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 174 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 175 176 --- 177 import arsd.minigui_xml; 178 import arsd.script; 179 180 var globals = var.emptyObject; 181 globals.makeWidgetFromString = &makeWidgetFromString; 182 183 // this now works 184 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 185 --- 186 187 More to come. 188 189 History: 190 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 191 192 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 193 tag this as version 2.0. 194 195 Among the changes: 196 $(LIST 197 * The event model changed to prefer strongly-typed events, though the Javascript string style ones still work, using properties off them is deprecated. It will still compile and function, but you should change the handler to use the classes in its argument list. I adapted my code to use the new model in just a few minutes, so it shouldn't too hard. 198 199 See [Event] for details. 200 201 * A [DoubleClickEvent] was added. Previously, you'd get two rapidly repeated click events. Now, you get one click event followed by a double click event. If you must recreate the old way exactly, you can listen for a DoubleClickEvent, set a flag upon receiving one, then send yourself a synthetic ClickEvent on the next MouseUpEvent, but your program might be better served just working with [MouseDownEvent]s instead. 202 203 See [DoubleClickEvent] for details. 204 205 * Styling hints were added, and the few that existed before have been moved to a new helper class. Deprecated forwarders exist for the (few) old properties to help you transition. Note that most of these only affect a `custom_events` build, which is the default on Linux, but opt in only on Windows. 206 207 See [Widget.Style] for details. 208 209 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 210 211 * Widgets now draw their keyboard focus by default instead of opt in. You may wish to set `tabStop = false;` if it wasn't supposed to receive it. 212 213 * Most Widget constructors no longer have a default `parent` argument. You must pass the parent to almost all widgets, or in rare cases, an explict `null`, but more often than not, you need the parent so the default argument was not very useful at best and misleading to a crash at worst. 214 215 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 216 217 * Several conversions of public fields to properties, deprecated, or made private. It is unlikely this will affect you, but the compiler will tell you if it does. 218 219 * Various non-breaking additions. 220 ) 221 +/ 222 module arsd.minigui; 223 224 /++ 225 This hello world sample will have an oversized button, but that's ok, you see your first window! 226 +/ 227 version(Demo) 228 unittest { 229 import arsd.minigui; 230 231 void main() { 232 auto window = new MainWindow(); 233 234 // note the parent widget is almost always passed as the last argument to a constructor 235 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 236 auto button = new Button("Close", window); 237 button.addWhenTriggered({ 238 window.close(); 239 }); 240 241 window.loop(); 242 } 243 244 main(); // exclude from docs 245 } 246 247 /++ 248 This example shows one way you can partition your window into a header 249 and sidebar. Here, the header and sidebar have a fixed width, while the 250 rest of the content sizes with the window. 251 252 It might be a new way of thinking about window layout to do things this 253 way - perhaps [GridLayout] more matches your style of thought - but the 254 concept here is to partition the window into sub-boxes with a particular 255 size, then partition those boxes into further boxes. 256 257 $(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.) 258 259 So to make the header, start with a child layout that has a max height. 260 It will use that space from the top, then the remaining children will 261 split the remaining area, meaning you can think of is as just being another 262 box you can split again. Keep splitting until you have the look you desire. 263 +/ 264 // https://github.com/adamdruppe/arsd/issues/310 265 version(minigui_screenshots) 266 @Screenshot("layout") 267 unittest { 268 import arsd.minigui; 269 270 // This helper class is just to help make the layout boxes visible. 271 // think of it like a <div style="background-color: whatever;"></div> in HTML. 272 class ColorWidget : Widget { 273 this(Color color, Widget parent) { 274 this.color = color; 275 super(parent); 276 } 277 Color color; 278 class Style : Widget.Style { 279 override WidgetBackground background() { return WidgetBackground(color); } 280 } 281 mixin OverrideStyle!Style; 282 } 283 284 void main() { 285 auto window = new Window; 286 287 // the key is to give it a max height. This is one way to do it: 288 auto header = new class HorizontalLayout { 289 this() { super(window); } 290 override int maxHeight() { return 50; } 291 }; 292 // this next line is a shortcut way of doing it too, but it only works 293 // for HorizontalLayout and VerticalLayout, and is less explicit, so it 294 // is good to know how to make a new class like above anyway. 295 // auto header = new HorizontalLayout(50, window); 296 297 auto bar = new HorizontalLayout(window); 298 299 // or since this is so common, VerticalLayout and HorizontalLayout both 300 // can just take an argument in their constructor for max width/height respectively 301 302 // (could have tone this above too, but I wanted to demo both techniques) 303 auto left = new VerticalLayout(100, bar); 304 305 // and this is the main section's container. A plain Widget instance is good enough here. 306 auto container = new Widget(bar); 307 308 // and these just add color to the containers we made above for the screenshot. 309 // in a real application, you can just add your actual controls instead of these. 310 auto headerColorBox = new ColorWidget(Color.teal, header); 311 auto leftColorBox = new ColorWidget(Color.green, left); 312 auto rightColorBox = new ColorWidget(Color.purple, container); 313 314 window.loop(); 315 } 316 317 main(); // exclude from docs 318 } 319 320 321 import arsd.core; 322 public import arsd.simpledisplay; 323 /++ 324 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 325 326 History: 327 Was private until May 15, 2021. 328 +/ 329 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 330 331 version(Windows) { 332 import core.sys.windows.winnls; 333 import core.sys.windows.windef; 334 import core.sys.windows.basetyps; 335 import core.sys.windows.winbase; 336 import core.sys.windows.winuser; 337 import core.sys.windows.wingdi; 338 static import gdi = core.sys.windows.wingdi; 339 } 340 341 version(Windows) { 342 version(minigui_manifest) {} else version=minigui_no_manifest; 343 344 version(minigui_no_manifest) {} else 345 static if(__VERSION__ >= 2_083) 346 version(CRuntime_Microsoft) { // FIXME: mingw? 347 // assume we want commctrl6 whenever possible since there's really no reason not to 348 // and this avoids some of the manifest hassle 349 pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""); 350 } 351 } 352 353 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 354 private bool lastDefaultPrevented; 355 356 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 357 alias scriptable = arsd_jsvar_compatible; 358 359 version(Windows) { 360 // use native widgets when available unless specifically asked otherwise 361 version(custom_widgets) { 362 enum bool UsingCustomWidgets = true; 363 enum bool UsingWin32Widgets = false; 364 } else { 365 version = win32_widgets; 366 enum bool UsingCustomWidgets = false; 367 enum bool UsingWin32Widgets = true; 368 } 369 // and native theming when needed 370 //version = win32_theming; 371 } else { 372 enum bool UsingCustomWidgets = true; 373 enum bool UsingWin32Widgets = false; 374 version=custom_widgets; 375 } 376 377 378 379 /* 380 381 The main goals of minigui.d are to: 382 1) Provide basic widgets that just work in a lightweight lib. 383 I basically want things comparable to a plain HTML form, 384 plus the easy and obvious things you expect from Windows 385 apps like a menu. 386 2) Use native things when possible for best functionality with 387 least library weight. 388 3) Give building blocks to provide easy extension for your 389 custom widgets, or hooking into additional native widgets 390 I didn't wrap. 391 4) Provide interfaces for easy interaction between third 392 party minigui extensions. (event model, perhaps 393 signals/slots, drop-in ease of use bits.) 394 5) Zero non-system dependencies, including Phobos as much as 395 I reasonably can. It must only import arsd.color and 396 my simpledisplay.d. If you need more, it will have to be 397 an extension module. 398 6) An easy layout system that generally works. 399 400 A stretch goal is to make it easy to make gui forms with code, 401 some kind of resource file (xml?) and even a wysiwyg designer. 402 403 Another stretch goal is to make it easy to hook data into the gui, 404 including from reflection. So like auto-generate a form from a 405 function signature or struct definition, or show a list from an 406 array that automatically updates as the array is changed. Then, 407 your program focuses on the data more than the gui interaction. 408 409 410 411 STILL NEEDED: 412 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 413 * slider 414 * listbox 415 * spinner 416 * label? 417 * rich text 418 */ 419 420 421 /+ 422 enum LayoutMethods { 423 verticalFlex, 424 horizontalFlex, 425 inlineBlock, // left to right, no stretch, goes to next line as needed 426 static, // just set to x, y 427 verticalNoStretch, // browser style default 428 429 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 430 431 grid, // magic 432 } 433 +/ 434 435 /++ 436 The `Widget` is the base class for minigui's functionality, ranging from UI components like checkboxes or text displays to abstract groupings of other widgets like a layout container or a html `<div>`. You will likely want to use pre-made widgets as well as creating your own. 437 438 439 To create your own widget, you must inherit from it and create a constructor that passes a parent to `super`. Everything else after that is optional. 440 441 --- 442 class MinimalWidget : Widget { 443 this(Widget parent) { 444 super(parent); 445 } 446 } 447 --- 448 449 $(SIDEBAR 450 I'm not entirely happy with leaf, container, and windows all coming from the same base Widget class, but I so far haven't thought of a better solution that's good enough to justify the breakage of a transition. It hasn't been a major problem in practice anyway. 451 ) 452 453 Broadly, there's two kinds of widgets: leaf widgets, which are intended to be the direct user-interactive components, and container widgets, which organize, lay out, and aggregate other widgets in the object tree. A special case of a container widget is [Window], which represents a separate top-level window on the screen. Both leaf and container widgets inherit from `Widget`, so this distinction is more conventional than formal. 454 455 Among the things you'll most likely want to change in your custom widget: 456 457 $(LIST 458 * In your constructor, set `tabStop = false;` if the widget is not supposed to receive keyboard focus. (Please note its childen still can, so `tabStop = false;` is appropriate on most container widgets.) 459 460 You may explicitly set `tabStop = true;` to ensure you get it, even against future changes to the library, though that's the default right now. 461 462 Do this $(I after) calling the `super` constructor. 463 464 * Override [paint] if you want full control of the widget's drawing area (except the area obscured by children!), or [paintContent] if you want to participate in the styling engine's system. You'll also possibly want to make a subclass of [Style] and use [OverrideStyle] to change the default hints given to the styling engine for widget. 465 466 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 467 468 * Override default event handlers with your behavior. For example [defaultEventHandler_click] may be overridden to make clicks do something. Again, this is generally a job for leaf widgets rather than containers; most events are dispatched to the lowest leaf on the widget tree, but they also pass through all their parents. See [Event] for more details about the event model. 469 470 * You may also want to override the various layout hints like [minWidth], [maxHeight], etc. In particular [Padding] and [Margin] are often relevant for both container and leaf widgets and the default values of 0 are often not what you want. 471 ) 472 473 On Microsoft Windows, many widgets are also based on native controls. You can also do this if `static if(UsingWin32Widgets)` passes. You should use the helper function [createWin32Window] to create the window and let minigui do what it needs to do to create its bridge structures. This will populate [Widget.hwnd] which you can access later for communcating with the native window. You may also consider overriding [Widget.handleWmCommand] and [Widget.handleWmNotify] for the widget to translate those messages into appropriate minigui [Event]s. 474 475 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 476 477 Your own custom-drawn and native system controls can exist side-by-side. 478 479 Later I'll add more complete examples, but for now [TextLabel] and [LabeledPasswordEdit] are both simple widgets you can view implementation to get some ideas. 480 +/ 481 class Widget : ReflectableProperties { 482 483 private bool willDraw() { 484 return true; 485 } 486 487 /+ 488 /++ 489 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 490 491 History: 492 Added September 15, 2021 493 implemented.... ??? 494 +/ 495 void prepareReflection(this This)() { 496 497 } 498 +/ 499 500 private bool _enabled = true; 501 502 /++ 503 Determines whether the control is marked enabled. Disabled controls are generally displayed as greyed out and clicking on them does nothing. It is also possible for a control to be disabled because its parent is disabled, in which case this will still return `true`, but setting `enabled = true` may have no effect. Check [disabledBy] to see which parent caused it to be disabled. 504 505 I also recommend you set a [disabledReason] if you chose to set `enabled = false` to tell the user why the control does not work and what they can do to enable it. 506 507 History: 508 Added November 23, 2021 (dub v10.4) 509 510 Warning: the specific behavior of disabling with parents may change in the future. 511 Bugs: 512 Currently only implemented for widgets backed by native Windows controls. 513 514 See_Also: [disabledReason], [disabledBy] 515 +/ 516 @property bool enabled() { 517 return disabledBy() is null; 518 } 519 520 /// ditto 521 @property void enabled(bool yes) { 522 _enabled = yes; 523 version(win32_widgets) { 524 if(hwnd) 525 EnableWindow(hwnd, yes); 526 } 527 setDynamicState(DynamicState.disabled, yes); 528 } 529 530 private string disabledReason_; 531 532 /++ 533 If the widget is not [enabled] this string may be presented to the user when they try to use it. The exact manner and time it gets displayed is up to the implementation of the control. 534 535 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 536 537 History: 538 Added November 23, 2021 (dub v10.4) 539 See_Also: [enabled], [disabledBy] 540 +/ 541 @property string disabledReason() { 542 auto w = disabledBy(); 543 return (w is null) ? null : w.disabledReason_; 544 } 545 546 /// ditto 547 @property void disabledReason(string reason) { 548 disabledReason_ = reason; 549 } 550 551 /++ 552 Returns the widget that disabled this. It might be this or one of its parents all the way up the chain, or `null` if the widget is not disabled by anything. You can check [disabledReason] on the return value (after the null check!) to get a hint to display to the user. 553 554 History: 555 Added November 25, 2021 (dub v10.4) 556 See_Also: [enabled], [disabledReason] 557 +/ 558 Widget disabledBy() { 559 Widget p = this; 560 while(p) { 561 if(!p._enabled) 562 return p; 563 p = p.parent; 564 } 565 return null; 566 } 567 568 /// Implementations of [ReflectableProperties] interface. See the interface for details. 569 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 570 if(valueIsJson) 571 return SetPropertyResult.wrongFormat; 572 switch(name) { 573 case "name": 574 this.name = value.idup; 575 return SetPropertyResult.success; 576 case "statusTip": 577 this.statusTip = value.idup; 578 return SetPropertyResult.success; 579 default: 580 return SetPropertyResult.noSuchProperty; 581 } 582 } 583 /// ditto 584 void getPropertiesList(scope void delegate(string name) sink) const { 585 sink("name"); 586 sink("statusTip"); 587 } 588 /// ditto 589 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 590 switch(name) { 591 case "name": 592 sink(name, this.name, false); 593 return; 594 case "statusTip": 595 sink(name, this.statusTip, false); 596 return; 597 default: 598 sink(name, null, true); 599 } 600 } 601 602 /++ 603 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 604 605 History: 606 Added November 25, 2021 (dub v10.5) 607 `Point` overload added January 12, 2022 (dub v10.6) 608 +/ 609 int scaleWithDpi(int value, int assumedDpi = 96) { 610 // avoid potential overflow with common special values 611 if(value == int.max) 612 return int.max; 613 if(value == int.min) 614 return int.min; 615 if(value == 0) 616 return 0; 617 return value * currentDpi(assumedDpi) / assumedDpi; 618 } 619 620 /// ditto 621 Point scaleWithDpi(Point value, int assumedDpi = 96) { 622 return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi)); 623 } 624 625 /++ 626 Returns the current scaling factor as a logical dpi value for this widget. Generally speaking, this divided by 96 gives you the user scaling factor. 627 628 Not entirely stable. 629 630 History: 631 Added August 25, 2023 (dub v11.1) 632 +/ 633 final int currentDpi(int assumedDpi = 96) { 634 // assert(parentWindow !is null); 635 // assert(parentWindow.win !is null); 636 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 637 //divide = 138; // to test 1.5x 638 // for lower values it is something i don't really want changed anyway since it is an old monitor and you don't want to scale down. 639 // this also covers the case when actualDpi returns 0. 640 if(divide < 96) 641 divide = 96; 642 return divide; 643 } 644 645 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 646 // I'll think up something better eventually 647 648 // FIXME: the defaultLineHeight should probably be removed and replaced with the calculations on the outside based on defaultTextHeight. 649 protected final int defaultLineHeight() { 650 auto cs = getComputedStyle(); 651 if(cs.font && !cs.font.isNull) 652 return cs.font.height() * 5 / 4; 653 else 654 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * 5/4); 655 } 656 657 /++ 658 659 History: 660 Added August 25, 2023 (dub v11.1) 661 +/ 662 protected final int defaultTextHeight(int numberOfLines = 1) { 663 auto cs = getComputedStyle(); 664 if(cs.font && !cs.font.isNull) 665 return cs.font.height() * numberOfLines; 666 else 667 return Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * numberOfLines; 668 } 669 670 protected final int defaultTextWidth(const(char)[] text) { 671 auto cs = getComputedStyle(); 672 if(cs.font && !cs.font.isNull) 673 return cs.font.stringWidth(text); 674 else 675 return scaleWithDpi(Window.lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback * cast(int) text.length / 2); 676 } 677 678 /++ 679 If `encapsulatedChildren` returns true, it changes the event handling mechanism to act as if events from the child widgets are actually targeted on this widget. 680 681 The idea is then you can use child widgets as part of your implementation, but not expose those details through the event system; if someone checks the mouse coordinates and target of the event once it bubbles past you, it will show as it it came from you. 682 683 History: 684 Added May 22, 2021 685 +/ 686 protected bool encapsulatedChildren() { 687 return false; 688 } 689 690 private void privateDpiChanged() { 691 dpiChanged(); 692 foreach(child; children) 693 child.privateDpiChanged(); 694 } 695 696 /++ 697 Virtual hook to update any caches or fonts you need on the event of a dpi scaling change. 698 699 History: 700 Added January 12, 2022 (dub v10.6) 701 +/ 702 protected void dpiChanged() { 703 704 } 705 706 // Default layout properties { 707 708 int minWidth() { return 0; } 709 int minHeight() { 710 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 711 int sum = this.paddingTop + this.paddingBottom; 712 foreach(child; children) { 713 if(child.hidden) 714 continue; 715 sum += child.minHeight(); 716 sum += child.marginTop(); 717 sum += child.marginBottom(); 718 } 719 720 return sum; 721 } 722 int maxWidth() { return int.max; } 723 int maxHeight() { return int.max; } 724 int widthStretchiness() { return 4; } 725 int heightStretchiness() { return 4; } 726 727 /++ 728 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 729 730 History: 731 Added June 15, 2021 (dub v10.1) 732 +/ 733 int widthShrinkiness() { return 0; } 734 /// ditto 735 int heightShrinkiness() { return 0; } 736 737 /++ 738 The initial size of the widget for layout calculations. Default is 0. 739 740 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 741 742 History: 743 Added June 15, 2021 (dub v10.1) 744 +/ 745 int flexBasisWidth() { return 0; } 746 /// ditto 747 int flexBasisHeight() { return 0; } 748 749 /++ 750 Not stable. 751 752 Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored. 753 754 So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6. 755 756 History: 757 Added January 5, 2023 758 +/ 759 Rectangle defaultMargin; 760 /// ditto 761 Rectangle defaultPadding; 762 763 int marginLeft() { return scaleWithDpi(defaultMargin.left); } 764 int marginRight() { return scaleWithDpi(defaultMargin.right); } 765 int marginTop() { return scaleWithDpi(defaultMargin.top); } 766 int marginBottom() { return scaleWithDpi(defaultMargin.bottom); } 767 int paddingLeft() { return scaleWithDpi(defaultPadding.left); } 768 int paddingRight() { return scaleWithDpi(defaultPadding.right); } 769 int paddingTop() { return scaleWithDpi(defaultPadding.top); } 770 int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); } 771 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 772 773 private bool recomputeChildLayoutRequired = true; 774 private static class RecomputeEvent {} 775 private __gshared rce = new RecomputeEvent(); 776 protected final void queueRecomputeChildLayout() { 777 recomputeChildLayoutRequired = true; 778 779 if(this.parentWindow) { 780 auto sw = this.parentWindow.win; 781 assert(sw !is null); 782 if(!sw.eventQueued!RecomputeEvent) { 783 sw.postEvent(rce); 784 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 785 } 786 } 787 788 } 789 790 protected final void recomputeChildLayoutEntry() { 791 if(recomputeChildLayoutRequired) { 792 recomputeChildLayout(); 793 recomputeChildLayoutRequired = false; 794 redraw(); 795 } else { 796 // I still need to check the tree just in case one of them was queued up 797 // and the event came up here instead of there. 798 foreach(child; children) 799 child.recomputeChildLayoutEntry(); 800 } 801 } 802 803 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 804 void recomputeChildLayout() { 805 .recomputeChildLayout!"height"(this); 806 } 807 808 // } 809 810 811 /++ 812 Returns the style's tag name string this object uses. 813 814 The default is to use the typeid() name trimmed down to whatever is after the last dot which is typically the identifier of the class. 815 816 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 817 818 History: 819 Added May 10, 2021 820 +/ 821 string styleTagName() const { 822 string n = typeid(this).name; 823 foreach_reverse(idx, ch; n) 824 if(ch == '.') { 825 n = n[idx + 1 .. $]; 826 break; 827 } 828 return n; 829 } 830 831 /// API for the [styleClassList] 832 static struct ClassList { 833 private Widget widget; 834 835 /// 836 void add(string s) { 837 widget.styleClassList_ ~= s; 838 } 839 840 /// 841 void remove(string s) { 842 foreach(idx, s1; widget.styleClassList_) 843 if(s1 == s) { 844 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 845 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 846 widget.styleClassList_.assumeSafeAppend(); 847 return; 848 } 849 } 850 851 /// Returns true if it was added, false if it was removed. 852 bool toggle(string s) { 853 if(contains(s)) { 854 remove(s); 855 return false; 856 } else { 857 add(s); 858 return true; 859 } 860 } 861 862 /// 863 bool contains(string s) const { 864 foreach(s1; widget.styleClassList_) 865 if(s1 == s) 866 return true; 867 return false; 868 869 } 870 } 871 872 private string[] styleClassList_; 873 874 /++ 875 Returns a "class list" that can be used by the visual theme's style engine via [VisualTheme.getPropertyString] if it chooses to do something like CSS. 876 877 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 878 879 History: 880 Added May 10, 2021 881 +/ 882 inout(ClassList) styleClassList() inout { 883 return cast(inout(ClassList)) ClassList(cast() this); 884 } 885 886 /++ 887 List of dynamic states made available to the style engine, for cases like CSS pseudo-classes and also used by default paint methods. It is stored in a 64 bit variable attached to the widget that you can update. The style cache is aware of the fact that these can frequently change. 888 889 The lower 32 bits are defined here or reserved for future use by the library. You should keep these updated if you reasonably can on custom widgets if they apply to you, but don't use them for a purpose they aren't defined for. 890 891 The upper 32 bits are available for your own extensions. 892 893 History: 894 Added May 10, 2021 895 +/ 896 enum DynamicState : ulong { 897 focus = (1 << 0), /// the widget currently has the keyboard focus 898 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 899 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!) 900 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!) 901 checked = (1 << 4), /// the widget is toggleable and currently toggled on 902 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 903 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 904 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 905 depressed = (1 << 8), /// the widget is being actively pressed or clicked (compare to css `:active`). Can be combined with hover to visually indicate if a mouse up would result in a click event. 906 907 USER_BEGIN = (1UL << 32), 908 } 909 910 // I want to add the primary and cancel styles to buttons at least at some point somehow. 911 912 /// ditto 913 @property ulong dynamicState() { return dynamicState_; } 914 /// ditto 915 @property ulong dynamicState(ulong newValue) { 916 if(dynamicState != newValue) { 917 auto old = dynamicState_; 918 dynamicState_ = newValue; 919 920 useStyleProperties((scope Widget.Style s) { 921 if(s.variesWithState(old ^ newValue)) 922 redraw(); 923 }); 924 } 925 return dynamicState_; 926 } 927 928 /// ditto 929 void setDynamicState(ulong flags, bool state) { 930 auto ds = dynamicState_; 931 if(state) 932 ds |= flags; 933 else 934 ds &= ~flags; 935 936 dynamicState = ds; 937 } 938 939 private ulong dynamicState_; 940 941 deprecated("Use dynamic styles instead now") { 942 Color backgroundColor() { return backgroundColor_; } 943 void backgroundColor(Color c){ this.backgroundColor_ = c; } 944 945 MouseCursor cursor() { return GenericCursor.Default; } 946 } private Color backgroundColor_ = Color.transparent; 947 948 949 /++ 950 Style properties are defined as an accessory class so they can be referenced and overridden independently, but they are nested so you can refer to them easily by name (e.g. generic `Widget.Style` vs `Button.Style` and such). 951 952 It is here so there can be a specificity switch. 953 954 See [OverrideStyle] for a helper function to use your own. 955 956 History: 957 Added May 11, 2021 958 +/ 959 static class Style/* : StyleProperties*/ { 960 public Widget widget; // public because the mixin template needs access to it 961 962 /++ 963 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 964 965 History: 966 Added May 11, 2021, but changed on July 2, 2021 to return false by default. You MUST override this if you want declarative hover effects etc to take effect. 967 +/ 968 bool variesWithState(ulong dynamicStateFlags) { 969 version(win32_widgets) { 970 if(widget.hwnd) 971 return false; 972 } 973 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 974 } 975 976 /// 977 Color foregroundColor() { 978 return WidgetPainter.visualTheme.foregroundColor; 979 } 980 981 /// 982 WidgetBackground background() { 983 // the default is a "transparent" background, which means 984 // it goes as far up as it can to get the color 985 if (widget.backgroundColor_ != Color.transparent) 986 return WidgetBackground(widget.backgroundColor_); 987 if (widget.parent) 988 return widget.parent.getComputedStyle.background; 989 return WidgetBackground(widget.backgroundColor_); 990 } 991 992 private static OperatingSystemFont fontCached_; 993 private OperatingSystemFont fontCached() { 994 if(fontCached_ is null) 995 fontCached_ = font(); 996 return fontCached_; 997 } 998 999 /++ 1000 Returns the default font to be used with this widget. The return value will be cached by the library, so you can not expect live updates. 1001 +/ 1002 OperatingSystemFont font() { 1003 return null; 1004 } 1005 1006 /++ 1007 Returns the cursor that should be used over this widget. You may change this and updates will be reflected next time the mouse enters the widget. 1008 1009 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 1010 1011 History: 1012 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 1013 +/ 1014 MouseCursor cursor() { 1015 return GenericCursor.Default; 1016 } 1017 1018 FrameStyle borderStyle() { 1019 return FrameStyle.none; 1020 } 1021 1022 /++ 1023 +/ 1024 Color borderColor() { 1025 return Color.transparent; 1026 } 1027 1028 FrameStyle outlineStyle() { 1029 if(widget.dynamicState & DynamicState.focus) 1030 return FrameStyle.dotted; 1031 else 1032 return FrameStyle.none; 1033 } 1034 1035 Color outlineColor() { 1036 return foregroundColor; 1037 } 1038 } 1039 1040 /++ 1041 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 1042 The basic usage is simple: 1043 1044 --- 1045 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 1046 // override style hints as-needed here 1047 } 1048 OverrideStyle!Style; // add the method 1049 --- 1050 1051 $(TIP 1052 While the class is not forced to be `static`, for best results, it should be. A non-static class 1053 can not be inherited by other objects whereas the static one can. A property on the base class, 1054 called [Widget.Style.widget|widget], is available for you to access its properties. 1055 ) 1056 1057 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 1058 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 1059 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 1060 1061 1062 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 1063 You may also just override `variesWithState` when you use this flag. 1064 1065 --- 1066 mixin OverrideStyle!( 1067 DynamicState.focus, YourFocusedStyle, 1068 DynamicState.hover, YourHoverStyle, 1069 YourDefaultStyle 1070 ) 1071 --- 1072 1073 It checks if `dynamicState` matches the state and if so, returns the object given. 1074 1075 If there is no state mask given, the next one matches everything. The first match given is used. 1076 1077 However, since in most cases you'll want check state inside your individual methods, you probably won't 1078 find much use for this whole-class swap out. 1079 1080 History: 1081 Added May 16, 2021 1082 +/ 1083 static protected mixin template OverrideStyle(S...) { 1084 static import amg = arsd.minigui; 1085 override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) { 1086 ulong mask = 0; 1087 foreach(idx, thing; S) { 1088 static if(is(typeof(thing) : ulong)) { 1089 mask = thing; 1090 } else { 1091 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 1092 //static assert(!__traits(isNested, thing), thing.stringof ~ " is a nested class. For best results, mark it `static`. You can still access the widget through a `widget` variable inside the Style class."); 1093 scope amg.Widget.Style s = new thing(); 1094 s.widget = this; 1095 dg(s); 1096 return; 1097 } 1098 } 1099 } 1100 } 1101 } 1102 /++ 1103 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 1104 +/ 1105 void useStyleProperties(scope void delegate(scope Style props) dg) { 1106 scope Style s = new Style(); 1107 s.widget = this; 1108 dg(s); 1109 } 1110 1111 1112 protected void sendResizeEvent() { 1113 this.emit!ResizeEvent(); 1114 } 1115 1116 Menu contextMenu(int x, int y) { return null; } 1117 1118 final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) { 1119 if(parentWindow is null || parentWindow.win is null) return false; 1120 1121 auto menu = this.contextMenu(x, y); 1122 if(menu is null) 1123 return false; 1124 1125 version(win32_widgets) { 1126 // FIXME: if it is -1, -1, do it at the current selection location instead 1127 // tho the corner of the window, whcih it does now, isn't the literal worst. 1128 1129 if(screenX < 0 && screenY < 0) { 1130 auto p = this.globalCoordinates(); 1131 if(screenX == -2) 1132 p.x += x; 1133 if(screenY == -2) 1134 p.y += y; 1135 1136 screenX = p.x; 1137 screenY = p.y; 1138 } 1139 1140 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1141 throw new Exception("TrackContextMenuEx"); 1142 } else version(custom_widgets) { 1143 menu.popup(this, x, y); 1144 } 1145 1146 return true; 1147 } 1148 1149 /++ 1150 Removes this widget from its parent. 1151 1152 History: 1153 `removeWidget` was made `final` on May 11, 2021. 1154 +/ 1155 @scriptable 1156 final void removeWidget() { 1157 auto p = this.parent; 1158 if(p) { 1159 int item; 1160 for(item = 0; item < p._children.length; item++) 1161 if(p._children[item] is this) 1162 break; 1163 auto idx = item; 1164 for(; item < p._children.length - 1; item++) 1165 p._children[item] = p._children[item + 1]; 1166 p._children = p._children[0 .. $-1]; 1167 1168 this.parent.widgetRemoved(idx, this); 1169 //this.parent = null; 1170 1171 p.queueRecomputeChildLayout(); 1172 } 1173 version(win32_widgets) { 1174 removeAllChildren(); 1175 if(hwnd) { 1176 DestroyWindow(hwnd); 1177 hwnd = null; 1178 } 1179 } 1180 } 1181 1182 /++ 1183 Notifies the subclass that a widget was removed. If you keep auxillary data about your children, you can override this to help keep that data in sync. 1184 1185 History: 1186 Added September 19, 2021 1187 +/ 1188 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 1189 1190 /++ 1191 Removes all child widgets from `this`. You should not use the removed widgets again. 1192 1193 Note that on Windows, it also destroys the native handles for the removed children recursively. 1194 1195 History: 1196 Added July 1, 2021 (dub v10.2) 1197 +/ 1198 void removeAllChildren() { 1199 version(win32_widgets) 1200 foreach(child; _children) { 1201 child.removeAllChildren(); 1202 if(child.hwnd) { 1203 DestroyWindow(child.hwnd); 1204 child.hwnd = null; 1205 } 1206 } 1207 auto orig = this._children; 1208 this._children = null; 1209 foreach(idx, w; orig) 1210 this.widgetRemoved(idx, w); 1211 1212 queueRecomputeChildLayout(); 1213 } 1214 1215 /++ 1216 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1217 +/ 1218 @scriptable 1219 Widget getChildByName(string name) { 1220 return getByName(name); 1221 } 1222 /++ 1223 Finds the nearest descendant with the requested type and [name]. May return `this`. 1224 +/ 1225 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1226 if(this.name == name) 1227 if(auto c = cast(WidgetClass) this) 1228 return c; 1229 foreach(child; children) { 1230 auto w = child.getByName(name); 1231 if(auto c = cast(WidgetClass) w) 1232 return c; 1233 } 1234 return null; 1235 } 1236 1237 /++ 1238 The name is a string tag that is used to reference the widget from scripts, gui loaders, declarative ui templates, etc. Similar to a HTML id attribute. 1239 Names should be unique in a window. 1240 1241 See_Also: [getByName], [getChildByName] 1242 +/ 1243 @scriptable string name; 1244 1245 private EventHandler[][string] bubblingEventHandlers; 1246 private EventHandler[][string] capturingEventHandlers; 1247 1248 /++ 1249 Default event handlers. These are called on the appropriate 1250 event unless [Event.preventDefault] is called on the event at 1251 some point through the bubbling process. 1252 1253 1254 If you are implementing your own widget and want to add custom 1255 events, you should follow the same pattern here: create a virtual 1256 function named `defaultEventHandler_eventname` with the implementation, 1257 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1258 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1259 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1260 This ensures virtual dispatch based on the correct subclass. 1261 1262 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1263 overridden version. 1264 1265 You only need to do that on parent classes adding NEW event types. If you 1266 just want to change the default behavior of an existing event type in a subclass, 1267 you override the function (and optionally call `super.method_name`) like normal. 1268 1269 +/ 1270 protected EventHandler[string] defaultEventHandlers; 1271 1272 /// ditto 1273 void setupDefaultEventHandlers() { 1274 defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); }; 1275 defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); }; 1276 defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); }; 1277 defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); }; 1278 defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); }; 1279 defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); }; 1280 defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); }; 1281 defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); }; 1282 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); }; 1283 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); }; 1284 defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); }; 1285 defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); }; 1286 defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); }; 1287 defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); }; 1288 defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); }; 1289 defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); }; 1290 defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); }; 1291 defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); }; 1292 } 1293 1294 /// ditto 1295 void defaultEventHandler_click(ClickEvent event) {} 1296 /// ditto 1297 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1298 /// ditto 1299 void defaultEventHandler_keydown(KeyDownEvent event) {} 1300 /// ditto 1301 void defaultEventHandler_keyup(KeyUpEvent event) {} 1302 /// ditto 1303 void defaultEventHandler_mousedown(MouseDownEvent event) { 1304 if(event.button == MouseButton.left) { 1305 if(this.tabStop) 1306 this.focus(); 1307 } 1308 } 1309 /// ditto 1310 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1311 /// ditto 1312 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1313 /// ditto 1314 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1315 /// ditto 1316 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1317 /// ditto 1318 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1319 /// ditto 1320 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1321 /// ditto 1322 void defaultEventHandler_char(CharEvent event) {} 1323 /// ditto 1324 void defaultEventHandler_triggered(Event event) {} 1325 /// ditto 1326 void defaultEventHandler_change(Event event) {} 1327 /// ditto 1328 void defaultEventHandler_focus(Event event) {} 1329 /// ditto 1330 void defaultEventHandler_blur(Event event) {} 1331 /// ditto 1332 void defaultEventHandler_focusin(Event event) {} 1333 /// ditto 1334 void defaultEventHandler_focusout(Event event) {} 1335 1336 /++ 1337 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1338 1339 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1340 1341 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1342 of participating in handler delegation. 1343 1344 $(TIP 1345 Use `scope` on your handlers when you can. While it currently does nothing, this will future-proof your code against future optimizations I want to do. Instead of copying whole event objects out if you do need to store them, just copy the properties you need. 1346 ) 1347 +/ 1348 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1349 return addEventListener(event, (Widget, scope Event e) { 1350 if(e.srcElement is this) 1351 handler(); 1352 }, useCapture); 1353 } 1354 1355 /// ditto 1356 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1357 return addEventListener(event, (Widget, Event e) { 1358 if(e.srcElement is this) 1359 handler(e); 1360 }, useCapture); 1361 } 1362 1363 /// ditto 1364 EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) { 1365 static if(is(Handler Fn == delegate)) { 1366 static if(is(Fn Params == __parameters)) { 1367 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1368 if(e.srcElement !is this) 1369 return; 1370 auto ty = cast(Params[0]) e; 1371 if(ty !is null) 1372 handler(ty); 1373 }, useCapture); 1374 } else static assert(0); 1375 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1376 } 1377 1378 /// ditto 1379 @scriptable 1380 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1381 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1382 } 1383 1384 /// ditto 1385 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1386 static if(is(Handler Fn == delegate)) { 1387 static if(is(Fn Params == __parameters)) { 1388 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1389 auto ty = cast(Params[0]) e; 1390 if(ty !is null) 1391 handler(ty); 1392 }, useCapture); 1393 } else static assert(0); 1394 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1395 } 1396 1397 /// ditto 1398 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1399 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1400 } 1401 1402 /// ditto 1403 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1404 if(event.length > 2 && event[0..2] == "on") 1405 event = event[2 .. $]; 1406 1407 if(useCapture) 1408 capturingEventHandlers[event] ~= handler; 1409 else 1410 bubblingEventHandlers[event] ~= handler; 1411 1412 return EventListener(this, event, handler, useCapture); 1413 } 1414 1415 /// ditto 1416 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1417 if(event.length > 2 && event[0..2] == "on") 1418 event = event[2 .. $]; 1419 1420 if(useCapture) { 1421 if(event in capturingEventHandlers) 1422 foreach(ref evt; capturingEventHandlers[event]) 1423 if(evt is handler) evt = null; 1424 } else { 1425 if(event in bubblingEventHandlers) 1426 foreach(ref evt; bubblingEventHandlers[event]) 1427 if(evt is handler) evt = null; 1428 } 1429 } 1430 1431 /// ditto 1432 void removeEventListener(EventListener listener) { 1433 removeEventListener(listener.event, listener.handler, listener.useCapture); 1434 } 1435 1436 static if(UsingSimpledisplayX11) { 1437 void discardXConnectionState() { 1438 foreach(child; children) 1439 child.discardXConnectionState(); 1440 } 1441 1442 void recreateXConnectionState() { 1443 foreach(child; children) 1444 child.recreateXConnectionState(); 1445 redraw(); 1446 } 1447 } 1448 1449 /++ 1450 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1451 1452 History: 1453 `globalCoordinates` was made `final` on May 11, 2021. 1454 +/ 1455 Point globalCoordinates() { 1456 int x = this.x; 1457 int y = this.y; 1458 auto p = this.parent; 1459 while(p) { 1460 x += p.x; 1461 y += p.y; 1462 p = p.parent; 1463 } 1464 1465 static if(UsingSimpledisplayX11) { 1466 auto dpy = XDisplayConnection.get; 1467 arsd.simpledisplay.Window dummyw; 1468 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1469 } else version(Windows) { 1470 POINT pt; 1471 pt.x = x; 1472 pt.y = y; 1473 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1474 x = pt.x; 1475 y = pt.y; 1476 } else { 1477 featureNotImplemented(); 1478 } 1479 1480 return Point(x, y); 1481 } 1482 1483 version(win32_widgets) 1484 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1485 1486 version(win32_widgets) 1487 /// Called when a WM_COMMAND is sent to the associated hwnd. 1488 void handleWmCommand(ushort cmd, ushort id) {} 1489 1490 version(win32_widgets) 1491 /++ 1492 Called when a WM_NOTIFY is sent to the associated hwnd. 1493 1494 History: 1495 +/ 1496 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1497 1498 version(win32_widgets) 1499 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); } 1500 1501 /++ 1502 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1503 1504 Updates to this variable will only be made visible on the next mouse enter event. 1505 +/ 1506 @scriptable string statusTip; 1507 // string toolTip; 1508 // string helpText; 1509 1510 /++ 1511 If true, this widget can be focused via keyboard control with the tab key. 1512 1513 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1514 +/ 1515 bool tabStop = true; 1516 /++ 1517 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.) 1518 +/ 1519 int tabOrder; 1520 1521 version(win32_widgets) { 1522 static Widget[HWND] nativeMapping; 1523 /// The native handle, if there is one. 1524 HWND hwnd; 1525 WNDPROC originalWindowProcedure; 1526 1527 SimpleWindow simpleWindowWrappingHwnd; 1528 1529 // please note it IGNORES your return value and does NOT forward it to Windows! 1530 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1531 return 0; 1532 } 1533 } 1534 private bool implicitlyCreated; 1535 1536 /// 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. 1537 int x; 1538 /// ditto 1539 int y; 1540 private int _width; 1541 private int _height; 1542 private Widget[] _children; 1543 private Widget _parent; 1544 private Window _parentWindow; 1545 1546 /++ 1547 Returns the window to which this widget is attached. 1548 1549 History: 1550 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. 1551 +/ 1552 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1553 private @property void parentWindow(Window parent) { 1554 _parentWindow = parent; 1555 foreach(child; children) 1556 child.parentWindow = parent; // please note that this is recursive 1557 } 1558 1559 /++ 1560 Returns the list of the widget's children. 1561 1562 History: 1563 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1564 1565 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1566 +/ 1567 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1568 1569 /++ 1570 Returns the widget's parent. 1571 1572 History: 1573 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1574 1575 The parent should only be managed by the [addChild] and [removeWidget] method. 1576 +/ 1577 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1578 1579 /// The widget's current size. 1580 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1581 /// ditto 1582 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1583 1584 /// Only the layout manager should be calling these. 1585 final protected @property int width(int a) @safe { return _width = a; } 1586 /// ditto 1587 final protected @property int height(int a) @safe { return _height = a; } 1588 1589 /++ 1590 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. 1591 1592 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 1593 +/ 1594 protected void registerMovement() { 1595 version(win32_widgets) { 1596 if(hwnd) { 1597 auto pos = getChildPositionRelativeToParentHwnd(this); 1598 MoveWindow(hwnd, pos[0], pos[1], width, height, false); // setting this to false can sometimes speed things up but only if it is actually drawn later and that's kinda iffy to do right here so being slower but safer rn 1599 } 1600 } 1601 sendResizeEvent(); 1602 } 1603 1604 /// Creates the widget and adds it to the parent. 1605 this(Widget parent) { 1606 if(parent !is null) 1607 parent.addChild(this); 1608 setupDefaultEventHandlers(); 1609 } 1610 1611 /// 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. 1612 @scriptable 1613 bool isFocused() { 1614 return parentWindow && parentWindow.focusedWidget is this; 1615 } 1616 1617 private bool showing_ = true; 1618 /// 1619 bool showing() { return showing_; } 1620 /// 1621 bool hidden() { return !showing_; } 1622 /++ 1623 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. 1624 +/ 1625 void showing(bool s, bool recalculate = true) { 1626 auto so = showing_; 1627 showing_ = s; 1628 if(s != so) { 1629 version(win32_widgets) 1630 if(hwnd) 1631 ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE); 1632 1633 if(parent && recalculate) { 1634 parent.queueRecomputeChildLayout(); 1635 parent.redraw(); 1636 } 1637 1638 foreach(child; children) 1639 child.showing(s, false); 1640 1641 } 1642 queueRecomputeChildLayout(); 1643 redraw(); 1644 } 1645 /// Convenience method for `showing = true` 1646 @scriptable 1647 void show() { 1648 showing = true; 1649 } 1650 /// Convenience method for `showing = false` 1651 @scriptable 1652 void hide() { 1653 showing = false; 1654 } 1655 1656 /// 1657 @scriptable 1658 void focus() { 1659 assert(parentWindow !is null); 1660 if(isFocused()) 1661 return; 1662 1663 if(parentWindow.focusedWidget) { 1664 // FIXME: more details here? like from and to 1665 auto from = parentWindow.focusedWidget; 1666 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 1667 parentWindow.focusedWidget = null; 1668 from.emit!BlurEvent(); 1669 this.emit!FocusOutEvent(); 1670 } 1671 1672 1673 version(win32_widgets) { 1674 if(this.hwnd !is null) 1675 SetFocus(this.hwnd); 1676 } 1677 //else static if(UsingSimpledisplayX11) 1678 //this.parentWindow.win.focus(); 1679 1680 parentWindow.focusedWidget = this; 1681 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 1682 this.emit!FocusEvent(); 1683 this.emit!FocusInEvent(); 1684 } 1685 1686 /+ 1687 /++ 1688 Unfocuses the widget. This may reset 1689 +/ 1690 @scriptable 1691 void blur() { 1692 1693 } 1694 +/ 1695 1696 1697 /++ 1698 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 1699 1700 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 1701 +/ 1702 void attachedToWindow(Window w) {} 1703 /++ 1704 Callback when the widget is added to another widget. 1705 1706 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 1707 +/ 1708 void addedTo(Widget w) {} 1709 1710 /++ 1711 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. 1712 1713 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 1714 +/ 1715 protected void addChild(Widget w, int position = int.max) { 1716 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 1717 assert(w !is this, "Child cannot be its own parent!"); 1718 w._parent = this; 1719 if(position == int.max || position == children.length) { 1720 _children ~= w; 1721 } else { 1722 assert(position < _children.length); 1723 _children.length = _children.length + 1; 1724 for(int i = cast(int) _children.length - 1; i > position; i--) 1725 _children[i] = _children[i - 1]; 1726 _children[position] = w; 1727 } 1728 1729 this.parentWindow = this._parentWindow; 1730 1731 w.addedTo(this); 1732 1733 if(this.hidden) 1734 w.showing = false; 1735 1736 if(parentWindow !is null) { 1737 w.attachedToWindow(parentWindow); 1738 parentWindow.queueRecomputeChildLayout(); 1739 parentWindow.redraw(); 1740 } 1741 } 1742 1743 /++ 1744 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. 1745 +/ 1746 Widget getChildAtPosition(int x, int y) { 1747 // it goes backward so the last one to show gets picked first 1748 // might use z-index later 1749 foreach_reverse(child; children) { 1750 if(child.hidden) 1751 continue; 1752 if(child.x <= x && child.y <= y 1753 && ((x - child.x) < child.width) 1754 && ((y - child.y) < child.height)) 1755 { 1756 return child; 1757 } 1758 } 1759 1760 return null; 1761 } 1762 1763 /++ 1764 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. 1765 1766 History: 1767 Added July 2, 2021 (v10.2) 1768 +/ 1769 protected void addScrollPosition(ref int x, ref int y) {}; 1770 1771 /++ 1772 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. 1773 1774 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. 1775 1776 [paint] is not called for system widgets as the OS library draws them instead. 1777 1778 1779 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. 1780 1781 You should also look at [WidgetPainter.visualTheme] to be theme aware. 1782 1783 History: 1784 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. 1785 +/ 1786 void paint(WidgetPainter painter) { 1787 version(win32_widgets) 1788 if(hwnd) { 1789 return; 1790 } 1791 painter.drawThemed(&paintContent); // note this refers to the following overload 1792 } 1793 1794 /++ 1795 Responsible for drawing the content as the theme engine is responsible for other elements. 1796 1797 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 1798 1799 Params: 1800 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. 1801 1802 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. 1803 1804 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. 1805 1806 Returns: 1807 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. 1808 1809 History: 1810 Added May 15, 2021 1811 +/ 1812 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 1813 return bounds; 1814 } 1815 1816 deprecated("Change ScreenPainter to WidgetPainter") 1817 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 1818 1819 /// I don't actually like the name of this 1820 /// this draws a background on it 1821 void erase(WidgetPainter painter) { 1822 version(win32_widgets) 1823 if(hwnd) return; // Windows will do it. I think. 1824 1825 auto c = getComputedStyle().background.color; 1826 painter.fillColor = c; 1827 painter.outlineColor = c; 1828 1829 version(win32_widgets) { 1830 HANDLE b, p; 1831 if(c.a == 0 && parent is parentWindow) { 1832 // I don't remember why I had this really... 1833 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 1834 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 1835 } 1836 } 1837 painter.drawRectangle(Point(0, 0), width, height); 1838 version(win32_widgets) { 1839 if(c.a == 0 && parent is parentWindow) { 1840 SelectObject(painter.impl.hdc, p); 1841 SelectObject(painter.impl.hdc, b); 1842 } 1843 } 1844 } 1845 1846 /// 1847 WidgetPainter draw() { 1848 int x = this.x, y = this.y; 1849 auto parent = this.parent; 1850 while(parent) { 1851 x += parent.x; 1852 y += parent.y; 1853 parent = parent.parent; 1854 } 1855 1856 auto painter = parentWindow.win.draw(true); 1857 painter.originX = x; 1858 painter.originY = y; 1859 painter.setClipRectangle(Point(0, 0), width, height); 1860 return WidgetPainter(painter, this); 1861 } 1862 1863 /// 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. 1864 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 1865 if(hidden) 1866 return; 1867 1868 int paintX = x; 1869 int paintY = y; 1870 if(this.useNativeDrawing()) { 1871 paintX = 0; 1872 paintY = 0; 1873 lox = 0; 1874 loy = 0; 1875 containment = Rectangle(0, 0, int.max, int.max); 1876 } 1877 1878 painter.originX = lox + paintX; 1879 painter.originY = loy + paintY; 1880 1881 bool actuallyPainted = false; 1882 1883 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 1884 if(clip == Rectangle.init) { 1885 // writeln(this, " clipped out"); 1886 return; 1887 } 1888 1889 bool invalidateChildren = invalidate; 1890 1891 if(redrawRequested || force) { 1892 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 1893 1894 painter.drawingUpon = this; 1895 1896 erase(painter); 1897 if(painter.visualTheme) 1898 painter.visualTheme.doPaint(this, painter); 1899 else 1900 paint(painter); 1901 1902 if(invalidate) { 1903 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 1904 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 1905 painter.invalidateRect(region); 1906 // children are contained inside this, so no need to do extra work 1907 invalidateChildren = false; 1908 } 1909 1910 redrawRequested = false; 1911 actuallyPainted = true; 1912 } 1913 1914 foreach(child; children) { 1915 version(win32_widgets) 1916 if(child.useNativeDrawing()) continue; 1917 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 1918 } 1919 1920 version(win32_widgets) 1921 foreach(child; children) { 1922 if(child.useNativeDrawing) { 1923 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 1924 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 1925 } 1926 } 1927 } 1928 1929 protected bool useNativeDrawing() nothrow { 1930 version(win32_widgets) 1931 return hwnd !is null; 1932 else 1933 return false; 1934 } 1935 1936 private static class RedrawEvent {} 1937 private __gshared re = new RedrawEvent(); 1938 1939 private bool redrawRequested; 1940 /// 1941 final void redraw(string file = __FILE__, size_t line = __LINE__) { 1942 redrawRequested = true; 1943 1944 if(this.parentWindow) { 1945 auto sw = this.parentWindow.win; 1946 assert(sw !is null); 1947 if(!sw.eventQueued!RedrawEvent) { 1948 sw.postEvent(re); 1949 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1950 } 1951 } 1952 } 1953 1954 private SimpleWindow drawableWindow; 1955 1956 /++ 1957 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. 1958 1959 Returns: 1960 `true` if you should do your default behavior. 1961 1962 History: 1963 Added May 5, 2021 1964 1965 Bugs: 1966 It does not do the static checks on gdc right now. 1967 +/ 1968 final protected bool emit(EventType, this This, Args...)(Args args) { 1969 version(GNU) {} else 1970 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1971 auto e = new EventType(this, args); 1972 e.dispatch(); 1973 return !e.defaultPrevented; 1974 } 1975 /// ditto 1976 final protected bool emit(string eventString, this This)() { 1977 auto e = new Event(eventString, this); 1978 e.dispatch(); 1979 return !e.defaultPrevented; 1980 } 1981 1982 /++ 1983 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. 1984 1985 History: 1986 Added May 5, 2021 1987 +/ 1988 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 1989 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1990 return addEventListener(handler); 1991 } 1992 1993 /++ 1994 Gets the computed style properties from the visual theme. 1995 1996 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].) 1997 1998 History: 1999 Added May 8, 2021 2000 +/ 2001 final StyleInformation getComputedStyle() { 2002 return StyleInformation(this); 2003 } 2004 2005 int focusableWidgets(scope int delegate(Widget) dg) { 2006 foreach(widget; WidgetStream(this)) { 2007 if(widget.tabStop && !widget.hidden) { 2008 int result = dg(widget); 2009 if (result) 2010 return result; 2011 } 2012 } 2013 return 0; 2014 } 2015 2016 /++ 2017 Calculates the border box (that is, the full width/height of the widget, from border edge to border edge) 2018 for the given content box (the area between the padding) 2019 2020 History: 2021 Added January 4, 2023 (dub v11.0) 2022 +/ 2023 Rectangle borderBoxForContentBox(Rectangle contentBox) { 2024 auto cs = getComputedStyle(); 2025 2026 auto borderWidth = getBorderWidth(cs.borderStyle); 2027 2028 auto rect = contentBox; 2029 2030 rect.left -= borderWidth; 2031 rect.right += borderWidth; 2032 rect.top -= borderWidth; 2033 rect.bottom += borderWidth; 2034 2035 auto insideBorderRect = rect; 2036 2037 rect.left -= cs.paddingLeft; 2038 rect.right += cs.paddingRight; 2039 rect.top -= cs.paddingTop; 2040 rect.bottom += cs.paddingBottom; 2041 2042 return rect; 2043 } 2044 2045 2046 // FIXME: I kinda want to hide events from implementation widgets 2047 // so it just catches them all and stops propagation... 2048 // i guess i can do it with a event listener on star. 2049 2050 mixin Emits!KeyDownEvent; /// 2051 mixin Emits!KeyUpEvent; /// 2052 mixin Emits!CharEvent; /// 2053 2054 mixin Emits!MouseDownEvent; /// 2055 mixin Emits!MouseUpEvent; /// 2056 mixin Emits!ClickEvent; /// 2057 mixin Emits!DoubleClickEvent; /// 2058 mixin Emits!MouseMoveEvent; /// 2059 mixin Emits!MouseOverEvent; /// 2060 mixin Emits!MouseOutEvent; /// 2061 mixin Emits!MouseEnterEvent; /// 2062 mixin Emits!MouseLeaveEvent; /// 2063 2064 mixin Emits!ResizeEvent; /// 2065 2066 mixin Emits!BlurEvent; /// 2067 mixin Emits!FocusEvent; /// 2068 2069 mixin Emits!FocusInEvent; /// 2070 mixin Emits!FocusOutEvent; /// 2071 } 2072 2073 /+ 2074 /++ 2075 Interface to indicate that the widget has a simple value property. 2076 2077 History: 2078 Added August 26, 2021 2079 +/ 2080 interface HasValue!T { 2081 /// Getter 2082 @property T value(); 2083 /// Setter 2084 @property void value(T); 2085 } 2086 2087 /++ 2088 Interface to indicate that the widget has a range of possible values for its simple value property. 2089 This would be present on something like a slider or possibly a number picker. 2090 2091 History: 2092 Added September 11, 2021 2093 +/ 2094 interface HasRangeOfValues!T : HasValue!T { 2095 /// The minimum and maximum values in the range, inclusive. 2096 @property T minValue(); 2097 @property void minValue(T); /// ditto 2098 @property T maxValue(); /// ditto 2099 @property void maxValue(T); /// ditto 2100 2101 /// The smallest step the user interface allows. User may still type in values without this limitation. 2102 @property void step(T); 2103 @property T step(); /// ditto 2104 } 2105 2106 /++ 2107 Interface to indicate that the widget has a list of possible values the user can choose from. 2108 This would be present on something like a drop-down selector. 2109 2110 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2111 combobox. 2112 2113 History: 2114 Added September 11, 2021 2115 +/ 2116 interface HasListOfValues!T : HasValue!T { 2117 @property T[] values; 2118 @property void values(T[]); 2119 2120 @property int selectedIndex(); // note it may return -1! 2121 @property void selectedIndex(int); 2122 } 2123 +/ 2124 2125 /++ 2126 History: 2127 Added September 2021 (dub v10.4) 2128 +/ 2129 class GridLayout : Layout { 2130 2131 // 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. 2132 2133 /++ 2134 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2135 +/ 2136 enum Gravity { 2137 Center = 0, 2138 NorthWest = North | West, 2139 North = 0b10_00, 2140 NorthEast = North | East, 2141 West = 0b00_10, 2142 East = 0b00_01, 2143 SouthWest = South | West, 2144 South = 0b01_00, 2145 SouthEast = South | East, 2146 } 2147 2148 /++ 2149 The width and height are in some proportional units and can often just be 12. 2150 +/ 2151 this(int width, int height, Widget parent) { 2152 this.gridWidth = width; 2153 this.gridHeight = height; 2154 super(parent); 2155 } 2156 2157 /++ 2158 Sets the position of the given child. 2159 2160 The units of these arguments are in the proportional grid units you set in the constructor. 2161 +/ 2162 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2163 // ensure it is in bounds 2164 // then ensure no overlaps 2165 2166 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2167 2168 foreach(ref position; positions) { 2169 if(position.widget is child) { 2170 position = p; 2171 goto set; 2172 } 2173 } 2174 2175 positions ~= p; 2176 2177 set: 2178 2179 // FIXME: should this batch? 2180 queueRecomputeChildLayout(); 2181 2182 return child; 2183 } 2184 2185 override void addChild(Widget w, int position = int.max) { 2186 super.addChild(w, position); 2187 //positions ~= ChildPosition(w); 2188 if(position != int.max) { 2189 // FIXME: align it so they actually match. 2190 } 2191 } 2192 2193 override void widgetRemoved(size_t idx, Widget w) { 2194 // FIXME: keep the positions array aligned 2195 // positions[idx].widget = null; 2196 } 2197 2198 override void recomputeChildLayout() { 2199 registerMovement(); 2200 int onGrid = cast(int) positions.length; 2201 c: foreach(child; children) { 2202 // just snap it to the grid 2203 if(onGrid) 2204 foreach(position; positions) 2205 if(position.widget is child) { 2206 child.x = this.width * position.x / this.gridWidth; 2207 child.y = this.height * position.y / this.gridHeight; 2208 child.width = this.width * position.width / this.gridWidth; 2209 child.height = this.height * position.height / this.gridHeight; 2210 2211 auto diff = child.width - child.maxWidth(); 2212 // FIXME: gravity? 2213 if(diff > 0) { 2214 child.width = child.width - diff; 2215 2216 if(position.gravity & Gravity.West) { 2217 // nothing needed, already aligned 2218 } else if(position.gravity & Gravity.East) { 2219 child.x += diff; 2220 } else { 2221 child.x += diff / 2; 2222 } 2223 } 2224 2225 diff = child.height - child.maxHeight(); 2226 // FIXME: gravity? 2227 if(diff > 0) { 2228 child.height = child.height - diff; 2229 2230 if(position.gravity & Gravity.North) { 2231 // nothing needed, already aligned 2232 } else if(position.gravity & Gravity.South) { 2233 child.y += diff; 2234 } else { 2235 child.y += diff / 2; 2236 } 2237 } 2238 2239 2240 child.recomputeChildLayout(); 2241 onGrid--; 2242 continue c; 2243 } 2244 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2245 } 2246 } 2247 2248 private struct ChildPosition { 2249 Widget widget; 2250 int x; 2251 int y; 2252 int width; 2253 int height; 2254 Gravity gravity; 2255 } 2256 private ChildPosition[] positions; 2257 2258 int gridWidth = 12; 2259 int gridHeight = 12; 2260 } 2261 2262 /// 2263 abstract class ComboboxBase : Widget { 2264 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2265 // or to always show the list, we want CBS_SIMPLE == 1 2266 version(win32_widgets) 2267 this(uint style, Widget parent) { 2268 super(parent); 2269 createWin32Window(this, "ComboBox"w, null, style); 2270 } 2271 else version(custom_widgets) 2272 this(Widget parent) { 2273 super(parent); 2274 2275 addEventListener((KeyDownEvent event) { 2276 if(event.key == Key.Up) { 2277 if(selection_ > -1) { // -1 means select blank 2278 selection_--; 2279 fireChangeEvent(); 2280 } 2281 event.preventDefault(); 2282 } 2283 if(event.key == Key.Down) { 2284 if(selection_ + 1 < options.length) { 2285 selection_++; 2286 fireChangeEvent(); 2287 } 2288 event.preventDefault(); 2289 } 2290 2291 }); 2292 2293 } 2294 else static assert(false); 2295 2296 /++ 2297 Returns the current list of options in the selection. 2298 2299 History: 2300 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2301 +/ 2302 final @property string[] options() const { 2303 return cast(string[]) options_; 2304 } 2305 2306 private string[] options_; 2307 private int selection_ = -1; 2308 2309 /++ 2310 Adds an option to the end of options array. 2311 +/ 2312 void addOption(string s) { 2313 options_ ~= s; 2314 version(win32_widgets) 2315 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2316 } 2317 2318 /++ 2319 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2320 +/ 2321 int getSelection() { 2322 return selection_; 2323 } 2324 2325 /++ 2326 Returns the current selection as a string. 2327 2328 History: 2329 Added November 17, 2021 2330 +/ 2331 string getSelectionString() { 2332 return selection_ == -1 ? null : options[selection_]; 2333 } 2334 2335 /++ 2336 Sets the current selection to an index in the options array, or to the given option if present. 2337 Please note that the string version may do a linear lookup. 2338 2339 Returns: 2340 the index you passed in 2341 2342 History: 2343 The `string` based overload was added on March 1, 2022 (dub v10.7). 2344 2345 The return value was `void` prior to March 1, 2022. 2346 +/ 2347 int setSelection(int idx) { 2348 selection_ = idx; 2349 version(win32_widgets) 2350 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2351 2352 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2353 t.dispatch(); 2354 2355 return idx; 2356 } 2357 2358 /// ditto 2359 int setSelection(string s) { 2360 if(s !is null) 2361 foreach(idx, item; options) 2362 if(item == s) { 2363 return setSelection(cast(int) idx); 2364 } 2365 return setSelection(-1); 2366 } 2367 2368 /++ 2369 This event is fired when the selection changes. Note it inherits 2370 from ChangeEvent!string, meaning you can use that as well, and it also 2371 fills in [Event.intValue]. 2372 +/ 2373 static class SelectionChangedEvent : ChangeEvent!string { 2374 this(Widget target, int iv, string sv) { 2375 super(target, &stringValue); 2376 this.iv = iv; 2377 this.sv = sv; 2378 } 2379 immutable int iv; 2380 immutable string sv; 2381 2382 override @property string stringValue() { return sv; } 2383 override @property int intValue() { return iv; } 2384 } 2385 2386 version(win32_widgets) 2387 override void handleWmCommand(ushort cmd, ushort id) { 2388 if(cmd == CBN_SELCHANGE) { 2389 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2390 fireChangeEvent(); 2391 } 2392 } 2393 2394 private void fireChangeEvent() { 2395 if(selection_ >= options.length) 2396 selection_ = -1; 2397 2398 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2399 t.dispatch(); 2400 } 2401 2402 version(win32_widgets) { 2403 override int minHeight() { return defaultLineHeight + 6; } 2404 override int maxHeight() { return defaultLineHeight + 6; } 2405 } else { 2406 override int minHeight() { return defaultLineHeight + 4; } 2407 override int maxHeight() { return defaultLineHeight + 4; } 2408 } 2409 2410 version(custom_widgets) { 2411 2412 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2413 2414 SimpleWindow dropDown; 2415 void popup() { 2416 auto w = width; 2417 // FIXME: suggestedDropdownHeight see below 2418 auto h = cast(int) this.options.length * defaultLineHeight + 8; 2419 2420 auto coord = this.globalCoordinates(); 2421 auto dropDown = new SimpleWindow( 2422 w, h, 2423 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null); 2424 2425 dropDown.move(coord.x, coord.y + this.height); 2426 2427 { 2428 auto cs = getComputedStyle(); 2429 auto painter = dropDown.draw(); 2430 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2431 auto p = Point(4, 4); 2432 painter.outlineColor = cs.foregroundColor; 2433 foreach(option; options) { 2434 painter.drawText(p, option); 2435 p.y += defaultLineHeight; 2436 } 2437 } 2438 2439 dropDown.setEventHandlers( 2440 (MouseEvent event) { 2441 if(event.type == MouseEventType.buttonReleased) { 2442 dropDown.close(); 2443 auto element = (event.y - 4) / defaultLineHeight; 2444 if(element >= 0 && element <= options.length) { 2445 selection_ = element; 2446 2447 fireChangeEvent(); 2448 } 2449 } 2450 } 2451 ); 2452 2453 dropDown.visibilityChanged = (bool visible) { 2454 if(visible) { 2455 this.redraw(); 2456 dropDown.grabInput(); 2457 } else { 2458 dropDown.releaseInputGrab(); 2459 } 2460 }; 2461 2462 dropDown.show(); 2463 } 2464 2465 } 2466 } 2467 2468 /++ 2469 A drop-down list where the user must select one of the 2470 given options. Like `<select>` in HTML. 2471 +/ 2472 class DropDownSelection : ComboboxBase { 2473 this(Widget parent) { 2474 version(win32_widgets) 2475 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 2476 else version(custom_widgets) { 2477 super(parent); 2478 2479 addEventListener("focus", () { this.redraw; }); 2480 addEventListener("blur", () { this.redraw; }); 2481 addEventListener(EventType.change, () { this.redraw; }); 2482 addEventListener("mousedown", () { this.focus(); this.popup(); }); 2483 addEventListener((KeyDownEvent event) { 2484 if(event.key == Key.Space) 2485 popup(); 2486 }); 2487 } else static assert(false); 2488 } 2489 2490 mixin Padding!q{2}; 2491 static class Style : Widget.Style { 2492 override FrameStyle borderStyle() { return FrameStyle.risen; } 2493 } 2494 mixin OverrideStyle!Style; 2495 2496 version(custom_widgets) 2497 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2498 auto cs = getComputedStyle(); 2499 2500 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 2501 2502 painter.outlineColor = cs.foregroundColor; 2503 painter.fillColor = cs.foregroundColor; 2504 2505 /+ 2506 Point[4] triangle; 2507 enum padding = 6; 2508 enum paddingV = 7; 2509 enum triangleWidth = 10; 2510 triangle[0] = Point(width - padding - triangleWidth, paddingV); 2511 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 2512 triangle[2] = Point(width - padding - 0, paddingV); 2513 triangle[3] = triangle[0]; 2514 painter.drawPolygon(triangle[]); 2515 +/ 2516 2517 auto offset = Point((this.width - scaleWithDpi(16)), (this.height - scaleWithDpi(16)) / 2); 2518 2519 painter.drawPolygon( 2520 scaleWithDpi(Point(2, 6) + offset), 2521 scaleWithDpi(Point(7, 11) + offset), 2522 scaleWithDpi(Point(12, 6) + offset), 2523 scaleWithDpi(Point(2, 6) + offset) 2524 ); 2525 2526 2527 return bounds; 2528 } 2529 2530 version(win32_widgets) 2531 override void registerMovement() { 2532 version(win32_widgets) { 2533 if(hwnd) { 2534 auto pos = getChildPositionRelativeToParentHwnd(this); 2535 // the height given to this from Windows' perspective is supposed 2536 // to include the drop down's height. so I add to it to give some 2537 // room for that. 2538 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 2539 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 2540 } 2541 } 2542 sendResizeEvent(); 2543 } 2544 } 2545 2546 /++ 2547 A text box with a drop down arrow listing selections. 2548 The user can choose from the list, or type their own. 2549 +/ 2550 class FreeEntrySelection : ComboboxBase { 2551 this(Widget parent) { 2552 version(win32_widgets) 2553 super(2 /* CBS_DROPDOWN */, parent); 2554 else version(custom_widgets) { 2555 super(parent); 2556 auto hl = new HorizontalLayout(this); 2557 lineEdit = new LineEdit(hl); 2558 2559 tabStop = false; 2560 2561 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2562 2563 auto btn = new class ArrowButton { 2564 this() { 2565 super(ArrowDirection.down, hl); 2566 } 2567 override int maxHeight() { 2568 return lineEdit.maxHeight; 2569 } 2570 }; 2571 //btn.addDirectEventListener("focus", &lineEdit.focus); 2572 btn.addEventListener("triggered", &this.popup); 2573 addEventListener(EventType.change, (Event event) { 2574 lineEdit.content = event.stringValue; 2575 lineEdit.focus(); 2576 redraw(); 2577 }); 2578 } 2579 else static assert(false); 2580 } 2581 2582 version(custom_widgets) { 2583 LineEdit lineEdit; 2584 } 2585 } 2586 2587 /++ 2588 A combination of free entry with a list below it. 2589 +/ 2590 class ComboBox : ComboboxBase { 2591 this(Widget parent) { 2592 version(win32_widgets) 2593 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 2594 else version(custom_widgets) { 2595 super(parent); 2596 lineEdit = new LineEdit(this); 2597 listWidget = new ListWidget(this); 2598 listWidget.multiSelect = false; 2599 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 2600 string c = null; 2601 foreach(option; listWidget.options) 2602 if(option.selected) { 2603 c = option.label; 2604 break; 2605 } 2606 lineEdit.content = c; 2607 }); 2608 2609 listWidget.tabStop = false; 2610 this.tabStop = false; 2611 listWidget.addEventListener("focus", &lineEdit.focus); 2612 this.addEventListener("focus", &lineEdit.focus); 2613 2614 addDirectEventListener(EventType.change, { 2615 listWidget.setSelection(selection_); 2616 if(selection_ != -1) 2617 lineEdit.content = options[selection_]; 2618 lineEdit.focus(); 2619 redraw(); 2620 }); 2621 2622 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2623 2624 listWidget.addDirectEventListener(EventType.change, { 2625 int set = -1; 2626 foreach(idx, opt; listWidget.options) 2627 if(opt.selected) { 2628 set = cast(int) idx; 2629 break; 2630 } 2631 if(set != selection_) 2632 this.setSelection(set); 2633 }); 2634 } else static assert(false); 2635 } 2636 2637 override int minHeight() { return defaultLineHeight * 3; } 2638 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 2639 override int heightStretchiness() { return 5; } 2640 2641 version(custom_widgets) { 2642 LineEdit lineEdit; 2643 ListWidget listWidget; 2644 2645 override void addOption(string s) { 2646 listWidget.options ~= ListWidget.Option(s); 2647 ComboboxBase.addOption(s); 2648 } 2649 } 2650 } 2651 2652 /+ 2653 class Spinner : Widget { 2654 version(win32_widgets) 2655 this(Widget parent) { 2656 super(parent); 2657 parentWindow = parent.parentWindow; 2658 auto hlayout = new HorizontalLayout(this); 2659 lineEdit = new LineEdit(hlayout); 2660 upDownControl = new UpDownControl(hlayout); 2661 } 2662 2663 LineEdit lineEdit; 2664 UpDownControl upDownControl; 2665 } 2666 2667 class UpDownControl : Widget { 2668 version(win32_widgets) 2669 this(Widget parent) { 2670 super(parent); 2671 parentWindow = parent.parentWindow; 2672 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 2673 } 2674 2675 override int minHeight() { return defaultLineHeight; } 2676 override int maxHeight() { return defaultLineHeight * 3/2; } 2677 2678 override int minWidth() { return defaultLineHeight * 3/2; } 2679 override int maxWidth() { return defaultLineHeight * 3/2; } 2680 } 2681 +/ 2682 2683 /+ 2684 class DataView : Widget { 2685 // this is the omnibus data viewer 2686 // the internal data layout is something like: 2687 // string[string][] but also each node can have parents 2688 } 2689 +/ 2690 2691 2692 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 2693 2694 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 2695 2696 // FIXME: menus should prolly capture the mouse. ugh i kno. 2697 /* 2698 TextEdit needs: 2699 2700 * caret manipulation 2701 * selection control 2702 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 2703 2704 For example: 2705 2706 connect(paste, &textEdit.insertTextAtCaret); 2707 2708 would be nice. 2709 2710 2711 2712 I kinda want an omnibus dataview that combines list, tree, 2713 and table - it can be switched dynamically between them. 2714 2715 Flattening policy: only show top level, show recursive, show grouped 2716 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 2717 2718 Single select, multi select, organization, drag+drop 2719 */ 2720 2721 //static if(UsingSimpledisplayX11) 2722 version(win32_widgets) {} 2723 else version(custom_widgets) { 2724 enum scrollClickRepeatInterval = 50; 2725 2726 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 2727 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 2728 enum activeTabColor = lightAccentColor; 2729 enum hoveringColor = Color(228, 228, 228); 2730 enum buttonColor = windowBackgroundColor; 2731 enum depressedButtonColor = darkAccentColor; 2732 enum activeListXorColor = Color(255, 255, 127); 2733 enum progressBarColor = Color(0, 0, 128); 2734 enum activeMenuItemColor = Color(0, 0, 128); 2735 2736 }} 2737 else static assert(false); 2738 deprecated("Get these properties off the `visualTheme` instead.") { 2739 // these are used by horizontal rule so not just custom_widgets. for now at least. 2740 enum darkAccentColor = Color(172, 172, 172); 2741 enum lightAccentColor = Color(223, 223, 223); // used to be 223 2742 } 2743 2744 private const(wchar)* toWstringzInternal(in char[] s) { 2745 wchar[] str; 2746 str.reserve(s.length + 1); 2747 foreach(dchar ch; s) 2748 str ~= ch; 2749 str ~= '\0'; 2750 return str.ptr; 2751 } 2752 2753 static if(SimpledisplayTimerAvailable) 2754 void setClickRepeat(Widget w, int interval, int delay = 250) { 2755 Timer timer; 2756 int delayRemaining = delay / interval; 2757 if(delayRemaining <= 1) 2758 delayRemaining = 2; 2759 2760 immutable originalDelayRemaining = delayRemaining; 2761 2762 w.addDirectEventListener((scope MouseDownEvent ev) { 2763 if(ev.srcElement !is w) 2764 return; 2765 if(timer !is null) { 2766 timer.destroy(); 2767 timer = null; 2768 } 2769 delayRemaining = originalDelayRemaining; 2770 timer = new Timer(interval, () { 2771 if(delayRemaining > 0) 2772 delayRemaining--; 2773 else { 2774 auto ev = new Event("triggered", w); 2775 ev.sendDirectly(); 2776 } 2777 }); 2778 }); 2779 2780 w.addDirectEventListener((scope MouseUpEvent ev) { 2781 if(ev.srcElement !is w) 2782 return; 2783 if(timer !is null) { 2784 timer.destroy(); 2785 timer = null; 2786 } 2787 }); 2788 2789 w.addDirectEventListener((scope MouseLeaveEvent ev) { 2790 if(ev.srcElement !is w) 2791 return; 2792 if(timer !is null) { 2793 timer.destroy(); 2794 timer = null; 2795 } 2796 }); 2797 2798 } 2799 else 2800 void setClickRepeat(Widget w, int interval, int delay = 250) {} 2801 2802 enum FrameStyle { 2803 none, /// 2804 risen, /// a 3d pop-out effect (think Windows 95 button) 2805 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 2806 solid, /// 2807 dotted, /// 2808 fantasy, /// a style based on a popular fantasy video game 2809 } 2810 2811 version(custom_widgets) 2812 deprecated 2813 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 2814 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2815 } 2816 2817 version(custom_widgets) 2818 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 2819 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 2820 } 2821 2822 version(custom_widgets) 2823 deprecated 2824 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 2825 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2826 } 2827 2828 int getBorderWidth(FrameStyle style) { 2829 final switch(style) { 2830 case FrameStyle.sunk, FrameStyle.risen: 2831 return 2; 2832 case FrameStyle.none: 2833 return 0; 2834 case FrameStyle.solid: 2835 return 1; 2836 case FrameStyle.dotted: 2837 return 1; 2838 case FrameStyle.fantasy: 2839 return 3; 2840 } 2841 } 2842 2843 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 2844 int borderWidth = getBorderWidth(style); 2845 final switch(style) { 2846 case FrameStyle.sunk, FrameStyle.risen: 2847 // outer layer 2848 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 2849 break; 2850 case FrameStyle.none: 2851 painter.outlineColor = background; 2852 break; 2853 case FrameStyle.solid: 2854 painter.pen = Pen(border, 1); 2855 break; 2856 case FrameStyle.dotted: 2857 painter.pen = Pen(border, 1, Pen.Style.Dotted); 2858 break; 2859 case FrameStyle.fantasy: 2860 painter.pen = Pen(border, 3); 2861 break; 2862 } 2863 2864 painter.fillColor = background; 2865 painter.drawRectangle(Point(x + 0, y + 0), width, height); 2866 2867 2868 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 2869 // 3d effect 2870 auto vt = WidgetPainter.visualTheme; 2871 2872 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 2873 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 2874 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 2875 2876 // inner layer 2877 //right, bottom 2878 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 2879 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 2880 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 2881 // left, top 2882 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 2883 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 2884 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 2885 } else if(style == FrameStyle.fantasy) { 2886 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 2887 painter.fillColor = Color.transparent; 2888 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 2889 } 2890 2891 return borderWidth; 2892 } 2893 2894 /++ 2895 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. 2896 2897 See_Also: 2898 [MenuItem] 2899 [ToolButton] 2900 [Menu.addItem] 2901 +/ 2902 class Action { 2903 version(win32_widgets) { 2904 private int id; 2905 private static int lastId = 9000; 2906 private static Action[int] mapping; 2907 } 2908 2909 KeyEvent accelerator; 2910 2911 // FIXME: disable message 2912 // and toggle thing? 2913 // ??? and trigger arguments too ??? 2914 2915 /++ 2916 Params: 2917 label = the textual label 2918 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 2919 triggered = initial handler, more can be added via the [triggered] member. 2920 +/ 2921 this(string label, ushort icon = 0, void delegate() triggered = null) { 2922 this.label = label; 2923 this.iconId = icon; 2924 if(triggered !is null) 2925 this.triggered ~= triggered; 2926 version(win32_widgets) { 2927 id = ++lastId; 2928 mapping[id] = this; 2929 } 2930 } 2931 2932 private string label; 2933 private ushort iconId; 2934 // icon 2935 2936 // when it is triggered, the triggered event is fired on the window 2937 /// The list of handlers when it is triggered. 2938 void delegate()[] triggered; 2939 } 2940 2941 /* 2942 plan: 2943 keyboard accelerators 2944 2945 * menus (and popups and tooltips) 2946 * status bar 2947 * toolbars and buttons 2948 2949 sortable table view 2950 2951 maybe notification area icons 2952 basic clipboard 2953 2954 * radio box 2955 splitter 2956 toggle buttons (optionally mutually exclusive, like in Paint) 2957 label, rich text display, multi line plain text (selectable) 2958 * fieldset 2959 * nestable grid layout 2960 single line text input 2961 * multi line text input 2962 slider 2963 spinner 2964 list box 2965 drop down 2966 combo box 2967 auto complete box 2968 * progress bar 2969 2970 terminal window/widget (on unix it might even be a pty but really idk) 2971 2972 ok button 2973 cancel button 2974 2975 keyboard hotkeys 2976 2977 scroll widget 2978 2979 event redirections and network transparency 2980 script integration 2981 */ 2982 2983 2984 /* 2985 MENUS 2986 2987 auto bar = new MenuBar(window); 2988 window.menuBar = bar; 2989 2990 auto fileMenu = bar.addItem(new Menu("&File")); 2991 fileMenu.addItem(new MenuItem("&Exit")); 2992 2993 2994 EVENTS 2995 2996 For controls, you should usually use "triggered" rather than "click", etc., because 2997 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 2998 This is the case on menus and pushbuttons. 2999 3000 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 3001 */ 3002 3003 3004 /* 3005 enum LinePreference { 3006 AlwaysOnOwnLine, // always on its own line 3007 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 3008 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 3009 } 3010 */ 3011 3012 /++ 3013 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. 3014 3015 --- 3016 class MyWidget : Widget { 3017 this(Widget parent) { super(parent); } 3018 3019 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 3020 mixin Padding!q{4}; 3021 3022 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 3023 mixin Margin!q{8}; 3024 3025 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 3026 // while Top/Bottom/Right remain 8 from the mixin above. 3027 override int marginLeft() { return 2; } 3028 } 3029 --- 3030 3031 3032 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]). 3033 3034 Padding is the area inside a widget where its background is drawn, but the content avoids. 3035 3036 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!). 3037 3038 * 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. 3039 +/ 3040 mixin template Padding(string code) { 3041 override int paddingLeft() { return mixin(code);} 3042 override int paddingRight() { return mixin(code);} 3043 override int paddingTop() { return mixin(code);} 3044 override int paddingBottom() { return mixin(code);} 3045 } 3046 3047 /// ditto 3048 mixin template Margin(string code) { 3049 override int marginLeft() { return mixin(code);} 3050 override int marginRight() { return mixin(code);} 3051 override int marginTop() { return mixin(code);} 3052 override int marginBottom() { return mixin(code);} 3053 } 3054 3055 private 3056 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3057 enum calcingV = relevantMeasure == "height"; 3058 3059 parent.registerMovement(); 3060 3061 if(parent.children.length == 0) 3062 return; 3063 3064 auto parentStyle = parent.getComputedStyle(); 3065 3066 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3067 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3068 3069 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3070 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3071 3072 // my own width and height should already be set by the caller of this function... 3073 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3074 mixin("parentStyle.padding"~firstThingy~"()") - 3075 mixin("parentStyle.padding"~secondThingy~"()"); 3076 3077 int stretchinessSum; 3078 int stretchyChildSum; 3079 int lastMargin = 0; 3080 3081 int shrinkinessSum; 3082 int shrinkyChildSum; 3083 3084 // set initial size 3085 foreach(child; parent.children) { 3086 3087 auto childStyle = child.getComputedStyle(); 3088 3089 if(cast(StaticPosition) child) 3090 continue; 3091 if(child.hidden) 3092 continue; 3093 3094 const iw = child.flexBasisWidth(); 3095 const ih = child.flexBasisHeight(); 3096 3097 static if(calcingV) { 3098 child.width = parent.width - 3099 mixin("childStyle.margin"~otherFirstThingy~"()") - 3100 mixin("childStyle.margin"~otherSecondThingy~"()") - 3101 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3102 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3103 3104 if(child.width < 0) 3105 child.width = 0; 3106 if(child.width > childStyle.maxWidth()) 3107 child.width = childStyle.maxWidth(); 3108 3109 if(iw > 0) { 3110 auto totalPossible = child.width; 3111 if(child.width > iw && child.widthStretchiness() == 0) 3112 child.width = iw; 3113 } 3114 3115 child.height = mymax(childStyle.minHeight(), ih); 3116 } else { 3117 // set to take all the space 3118 child.height = parent.height - 3119 mixin("childStyle.margin"~firstThingy~"()") - 3120 mixin("childStyle.margin"~secondThingy~"()") - 3121 mixin("parentStyle.padding"~firstThingy~"()") - 3122 mixin("parentStyle.padding"~secondThingy~"()"); 3123 3124 // then clamp it 3125 if(child.height < 0) 3126 child.height = 0; 3127 if(child.height > childStyle.maxHeight()) 3128 child.height = childStyle.maxHeight(); 3129 3130 // and if possible, respect the ideal target 3131 if(ih > 0) { 3132 auto totalPossible = child.height; 3133 if(child.height > ih && child.heightStretchiness() == 0) 3134 child.height = ih; 3135 } 3136 3137 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3138 child.width = mymax(childStyle.minWidth(), iw); 3139 } 3140 3141 spaceRemaining -= mixin("child." ~ relevantMeasure); 3142 3143 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3144 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3145 lastMargin = margin; 3146 spaceRemaining -= thisMargin + margin; 3147 3148 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3149 stretchinessSum += s; 3150 if(s > 0) 3151 stretchyChildSum++; 3152 3153 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3154 shrinkinessSum += s2; 3155 if(s2 > 0) 3156 shrinkyChildSum++; 3157 } 3158 3159 if(spaceRemaining < 0 && shrinkyChildSum) { 3160 // shrink to get into the space if it is possible 3161 auto toRemove = -spaceRemaining; 3162 auto removalPerItem = toRemove * shrinkinessSum / shrinkyChildSum; 3163 auto remainder = toRemove * shrinkinessSum % shrinkyChildSum; 3164 3165 // FIXME: wtf why am i shrinking things with no shrinkiness? 3166 3167 foreach(child; parent.children) { 3168 auto childStyle = child.getComputedStyle(); 3169 if(cast(StaticPosition) child) 3170 continue; 3171 if(child.hidden) 3172 continue; 3173 static if(calcingV) { 3174 auto maximum = childStyle.maxHeight(); 3175 } else { 3176 auto maximum = childStyle.maxWidth(); 3177 } 3178 3179 if(mixin("child._" ~ relevantMeasure) >= maximum) 3180 continue; 3181 3182 mixin("child._" ~ relevantMeasure) -= removalPerItem + remainder; // this is removing more than needed to trigger the next thing. ugh. 3183 3184 spaceRemaining += removalPerItem + remainder; 3185 } 3186 } 3187 3188 // stretch to fill space 3189 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3190 auto spacePerChild = spaceRemaining / stretchinessSum; 3191 bool spreadEvenly; 3192 bool giveToBiggest; 3193 if(spacePerChild <= 0) { 3194 spacePerChild = spaceRemaining / stretchyChildSum; 3195 spreadEvenly = true; 3196 } 3197 if(spacePerChild <= 0) { 3198 giveToBiggest = true; 3199 } 3200 int previousSpaceRemaining = spaceRemaining; 3201 stretchinessSum = 0; 3202 Widget mostStretchy; 3203 int mostStretchyS; 3204 foreach(child; parent.children) { 3205 auto childStyle = child.getComputedStyle(); 3206 if(cast(StaticPosition) child) 3207 continue; 3208 if(child.hidden) 3209 continue; 3210 static if(calcingV) { 3211 auto maximum = childStyle.maxHeight(); 3212 } else { 3213 auto maximum = childStyle.maxWidth(); 3214 } 3215 3216 if(mixin("child." ~ relevantMeasure) >= maximum) { 3217 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3218 mixin("child._" ~ relevantMeasure) -= adj; 3219 spaceRemaining += adj; 3220 continue; 3221 } 3222 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3223 if(s <= 0) 3224 continue; 3225 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3226 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3227 spaceRemaining -= spaceAdjustment; 3228 if(mixin("child." ~ relevantMeasure) > maximum) { 3229 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3230 mixin("child._" ~ relevantMeasure) -= diff; 3231 spaceRemaining += diff; 3232 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3233 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3234 if(mostStretchy is null || s >= mostStretchyS) { 3235 mostStretchy = child; 3236 mostStretchyS = s; 3237 } 3238 } 3239 } 3240 3241 if(giveToBiggest && mostStretchy !is null) { 3242 auto child = mostStretchy; 3243 auto childStyle = child.getComputedStyle(); 3244 int spaceAdjustment = spaceRemaining; 3245 3246 static if(calcingV) 3247 auto maximum = childStyle.maxHeight(); 3248 else 3249 auto maximum = childStyle.maxWidth(); 3250 3251 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3252 spaceRemaining -= spaceAdjustment; 3253 if(mixin("child._" ~ relevantMeasure) > maximum) { 3254 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3255 mixin("child._" ~ relevantMeasure) -= diff; 3256 spaceRemaining += diff; 3257 } 3258 } 3259 3260 if(spaceRemaining == previousSpaceRemaining) { 3261 if(mostStretchy !is null) { 3262 static if(calcingV) 3263 auto maximum = mostStretchy.maxHeight(); 3264 else 3265 auto maximum = mostStretchy.maxWidth(); 3266 3267 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3268 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3269 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3270 } 3271 break; // apparently nothing more we can do 3272 } 3273 } 3274 3275 foreach(child; parent.children) { 3276 auto childStyle = child.getComputedStyle(); 3277 if(cast(StaticPosition) child) 3278 continue; 3279 if(child.hidden) 3280 continue; 3281 3282 static if(calcingV) 3283 auto maximum = childStyle.maxHeight(); 3284 else 3285 auto maximum = childStyle.maxWidth(); 3286 if(mixin("child._" ~ relevantMeasure) > maximum) 3287 mixin("child._" ~ relevantMeasure) = maximum; 3288 } 3289 3290 // position 3291 lastMargin = 0; 3292 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3293 foreach(child; parent.children) { 3294 auto childStyle = child.getComputedStyle(); 3295 if(cast(StaticPosition) child) { 3296 child.recomputeChildLayout(); 3297 continue; 3298 } 3299 if(child.hidden) 3300 continue; 3301 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3302 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3303 currentPos += thisMargin; 3304 static if(calcingV) { 3305 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3306 child.y = currentPos; 3307 } else { 3308 child.x = currentPos; 3309 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3310 3311 } 3312 currentPos += mixin("child." ~ relevantMeasure); 3313 currentPos += margin; 3314 lastMargin = margin; 3315 3316 child.recomputeChildLayout(); 3317 } 3318 } 3319 3320 int mymax(int a, int b) { return a > b ? a : b; } 3321 int mymax(int a, int b, int c) { 3322 auto d = mymax(a, b); 3323 return c > d ? c : d; 3324 } 3325 3326 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3327 // and here, it must be integrable with the layout, the event system, and not be painted over. 3328 version(win32_widgets) { 3329 3330 // this function just does stuff that a parent window needs for redirection 3331 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3332 this_.hookedWndProc(msg, wParam, lParam); 3333 3334 switch(msg) { 3335 3336 case WM_VSCROLL, WM_HSCROLL: 3337 auto pos = HIWORD(wParam); 3338 auto m = LOWORD(wParam); 3339 3340 auto scrollbarHwnd = cast(HWND) lParam; 3341 3342 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3343 3344 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3345 3346 switch(m) { 3347 /+ 3348 // I don't think those messages are ever actually sent normally by the widget itself, 3349 // they are more used for the keyboard interface. methinks. 3350 case SB_BOTTOM: 3351 // writeln("end"); 3352 auto event = new Event("scrolltoend", *widgetp); 3353 event.dispatch(); 3354 //if(!event.defaultPrevented) 3355 break; 3356 case SB_TOP: 3357 // writeln("top"); 3358 auto event = new Event("scrolltobeginning", *widgetp); 3359 event.dispatch(); 3360 break; 3361 case SB_ENDSCROLL: 3362 // idk 3363 break; 3364 +/ 3365 case SB_LINEDOWN: 3366 (*widgetp).emitCommand!"scrolltonextline"(); 3367 return 0; 3368 case SB_LINEUP: 3369 (*widgetp).emitCommand!"scrolltopreviousline"(); 3370 return 0; 3371 case SB_PAGEDOWN: 3372 (*widgetp).emitCommand!"scrolltonextpage"(); 3373 return 0; 3374 case SB_PAGEUP: 3375 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3376 return 0; 3377 case SB_THUMBPOSITION: 3378 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3379 ev.dispatch(); 3380 return 0; 3381 case SB_THUMBTRACK: 3382 // eh kinda lying but i like the real time update display 3383 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3384 ev.dispatch(); 3385 3386 // the event loop doesn't seem to carry on with a requested redraw.. 3387 // so we request it to get our dirty bit set... 3388 // then we need to immediately actually redraw it too for instant feedback to user 3389 SimpleWindow.processAllCustomEvents(); 3390 SimpleWindow.processAllCustomEvents(); 3391 //if(this_.parentWindow) 3392 //this_.parentWindow.actualRedraw(); 3393 3394 // and this ensures the WM_PAINT message is sent fairly quickly 3395 // still seems to lag a little in large windows but meh it basically works. 3396 if(this_.parentWindow) { 3397 // FIXME: if painting is slow, this does still lag 3398 // we probably will want to expose some user hook to ScrollWindowEx 3399 // or something. 3400 UpdateWindow(this_.parentWindow.hwnd); 3401 } 3402 return 0; 3403 default: 3404 } 3405 } 3406 break; 3407 3408 case WM_CONTEXTMENU: 3409 auto hwndFrom = cast(HWND) wParam; 3410 3411 auto xPos = cast(short) LOWORD(lParam); 3412 auto yPos = cast(short) HIWORD(lParam); 3413 3414 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3415 POINT p; 3416 p.x = xPos; 3417 p.y = yPos; 3418 ScreenToClient(hwnd, &p); 3419 auto clientX = cast(ushort) p.x; 3420 auto clientY = cast(ushort) p.y; 3421 3422 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 3423 3424 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 3425 return 0; 3426 } 3427 } 3428 break; 3429 3430 case WM_DRAWITEM: 3431 auto dis = cast(DRAWITEMSTRUCT*) lParam; 3432 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 3433 return (*widgetp).handleWmDrawItem(dis); 3434 } 3435 break; 3436 3437 case WM_NOTIFY: 3438 auto hdr = cast(NMHDR*) lParam; 3439 auto hwndFrom = hdr.hwndFrom; 3440 auto code = hdr.code; 3441 3442 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3443 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 3444 } 3445 break; 3446 case WM_COMMAND: 3447 auto handle = cast(HWND) lParam; 3448 auto cmd = HIWORD(wParam); 3449 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 3450 3451 default: 3452 // pass it on 3453 } 3454 return 0; 3455 } 3456 3457 3458 3459 extern(Windows) 3460 private 3461 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 3462 // but can i merge them?! 3463 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3464 // try { writeln(iMessage); } catch(Exception e) {}; 3465 3466 if(auto te = hWnd in Widget.nativeMapping) { 3467 try { 3468 3469 te.hookedWndProc(iMessage, wParam, lParam); 3470 3471 int mustReturn; 3472 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 3473 if(mustReturn) 3474 return ret; 3475 3476 if(iMessage == WM_SETFOCUS) { 3477 auto lol = *te; 3478 while(lol !is null && lol.implicitlyCreated) 3479 lol = lol.parent; 3480 lol.focus(); 3481 //(*te).parentWindow.focusedWidget = lol; 3482 } 3483 3484 3485 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 3486 SetBkMode(cast(HDC) wParam, TRANSPARENT); 3487 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 3488 //GetStockObject(NULL_BRUSH); 3489 } 3490 3491 auto pos = getChildPositionRelativeToParentOrigin(*te); 3492 lastDefaultPrevented = false; 3493 // try { writeln(typeid(*te)); } catch(Exception e) {} 3494 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 3495 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 3496 else { 3497 // it was something we recognized, should only call the window procedure if the default was not prevented 3498 } 3499 } catch(Exception e) { 3500 assert(0, e.toString()); 3501 } 3502 return 0; 3503 } 3504 assert(0, "shouldn't be receiving messages for this window...."); 3505 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 3506 } 3507 3508 extern(Windows) 3509 private 3510 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 3511 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3512 if(iMessage == WM_ERASEBKGND) { 3513 auto dc = GetDC(hWnd); 3514 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 3515 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 3516 RECT r; 3517 GetWindowRect(hWnd, &r); 3518 // since the pen is null, to fill the whole space, we need the +1 on both. 3519 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 3520 SelectObject(dc, p); 3521 SelectObject(dc, b); 3522 ReleaseDC(hWnd, dc); 3523 InvalidateRect(hWnd, null, false); // redraw the border 3524 return 1; 3525 } 3526 return HookedWndProc(hWnd, iMessage, wParam, lParam); 3527 } 3528 3529 /++ 3530 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 3531 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 3532 of minigui's expectations. 3533 3534 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 3535 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 3536 3537 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 3538 3539 To check if you can use this, use `static if(UsingWin32Widgets)`. 3540 +/ 3541 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 3542 assert(p.parentWindow !is null); 3543 assert(p.parentWindow.win.impl.hwnd !is null); 3544 3545 auto bsgroupbox = style == BS_GROUPBOX; 3546 3547 HWND phwnd; 3548 3549 auto wtf = p.parent; 3550 while(wtf) { 3551 if(wtf.hwnd !is null) { 3552 phwnd = wtf.hwnd; 3553 break; 3554 } 3555 wtf = wtf.parent; 3556 } 3557 3558 if(phwnd is null) 3559 phwnd = p.parentWindow.win.impl.hwnd; 3560 3561 assert(phwnd !is null); 3562 3563 WCharzBuffer wt = WCharzBuffer(windowText); 3564 3565 style |= WS_VISIBLE | WS_CHILD; 3566 //if(className != WC_TABCONTROL) 3567 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 3568 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 3569 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 3570 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 3571 3572 assert(p.hwnd !is null); 3573 3574 3575 static HFONT font; 3576 if(font is null) { 3577 NONCLIENTMETRICS params; 3578 params.cbSize = params.sizeof; 3579 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 3580 font = CreateFontIndirect(¶ms.lfMessageFont); 3581 } 3582 } 3583 3584 if(font) 3585 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 3586 3587 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 3588 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 3589 Widget.nativeMapping[p.hwnd] = p; 3590 3591 if(bsgroupbox) 3592 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 3593 else 3594 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3595 3596 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 3597 3598 p.registerMovement(); 3599 } 3600 } 3601 3602 version(win32_widgets) 3603 private 3604 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 3605 if(hwnd is null || hwnd in Widget.nativeMapping) 3606 return true; 3607 auto parent = cast(Widget) cast(void*) lparam; 3608 Widget p = new Widget(null); 3609 p._parent = parent; 3610 p.parentWindow = parent.parentWindow; 3611 p.hwnd = hwnd; 3612 p.implicitlyCreated = true; 3613 Widget.nativeMapping[p.hwnd] = p; 3614 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3615 return true; 3616 } 3617 3618 /++ 3619 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 3620 +/ 3621 struct WidgetPainter { 3622 this(ScreenPainter screenPainter, Widget drawingUpon) { 3623 this.drawingUpon = drawingUpon; 3624 this.screenPainter = screenPainter; 3625 if(auto font = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 3626 this.screenPainter.setFont(font); 3627 } 3628 3629 /++ 3630 EXPERIMENTAL. subject to change. 3631 3632 When you draw a cursor, you can draw this to notify your window of where it is, 3633 for IME systems to use. 3634 +/ 3635 void notifyCursorPosition(int x, int y, int width, int height) { 3636 if(auto a = drawingUpon.parentWindow) 3637 if(auto w = a.inputProxy) { 3638 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 3639 } 3640 } 3641 3642 3643 /// 3644 ScreenPainter screenPainter; 3645 /// Forward to the screen painter for other methods 3646 alias screenPainter this; 3647 3648 private Widget drawingUpon; 3649 3650 /++ 3651 This is the list of rectangles that actually need to be redrawn. 3652 3653 Not actually implemented yet. 3654 +/ 3655 Rectangle[] invalidatedRectangles; 3656 3657 private static BaseVisualTheme _visualTheme; 3658 3659 /++ 3660 Functions to access the visual theme and helpers to easily use it. 3661 3662 These are aware of the current widget's computed style out of the theme. 3663 +/ 3664 static @property BaseVisualTheme visualTheme() { 3665 if(_visualTheme is null) 3666 _visualTheme = new DefaultVisualTheme(); 3667 return _visualTheme; 3668 } 3669 3670 /// ditto 3671 static @property void visualTheme(BaseVisualTheme theme) { 3672 _visualTheme = theme; 3673 3674 // FIXME: notify all windows about the new theme 3675 } 3676 3677 /// ditto 3678 Color themeForeground() { 3679 return drawingUpon.getComputedStyle().foregroundColor(); 3680 } 3681 3682 /// ditto 3683 Color themeBackground() { 3684 return drawingUpon.getComputedStyle().background.color; 3685 } 3686 3687 int isDarkTheme() { 3688 return 0; // unspecified, yes, no as enum. FIXME 3689 } 3690 3691 /++ 3692 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. 3693 3694 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 3695 3696 If you change teh clip rectangle, you should change it back before you return. 3697 3698 3699 The sequence it uses is: 3700 background 3701 content (delegated to you) 3702 border 3703 focused outline 3704 selected overlay 3705 3706 Example code: 3707 3708 --- 3709 void paint(WidgetPainter painter) { 3710 painter.drawThemed((bounds) { 3711 return bounds; // if the selection overlay should be contained, you can return it here. 3712 }); 3713 } 3714 --- 3715 +/ 3716 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 3717 drawThemed((WidgetPainter painter, const Rectangle bounds) { 3718 return drawBody(bounds); 3719 }); 3720 } 3721 // this overload is actually mroe for setting the delegate to a virtual function 3722 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 3723 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 3724 3725 auto cs = drawingUpon.getComputedStyle(); 3726 3727 auto bg = cs.background.color; 3728 3729 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 3730 3731 rect.left += borderWidth; 3732 rect.right -= borderWidth; 3733 rect.top += borderWidth; 3734 rect.bottom -= borderWidth; 3735 3736 auto insideBorderRect = rect; 3737 3738 rect.left += cs.paddingLeft; 3739 rect.right -= cs.paddingRight; 3740 rect.top += cs.paddingTop; 3741 rect.bottom -= cs.paddingBottom; 3742 3743 this.outlineColor = this.themeForeground; 3744 this.fillColor = bg; 3745 3746 auto widgetFont = cs.fontCached; 3747 if(widgetFont !is null) 3748 this.setFont(widgetFont); 3749 3750 rect = drawBody(this, rect); 3751 3752 if(widgetFont !is null) { 3753 if(auto vtFont = visualTheme.defaultFontCached(drawingUpon.currentDpi)) 3754 this.setFont(vtFont); 3755 else 3756 this.setFont(null); 3757 } 3758 3759 if(auto os = cs.outlineStyle()) { 3760 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 3761 this.fillColor = Color.transparent; 3762 this.drawRectangle(insideBorderRect); 3763 } 3764 } 3765 3766 /++ 3767 First, draw the background. 3768 Then draw your content. 3769 Next, draw the border. 3770 And the focused indicator. 3771 And the is-selected box. 3772 3773 If it is focused i can draw the outline too... 3774 3775 If selected i can even do the xor action but that's at the end. 3776 +/ 3777 void drawThemeBackground() { 3778 3779 } 3780 3781 void drawThemeBorder() { 3782 3783 } 3784 3785 // all this stuff is a dangerous experiment.... 3786 static class ScriptableVersion { 3787 ScreenPainterImplementation* p; 3788 int originX, originY; 3789 3790 @scriptable: 3791 void drawRectangle(int x, int y, int width, int height) { 3792 p.drawRectangle(x + originX, y + originY, width, height); 3793 } 3794 void drawLine(int x1, int y1, int x2, int y2) { 3795 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 3796 } 3797 void drawText(int x, int y, string text) { 3798 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 3799 } 3800 void setOutlineColor(int r, int g, int b) { 3801 p.pen = Pen(Color(r,g,b), 1); 3802 } 3803 void setFillColor(int r, int g, int b) { 3804 p.fillColor = Color(r,g,b); 3805 } 3806 } 3807 3808 ScriptableVersion toArsdJsvar() { 3809 auto sv = new ScriptableVersion; 3810 sv.p = this.screenPainter.impl; 3811 sv.originX = this.screenPainter.originX; 3812 sv.originY = this.screenPainter.originY; 3813 return sv; 3814 } 3815 3816 static WidgetPainter fromJsVar(T)(T t) { 3817 return WidgetPainter.init; 3818 } 3819 // done.......... 3820 } 3821 3822 3823 struct Style { 3824 static struct helper(string m, T) { 3825 enum method = m; 3826 T v; 3827 3828 mixin template MethodOverride(typeof(this) v) { 3829 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 3830 } 3831 } 3832 3833 static auto opDispatch(string method, T)(T value) { 3834 return helper!(method, T)(value); 3835 } 3836 } 3837 3838 /++ 3839 Implementation detail of the [ControlledBy] UDA. 3840 3841 History: 3842 Added Oct 28, 2020 3843 +/ 3844 struct ControlledBy_(T, Args...) { 3845 Args args; 3846 3847 static if(Args.length) 3848 this(Args args) { 3849 this.args = args; 3850 } 3851 3852 private T construct(Widget parent) { 3853 return new T(args, parent); 3854 } 3855 } 3856 3857 /++ 3858 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 3859 3860 History: 3861 Added Oct 28, 2020 3862 +/ 3863 auto ControlledBy(T, Args...)(Args args) { 3864 return ControlledBy_!(T, Args)(args); 3865 } 3866 3867 struct ContainerMeta { 3868 string name; 3869 ContainerMeta[] children; 3870 Widget function(Widget parent) factory; 3871 3872 Widget instantiate(Widget parent) { 3873 auto n = factory(parent); 3874 n.name = name; 3875 foreach(child; children) 3876 child.instantiate(n); 3877 return n; 3878 } 3879 } 3880 3881 /++ 3882 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 3883 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 3884 3885 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 3886 structures. It works fine on structs declared inside functions though. 3887 3888 See: https://issues.dlang.org/show_bug.cgi?id=21984 3889 +/ 3890 template Container(CArgs...) { 3891 static if(CArgs.length && is(CArgs[0] : Widget)) { 3892 private alias Super = CArgs[0]; 3893 private alias CArgs2 = CArgs[1 .. $]; 3894 } else { 3895 private alias Super = Layout; 3896 private alias CArgs2 = CArgs; 3897 } 3898 3899 class Container : Super { 3900 this(Widget parent) { super(parent); } 3901 3902 // just to partially support old gdc versions 3903 version(GNU) { 3904 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 3905 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 3906 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 3907 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 3908 } else mixin(q{ 3909 static foreach(Arg; CArgs2) { 3910 mixin Arg.MethodOverride!(Arg); 3911 } 3912 }); 3913 3914 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 3915 return ContainerMeta( 3916 name, 3917 children.dup, 3918 function (Widget parent) { return new typeof(this)(parent); } 3919 ); 3920 } 3921 3922 static ContainerMeta opCall(ContainerMeta[] children...) { 3923 return opCall(null, children); 3924 } 3925 } 3926 } 3927 3928 /++ 3929 The data controller widget is created by reflecting over the given 3930 data type. You can use [ControlledBy] as a UDA on a struct or 3931 just let it create things automatically. 3932 3933 Unlike [dialog], this uses real-time updating of the data and 3934 you add it to another window yourself. 3935 3936 --- 3937 struct Test { 3938 int x; 3939 int y; 3940 } 3941 3942 auto window = new Window(); 3943 auto dcw = new DataControllerWidget!Test(new Test, window); 3944 --- 3945 3946 The way it works is any public members are given a widget based 3947 on their data type, and public methods trigger an action button 3948 if no relevant parameters or a dialog action if it does have 3949 parameters, similar to the [menu] facility. 3950 3951 If you change data programmatically, without going through the 3952 DataControllerWidget methods, you will have to tell it something 3953 has changed and it needs to redraw. This is done with the `invalidate` 3954 method. 3955 3956 History: 3957 Added Oct 28, 2020 3958 +/ 3959 /// Group: generating_from_code 3960 class DataControllerWidget(T) : WidgetContainer { 3961 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 3962 private alias Tref = T; 3963 else 3964 private alias Tref = T*; 3965 3966 Tref datum; 3967 3968 /++ 3969 See_also: [addDataControllerWidget] 3970 +/ 3971 this(Tref datum, Widget parent) { 3972 this.datum = datum; 3973 3974 Widget cp = this; 3975 3976 super(parent); 3977 3978 foreach(attr; __traits(getAttributes, T)) 3979 static if(is(typeof(attr) == ContainerMeta)) { 3980 cp = attr.instantiate(this); 3981 } 3982 3983 auto def = this.getByName("default"); 3984 if(def !is null) 3985 cp = def; 3986 3987 Widget helper(string name) { 3988 auto maybe = this.getByName(name); 3989 if(maybe is null) 3990 return cp; 3991 return maybe; 3992 3993 } 3994 3995 foreach(member; __traits(allMembers, T)) 3996 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 3997 static if(is(typeof(__traits(getMember, this.datum, member)))) 3998 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 3999 void delegate() update; 4000 4001 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 4002 4003 if(update) 4004 updaters ~= update; 4005 4006 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 4007 w.addEventListener("triggered", delegate() { 4008 makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))(); 4009 notifyDataUpdated(); 4010 }); 4011 } else static if(is(typeof(w.isChecked) == bool)) { 4012 w.addEventListener(EventType.change, (Event ev) { 4013 __traits(getMember, this.datum, member) = w.isChecked; 4014 }); 4015 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 4016 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 4017 } else static if(is(typeof(w.value) == int)) { 4018 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4019 } else static if(is(typeof(w) == DropDownSelection)) { 4020 // special case for this to kinda support enums and such. coudl be better though 4021 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 4022 } else { 4023 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 4024 } 4025 } 4026 } 4027 4028 /++ 4029 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 4030 4031 History: 4032 Added May 28, 2021 4033 +/ 4034 void notifyDataUpdated() { 4035 foreach(updater; updaters) 4036 updater(); 4037 4038 this.emit!(ChangeEvent!void)(delegate{}); 4039 } 4040 4041 private Widget[string] memberWidgets; 4042 private void delegate()[] updaters; 4043 4044 mixin Emits!(ChangeEvent!void); 4045 } 4046 4047 private int saturatedSum(int[] values...) { 4048 int sum; 4049 foreach(value; values) { 4050 if(value == int.max) 4051 return int.max; 4052 sum += value; 4053 } 4054 return sum; 4055 } 4056 4057 void genericSetValue(T, W)(T* where, W what) { 4058 import std.conv; 4059 *where = to!T(what); 4060 //*where = cast(T) stringToLong(what); 4061 } 4062 4063 /++ 4064 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4065 4066 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4067 4068 Note that this creates the widget but does not attach any event handlers to it. 4069 +/ 4070 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 4071 4072 string displayName = __traits(identifier, tt).beautify; 4073 4074 static if(controlledByCount!tt == 1) { 4075 foreach(i, attr; __traits(getAttributes, tt)) { 4076 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4077 auto w = attr.construct(parent); 4078 static if(__traits(compiles, w.setPosition(*valptr))) 4079 update = () { w.setPosition(*valptr); }; 4080 else static if(__traits(compiles, w.setValue(*valptr))) 4081 update = () { w.setValue(*valptr); }; 4082 4083 if(update) 4084 update(); 4085 return w; 4086 } 4087 } 4088 } else static if(controlledByCount!tt == 0) { 4089 static if(is(typeof(tt) == enum)) { 4090 // FIXME: update 4091 auto dds = new DropDownSelection(parent); 4092 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4093 dds.addOption(option); 4094 if(__traits(getMember, typeof(tt), option) == *valptr) 4095 dds.setSelection(cast(int) idx); 4096 } 4097 return dds; 4098 } else static if(is(typeof(tt) == bool)) { 4099 auto box = new Checkbox(displayName, parent); 4100 update = () { box.isChecked = *valptr; }; 4101 update(); 4102 return box; 4103 } else static if(is(typeof(tt) : const long)) { 4104 auto le = new LabeledLineEdit(displayName, parent); 4105 update = () { le.content = toInternal!string(*valptr); }; 4106 update(); 4107 return le; 4108 } else static if(is(typeof(tt) : const double)) { 4109 auto le = new LabeledLineEdit(displayName, parent); 4110 import std.conv; 4111 update = () { le.content = to!string(*valptr); }; 4112 update(); 4113 return le; 4114 } else static if(is(typeof(tt) : const string)) { 4115 auto le = new LabeledLineEdit(displayName, parent); 4116 update = () { le.content = *valptr; }; 4117 update(); 4118 return le; 4119 } else static if(is(typeof(tt) == function)) { 4120 auto w = new Button(displayName, parent); 4121 return w; 4122 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4123 return parent.addDataControllerWidget(tt); 4124 } else static assert(0, typeof(tt).stringof); 4125 } else static assert(0, "multiple controllers not yet supported"); 4126 } 4127 4128 private template controlledByCount(alias tt) { 4129 static int helper() { 4130 int count; 4131 foreach(i, attr; __traits(getAttributes, tt)) 4132 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 4133 count++; 4134 return count; 4135 } 4136 4137 enum controlledByCount = helper; 4138 } 4139 4140 /++ 4141 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 4142 4143 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 4144 4145 History: 4146 The `redrawOnChange` parameter was added on May 28, 2021. 4147 +/ 4148 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 4149 auto dcw = new DataControllerWidget!T(t, parent); 4150 initializeDataControllerWidget(dcw, redrawOnChange); 4151 return dcw; 4152 } 4153 4154 /// ditto 4155 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 4156 auto dcw = new DataControllerWidget!T(t, parent); 4157 initializeDataControllerWidget(dcw, redrawOnChange); 4158 return dcw; 4159 } 4160 4161 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 4162 if(redrawOnChange !is null) 4163 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 4164 } 4165 4166 /++ 4167 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. 4168 4169 History: 4170 Finalized on June 3, 2021 for the dub v10.0 release 4171 +/ 4172 struct StyleInformation { 4173 private Widget w; 4174 private BaseVisualTheme visualTheme; 4175 4176 private this(Widget w) { 4177 this.w = w; 4178 this.visualTheme = WidgetPainter.visualTheme; 4179 } 4180 4181 /++ 4182 Forwards to [Widget.Style] 4183 4184 Bugs: 4185 It is supposed to fall back to the [VisualTheme] if 4186 the style doesn't override the default, but that is 4187 not generally implemented. Many of them may end up 4188 being explicit overloads instead of the generic 4189 opDispatch fallback, like [font] is now. 4190 +/ 4191 public @property opDispatch(string name)() { 4192 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4193 w.useStyleProperties((scope Widget.Style props) { 4194 //visualTheme.useStyleProperties(w, (props) { 4195 prop = __traits(getMember, props, name); 4196 }); 4197 return prop; 4198 } 4199 4200 /++ 4201 Returns the cached font object associated with the widget, 4202 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 4203 4204 History: 4205 Prior to March 21, 2022 (dub v10.7), `font` went through 4206 [opDispatch], which did not use the cache. You can now call it 4207 repeatedly without guilt. 4208 +/ 4209 public @property OperatingSystemFont font() { 4210 OperatingSystemFont prop; 4211 w.useStyleProperties((scope Widget.Style props) { 4212 prop = props.fontCached; 4213 }); 4214 if(prop is null) { 4215 prop = visualTheme.defaultFontCached(w.currentDpi); 4216 } 4217 return prop; 4218 } 4219 4220 @property { 4221 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 4222 /** */ int paddingLeft() { return w.paddingLeft(); } 4223 /** */ int paddingRight() { return w.paddingRight(); } 4224 /** */ int paddingTop() { return w.paddingTop(); } 4225 /** */ int paddingBottom() { return w.paddingBottom(); } 4226 4227 /** */ int marginLeft() { return w.marginLeft(); } 4228 /** */ int marginRight() { return w.marginRight(); } 4229 /** */ int marginTop() { return w.marginTop(); } 4230 /** */ int marginBottom() { return w.marginBottom(); } 4231 4232 /** */ int maxHeight() { return w.maxHeight(); } 4233 /** */ int minHeight() { return w.minHeight(); } 4234 4235 /** */ int maxWidth() { return w.maxWidth(); } 4236 /** */ int minWidth() { return w.minWidth(); } 4237 4238 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 4239 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 4240 4241 /** */ int heightStretchiness() { return w.heightStretchiness(); } 4242 /** */ int widthStretchiness() { return w.widthStretchiness(); } 4243 4244 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 4245 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 4246 4247 // Global helpers some of these are unstable. 4248 static: 4249 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4250 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4251 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4252 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4253 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 4254 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4255 4256 /** */ Color activeTabColor() { return lightAccentColor; } 4257 /** */ Color buttonColor() { return windowBackgroundColor; } 4258 /** */ Color depressedButtonColor() { return darkAccentColor; } 4259 /** */ Color hoveringColor() { return lightAccentColor; } 4260 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 4261 auto c = WidgetPainter.visualTheme.selectionColor(); 4262 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4263 } 4264 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4265 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4266 } 4267 4268 4269 4270 /+ 4271 4272 private static auto extractStyleProperty(string name)(Widget w) { 4273 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4274 w.useStyleProperties((props) { 4275 prop = __traits(getMember, props, name); 4276 }); 4277 return prop; 4278 } 4279 4280 // FIXME: clear this upon a X server disconnect 4281 private static OperatingSystemFont[string] fontCache; 4282 4283 T getProperty(T)(string name, lazy T default_) { 4284 if(visualTheme !is null) { 4285 auto str = visualTheme.getPropertyString(w, name); 4286 if(str is null) 4287 return default_; 4288 static if(is(T == Color)) 4289 return Color.fromString(str); 4290 else static if(is(T == Measurement)) 4291 return Measurement(cast(int) toInternal!int(str)); 4292 else static if(is(T == WidgetBackground)) 4293 return WidgetBackground.fromString(str); 4294 else static if(is(T == OperatingSystemFont)) { 4295 if(auto f = str in fontCache) 4296 return *f; 4297 else 4298 return fontCache[str] = new OperatingSystemFont(str); 4299 } else static if(is(T == FrameStyle)) { 4300 switch(str) { 4301 default: 4302 return FrameStyle.none; 4303 foreach(style; __traits(allMembers, FrameStyle)) 4304 case style: 4305 return __traits(getMember, FrameStyle, style); 4306 } 4307 } else static assert(0); 4308 } else 4309 return default_; 4310 } 4311 4312 static struct Measurement { 4313 int value; 4314 alias value this; 4315 } 4316 4317 @property: 4318 4319 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 4320 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 4321 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 4322 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 4323 4324 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 4325 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 4326 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 4327 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 4328 4329 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 4330 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 4331 4332 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 4333 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 4334 4335 4336 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 4337 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 4338 4339 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 4340 4341 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 4342 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 4343 4344 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 4345 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 4346 4347 4348 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4349 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4350 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4351 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4352 4353 Color activeTabColor() { return lightAccentColor; } 4354 Color buttonColor() { return windowBackgroundColor; } 4355 Color depressedButtonColor() { return darkAccentColor; } 4356 Color hoveringColor() { return Color(228, 228, 228); } 4357 Color activeListXorColor() { 4358 auto c = WidgetPainter.visualTheme.selectionColor(); 4359 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4360 } 4361 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4362 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4363 +/ 4364 } 4365 4366 4367 4368 // pragma(msg, __traits(classInstanceSize, Widget)); 4369 4370 /*private*/ template EventString(E) { 4371 static if(is(typeof(E.EventString))) 4372 enum EventString = E.EventString; 4373 else 4374 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 4375 } 4376 4377 /*private*/ template EventStringIdentifier(E) { 4378 string helper() { 4379 auto es = EventString!E; 4380 char[] id = new char[](es.length * 2); 4381 size_t idx; 4382 foreach(char ch; es) { 4383 id[idx++] = cast(char)('a' + (ch >> 4)); 4384 id[idx++] = cast(char)('a' + (ch & 0x0f)); 4385 } 4386 return cast(string) id; 4387 } 4388 4389 enum EventStringIdentifier = helper(); 4390 } 4391 4392 4393 template classStaticallyEmits(This, EventType) { 4394 static if(is(This Base == super)) 4395 static if(is(Base : Widget)) 4396 enum baseEmits = classStaticallyEmits!(Base, EventType); 4397 else 4398 enum baseEmits = false; 4399 else 4400 enum baseEmits = false; 4401 4402 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 4403 4404 enum classStaticallyEmits = thisEmits || baseEmits; 4405 } 4406 4407 /++ 4408 A helper to make widgets out of other native windows. 4409 4410 History: 4411 Factored out of OpenGlWidget on November 5, 2021 4412 +/ 4413 class NestedChildWindowWidget : Widget { 4414 SimpleWindow win; 4415 4416 /++ 4417 Used on X to send focus to the appropriate child window when requested by the window manager. 4418 4419 Normally returns its own nested window. Can also return another child or null to revert to the parent 4420 if you override it in a child class. 4421 4422 History: 4423 Added April 2, 2022 (dub v10.8) 4424 +/ 4425 SimpleWindow focusableWindow() { 4426 return win; 4427 } 4428 4429 /// 4430 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4431 this(SimpleWindow win, Widget parent) { 4432 this.parentWindow = parent.parentWindow; 4433 this.win = win; 4434 4435 super(parent); 4436 windowsetup(win); 4437 } 4438 4439 static protected SimpleWindow getParentWindow(Widget parent) { 4440 assert(parent !is null); 4441 SimpleWindow pwin = parent.parentWindow.win; 4442 4443 version(win32_widgets) { 4444 HWND phwnd; 4445 auto wtf = parent; 4446 while(wtf) { 4447 if(wtf.hwnd) { 4448 phwnd = wtf.hwnd; 4449 break; 4450 } 4451 wtf = wtf.parent; 4452 } 4453 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 4454 if(phwnd) 4455 pwin = new SimpleWindow(phwnd); 4456 } 4457 4458 return pwin; 4459 } 4460 4461 /++ 4462 Called upon the nested window being destroyed. 4463 Remember the window has already been destroyed at 4464 this point, so don't use the native handle for anything. 4465 4466 History: 4467 Added April 3, 2022 (dub v10.8) 4468 +/ 4469 protected void dispose() { 4470 4471 } 4472 4473 protected void windowsetup(SimpleWindow w) { 4474 /* 4475 win.onFocusChange = (bool getting) { 4476 if(getting) 4477 this.focus(); 4478 }; 4479 */ 4480 4481 /+ 4482 win.onFocusChange = (bool getting) { 4483 if(getting) { 4484 this.parentWindow.focusedWidget = this; 4485 this.emit!FocusEvent(); 4486 this.emit!FocusInEvent(); 4487 } else { 4488 this.emit!BlurEvent(); 4489 this.emit!FocusOutEvent(); 4490 } 4491 }; 4492 +/ 4493 4494 win.onDestroyed = () { 4495 this.dispose(); 4496 }; 4497 4498 version(win32_widgets) { 4499 Widget.nativeMapping[win.hwnd] = this; 4500 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4501 } else { 4502 win.setEventHandlers( 4503 (MouseEvent e) { 4504 Widget p = this; 4505 while(p ! is parentWindow) { 4506 e.x += p.x; 4507 e.y += p.y; 4508 p = p.parent; 4509 } 4510 parentWindow.dispatchMouseEvent(e); 4511 }, 4512 (KeyEvent e) { 4513 //writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 4514 parentWindow.dispatchKeyEvent(e); 4515 }, 4516 (dchar e) { 4517 parentWindow.dispatchCharEvent(e); 4518 }, 4519 ); 4520 } 4521 4522 } 4523 4524 override void showing(bool s, bool recalc) { 4525 auto cur = hidden; 4526 win.hidden = !s; 4527 if(cur != s && s) 4528 redraw(); 4529 } 4530 4531 /// OpenGL widgets cannot have child widgets. Do not call this. 4532 /* @disable */ final override void addChild(Widget, int) { 4533 throw new Error("cannot add children to OpenGL widgets"); 4534 } 4535 4536 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 4537 /// Keep in mind that events like mouse coordinates are still relative to your size. 4538 override void registerMovement() { 4539 // writefln("%d %d %d %d", x,y,width,height); 4540 version(win32_widgets) 4541 auto pos = getChildPositionRelativeToParentHwnd(this); 4542 else 4543 auto pos = getChildPositionRelativeToParentOrigin(this); 4544 win.moveResize(pos[0], pos[1], width, height); 4545 4546 registerMovementAdditionalWork(); 4547 sendResizeEvent(); 4548 } 4549 4550 abstract void registerMovementAdditionalWork(); 4551 } 4552 4553 /++ 4554 Nests an opengl capable window inside this window as a widget. 4555 4556 You may also just want to create an additional [SimpleWindow] with 4557 [OpenGlOptions.yes] yourself. 4558 4559 An OpenGL widget cannot have child widgets. It will throw if you try. 4560 +/ 4561 static if(OpenGlEnabled) 4562 class OpenGlWidget : NestedChildWindowWidget { 4563 4564 override void registerMovementAdditionalWork() { 4565 win.setAsCurrentOpenGlContext(); 4566 } 4567 4568 /// 4569 this(Widget parent) { 4570 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4571 super(win, parent); 4572 } 4573 4574 override void paint(WidgetPainter painter) { 4575 win.setAsCurrentOpenGlContext(); 4576 glViewport(0, 0, this.width, this.height); 4577 win.redrawOpenGlSceneNow(); 4578 } 4579 4580 void redrawOpenGlScene(void delegate() dg) { 4581 win.redrawOpenGlScene = dg; 4582 } 4583 } 4584 4585 /++ 4586 This demo shows how to draw text in an opengl scene. 4587 +/ 4588 unittest { 4589 import arsd.minigui; 4590 import arsd.ttf; 4591 4592 void main() { 4593 auto window = new Window(); 4594 4595 auto widget = new OpenGlWidget(window); 4596 4597 // old means non-shader code so compatible with glBegin etc. 4598 // tbh I haven't implemented new one in font yet... 4599 // anyway, declaring here, will construct soon. 4600 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 4601 4602 // this is a little bit awkward, calling some methods through 4603 // the underlying SimpleWindow `win` method, and you can't do this 4604 // on a nanovega widget due to conflicts so I should probably fix 4605 // the api to be a bit easier. But here it will work. 4606 // 4607 // Alternatively, you could load the font on the first draw, inside 4608 // the redrawOpenGlScene, and keep a flag so you don't do it every 4609 // time. That'd be a bit easier since the lib sets up the context 4610 // by then guaranteed. 4611 // 4612 // But still, I wanna show this. 4613 widget.win.visibleForTheFirstTime = delegate { 4614 // must set the opengl context 4615 widget.win.setAsCurrentOpenGlContext(); 4616 4617 // if you were doing a OpenGL 3+ shader, this 4618 // gets especially important to do in order. With 4619 // old-style opengl, I think you can even do it 4620 // in main(), but meh, let's show it more correctly. 4621 4622 // Anyway, now it is time to load the font from the 4623 // OS (you can alternatively load one from a .ttf file 4624 // you bundle with the application), then load the 4625 // font into texture for drawing. 4626 4627 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 4628 4629 assert(!osfont.isNull()); // make sure it actually loaded 4630 4631 // using typeof to avoid repeating the long name lol 4632 glfont = new typeof(glfont)( 4633 // get the raw data from the font for loading in here 4634 // since it doesn't use the OS function to draw the 4635 // text, we gotta treat it more as a file than as 4636 // a drawing api. 4637 osfont.getTtfBytes(), 4638 18, // need to respecify size since opengl world is different coordinate system 4639 4640 // these last two numbers are why it is called 4641 // "Limited" font. It only loads the characters 4642 // in the given range, since the texture atlas 4643 // it references is all a big image generated ahead 4644 // of time. You could maybe do the whole thing but 4645 // idk how much memory that is. 4646 // 4647 // But here, 0-128 represents the ASCII range, so 4648 // good enough for most English things, numeric labels, 4649 // etc. 4650 0, 4651 128 4652 ); 4653 }; 4654 4655 widget.redrawOpenGlScene = () { 4656 // now we can use the glfont's drawString function 4657 4658 // first some opengl setup. You can do this in one place 4659 // on window first visible too in many cases, just showing 4660 // here cuz it is easier for me. 4661 4662 // gonna need some alpha blending or it just looks awful 4663 glEnable(GL_BLEND); 4664 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 4665 glClearColor(0,0,0,0); 4666 glDepthFunc(GL_LEQUAL); 4667 4668 // Also need to enable 2d textures, since it draws the 4669 // font characters as images baked in 4670 glMatrixMode(GL_MODELVIEW); 4671 glLoadIdentity(); 4672 glDisable(GL_DEPTH_TEST); 4673 glEnable(GL_TEXTURE_2D); 4674 4675 // the orthographic matrix is best for 2d things like text 4676 // so let's set that up. This matrix makes the coordinates 4677 // in the opengl scene be one-to-one with the actual pixels 4678 // on screen. (Not necessarily best, you may wish to scale 4679 // things, but it does help keep fonts looking normal.) 4680 glMatrixMode(GL_PROJECTION); 4681 glLoadIdentity(); 4682 glOrtho(0, widget.width, widget.height, 0, 0, 1); 4683 4684 // you can do other glScale, glRotate, glTranslate, etc 4685 // to the matrix here of course if you want. 4686 4687 // note the x,y coordinates here are for the text baseline 4688 // NOT the upper-left corner. The baseline is like the line 4689 // in the notebook you write on. Most the letters are actually 4690 // above it, but some, like p and q, dip a bit below it. 4691 // 4692 // So if you're used to the upper left coordinate like the 4693 // rest of simpledisplay/minigui usually do, do the 4694 // y + glfont.ascent to bring it down a little. So this 4695 // example puts the string in the upper left of the window. 4696 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 4697 4698 // re color btw: the function sets a solid color internally, 4699 // but you actually COULD do your own thing for rainbow effects 4700 // and the sort if you wanted too, by pulling its guts out. 4701 // Just view its source for an idea of how it actually draws: 4702 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 4703 4704 // it gets a bit complicated with the character positioning, 4705 // but the opengl parts are fairly simple: bind a texture, 4706 // set the color, draw a quad for each letter. 4707 4708 4709 // the last optional argument there btw is a bounding box 4710 // it will/ use to word wrap and return an object you can 4711 // use to implement scrolling or pagination; it tells how 4712 // much of the string didn't fit in the box. But for simple 4713 // labels we can just ignore that. 4714 4715 4716 // I'd suggest drawing text as the last step, after you 4717 // do your other drawing. You might use the push/pop matrix 4718 // stuff to keep your place. You, in theory, should be able 4719 // to do text in a 3d space but I've never actually tried 4720 // that.... 4721 }; 4722 4723 window.loop(); 4724 } 4725 } 4726 4727 version(custom_widgets) 4728 private alias ListWidgetBase = ScrollableWidget; 4729 else 4730 private alias ListWidgetBase = Widget; 4731 4732 /++ 4733 A list widget contains a list of strings that the user can examine and select. 4734 4735 4736 In the future, items in the list may be possible to be more than just strings. 4737 4738 See_Also: 4739 [TableView] 4740 +/ 4741 class ListWidget : ListWidgetBase { 4742 /// 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. 4743 mixin Emits!(ChangeEvent!void); 4744 4745 static struct Option { 4746 string label; 4747 bool selected; 4748 void* tag; 4749 } 4750 4751 /++ 4752 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 4753 +/ 4754 void setSelection(int y) { 4755 if(!multiSelect) 4756 foreach(ref opt; options) 4757 opt.selected = false; 4758 if(y >= 0 && y < options.length) 4759 options[y].selected = !options[y].selected; 4760 4761 this.emit!(ChangeEvent!void)(delegate {}); 4762 4763 version(custom_widgets) 4764 redraw(); 4765 } 4766 4767 /++ 4768 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 4769 Returns -1 if nothing is selected. 4770 +/ 4771 int getSelection() 4772 { 4773 foreach(i, opt; options) { 4774 if (opt.selected) 4775 return cast(int) i; 4776 } 4777 return -1; 4778 } 4779 4780 version(custom_widgets) 4781 override void defaultEventHandler_click(ClickEvent event) { 4782 this.focus(); 4783 if(event.button == MouseButton.left) { 4784 auto y = (event.clientY - 4) / defaultLineHeight; 4785 if(y >= 0 && y < options.length) { 4786 setSelection(y); 4787 } 4788 } 4789 super.defaultEventHandler_click(event); 4790 } 4791 4792 this(Widget parent) { 4793 tabStop = false; 4794 super(parent); 4795 version(win32_widgets) 4796 createWin32Window(this, WC_LISTBOX, "", 4797 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 4798 } 4799 4800 version(win32_widgets) 4801 override void handleWmCommand(ushort code, ushort id) { 4802 switch(code) { 4803 case LBN_SELCHANGE: 4804 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 4805 setSelection(cast(int) sel); 4806 break; 4807 default: 4808 } 4809 } 4810 4811 4812 version(custom_widgets) 4813 override void paintFrameAndBackground(WidgetPainter painter) { 4814 draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor); 4815 } 4816 4817 version(custom_widgets) 4818 override void paint(WidgetPainter painter) { 4819 auto cs = getComputedStyle(); 4820 auto pos = Point(4, 4); 4821 foreach(idx, option; options) { 4822 painter.fillColor = painter.visualTheme.widgetBackgroundColor; 4823 painter.outlineColor = painter.visualTheme.widgetBackgroundColor; 4824 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4825 if(option.selected) { 4826 //painter.rasterOp = RasterOp.xor; 4827 painter.outlineColor = cs.selectionForegroundColor; 4828 painter.fillColor = cs.selectionBackgroundColor; 4829 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4830 //painter.rasterOp = RasterOp.normal; 4831 } 4832 painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor; 4833 painter.drawText(pos, option.label); 4834 pos.y += defaultLineHeight; 4835 } 4836 } 4837 4838 static class Style : Widget.Style { 4839 override WidgetBackground background() { 4840 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 4841 } 4842 } 4843 mixin OverrideStyle!Style; 4844 //mixin Padding!q{2}; 4845 4846 void addOption(string text, void* tag = null) { 4847 options ~= Option(text, false, tag); 4848 version(win32_widgets) { 4849 WCharzBuffer buffer = WCharzBuffer(text); 4850 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 4851 } 4852 version(custom_widgets) { 4853 setContentSize(width, cast(int) (options.length * defaultLineHeight)); 4854 redraw(); 4855 } 4856 } 4857 4858 void clear() { 4859 options = null; 4860 version(win32_widgets) { 4861 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 4862 {} 4863 4864 } else version(custom_widgets) { 4865 scrollTo(Point(0, 0)); 4866 redraw(); 4867 } 4868 } 4869 4870 Option[] options; 4871 version(win32_widgets) 4872 enum multiSelect = false; /// not implemented yet 4873 else 4874 bool multiSelect; 4875 4876 override int heightStretchiness() { return 6; } 4877 } 4878 4879 4880 4881 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 4882 enum ScrollBarShowPolicy { 4883 automatic, /// automatically show the scroll bar if it is necessary 4884 never, /// never show the scroll bar (scrolling must be done programmatically) 4885 always /// always show the scroll bar, even if it is disabled 4886 } 4887 4888 /++ 4889 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 4890 4891 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 4892 +/ 4893 // FIXME ScrollBarShowPolicy 4894 // FIXME: use the ScrollMessageWidget in here now that it exists 4895 class ScrollableWidget : Widget { 4896 // FIXME: make line size configurable 4897 // FIXME: add keyboard controls 4898 version(win32_widgets) { 4899 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 4900 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 4901 auto pos = HIWORD(wParam); 4902 auto m = LOWORD(wParam); 4903 4904 // FIXME: I can reintroduce the 4905 // scroll bars now by using this 4906 // in the top-level window handler 4907 // to forward comamnds 4908 auto scrollbarHwnd = lParam; 4909 switch(m) { 4910 case SB_BOTTOM: 4911 if(msg == WM_HSCROLL) 4912 horizontalScrollTo(contentWidth_); 4913 else 4914 verticalScrollTo(contentHeight_); 4915 break; 4916 case SB_TOP: 4917 if(msg == WM_HSCROLL) 4918 horizontalScrollTo(0); 4919 else 4920 verticalScrollTo(0); 4921 break; 4922 case SB_ENDSCROLL: 4923 // idk 4924 break; 4925 case SB_LINEDOWN: 4926 if(msg == WM_HSCROLL) 4927 horizontalScroll(scaleWithDpi(16)); 4928 else 4929 verticalScroll(scaleWithDpi(16)); 4930 break; 4931 case SB_LINEUP: 4932 if(msg == WM_HSCROLL) 4933 horizontalScroll(scaleWithDpi(-16)); 4934 else 4935 verticalScroll(scaleWithDpi(-16)); 4936 break; 4937 case SB_PAGEDOWN: 4938 if(msg == WM_HSCROLL) 4939 horizontalScroll(scaleWithDpi(100)); 4940 else 4941 verticalScroll(scaleWithDpi(100)); 4942 break; 4943 case SB_PAGEUP: 4944 if(msg == WM_HSCROLL) 4945 horizontalScroll(scaleWithDpi(-100)); 4946 else 4947 verticalScroll(scaleWithDpi(-100)); 4948 break; 4949 case SB_THUMBPOSITION: 4950 case SB_THUMBTRACK: 4951 if(msg == WM_HSCROLL) 4952 horizontalScrollTo(pos); 4953 else 4954 verticalScrollTo(pos); 4955 4956 if(m == SB_THUMBTRACK) { 4957 // the event loop doesn't seem to carry on with a requested redraw.. 4958 // so we request it to get our dirty bit set... 4959 redraw(); 4960 4961 // then we need to immediately actually redraw it too for instant feedback to user 4962 4963 SimpleWindow.processAllCustomEvents(); 4964 //if(parentWindow) 4965 //parentWindow.actualRedraw(); 4966 } 4967 break; 4968 default: 4969 } 4970 } 4971 return super.hookedWndProc(msg, wParam, lParam); 4972 } 4973 } 4974 /// 4975 this(Widget parent) { 4976 this.parentWindow = parent.parentWindow; 4977 4978 version(win32_widgets) { 4979 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 4980 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 4981 super(parent); 4982 } else version(custom_widgets) { 4983 outerContainer = new InternalScrollableContainerWidget(this, parent); 4984 super(outerContainer); 4985 } else static assert(0); 4986 } 4987 4988 version(custom_widgets) 4989 InternalScrollableContainerWidget outerContainer; 4990 4991 override void defaultEventHandler_click(ClickEvent event) { 4992 if(event.button == MouseButton.wheelUp) 4993 verticalScroll(scaleWithDpi(-16)); 4994 if(event.button == MouseButton.wheelDown) 4995 verticalScroll(scaleWithDpi(16)); 4996 super.defaultEventHandler_click(event); 4997 } 4998 4999 override void defaultEventHandler_keydown(KeyDownEvent event) { 5000 switch(event.key) { 5001 case Key.Left: 5002 horizontalScroll(scaleWithDpi(-16)); 5003 break; 5004 case Key.Right: 5005 horizontalScroll(scaleWithDpi(16)); 5006 break; 5007 case Key.Up: 5008 verticalScroll(scaleWithDpi(-16)); 5009 break; 5010 case Key.Down: 5011 verticalScroll(scaleWithDpi(16)); 5012 break; 5013 case Key.Home: 5014 verticalScrollTo(0); 5015 break; 5016 case Key.End: 5017 verticalScrollTo(contentHeight); 5018 break; 5019 case Key.PageUp: 5020 verticalScroll(scaleWithDpi(-160)); 5021 break; 5022 case Key.PageDown: 5023 verticalScroll(scaleWithDpi(160)); 5024 break; 5025 default: 5026 } 5027 super.defaultEventHandler_keydown(event); 5028 } 5029 5030 5031 version(win32_widgets) 5032 override void recomputeChildLayout() { 5033 super.recomputeChildLayout(); 5034 SCROLLINFO info; 5035 info.cbSize = info.sizeof; 5036 info.nPage = viewportHeight; 5037 info.fMask = SIF_PAGE | SIF_RANGE; 5038 info.nMin = 0; 5039 info.nMax = contentHeight_; 5040 SetScrollInfo(hwnd, SB_VERT, &info, true); 5041 5042 info.cbSize = info.sizeof; 5043 info.nPage = viewportWidth; 5044 info.fMask = SIF_PAGE | SIF_RANGE; 5045 info.nMin = 0; 5046 info.nMax = contentWidth_; 5047 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5048 } 5049 5050 /* 5051 Scrolling 5052 ------------ 5053 5054 You are assigned a width and a height by the layout engine, which 5055 is your viewport box. However, you may draw more than that by setting 5056 a contentWidth and contentHeight. 5057 5058 If these can be contained by the viewport, no scrollbar is displayed. 5059 If they cannot fit though, it will automatically show scroll as necessary. 5060 5061 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 5062 is zero, no vertical scrolling is performed. 5063 5064 If scrolling is necessary, the lib will automatically work with the bars. 5065 When you redraw, the origin and clipping info in the painter is set so if 5066 you just draw everything, it will work, but you can be more efficient by checking 5067 the viewportWidth, viewportHeight, and scrollOrigin members. 5068 */ 5069 5070 /// 5071 final @property int viewportWidth() { 5072 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 5073 } 5074 /// 5075 final @property int viewportHeight() { 5076 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 5077 } 5078 5079 // FIXME property 5080 Point scrollOrigin_; 5081 5082 /// 5083 final const(Point) scrollOrigin() { 5084 return scrollOrigin_; 5085 } 5086 5087 // the user sets these two 5088 private int contentWidth_ = 0; 5089 private int contentHeight_ = 0; 5090 5091 /// 5092 int contentWidth() { return contentWidth_; } 5093 /// 5094 int contentHeight() { return contentHeight_; } 5095 5096 /// 5097 void setContentSize(int width, int height) { 5098 contentWidth_ = width; 5099 contentHeight_ = height; 5100 5101 version(custom_widgets) { 5102 if(showingVerticalScroll || showingHorizontalScroll) { 5103 outerContainer.recomputeChildLayout(); 5104 } 5105 5106 if(showingVerticalScroll()) 5107 outerContainer.verticalScrollBar.redraw(); 5108 if(showingHorizontalScroll()) 5109 outerContainer.horizontalScrollBar.redraw(); 5110 } else version(win32_widgets) { 5111 recomputeChildLayout(); 5112 } else static assert(0); 5113 } 5114 5115 /// 5116 void verticalScroll(int delta) { 5117 verticalScrollTo(scrollOrigin.y + delta); 5118 } 5119 /// 5120 void verticalScrollTo(int pos) { 5121 scrollOrigin_.y = pos; 5122 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 5123 scrollOrigin_.y = contentHeight - viewportHeight; 5124 5125 if(scrollOrigin_.y < 0) 5126 scrollOrigin_.y = 0; 5127 5128 version(win32_widgets) { 5129 SCROLLINFO info; 5130 info.cbSize = info.sizeof; 5131 info.fMask = SIF_POS; 5132 info.nPos = scrollOrigin_.y; 5133 SetScrollInfo(hwnd, SB_VERT, &info, true); 5134 } else version(custom_widgets) { 5135 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 5136 } else static assert(0); 5137 5138 redraw(); 5139 } 5140 5141 /// 5142 void horizontalScroll(int delta) { 5143 horizontalScrollTo(scrollOrigin.x + delta); 5144 } 5145 /// 5146 void horizontalScrollTo(int pos) { 5147 scrollOrigin_.x = pos; 5148 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 5149 scrollOrigin_.x = contentWidth - viewportWidth; 5150 5151 if(scrollOrigin_.x < 0) 5152 scrollOrigin_.x = 0; 5153 5154 version(win32_widgets) { 5155 SCROLLINFO info; 5156 info.cbSize = info.sizeof; 5157 info.fMask = SIF_POS; 5158 info.nPos = scrollOrigin_.x; 5159 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5160 } else version(custom_widgets) { 5161 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 5162 } else static assert(0); 5163 5164 redraw(); 5165 } 5166 /// 5167 void scrollTo(Point p) { 5168 verticalScrollTo(p.y); 5169 horizontalScrollTo(p.x); 5170 } 5171 5172 /// 5173 void ensureVisibleInScroll(Point p) { 5174 auto rect = viewportRectangle(); 5175 if(rect.contains(p)) 5176 return; 5177 if(p.x < rect.left) 5178 horizontalScroll(p.x - rect.left); 5179 else if(p.x > rect.right) 5180 horizontalScroll(p.x - rect.right); 5181 5182 if(p.y < rect.top) 5183 verticalScroll(p.y - rect.top); 5184 else if(p.y > rect.bottom) 5185 verticalScroll(p.y - rect.bottom); 5186 } 5187 5188 /// 5189 void ensureVisibleInScroll(Rectangle rect) { 5190 ensureVisibleInScroll(rect.upperLeft); 5191 ensureVisibleInScroll(rect.lowerRight); 5192 } 5193 5194 /// 5195 Rectangle viewportRectangle() { 5196 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 5197 } 5198 5199 /// 5200 bool showingHorizontalScroll() { 5201 return contentWidth > width; 5202 } 5203 /// 5204 bool showingVerticalScroll() { 5205 return contentHeight > height; 5206 } 5207 5208 /// This is called before the ordinary paint delegate, 5209 /// giving you a chance to draw the window frame, etc, 5210 /// before the scroll clip takes effect 5211 void paintFrameAndBackground(WidgetPainter painter) { 5212 version(win32_widgets) { 5213 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5214 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5215 // since the pen is null, to fill the whole space, we need the +1 on both. 5216 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5217 SelectObject(painter.impl.hdc, p); 5218 SelectObject(painter.impl.hdc, b); 5219 } 5220 5221 } 5222 5223 // make space for the scroll bar, and that's it. 5224 final override int paddingRight() { return scaleWithDpi(16); } 5225 final override int paddingBottom() { return scaleWithDpi(16); } 5226 5227 /* 5228 END SCROLLING 5229 */ 5230 5231 override WidgetPainter draw() { 5232 int x = this.x, y = this.y; 5233 auto parent = this.parent; 5234 while(parent) { 5235 x += parent.x; 5236 y += parent.y; 5237 parent = parent.parent; 5238 } 5239 5240 //version(win32_widgets) { 5241 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5242 //} else { 5243 auto painter = parentWindow.win.draw(true); 5244 //} 5245 painter.originX = x; 5246 painter.originY = y; 5247 5248 painter.originX = painter.originX - scrollOrigin.x; 5249 painter.originY = painter.originY - scrollOrigin.y; 5250 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 5251 5252 return WidgetPainter(painter, this); 5253 } 5254 5255 mixin ScrollableChildren; 5256 } 5257 5258 // you need to have a Point scrollOrigin in the class somewhere 5259 // and a paintFrameAndBackground 5260 private mixin template ScrollableChildren() { 5261 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5262 if(hidden) 5263 return; 5264 5265 //version(win32_widgets) 5266 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5267 5268 painter.originX = lox + x; 5269 painter.originY = loy + y; 5270 5271 bool actuallyPainted = false; 5272 5273 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 5274 if(clip == Rectangle.init) 5275 return; 5276 5277 if(force || redrawRequested) { 5278 //painter.setClipRectangle(scrollOrigin, width, height); 5279 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5280 paintFrameAndBackground(painter); 5281 } 5282 5283 painter.originX = painter.originX - scrollOrigin.x; 5284 painter.originY = painter.originY - scrollOrigin.y; 5285 if(force || redrawRequested) { 5286 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 5287 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 5288 5289 //erase(painter); // we paintFrameAndBackground above so no need 5290 if(painter.visualTheme) 5291 painter.visualTheme.doPaint(this, painter); 5292 else 5293 paint(painter); 5294 5295 if(invalidate) { 5296 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5297 // children are contained inside this, so no need to do extra work 5298 invalidate = false; 5299 } 5300 5301 5302 actuallyPainted = true; 5303 redrawRequested = false; 5304 } 5305 foreach(child; children) { 5306 if(cast(FixedPosition) child) 5307 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5308 else 5309 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5310 } 5311 } 5312 } 5313 5314 private class InternalScrollableContainerInsideWidget : ContainerWidget { 5315 ScrollableContainerWidget scw; 5316 5317 this(ScrollableContainerWidget parent) { 5318 scw = parent; 5319 super(parent); 5320 } 5321 5322 version(custom_widgets) 5323 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5324 if(hidden) 5325 return; 5326 5327 bool actuallyPainted = false; 5328 5329 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 5330 5331 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 5332 if(clip == Rectangle.init) 5333 return; 5334 5335 painter.originX = lox + x - scrollOrigin.x; 5336 painter.originY = loy + y - scrollOrigin.y; 5337 if(force || redrawRequested) { 5338 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5339 5340 erase(painter); 5341 if(painter.visualTheme) 5342 painter.visualTheme.doPaint(this, painter); 5343 else 5344 paint(painter); 5345 5346 if(invalidate) { 5347 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5348 // children are contained inside this, so no need to do extra work 5349 invalidate = false; 5350 } 5351 5352 actuallyPainted = true; 5353 redrawRequested = false; 5354 } 5355 foreach(child; children) { 5356 if(cast(FixedPosition) child) 5357 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5358 else 5359 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5360 } 5361 } 5362 5363 version(custom_widgets) 5364 override protected void addScrollPosition(ref int x, ref int y) { 5365 x += scw.scrollX_; 5366 y += scw.scrollY_; 5367 } 5368 } 5369 5370 /++ 5371 A widget meant to contain other widgets that may need to scroll. 5372 5373 Currently buggy. 5374 5375 History: 5376 Added July 1, 2021 (dub v10.2) 5377 5378 On January 3, 2022, I tried to use it in a few other cases 5379 and found it only worked well in the original test case. Since 5380 it still sucks, I think I'm going to rewrite it again. 5381 +/ 5382 class ScrollableContainerWidget : ContainerWidget { 5383 /// 5384 this(Widget parent) { 5385 super(parent); 5386 5387 container = new InternalScrollableContainerInsideWidget(this); 5388 hsb = new HorizontalScrollbar(this); 5389 vsb = new VerticalScrollbar(this); 5390 5391 tabStop = false; 5392 container.tabStop = false; 5393 magic = true; 5394 5395 5396 vsb.addEventListener("scrolltonextline", () { 5397 scrollBy(0, scaleWithDpi(16)); 5398 }); 5399 vsb.addEventListener("scrolltopreviousline", () { 5400 scrollBy(0,scaleWithDpi( -16)); 5401 }); 5402 vsb.addEventListener("scrolltonextpage", () { 5403 scrollBy(0, container.height); 5404 }); 5405 vsb.addEventListener("scrolltopreviouspage", () { 5406 scrollBy(0, -container.height); 5407 }); 5408 vsb.addEventListener((scope ScrollToPositionEvent spe) { 5409 scrollTo(scrollX_, spe.value); 5410 }); 5411 5412 this.addEventListener(delegate (scope ClickEvent e) { 5413 if(e.button == MouseButton.wheelUp) { 5414 if(!e.defaultPrevented) 5415 scrollBy(0, scaleWithDpi(-16)); 5416 e.stopPropagation(); 5417 } else if(e.button == MouseButton.wheelDown) { 5418 if(!e.defaultPrevented) 5419 scrollBy(0, scaleWithDpi(16)); 5420 e.stopPropagation(); 5421 } 5422 }); 5423 } 5424 5425 /+ 5426 override void defaultEventHandler_click(ClickEvent e) { 5427 } 5428 +/ 5429 5430 override void removeAllChildren() { 5431 container.removeAllChildren(); 5432 } 5433 5434 void scrollTo(int x, int y) { 5435 scrollBy(x - scrollX_, y - scrollY_); 5436 } 5437 5438 void scrollBy(int x, int y) { 5439 auto ox = scrollX_; 5440 auto oy = scrollY_; 5441 5442 auto nx = ox + x; 5443 auto ny = oy + y; 5444 5445 if(nx < 0) 5446 nx = 0; 5447 if(ny < 0) 5448 ny = 0; 5449 5450 auto maxX = hsb.max - container.width; 5451 if(maxX < 0) maxX = 0; 5452 auto maxY = vsb.max - container.height; 5453 if(maxY < 0) maxY = 0; 5454 5455 if(nx > maxX) 5456 nx = maxX; 5457 if(ny > maxY) 5458 ny = maxY; 5459 5460 auto dx = nx - ox; 5461 auto dy = ny - oy; 5462 5463 if(dx || dy) { 5464 version(win32_widgets) 5465 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 5466 else { 5467 redraw(); 5468 } 5469 5470 hsb.setPosition = nx; 5471 vsb.setPosition = ny; 5472 5473 scrollX_ = nx; 5474 scrollY_ = ny; 5475 } 5476 } 5477 5478 private int scrollX_; 5479 private int scrollY_; 5480 5481 void setTotalArea(int width, int height) { 5482 hsb.setMax(width); 5483 vsb.setMax(height); 5484 } 5485 5486 /// 5487 void setViewableArea(int width, int height) { 5488 hsb.setViewableArea(width); 5489 vsb.setViewableArea(height); 5490 } 5491 5492 private bool magic; 5493 override void addChild(Widget w, int position = int.max) { 5494 if(magic) 5495 container.addChild(w, position); 5496 else 5497 super.addChild(w, position); 5498 } 5499 5500 override void recomputeChildLayout() { 5501 if(hsb is null || vsb is null || container is null) return; 5502 5503 /+ 5504 writeln(x, " ", y , " ", width, " ", height); 5505 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 5506 +/ 5507 5508 registerMovement(); 5509 5510 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 5511 hsb.x = 0; 5512 hsb.y = this.height - hsb.height; 5513 hsb.width = this.width - scaleWithDpi(16); 5514 hsb.recomputeChildLayout(); 5515 5516 vsb.width = scaleWithDpi(16); // FIXME? 5517 vsb.x = this.width - vsb.width; 5518 vsb.y = 0; 5519 vsb.height = this.height - scaleWithDpi(16); 5520 vsb.recomputeChildLayout(); 5521 5522 container.x = 0; 5523 container.y = 0; 5524 container.width = this.width - vsb.width; 5525 container.height = this.height - hsb.height; 5526 container.recomputeChildLayout(); 5527 5528 scrollX_ = 0; 5529 scrollY_ = 0; 5530 5531 hsb.setPosition(0); 5532 vsb.setPosition(0); 5533 5534 int mw, mh; 5535 Widget c = container; 5536 // FIXME: hack here to handle a layout inside... 5537 if(c.children.length == 1 && cast(Layout) c.children[0]) 5538 c = c.children[0]; 5539 foreach(child; c.children) { 5540 auto w = child.x + child.width; 5541 auto h = child.y + child.height; 5542 5543 if(w > mw) mw = w; 5544 if(h > mh) mh = h; 5545 } 5546 5547 setTotalArea(mw, mh); 5548 setViewableArea(width, height); 5549 } 5550 5551 override int minHeight() { return scaleWithDpi(64); } 5552 5553 HorizontalScrollbar hsb; 5554 VerticalScrollbar vsb; 5555 ContainerWidget container; 5556 } 5557 5558 5559 version(custom_widgets) 5560 private class InternalScrollableContainerWidget : Widget { 5561 5562 ScrollableWidget sw; 5563 5564 VerticalScrollbar verticalScrollBar; 5565 HorizontalScrollbar horizontalScrollBar; 5566 5567 this(ScrollableWidget sw, Widget parent) { 5568 this.sw = sw; 5569 5570 this.tabStop = false; 5571 5572 super(parent); 5573 5574 horizontalScrollBar = new HorizontalScrollbar(this); 5575 verticalScrollBar = new VerticalScrollbar(this); 5576 5577 horizontalScrollBar.showing_ = false; 5578 verticalScrollBar.showing_ = false; 5579 5580 horizontalScrollBar.addEventListener("scrolltonextline", { 5581 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 5582 sw.horizontalScrollTo(horizontalScrollBar.position); 5583 }); 5584 horizontalScrollBar.addEventListener("scrolltopreviousline", { 5585 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 5586 sw.horizontalScrollTo(horizontalScrollBar.position); 5587 }); 5588 verticalScrollBar.addEventListener("scrolltonextline", { 5589 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 5590 sw.verticalScrollTo(verticalScrollBar.position); 5591 }); 5592 verticalScrollBar.addEventListener("scrolltopreviousline", { 5593 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 5594 sw.verticalScrollTo(verticalScrollBar.position); 5595 }); 5596 horizontalScrollBar.addEventListener("scrolltonextpage", { 5597 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 5598 sw.horizontalScrollTo(horizontalScrollBar.position); 5599 }); 5600 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 5601 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 5602 sw.horizontalScrollTo(horizontalScrollBar.position); 5603 }); 5604 verticalScrollBar.addEventListener("scrolltonextpage", { 5605 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 5606 sw.verticalScrollTo(verticalScrollBar.position); 5607 }); 5608 verticalScrollBar.addEventListener("scrolltopreviouspage", { 5609 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 5610 sw.verticalScrollTo(verticalScrollBar.position); 5611 }); 5612 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 5613 horizontalScrollBar.setPosition(event.intValue); 5614 sw.horizontalScrollTo(horizontalScrollBar.position); 5615 }); 5616 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 5617 verticalScrollBar.setPosition(event.intValue); 5618 sw.verticalScrollTo(verticalScrollBar.position); 5619 }); 5620 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 5621 horizontalScrollBar.setPosition(event.intValue); 5622 sw.horizontalScrollTo(horizontalScrollBar.position); 5623 }); 5624 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 5625 verticalScrollBar.setPosition(event.intValue); 5626 }); 5627 } 5628 5629 // this is supposed to be basically invisible... 5630 override int minWidth() { return sw.minWidth; } 5631 override int minHeight() { return sw.minHeight; } 5632 override int maxWidth() { return sw.maxWidth; } 5633 override int maxHeight() { return sw.maxHeight; } 5634 override int widthStretchiness() { return sw.widthStretchiness; } 5635 override int heightStretchiness() { return sw.heightStretchiness; } 5636 override int marginLeft() { return sw.marginLeft; } 5637 override int marginRight() { return sw.marginRight; } 5638 override int marginTop() { return sw.marginTop; } 5639 override int marginBottom() { return sw.marginBottom; } 5640 override int paddingLeft() { return sw.paddingLeft; } 5641 override int paddingRight() { return sw.paddingRight; } 5642 override int paddingTop() { return sw.paddingTop; } 5643 override int paddingBottom() { return sw.paddingBottom; } 5644 override void focus() { sw.focus(); } 5645 5646 5647 override void recomputeChildLayout() { 5648 // The stupid thing needs to calculate if a scroll bar is needed... 5649 recomputeChildLayoutHelper(); 5650 // then running it again will position things correctly if the bar is NOT needed 5651 recomputeChildLayoutHelper(); 5652 5653 // this sucks but meh it barely works 5654 } 5655 5656 private void recomputeChildLayoutHelper() { 5657 if(sw is null) return; 5658 5659 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 5660 if(horizontalScrollBar && verticalScrollBar) { 5661 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 5662 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 5663 horizontalScrollBar.x = 0; 5664 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 5665 5666 verticalScrollBar.width = verticalScrollBar.minWidth(); 5667 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 5668 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 5669 verticalScrollBar.y = 0 + 2; 5670 5671 sw.x = 0; 5672 sw.y = 0; 5673 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 5674 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 5675 5676 if(sw.contentWidth_ <= this.width) 5677 sw.scrollOrigin_.x = 0; 5678 if(sw.contentHeight_ <= this.height) 5679 sw.scrollOrigin_.y = 0; 5680 5681 horizontalScrollBar.recomputeChildLayout(); 5682 verticalScrollBar.recomputeChildLayout(); 5683 sw.recomputeChildLayout(); 5684 } 5685 5686 if(sw.contentWidth_ <= this.width) 5687 sw.scrollOrigin_.x = 0; 5688 if(sw.contentHeight_ <= this.height) 5689 sw.scrollOrigin_.y = 0; 5690 5691 if(sw.showingHorizontalScroll()) 5692 horizontalScrollBar.showing(true, false); 5693 else 5694 horizontalScrollBar.showing(false, false); 5695 if(sw.showingVerticalScroll()) 5696 verticalScrollBar.showing(true, false); 5697 else 5698 verticalScrollBar.showing(false, false); 5699 5700 verticalScrollBar.setViewableArea(sw.viewportHeight()); 5701 verticalScrollBar.setMax(sw.contentHeight); 5702 verticalScrollBar.setPosition(sw.scrollOrigin.y); 5703 5704 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 5705 horizontalScrollBar.setMax(sw.contentWidth); 5706 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 5707 } 5708 } 5709 5710 /* 5711 class ScrollableClientWidget : Widget { 5712 this(Widget parent) { 5713 super(parent); 5714 } 5715 override void paint(WidgetPainter p) { 5716 parent.paint(p); 5717 } 5718 } 5719 */ 5720 5721 /++ 5722 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. 5723 +/ 5724 abstract class Slider : Widget { 5725 this(int min, int max, int step, Widget parent) { 5726 min_ = min; 5727 max_ = max; 5728 step_ = step; 5729 page_ = step; 5730 super(parent); 5731 } 5732 5733 private int min_; 5734 private int max_; 5735 private int step_; 5736 private int position_; 5737 private int page_; 5738 5739 // selection start and selection end 5740 // tics 5741 // tooltip? 5742 // some way to see and just type the value 5743 // win32 buddy controls are labels 5744 5745 /// 5746 void setMin(int a) { 5747 min_ = a; 5748 version(custom_widgets) 5749 redraw(); 5750 version(win32_widgets) 5751 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 5752 } 5753 /// 5754 int min() { 5755 return min_; 5756 } 5757 /// 5758 void setMax(int a) { 5759 max_ = a; 5760 version(custom_widgets) 5761 redraw(); 5762 version(win32_widgets) 5763 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 5764 } 5765 /// 5766 int max() { 5767 return max_; 5768 } 5769 /// 5770 void setPosition(int a) { 5771 if(a > max) 5772 a = max; 5773 if(a < min) 5774 a = min; 5775 position_ = a; 5776 version(custom_widgets) 5777 setPositionCustom(a); 5778 5779 version(win32_widgets) 5780 setPositionWindows(a); 5781 } 5782 version(win32_widgets) { 5783 protected abstract void setPositionWindows(int a); 5784 } 5785 5786 protected abstract int win32direction(); 5787 5788 /++ 5789 Alias for [position] for better compatibility with generic code. 5790 5791 History: 5792 Added October 5, 2021 5793 +/ 5794 @property int value() { 5795 return position; 5796 } 5797 5798 /// 5799 int position() { 5800 return position_; 5801 } 5802 /// 5803 void setStep(int a) { 5804 step_ = a; 5805 version(win32_widgets) 5806 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 5807 } 5808 /// 5809 int step() { 5810 return step_; 5811 } 5812 /// 5813 void setPageSize(int a) { 5814 page_ = a; 5815 version(win32_widgets) 5816 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 5817 } 5818 /// 5819 int pageSize() { 5820 return page_; 5821 } 5822 5823 private void notify() { 5824 auto event = new ChangeEvent!int(this, &this.position); 5825 event.dispatch(); 5826 } 5827 5828 version(win32_widgets) 5829 void win32Setup(int style) { 5830 createWin32Window(this, TRACKBAR_CLASS, "", 5831 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 5832 5833 // the trackbar sends the same messages as scroll, which 5834 // our other layer sends as these... just gonna translate 5835 // here 5836 this.addDirectEventListener("scrolltoposition", (Event event) { 5837 event.stopPropagation(); 5838 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 5839 notify(); 5840 }); 5841 this.addDirectEventListener("scrolltonextline", (Event event) { 5842 event.stopPropagation(); 5843 this.setPosition(this.position + this.step_ * this.win32direction); 5844 notify(); 5845 }); 5846 this.addDirectEventListener("scrolltopreviousline", (Event event) { 5847 event.stopPropagation(); 5848 this.setPosition(this.position - this.step_ * this.win32direction); 5849 notify(); 5850 }); 5851 this.addDirectEventListener("scrolltonextpage", (Event event) { 5852 event.stopPropagation(); 5853 this.setPosition(this.position + this.page_ * this.win32direction); 5854 notify(); 5855 }); 5856 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 5857 event.stopPropagation(); 5858 this.setPosition(this.position - this.page_ * this.win32direction); 5859 notify(); 5860 }); 5861 5862 setMin(min_); 5863 setMax(max_); 5864 setStep(step_); 5865 setPageSize(page_); 5866 } 5867 5868 version(custom_widgets) { 5869 protected MouseTrackingWidget thumb; 5870 5871 protected abstract void setPositionCustom(int a); 5872 5873 override void defaultEventHandler_keydown(KeyDownEvent event) { 5874 switch(event.key) { 5875 case Key.Up: 5876 case Key.Right: 5877 setPosition(position() - step() * win32direction); 5878 changed(); 5879 break; 5880 case Key.Down: 5881 case Key.Left: 5882 setPosition(position() + step() * win32direction); 5883 changed(); 5884 break; 5885 case Key.Home: 5886 setPosition(win32direction > 0 ? min() : max()); 5887 changed(); 5888 break; 5889 case Key.End: 5890 setPosition(win32direction > 0 ? max() : min()); 5891 changed(); 5892 break; 5893 case Key.PageUp: 5894 setPosition(position() - pageSize() * win32direction); 5895 changed(); 5896 break; 5897 case Key.PageDown: 5898 setPosition(position() + pageSize() * win32direction); 5899 changed(); 5900 break; 5901 default: 5902 } 5903 super.defaultEventHandler_keydown(event); 5904 } 5905 5906 protected void changed() { 5907 auto ev = new ChangeEvent!int(this, &position); 5908 ev.dispatch(); 5909 } 5910 } 5911 } 5912 5913 /++ 5914 5915 +/ 5916 class VerticalSlider : Slider { 5917 this(int min, int max, int step, Widget parent) { 5918 version(custom_widgets) 5919 initialize(); 5920 5921 super(min, max, step, parent); 5922 5923 version(win32_widgets) 5924 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 5925 } 5926 5927 protected override int win32direction() { 5928 return -1; 5929 } 5930 5931 version(win32_widgets) 5932 protected override void setPositionWindows(int a) { 5933 // the windows thing makes the top 0 and i don't like that. 5934 SendMessage(hwnd, TBM_SETPOS, true, max - a); 5935 } 5936 5937 version(custom_widgets) 5938 private void initialize() { 5939 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 5940 5941 thumb.tabStop = false; 5942 5943 thumb.thumbWidth = width; 5944 thumb.thumbHeight = scaleWithDpi(16); 5945 5946 thumb.addEventListener(EventType.change, () { 5947 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 5948 sx = max - sx; 5949 //informProgramThatUserChangedPosition(sx); 5950 5951 position_ = sx; 5952 5953 changed(); 5954 }); 5955 } 5956 5957 version(custom_widgets) 5958 override void recomputeChildLayout() { 5959 thumb.thumbWidth = this.width; 5960 super.recomputeChildLayout(); 5961 setPositionCustom(position_); 5962 } 5963 5964 version(custom_widgets) 5965 protected override void setPositionCustom(int a) { 5966 if(max()) 5967 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 5968 redraw(); 5969 } 5970 } 5971 5972 /++ 5973 5974 +/ 5975 class HorizontalSlider : Slider { 5976 this(int min, int max, int step, Widget parent) { 5977 version(custom_widgets) 5978 initialize(); 5979 5980 super(min, max, step, parent); 5981 5982 version(win32_widgets) 5983 win32Setup(TBS_HORZ); 5984 } 5985 5986 version(win32_widgets) 5987 protected override void setPositionWindows(int a) { 5988 SendMessage(hwnd, TBM_SETPOS, true, a); 5989 } 5990 5991 protected override int win32direction() { 5992 return 1; 5993 } 5994 5995 version(custom_widgets) 5996 private void initialize() { 5997 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 5998 5999 thumb.tabStop = false; 6000 6001 thumb.thumbWidth = scaleWithDpi(16); 6002 thumb.thumbHeight = height; 6003 6004 thumb.addEventListener(EventType.change, () { 6005 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 6006 //informProgramThatUserChangedPosition(sx); 6007 6008 position_ = sx; 6009 6010 changed(); 6011 }); 6012 } 6013 6014 version(custom_widgets) 6015 override void recomputeChildLayout() { 6016 thumb.thumbHeight = this.height; 6017 super.recomputeChildLayout(); 6018 setPositionCustom(position_); 6019 } 6020 6021 version(custom_widgets) 6022 protected override void setPositionCustom(int a) { 6023 if(max()) 6024 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 6025 redraw(); 6026 } 6027 } 6028 6029 6030 /// 6031 abstract class ScrollbarBase : Widget { 6032 /// 6033 this(Widget parent) { 6034 super(parent); 6035 tabStop = false; 6036 step_ = scaleWithDpi(16); 6037 } 6038 6039 private int viewableArea_; 6040 private int max_; 6041 private int step_;// = 16; 6042 private int position_; 6043 6044 /// 6045 bool atEnd() { 6046 return position_ + viewableArea_ >= max_; 6047 } 6048 6049 /// 6050 bool atStart() { 6051 return position_ == 0; 6052 } 6053 6054 /// 6055 void setViewableArea(int a) { 6056 viewableArea_ = a; 6057 version(custom_widgets) 6058 redraw(); 6059 } 6060 /// 6061 void setMax(int a) { 6062 max_ = a; 6063 version(custom_widgets) 6064 redraw(); 6065 } 6066 /// 6067 int max() { 6068 return max_; 6069 } 6070 /// 6071 void setPosition(int a) { 6072 auto logicalMax = max_ - viewableArea_; 6073 if(a == int.max) 6074 a = logicalMax; 6075 6076 if(a > logicalMax) 6077 a = logicalMax; 6078 if(a < 0) 6079 a = 0; 6080 6081 position_ = a; 6082 6083 version(custom_widgets) 6084 redraw(); 6085 } 6086 /// 6087 int position() { 6088 return position_; 6089 } 6090 /// 6091 void setStep(int a) { 6092 step_ = a; 6093 } 6094 /// 6095 int step() { 6096 return step_; 6097 } 6098 6099 // FIXME: remove this.... maybe 6100 /+ 6101 protected void informProgramThatUserChangedPosition(int n) { 6102 position_ = n; 6103 auto evt = new Event(EventType.change, this); 6104 evt.intValue = n; 6105 evt.dispatch(); 6106 } 6107 +/ 6108 6109 version(custom_widgets) { 6110 enum MIN_THUMB_SIZE = 8; 6111 6112 abstract protected int getBarDim(); 6113 int thumbSize() { 6114 if(viewableArea_ >= max_ || max_ == 0) 6115 return getBarDim(); 6116 6117 int res = viewableArea_ * getBarDim() / max_; 6118 6119 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 6120 res = scaleWithDpi(MIN_THUMB_SIZE); 6121 6122 return res; 6123 } 6124 6125 int thumbPosition() { 6126 /* 6127 viewableArea_ is the viewport height/width 6128 position_ is where we are 6129 */ 6130 //if(position_ + viewableArea_ >= max_) 6131 //return getBarDim - thumbSize; 6132 6133 auto maximumPossibleValue = getBarDim() - thumbSize; 6134 auto maximiumLogicalValue = max_ - viewableArea_; 6135 6136 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 6137 6138 return p; 6139 } 6140 } 6141 } 6142 6143 //public import mgt; 6144 6145 /++ 6146 A mouse tracking widget is one that follows the mouse when dragged inside it. 6147 6148 Concrete subclasses may include a scrollbar thumb and a volume control. 6149 +/ 6150 //version(custom_widgets) 6151 class MouseTrackingWidget : Widget { 6152 6153 /// 6154 int positionX() { return positionX_; } 6155 /// 6156 int positionY() { return positionY_; } 6157 6158 /// 6159 void positionX(int p) { positionX_ = p; } 6160 /// 6161 void positionY(int p) { positionY_ = p; } 6162 6163 private int positionX_; 6164 private int positionY_; 6165 6166 /// 6167 enum Orientation { 6168 horizontal, /// 6169 vertical, /// 6170 twoDimensional, /// 6171 } 6172 6173 private int thumbWidth_; 6174 private int thumbHeight_; 6175 6176 /// 6177 int thumbWidth() { return thumbWidth_; } 6178 /// 6179 int thumbHeight() { return thumbHeight_; } 6180 /// 6181 int thumbWidth(int a) { return thumbWidth_ = a; } 6182 /// 6183 int thumbHeight(int a) { return thumbHeight_ = a; } 6184 6185 private bool dragging; 6186 private bool hovering; 6187 private int startMouseX, startMouseY; 6188 6189 /// 6190 this(Orientation orientation, Widget parent) { 6191 super(parent); 6192 6193 //assert(parentWindow !is null); 6194 6195 addEventListener((MouseDownEvent event) { 6196 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6197 dragging = true; 6198 startMouseX = event.clientX - positionX; 6199 startMouseY = event.clientY - positionY; 6200 parentWindow.captureMouse(this); 6201 } else { 6202 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6203 positionX = event.clientX - thumbWidth / 2; 6204 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6205 positionY = event.clientY - thumbHeight / 2; 6206 6207 if(positionX + thumbWidth > this.width) 6208 positionX = this.width - thumbWidth; 6209 if(positionY + thumbHeight > this.height) 6210 positionY = this.height - thumbHeight; 6211 6212 if(positionX < 0) 6213 positionX = 0; 6214 if(positionY < 0) 6215 positionY = 0; 6216 6217 6218 // this.emit!(ChangeEvent!void)(); 6219 auto evt = new Event(EventType.change, this); 6220 evt.sendDirectly(); 6221 6222 redraw(); 6223 6224 } 6225 }); 6226 6227 addEventListener(EventType.mouseup, (Event event) { 6228 dragging = false; 6229 parentWindow.releaseMouseCapture(); 6230 }); 6231 6232 addEventListener(EventType.mouseout, (Event event) { 6233 if(!hovering) 6234 return; 6235 hovering = false; 6236 redraw(); 6237 }); 6238 6239 int lpx, lpy; 6240 6241 addEventListener((MouseMoveEvent event) { 6242 auto oh = hovering; 6243 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6244 hovering = true; 6245 } else { 6246 hovering = false; 6247 } 6248 if(!dragging) { 6249 if(hovering != oh) 6250 redraw(); 6251 return; 6252 } 6253 6254 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6255 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 6256 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6257 positionY = event.clientY - startMouseY; 6258 6259 if(positionX + thumbWidth > this.width) 6260 positionX = this.width - thumbWidth; 6261 if(positionY + thumbHeight > this.height) 6262 positionY = this.height - thumbHeight; 6263 6264 if(positionX < 0) 6265 positionX = 0; 6266 if(positionY < 0) 6267 positionY = 0; 6268 6269 if(positionX != lpx || positionY != lpy) { 6270 lpx = positionX; 6271 lpy = positionY; 6272 6273 auto evt = new Event(EventType.change, this); 6274 evt.sendDirectly(); 6275 } 6276 6277 redraw(); 6278 }); 6279 } 6280 6281 version(custom_widgets) 6282 override void paint(WidgetPainter painter) { 6283 auto cs = getComputedStyle(); 6284 auto c = darken(cs.windowBackgroundColor, 0.2); 6285 painter.outlineColor = c; 6286 painter.fillColor = c; 6287 painter.drawRectangle(Point(0, 0), this.width, this.height); 6288 6289 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 6290 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 6291 } 6292 } 6293 6294 //version(custom_widgets) 6295 //private 6296 class HorizontalScrollbar : ScrollbarBase { 6297 6298 version(custom_widgets) { 6299 private MouseTrackingWidget thumb; 6300 6301 override int getBarDim() { 6302 return thumb.width; 6303 } 6304 } 6305 6306 override void setViewableArea(int a) { 6307 super.setViewableArea(a); 6308 6309 version(win32_widgets) { 6310 SCROLLINFO info; 6311 info.cbSize = info.sizeof; 6312 info.nPage = a + 1; 6313 info.fMask = SIF_PAGE; 6314 SetScrollInfo(hwnd, SB_CTL, &info, true); 6315 } else version(custom_widgets) { 6316 thumb.positionX = thumbPosition; 6317 thumb.thumbWidth = thumbSize; 6318 thumb.redraw(); 6319 } else static assert(0); 6320 6321 } 6322 6323 override void setMax(int a) { 6324 super.setMax(a); 6325 version(win32_widgets) { 6326 SCROLLINFO info; 6327 info.cbSize = info.sizeof; 6328 info.nMin = 0; 6329 info.nMax = max; 6330 info.fMask = SIF_RANGE; 6331 SetScrollInfo(hwnd, SB_CTL, &info, true); 6332 } else version(custom_widgets) { 6333 thumb.positionX = thumbPosition; 6334 thumb.thumbWidth = thumbSize; 6335 thumb.redraw(); 6336 } 6337 } 6338 6339 override void setPosition(int a) { 6340 super.setPosition(a); 6341 version(win32_widgets) { 6342 SCROLLINFO info; 6343 info.cbSize = info.sizeof; 6344 info.fMask = SIF_POS; 6345 info.nPos = position; 6346 SetScrollInfo(hwnd, SB_CTL, &info, true); 6347 } else version(custom_widgets) { 6348 thumb.positionX = thumbPosition(); 6349 thumb.thumbWidth = thumbSize; 6350 thumb.redraw(); 6351 } else static assert(0); 6352 } 6353 6354 this(Widget parent) { 6355 super(parent); 6356 6357 version(win32_widgets) { 6358 createWin32Window(this, "Scrollbar"w, "", 6359 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 6360 } else version(custom_widgets) { 6361 auto vl = new HorizontalLayout(this); 6362 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 6363 leftButton.setClickRepeat(scrollClickRepeatInterval); 6364 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 6365 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 6366 rightButton.setClickRepeat(scrollClickRepeatInterval); 6367 6368 leftButton.tabStop = false; 6369 rightButton.tabStop = false; 6370 thumb.tabStop = false; 6371 6372 leftButton.addEventListener(EventType.triggered, () { 6373 this.emitCommand!"scrolltopreviousline"(); 6374 //informProgramThatUserChangedPosition(position - step()); 6375 }); 6376 rightButton.addEventListener(EventType.triggered, () { 6377 this.emitCommand!"scrolltonextline"(); 6378 //informProgramThatUserChangedPosition(position + step()); 6379 }); 6380 6381 thumb.thumbWidth = this.minWidth; 6382 thumb.thumbHeight = scaleWithDpi(16); 6383 6384 thumb.addEventListener(EventType.change, () { 6385 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 6386 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 6387 6388 //informProgramThatUserChangedPosition(sx); 6389 6390 auto ev = new ScrollToPositionEvent(this, sx); 6391 ev.dispatch(); 6392 }); 6393 } 6394 } 6395 6396 override int minHeight() { return scaleWithDpi(16); } 6397 override int maxHeight() { return scaleWithDpi(16); } 6398 override int minWidth() { return scaleWithDpi(48); } 6399 } 6400 6401 class ScrollToPositionEvent : Event { 6402 enum EventString = "scrolltoposition"; 6403 6404 this(Widget target, int value) { 6405 this.value = value; 6406 super(EventString, target); 6407 } 6408 6409 immutable int value; 6410 6411 override @property int intValue() { 6412 return value; 6413 } 6414 } 6415 6416 //version(custom_widgets) 6417 //private 6418 class VerticalScrollbar : ScrollbarBase { 6419 6420 version(custom_widgets) { 6421 override int getBarDim() { 6422 return thumb.height; 6423 } 6424 6425 private MouseTrackingWidget thumb; 6426 } 6427 6428 override void setViewableArea(int a) { 6429 super.setViewableArea(a); 6430 6431 version(win32_widgets) { 6432 SCROLLINFO info; 6433 info.cbSize = info.sizeof; 6434 info.nPage = a + 1; 6435 info.fMask = SIF_PAGE; 6436 SetScrollInfo(hwnd, SB_CTL, &info, true); 6437 } else version(custom_widgets) { 6438 thumb.positionY = thumbPosition; 6439 thumb.thumbHeight = thumbSize; 6440 thumb.redraw(); 6441 } else static assert(0); 6442 6443 } 6444 6445 override void setMax(int a) { 6446 super.setMax(a); 6447 version(win32_widgets) { 6448 SCROLLINFO info; 6449 info.cbSize = info.sizeof; 6450 info.nMin = 0; 6451 info.nMax = max; 6452 info.fMask = SIF_RANGE; 6453 SetScrollInfo(hwnd, SB_CTL, &info, true); 6454 } else version(custom_widgets) { 6455 thumb.positionY = thumbPosition; 6456 thumb.thumbHeight = thumbSize; 6457 thumb.redraw(); 6458 } 6459 } 6460 6461 override void setPosition(int a) { 6462 super.setPosition(a); 6463 version(win32_widgets) { 6464 SCROLLINFO info; 6465 info.cbSize = info.sizeof; 6466 info.fMask = SIF_POS; 6467 info.nPos = position; 6468 SetScrollInfo(hwnd, SB_CTL, &info, true); 6469 } else version(custom_widgets) { 6470 thumb.positionY = thumbPosition; 6471 thumb.thumbHeight = thumbSize; 6472 thumb.redraw(); 6473 } else static assert(0); 6474 } 6475 6476 this(Widget parent) { 6477 super(parent); 6478 6479 version(win32_widgets) { 6480 createWin32Window(this, "Scrollbar"w, "", 6481 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 6482 } else version(custom_widgets) { 6483 auto vl = new VerticalLayout(this); 6484 auto upButton = new ArrowButton(ArrowDirection.up, vl); 6485 upButton.setClickRepeat(scrollClickRepeatInterval); 6486 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 6487 auto downButton = new ArrowButton(ArrowDirection.down, vl); 6488 downButton.setClickRepeat(scrollClickRepeatInterval); 6489 6490 upButton.addEventListener(EventType.triggered, () { 6491 this.emitCommand!"scrolltopreviousline"(); 6492 //informProgramThatUserChangedPosition(position - step()); 6493 }); 6494 downButton.addEventListener(EventType.triggered, () { 6495 this.emitCommand!"scrolltonextline"(); 6496 //informProgramThatUserChangedPosition(position + step()); 6497 }); 6498 6499 thumb.thumbWidth = this.minWidth; 6500 thumb.thumbHeight = scaleWithDpi(16); 6501 6502 thumb.addEventListener(EventType.change, () { 6503 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 6504 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 6505 6506 auto ev = new ScrollToPositionEvent(this, sy); 6507 ev.dispatch(); 6508 6509 //informProgramThatUserChangedPosition(sy); 6510 }); 6511 6512 upButton.tabStop = false; 6513 downButton.tabStop = false; 6514 thumb.tabStop = false; 6515 } 6516 } 6517 6518 override int minWidth() { return scaleWithDpi(16); } 6519 override int maxWidth() { return scaleWithDpi(16); } 6520 override int minHeight() { return scaleWithDpi(48); } 6521 } 6522 6523 6524 /++ 6525 EXPERIMENTAL 6526 6527 A widget specialized for being a container for other widgets. 6528 6529 History: 6530 Added May 29, 2021. Not stabilized at this time. 6531 +/ 6532 class WidgetContainer : Widget { 6533 this(Widget parent) { 6534 tabStop = false; 6535 super(parent); 6536 } 6537 6538 override int maxHeight() { 6539 if(this.children.length == 1) { 6540 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 6541 } else { 6542 return int.max; 6543 } 6544 } 6545 6546 override int maxWidth() { 6547 if(this.children.length == 1) { 6548 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 6549 } else { 6550 return int.max; 6551 } 6552 } 6553 6554 /+ 6555 6556 override int minHeight() { 6557 int largest = 0; 6558 int margins = 0; 6559 int lastMargin = 0; 6560 foreach(child; children) { 6561 auto mh = child.minHeight(); 6562 if(mh > largest) 6563 largest = mh; 6564 margins += mymax(lastMargin, child.marginTop()); 6565 lastMargin = child.marginBottom(); 6566 } 6567 return largest + margins; 6568 } 6569 6570 override int maxHeight() { 6571 int largest = 0; 6572 int margins = 0; 6573 int lastMargin = 0; 6574 foreach(child; children) { 6575 auto mh = child.maxHeight(); 6576 if(mh == int.max) 6577 return int.max; 6578 if(mh > largest) 6579 largest = mh; 6580 margins += mymax(lastMargin, child.marginTop()); 6581 lastMargin = child.marginBottom(); 6582 } 6583 return largest + margins; 6584 } 6585 6586 override int minWidth() { 6587 int min; 6588 foreach(child; children) { 6589 auto cm = child.minWidth; 6590 if(cm > min) 6591 min = cm; 6592 } 6593 return min + paddingLeft + paddingRight; 6594 } 6595 6596 override int minHeight() { 6597 int min; 6598 foreach(child; children) { 6599 auto cm = child.minHeight; 6600 if(cm > min) 6601 min = cm; 6602 } 6603 return min + paddingTop + paddingBottom; 6604 } 6605 6606 override int maxHeight() { 6607 int largest = 0; 6608 int margins = 0; 6609 int lastMargin = 0; 6610 foreach(child; children) { 6611 auto mh = child.maxHeight(); 6612 if(mh == int.max) 6613 return int.max; 6614 if(mh > largest) 6615 largest = mh; 6616 margins += mymax(lastMargin, child.marginTop()); 6617 lastMargin = child.marginBottom(); 6618 } 6619 return largest + margins; 6620 } 6621 6622 override int heightStretchiness() { 6623 int max; 6624 foreach(child; children) { 6625 auto c = child.heightStretchiness; 6626 if(c > max) 6627 max = c; 6628 } 6629 return max; 6630 } 6631 6632 override int marginTop() { 6633 if(this.children.length) 6634 return this.children[0].marginTop; 6635 return 0; 6636 } 6637 +/ 6638 } 6639 6640 /// 6641 abstract class Layout : Widget { 6642 this(Widget parent) { 6643 tabStop = false; 6644 super(parent); 6645 } 6646 } 6647 6648 /++ 6649 Makes all children minimum width and height, placing them down 6650 left to right, top to bottom. 6651 6652 Useful if you want to make a list of buttons that automatically 6653 wrap to a new line when necessary. 6654 +/ 6655 class InlineBlockLayout : Layout { 6656 /// 6657 this(Widget parent) { super(parent); } 6658 6659 override void recomputeChildLayout() { 6660 registerMovement(); 6661 6662 int x = this.paddingLeft, y = this.paddingTop; 6663 6664 int lineHeight; 6665 int previousMargin = 0; 6666 int previousMarginBottom = 0; 6667 6668 foreach(child; children) { 6669 if(child.hidden) 6670 continue; 6671 if(cast(FixedPosition) child) { 6672 child.recomputeChildLayout(); 6673 continue; 6674 } 6675 child.width = child.flexBasisWidth(); 6676 if(child.width == 0) 6677 child.width = child.minWidth(); 6678 if(child.width == 0) 6679 child.width = 32; 6680 6681 child.height = child.flexBasisHeight(); 6682 if(child.height == 0) 6683 child.height = child.minHeight(); 6684 if(child.height == 0) 6685 child.height = 32; 6686 6687 if(x + child.width + paddingRight > this.width) { 6688 x = this.paddingLeft; 6689 y += lineHeight; 6690 lineHeight = 0; 6691 previousMargin = 0; 6692 previousMarginBottom = 0; 6693 } 6694 6695 auto margin = child.marginLeft; 6696 if(previousMargin > margin) 6697 margin = previousMargin; 6698 6699 x += margin; 6700 6701 child.x = x; 6702 child.y = y; 6703 6704 int marginTopApplied; 6705 if(child.marginTop > previousMarginBottom) { 6706 child.y += child.marginTop; 6707 marginTopApplied = child.marginTop; 6708 } 6709 6710 x += child.width; 6711 previousMargin = child.marginRight; 6712 6713 if(child.marginBottom > previousMarginBottom) 6714 previousMarginBottom = child.marginBottom; 6715 6716 auto h = child.height + previousMarginBottom + marginTopApplied; 6717 if(h > lineHeight) 6718 lineHeight = h; 6719 6720 child.recomputeChildLayout(); 6721 } 6722 6723 } 6724 6725 override int minWidth() { 6726 int min; 6727 foreach(child; children) { 6728 auto cm = child.minWidth; 6729 if(cm > min) 6730 min = cm; 6731 } 6732 return min + paddingLeft + paddingRight; 6733 } 6734 6735 override int minHeight() { 6736 int min; 6737 foreach(child; children) { 6738 auto cm = child.minHeight; 6739 if(cm > min) 6740 min = cm; 6741 } 6742 return min + paddingTop + paddingBottom; 6743 } 6744 } 6745 6746 /++ 6747 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 6748 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 6749 the [TabWidget] will automatically change pages of child widgets. 6750 6751 This allows you to react to it however you see fit rather than having to 6752 be tied to just the new sets of child widgets. 6753 6754 It sends the message in the form of `this.emitCommand!"changetab"();`. 6755 6756 History: 6757 Added December 24, 2021 (dub v10.5) 6758 +/ 6759 class TabMessageWidget : Widget { 6760 6761 protected void tabIndexClicked(int item) { 6762 this.emitCommand!"changetab"(); 6763 } 6764 6765 /++ 6766 Adds the a new tab to the control with the given title. 6767 6768 Returns: 6769 The index of the newly added tab. You will need to know 6770 this index to refer to it later and to know which tab to 6771 change to when you get a changetab message. 6772 +/ 6773 int addTab(string title, int pos = int.max) { 6774 version(win32_widgets) { 6775 TCITEM item; 6776 item.mask = TCIF_TEXT; 6777 WCharzBuffer buf = WCharzBuffer(title); 6778 item.pszText = buf.ptr; 6779 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 6780 } else version(custom_widgets) { 6781 if(pos >= tabs.length) { 6782 tabs ~= title; 6783 redraw(); 6784 return cast(int) tabs.length - 1; 6785 } else if(pos <= 0) { 6786 tabs = title ~ tabs; 6787 redraw(); 6788 return 0; 6789 } else { 6790 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 6791 redraw(); 6792 return pos; 6793 } 6794 } 6795 } 6796 6797 override void addChild(Widget child, int pos = int.max) { 6798 if(container) 6799 container.addChild(child, pos); 6800 else 6801 super.addChild(child, pos); 6802 } 6803 6804 protected Widget makeContainer() { 6805 return new Widget(this); 6806 } 6807 6808 private Widget container; 6809 6810 override void recomputeChildLayout() { 6811 version(win32_widgets) { 6812 this.registerMovement(); 6813 6814 RECT rect; 6815 GetWindowRect(hwnd, &rect); 6816 6817 auto left = rect.left; 6818 auto top = rect.top; 6819 6820 TabCtrl_AdjustRect(hwnd, false, &rect); 6821 foreach(child; children) { 6822 if(!child.showing) continue; 6823 child.x = rect.left - left; 6824 child.y = rect.top - top; 6825 child.width = rect.right - rect.left; 6826 child.height = rect.bottom - rect.top; 6827 child.recomputeChildLayout(); 6828 } 6829 } else version(custom_widgets) { 6830 this.registerMovement(); 6831 foreach(child; children) { 6832 if(!child.showing) continue; 6833 child.x = 2; 6834 child.y = tabBarHeight + 2; // for the border 6835 child.width = width - 4; // for the border 6836 child.height = height - tabBarHeight - 2 - 2; // for the border 6837 child.recomputeChildLayout(); 6838 } 6839 } else static assert(0); 6840 } 6841 6842 version(custom_widgets) 6843 string[] tabs; 6844 6845 this(Widget parent) { 6846 super(parent); 6847 6848 tabStop = false; 6849 6850 version(win32_widgets) { 6851 createWin32Window(this, WC_TABCONTROL, "", 0); 6852 } else version(custom_widgets) { 6853 addEventListener((ClickEvent event) { 6854 if(event.target !is this) 6855 return; 6856 if(event.clientY >= 0 && event.clientY < tabBarHeight) { 6857 auto t = (event.clientX / tabWidth); 6858 if(t >= 0 && t < tabs.length) { 6859 currentTab_ = t; 6860 tabIndexClicked(t); 6861 redraw(); 6862 } 6863 } 6864 }); 6865 } else static assert(0); 6866 6867 this.container = makeContainer(); 6868 } 6869 6870 override int marginTop() { return 4; } 6871 override int paddingBottom() { return 4; } 6872 6873 override int minHeight() { 6874 int max = 0; 6875 foreach(child; children) 6876 max = mymax(child.minHeight, max); 6877 6878 6879 version(win32_widgets) { 6880 RECT rect; 6881 rect.right = this.width; 6882 rect.bottom = max; 6883 TabCtrl_AdjustRect(hwnd, true, &rect); 6884 6885 max = rect.bottom; 6886 } else { 6887 max += defaultLineHeight + 4; 6888 } 6889 6890 6891 return max; 6892 } 6893 6894 version(win32_widgets) 6895 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 6896 switch(code) { 6897 case TCN_SELCHANGE: 6898 auto sel = TabCtrl_GetCurSel(hwnd); 6899 tabIndexClicked(sel); 6900 break; 6901 default: 6902 } 6903 return 0; 6904 } 6905 6906 version(custom_widgets) { 6907 private int currentTab_; 6908 private int tabBarHeight() { return defaultLineHeight; } 6909 int tabWidth() { return scaleWithDpi(80); } 6910 } 6911 6912 version(win32_widgets) 6913 override void paint(WidgetPainter painter) {} 6914 6915 version(custom_widgets) 6916 override void paint(WidgetPainter painter) { 6917 auto cs = getComputedStyle(); 6918 6919 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 6920 6921 int posX = 0; 6922 foreach(idx, title; tabs) { 6923 auto isCurrent = idx == getCurrentTab(); 6924 6925 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 6926 6927 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 6928 painter.outlineColor = cs.foregroundColor; 6929 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 6930 6931 if(isCurrent) { 6932 painter.outlineColor = cs.windowBackgroundColor; 6933 painter.fillColor = Color.transparent; 6934 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 6935 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 6936 6937 painter.outlineColor = Color.white; 6938 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 6939 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 6940 painter.outlineColor = cs.activeTabColor; 6941 painter.drawPixel(Point(posX, tabBarHeight - 1)); 6942 } 6943 6944 posX += tabWidth - 2; 6945 } 6946 } 6947 6948 /// 6949 @scriptable 6950 void setCurrentTab(int item) { 6951 version(win32_widgets) 6952 TabCtrl_SetCurSel(hwnd, item); 6953 else version(custom_widgets) 6954 currentTab_ = item; 6955 else static assert(0); 6956 6957 tabIndexClicked(item); 6958 } 6959 6960 /// 6961 @scriptable 6962 int getCurrentTab() { 6963 version(win32_widgets) 6964 return TabCtrl_GetCurSel(hwnd); 6965 else version(custom_widgets) 6966 return currentTab_; // FIXME 6967 else static assert(0); 6968 } 6969 6970 /// 6971 @scriptable 6972 void removeTab(int item) { 6973 if(item && item == getCurrentTab()) 6974 setCurrentTab(item - 1); 6975 6976 version(win32_widgets) { 6977 TabCtrl_DeleteItem(hwnd, item); 6978 } 6979 6980 for(int a = item; a < children.length - 1; a++) 6981 this._children[a] = this._children[a + 1]; 6982 this._children = this._children[0 .. $-1]; 6983 } 6984 6985 } 6986 6987 6988 /++ 6989 A tab widget is a set of clickable tab buttons followed by a content area. 6990 6991 6992 Tabs can change existing content or can be new pages. 6993 6994 When the user picks a different tab, a `change` message is generated. 6995 +/ 6996 class TabWidget : TabMessageWidget { 6997 this(Widget parent) { 6998 super(parent); 6999 } 7000 7001 override protected Widget makeContainer() { 7002 return null; 7003 } 7004 7005 override void addChild(Widget child, int pos = int.max) { 7006 if(auto twp = cast(TabWidgetPage) child) { 7007 Widget.addChild(child, pos); 7008 if(pos == int.max) 7009 pos = cast(int) this.children.length - 1; 7010 7011 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 7012 7013 if(pos != getCurrentTab) { 7014 child.showing = false; 7015 } 7016 } else { 7017 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 7018 } 7019 } 7020 7021 // FIXME: add tab icons at some point, Windows supports them 7022 /++ 7023 Adds a page and its associated tab with the given label to the widget. 7024 7025 Returns: 7026 The added page object, to which you can add other widgets. 7027 +/ 7028 @scriptable 7029 TabWidgetPage addPage(string title) { 7030 return new TabWidgetPage(title, this); 7031 } 7032 7033 /++ 7034 Gets the page at the given tab index, or `null` if the index is bad. 7035 7036 History: 7037 Added December 24, 2021. 7038 +/ 7039 TabWidgetPage getPage(int index) { 7040 if(index < this.children.length) 7041 return null; 7042 return cast(TabWidgetPage) this.children[index]; 7043 } 7044 7045 /++ 7046 While you can still use the addTab from the parent class, 7047 *strongly* recommend you use [addPage] insteaad. 7048 7049 History: 7050 Added December 24, 2021 to fulful the interface 7051 requirement that came from adding [TabMessageWidget]. 7052 7053 You should not use it though since the [addPage] function 7054 is much easier to use here. 7055 +/ 7056 override int addTab(string title, int pos = int.max) { 7057 auto p = addPage(title); 7058 foreach(idx, child; this.children) 7059 if(child is p) 7060 return cast(int) idx; 7061 return -1; 7062 } 7063 7064 protected override void tabIndexClicked(int item) { 7065 foreach(idx, child; children) { 7066 child.showing(false, false); // batch the recalculates for the end 7067 } 7068 7069 foreach(idx, child; children) { 7070 if(idx == item) { 7071 child.showing(true, false); 7072 if(parentWindow) { 7073 auto f = parentWindow.getFirstFocusable(child); 7074 if(f) 7075 f.focus(); 7076 } 7077 recomputeChildLayout(); 7078 } 7079 } 7080 7081 version(win32_widgets) { 7082 InvalidateRect(hwnd, null, true); 7083 } else version(custom_widgets) { 7084 this.redraw(); 7085 } 7086 } 7087 7088 } 7089 7090 /++ 7091 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 7092 7093 You add [TabWidgetPage]s to it. 7094 +/ 7095 class PageWidget : Widget { 7096 this(Widget parent) { 7097 super(parent); 7098 } 7099 7100 override int minHeight() { 7101 int max = 0; 7102 foreach(child; children) 7103 max = mymax(child.minHeight, max); 7104 7105 return max; 7106 } 7107 7108 7109 override void addChild(Widget child, int pos = int.max) { 7110 if(auto twp = cast(TabWidgetPage) child) { 7111 super.addChild(child, pos); 7112 if(pos == int.max) 7113 pos = cast(int) this.children.length - 1; 7114 7115 if(pos != getCurrentTab) { 7116 child.showing = false; 7117 } 7118 } else { 7119 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 7120 } 7121 } 7122 7123 override void recomputeChildLayout() { 7124 this.registerMovement(); 7125 foreach(child; children) { 7126 child.x = 0; 7127 child.y = 0; 7128 child.width = width; 7129 child.height = height; 7130 child.recomputeChildLayout(); 7131 } 7132 } 7133 7134 private int currentTab_; 7135 7136 /// 7137 @scriptable 7138 void setCurrentTab(int item) { 7139 currentTab_ = item; 7140 7141 showOnly(item); 7142 } 7143 7144 /// 7145 @scriptable 7146 int getCurrentTab() { 7147 return currentTab_; 7148 } 7149 7150 /// 7151 @scriptable 7152 void removeTab(int item) { 7153 if(item && item == getCurrentTab()) 7154 setCurrentTab(item - 1); 7155 7156 for(int a = item; a < children.length - 1; a++) 7157 this._children[a] = this._children[a + 1]; 7158 this._children = this._children[0 .. $-1]; 7159 } 7160 7161 /// 7162 @scriptable 7163 TabWidgetPage addPage(string title) { 7164 return new TabWidgetPage(title, this); 7165 } 7166 7167 private void showOnly(int item) { 7168 foreach(idx, child; children) 7169 if(idx == item) { 7170 child.show(); 7171 child.recomputeChildLayout(); 7172 } else { 7173 child.hide(); 7174 } 7175 } 7176 7177 } 7178 7179 /++ 7180 7181 +/ 7182 class TabWidgetPage : Widget { 7183 string title; 7184 this(string title, Widget parent) { 7185 this.title = title; 7186 this.tabStop = false; 7187 super(parent); 7188 7189 ///* 7190 version(win32_widgets) { 7191 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 7192 } 7193 //*/ 7194 } 7195 7196 override int minHeight() { 7197 int sum = 0; 7198 foreach(child; children) 7199 sum += child.minHeight(); 7200 return sum; 7201 } 7202 } 7203 7204 version(none) 7205 /++ 7206 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 7207 7208 I think I need to modify the layout algorithms to support this. 7209 +/ 7210 class CollapsableSidebar : Widget { 7211 7212 } 7213 7214 /// Stacks the widgets vertically, taking all the available width for each child. 7215 class VerticalLayout : Layout { 7216 // most of this is intentionally blank - widget's default is vertical layout right now 7217 /// 7218 this(Widget parent) { super(parent); } 7219 7220 /++ 7221 Sets a max width for the layout so you don't have to subclass. The max width 7222 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7223 7224 History: 7225 Added November 29, 2021 (dub v10.5) 7226 +/ 7227 this(int maxWidth, Widget parent) { 7228 this.mw = maxWidth; 7229 super(parent); 7230 } 7231 7232 private int mw = int.max; 7233 7234 override int maxWidth() { return scaleWithDpi(mw); } 7235 } 7236 7237 /// Stacks the widgets horizontally, taking all the available height for each child. 7238 class HorizontalLayout : Layout { 7239 /// 7240 this(Widget parent) { super(parent); } 7241 7242 /++ 7243 Sets a max height for the layout so you don't have to subclass. The max height 7244 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7245 7246 History: 7247 Added November 29, 2021 (dub v10.5) 7248 +/ 7249 this(int maxHeight, Widget parent) { 7250 this.mh = maxHeight; 7251 super(parent); 7252 } 7253 7254 private int mh = 0; 7255 7256 7257 7258 override void recomputeChildLayout() { 7259 .recomputeChildLayout!"width"(this); 7260 } 7261 7262 override int minHeight() { 7263 int largest = 0; 7264 int margins = 0; 7265 int lastMargin = 0; 7266 foreach(child; children) { 7267 auto mh = child.minHeight(); 7268 if(mh > largest) 7269 largest = mh; 7270 margins += mymax(lastMargin, child.marginTop()); 7271 lastMargin = child.marginBottom(); 7272 } 7273 return largest + margins; 7274 } 7275 7276 override int maxHeight() { 7277 if(mh != 0) 7278 return mymax(minHeight, scaleWithDpi(mh)); 7279 7280 int largest = 0; 7281 int margins = 0; 7282 int lastMargin = 0; 7283 foreach(child; children) { 7284 auto mh = child.maxHeight(); 7285 if(mh == int.max) 7286 return int.max; 7287 if(mh > largest) 7288 largest = mh; 7289 margins += mymax(lastMargin, child.marginTop()); 7290 lastMargin = child.marginBottom(); 7291 } 7292 return largest + margins; 7293 } 7294 7295 override int heightStretchiness() { 7296 int max; 7297 foreach(child; children) { 7298 auto c = child.heightStretchiness; 7299 if(c > max) 7300 max = c; 7301 } 7302 return max; 7303 } 7304 7305 } 7306 7307 version(win32_widgets) 7308 private 7309 extern(Windows) 7310 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 7311 Widget* pwin = hwnd in Widget.nativeMapping; 7312 if(pwin is null) 7313 return DefWindowProc(hwnd, message, wparam, lparam); 7314 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 7315 if(win is null) 7316 return DefWindowProc(hwnd, message, wparam, lparam); 7317 7318 switch(message) { 7319 case WM_SIZE: 7320 auto width = LOWORD(lparam); 7321 auto height = HIWORD(lparam); 7322 7323 auto hdc = GetDC(hwnd); 7324 auto hdcBmp = CreateCompatibleDC(hdc); 7325 7326 // FIXME: could this be more efficient? it never relinquishes a large bitmap 7327 if(width > win.bmpWidth || height > win.bmpHeight) { 7328 auto oldBuffer = win.buffer; 7329 win.buffer = CreateCompatibleBitmap(hdc, width, height); 7330 7331 if(oldBuffer) 7332 DeleteObject(oldBuffer); 7333 7334 win.bmpWidth = width; 7335 win.bmpHeight = height; 7336 } 7337 7338 // just always erase it upon resizing so minigui can draw over with a clean slate 7339 auto oldBmp = SelectObject(hdcBmp, win.buffer); 7340 7341 auto brush = GetSysColorBrush(COLOR_3DFACE); 7342 RECT r; 7343 r.left = 0; 7344 r.top = 0; 7345 r.right = width; 7346 r.bottom = height; 7347 FillRect(hdcBmp, &r, brush); 7348 7349 SelectObject(hdcBmp, oldBmp); 7350 DeleteDC(hdcBmp); 7351 ReleaseDC(hwnd, hdc); 7352 break; 7353 case WM_PAINT: 7354 if(win.buffer is null) 7355 goto default; 7356 7357 BITMAP bm; 7358 PAINTSTRUCT ps; 7359 7360 HDC hdc = BeginPaint(hwnd, &ps); 7361 7362 HDC hdcMem = CreateCompatibleDC(hdc); 7363 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 7364 7365 GetObject(win.buffer, bm.sizeof, &bm); 7366 7367 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 7368 7369 SelectObject(hdcMem, hbmOld); 7370 DeleteDC(hdcMem); 7371 EndPaint(hwnd, &ps); 7372 break; 7373 default: 7374 return DefWindowProc(hwnd, message, wparam, lparam); 7375 } 7376 7377 return 0; 7378 } 7379 7380 private wstring Win32Class(wstring name)() { 7381 static bool classRegistered; 7382 if(!classRegistered) { 7383 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 7384 WNDCLASSEX wc; 7385 wc.cbSize = wc.sizeof; 7386 wc.hInstance = hInstance; 7387 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 7388 wc.lpfnWndProc = &DoubleBufferWndProc; 7389 wc.lpszClassName = name.ptr; 7390 if(!RegisterClassExW(&wc)) 7391 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 7392 classRegistered = true; 7393 } 7394 7395 return name; 7396 } 7397 7398 /+ 7399 version(win32_widgets) 7400 extern(Windows) 7401 private 7402 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 7403 switch(iMessage) { 7404 case WM_PAINT: 7405 if(auto te = hWnd in Widget.nativeMapping) { 7406 try { 7407 //te.redraw(); 7408 writeln(te, " drawing"); 7409 } catch(Exception) {} 7410 } 7411 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7412 default: 7413 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7414 } 7415 } 7416 +/ 7417 7418 7419 /++ 7420 A widget specifically designed to hold other widgets. 7421 7422 History: 7423 Added July 1, 2021 7424 +/ 7425 class ContainerWidget : Widget { 7426 this(Widget parent) { 7427 super(parent); 7428 this.tabStop = false; 7429 7430 version(win32_widgets) { 7431 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 7432 } 7433 } 7434 } 7435 7436 /++ 7437 A widget that takes your widget, puts scroll bars around it, and sends 7438 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 7439 no effort to automatically scroll or clip its child widgets - it just sends 7440 the messages. 7441 7442 7443 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 7444 The scroll coordinates are all given in a unit you interpret as you wish. One 7445 of these units is moved on each press of the arrow buttons and represents the 7446 smallest amount the user can scroll. The intention is for this to be one line, 7447 one item in a list, one row in a table, etc. Whatever makes sense for your widget 7448 in each direction that the user might be interested in. 7449 7450 You can set a "page size" with the [step] property. (Yes, I regret the name...) 7451 This is the amount it jumps when the user pressed page up and page down, or clicks 7452 in the exposed part of the scroll bar. 7453 7454 You should add child content to the ScrollMessageWidget. However, it is important to 7455 note that the coordinates are always independent of the scroll position! It is YOUR 7456 responsibility to do any necessary transforms, clipping, etc., while drawing the 7457 content and interpreting mouse events if they are supposed to change with the scroll. 7458 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 7459 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 7460 you more control (which can be considerably more efficient and adapted to your actual data) 7461 at the expense of you also needing to be aware of its reality. 7462 7463 Please note that it does NOT react to mouse wheel events or various keyboard events as of 7464 version 10.3. Maybe this will change in the future.... but for now you must call 7465 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 7466 +/ 7467 class ScrollMessageWidget : Widget { 7468 this(Widget parent) { 7469 super(parent); 7470 7471 container = new Widget(this); 7472 hsb = new HorizontalScrollbar(this); 7473 vsb = new VerticalScrollbar(this); 7474 7475 hsb.addEventListener("scrolltonextline", { 7476 hsb.setPosition(hsb.position + movementPerButtonClickH_); 7477 notify(); 7478 }); 7479 hsb.addEventListener("scrolltopreviousline", { 7480 hsb.setPosition(hsb.position - movementPerButtonClickH_); 7481 notify(); 7482 }); 7483 vsb.addEventListener("scrolltonextline", { 7484 vsb.setPosition(vsb.position + movementPerButtonClickV_); 7485 notify(); 7486 }); 7487 vsb.addEventListener("scrolltopreviousline", { 7488 vsb.setPosition(vsb.position - movementPerButtonClickV_); 7489 notify(); 7490 }); 7491 hsb.addEventListener("scrolltonextpage", { 7492 hsb.setPosition(hsb.position + hsb.step_); 7493 notify(); 7494 }); 7495 hsb.addEventListener("scrolltopreviouspage", { 7496 hsb.setPosition(hsb.position - hsb.step_); 7497 notify(); 7498 }); 7499 vsb.addEventListener("scrolltonextpage", { 7500 vsb.setPosition(vsb.position + vsb.step_); 7501 notify(); 7502 }); 7503 vsb.addEventListener("scrolltopreviouspage", { 7504 vsb.setPosition(vsb.position - vsb.step_); 7505 notify(); 7506 }); 7507 hsb.addEventListener("scrolltoposition", (Event event) { 7508 hsb.setPosition(event.intValue); 7509 notify(); 7510 }); 7511 vsb.addEventListener("scrolltoposition", (Event event) { 7512 vsb.setPosition(event.intValue); 7513 notify(); 7514 }); 7515 7516 7517 tabStop = false; 7518 container.tabStop = false; 7519 magic = true; 7520 } 7521 7522 private int movementPerButtonClickH_ = 1; 7523 private int movementPerButtonClickV_ = 1; 7524 public void movementPerButtonClick(int h, int v) { 7525 movementPerButtonClickH_ = h; 7526 movementPerButtonClickV_ = v; 7527 } 7528 7529 /++ 7530 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 7531 7532 7533 The defaults for [addDefaultWheelListeners] are: 7534 7535 $(LIST 7536 * Mouse wheel scrolls vertically 7537 * Alt key + mouse wheel scrolls horiontally 7538 * Shift + mouse wheel scrolls faster. 7539 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 7540 ) 7541 7542 The defaults for [addDefaultKeyboardListeners] are: 7543 7544 $(LIST 7545 * Arrow keys scroll by the given amounts 7546 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 7547 * Page up and down scroll by the vertical viewable area 7548 * Home and end scroll to the start and end of the verticle viewable area. 7549 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 7550 ) 7551 7552 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 7553 7554 Params: 7555 horizontalArrowScrollAmount = 7556 verticalArrowScrollAmount = 7557 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 7558 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 7559 shiftMultiplier = multiplies the scroll amount by this when shift is held 7560 +/ 7561 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 7562 auto _this = this; 7563 7564 container.addEventListener((scope KeyDownEvent ke) { 7565 switch(ke.key) { 7566 case Key.Left: 7567 _this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7568 break; 7569 case Key.Right: 7570 _this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7571 break; 7572 case Key.Up: 7573 _this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7574 break; 7575 case Key.Down: 7576 _this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7577 break; 7578 case Key.PageUp: 7579 if(ke.altKey) 7580 _this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7581 else 7582 _this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7583 break; 7584 case Key.PageDown: 7585 if(ke.altKey) 7586 _this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7587 else 7588 _this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7589 break; 7590 case Key.Home: 7591 if(ke.altKey) 7592 _this.scrollLeft(short.max * 16); 7593 else 7594 _this.scrollUp(short.max * 16); 7595 break; 7596 case Key.End: 7597 if(ke.altKey) 7598 _this.scrollRight(short.max * 16); 7599 else 7600 _this.scrollDown(short.max * 16); 7601 break; 7602 7603 default: 7604 // ignore, not for us. 7605 } 7606 7607 }); 7608 } 7609 7610 /// ditto 7611 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 7612 auto _this = this; 7613 container.addEventListener((scope ClickEvent ce) { 7614 7615 if(ce.target && ce.target.tabStop) 7616 ce.target.focus(); 7617 7618 // ctrl is reserved for the application 7619 if(ce.ctrlKey) 7620 return; 7621 7622 if(horizontalWheelScrollAmount == 0 && ce.altKey) 7623 return; 7624 7625 if(shiftMultiplier == 0 && ce.shiftKey) 7626 return; 7627 7628 if(ce.button == MouseButton.wheelDown) { 7629 if(ce.altKey) 7630 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7631 else 7632 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7633 } else if(ce.button == MouseButton.wheelUp) { 7634 if(ce.altKey) 7635 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7636 else 7637 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7638 } 7639 }); 7640 } 7641 7642 /++ 7643 Scrolls the given amount. 7644 7645 History: 7646 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. 7647 +/ 7648 void scrollUp(int amount = 1) { 7649 vsb.setPosition(vsb.position - amount); 7650 notify(); 7651 } 7652 /// ditto 7653 void scrollDown(int amount = 1) { 7654 vsb.setPosition(vsb.position + amount); 7655 notify(); 7656 } 7657 /// ditto 7658 void scrollLeft(int amount = 1) { 7659 hsb.setPosition(hsb.position - amount); 7660 notify(); 7661 } 7662 /// ditto 7663 void scrollRight(int amount = 1) { 7664 hsb.setPosition(hsb.position + amount); 7665 notify(); 7666 } 7667 7668 /// 7669 VerticalScrollbar verticalScrollBar() { return vsb; } 7670 /// 7671 HorizontalScrollbar horizontalScrollBar() { return hsb; } 7672 7673 void notify() { 7674 static bool insideNotify; 7675 7676 if(insideNotify) 7677 return; // avoid the recursive call, even if it isn't strictly correct 7678 7679 insideNotify = true; 7680 scope(exit) insideNotify = false; 7681 7682 this.emit!ScrollEvent(); 7683 } 7684 7685 mixin Emits!ScrollEvent; 7686 7687 /// 7688 Point position() { 7689 return Point(hsb.position, vsb.position); 7690 } 7691 7692 /// 7693 void setPosition(int x, int y) { 7694 hsb.setPosition(x); 7695 vsb.setPosition(y); 7696 } 7697 7698 /// 7699 void setPageSize(int unitsX, int unitsY) { 7700 hsb.setStep(unitsX); 7701 vsb.setStep(unitsY); 7702 } 7703 7704 /// Always call this BEFORE setViewableArea 7705 void setTotalArea(int width, int height) { 7706 hsb.setMax(width); 7707 vsb.setMax(height); 7708 } 7709 7710 /++ 7711 Always set the viewable area AFTER setitng the total area if you are going to change both. 7712 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 7713 If you need to do that, use [queueRecomputeChildLayout]. 7714 +/ 7715 void setViewableArea(int width, int height) { 7716 7717 // actually there IS A need to dothis cuz the max might have changed since then 7718 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 7719 //return; // no need to do what is already done 7720 hsb.setViewableArea(width); 7721 vsb.setViewableArea(height); 7722 7723 bool needsNotify = false; 7724 7725 // FIXME: if at any point the rhs is outside the scrollbar, we need 7726 // to reset to 0. but it should remember the old position in case the 7727 // window resizes again, so it can kinda return ot where it was. 7728 // 7729 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 7730 if(width >= hsb.max) { 7731 // there's plenty of room to display it all so we need to reset to zero 7732 // FIXME: adjust so it matches the note above 7733 hsb.setPosition(0); 7734 needsNotify = true; 7735 } 7736 if(height >= vsb.max) { 7737 // there's plenty of room to display it all so we need to reset to zero 7738 // FIXME: adjust so it matches the note above 7739 vsb.setPosition(0); 7740 needsNotify = true; 7741 } 7742 if(needsNotify) 7743 notify(); 7744 } 7745 7746 private bool magic; 7747 override void addChild(Widget w, int position = int.max) { 7748 if(magic) 7749 container.addChild(w, position); 7750 else 7751 super.addChild(w, position); 7752 } 7753 7754 override void recomputeChildLayout() { 7755 if(hsb is null || vsb is null || container is null) return; 7756 7757 registerMovement(); 7758 7759 enum BUTTON_SIZE = 16; 7760 7761 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 7762 hsb.x = 0; 7763 hsb.y = this.height - hsb.height; 7764 7765 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 7766 vsb.x = this.width - vsb.width; 7767 vsb.y = 0; 7768 7769 auto vsb_width = vsb.showing ? vsb.width : 0; 7770 auto hsb_height = hsb.showing ? hsb.height : 0; 7771 7772 hsb.width = this.width - vsb_width; 7773 vsb.height = this.height - hsb_height; 7774 7775 hsb.recomputeChildLayout(); 7776 vsb.recomputeChildLayout(); 7777 7778 if(this.header is null) { 7779 container.x = 0; 7780 container.y = 0; 7781 container.width = this.width - vsb_width; 7782 container.height = this.height - hsb_height; 7783 container.recomputeChildLayout(); 7784 } else { 7785 header.x = 0; 7786 header.y = 0; 7787 header.width = this.width - vsb_width; 7788 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 7789 header.recomputeChildLayout(); 7790 7791 container.x = 0; 7792 container.y = scaleWithDpi(BUTTON_SIZE); 7793 container.width = this.width - vsb_width; 7794 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 7795 container.recomputeChildLayout(); 7796 } 7797 } 7798 7799 private HorizontalScrollbar hsb; 7800 private VerticalScrollbar vsb; 7801 Widget container; 7802 private Widget header; 7803 7804 /++ 7805 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 7806 7807 History: 7808 Added September 27, 2021 (dub v10.3) 7809 +/ 7810 Widget getHeader() { 7811 if(this.header is null) { 7812 magic = false; 7813 scope(exit) magic = true; 7814 this.header = new Widget(this); 7815 recomputeChildLayout(); 7816 } 7817 return this.header; 7818 } 7819 7820 /++ 7821 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 7822 7823 History: 7824 Added January 3, 2023 (dub v11.0) 7825 +/ 7826 void scrollIntoView(Rectangle rect) { 7827 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 7828 7829 // import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1))); 7830 7831 // the lower right is exclusive normally 7832 auto test = rect.lowerRight; 7833 if(test.x > 0) test.x--; 7834 if(test.y > 0) test.y--; 7835 7836 if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) { 7837 // try to scroll only one dimension at a time if we can 7838 if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y))) 7839 setPosition(rect.upperLeft.x, position.y); 7840 if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y))) 7841 setPosition(position.x, rect.upperLeft.y); 7842 } 7843 7844 } 7845 7846 override int minHeight() { 7847 int min = mymax(container ? container.minHeight : 0, (verticalScrollBar.showing ? verticalScrollBar.minHeight : 0)); 7848 if(header !is null) 7849 min += header.minHeight; 7850 if(horizontalScrollBar.showing) 7851 min += horizontalScrollBar.minHeight; 7852 return min; 7853 } 7854 7855 override int maxHeight() { 7856 int max = container ? container.maxHeight : int.max; 7857 if(max == int.max) 7858 return max; 7859 if(horizontalScrollBar.showing) 7860 max += horizontalScrollBar.minHeight; 7861 return max; 7862 } 7863 } 7864 7865 /++ 7866 $(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") 7867 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 7868 +/ 7869 version(minigui_screenshots) 7870 @Screenshot("ScrollMessageWidget") 7871 unittest { 7872 auto window = new Window("ScrollMessageWidget"); 7873 7874 auto smw = new ScrollMessageWidget(window); 7875 smw.addDefaultKeyboardListeners(); 7876 smw.addDefaultWheelListeners(); 7877 7878 window.loop(); 7879 } 7880 7881 /++ 7882 Bypasses automatic layout for its children, using manual positioning and sizing only. 7883 While you need to manually position them, you must ensure they are inside the StaticLayout's 7884 bounding box to avoid undefined behavior. 7885 7886 You should almost never use this. 7887 +/ 7888 class StaticLayout : Layout { 7889 /// 7890 this(Widget parent) { super(parent); } 7891 override void recomputeChildLayout() { 7892 registerMovement(); 7893 foreach(child; children) 7894 child.recomputeChildLayout(); 7895 } 7896 } 7897 7898 /++ 7899 Bypasses automatic positioning when being laid out. It is your responsibility to make 7900 room for this widget in the parent layout. 7901 7902 Its children are laid out normally, unless there is exactly one, in which case it takes 7903 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 7904 can do that with `padding`). 7905 +/ 7906 class StaticPosition : Layout { 7907 /// 7908 this(Widget parent) { super(parent); } 7909 7910 override void recomputeChildLayout() { 7911 registerMovement(); 7912 if(this.children.length == 1) { 7913 auto child = children[0]; 7914 child.x = 0; 7915 child.y = 0; 7916 child.width = this.width; 7917 child.height = this.height; 7918 child.recomputeChildLayout(); 7919 } else 7920 foreach(child; children) 7921 child.recomputeChildLayout(); 7922 } 7923 7924 alias width = typeof(super).width; 7925 alias height = typeof(super).height; 7926 7927 @property int width(int w) @nogc pure @safe nothrow { 7928 return this._width = w; 7929 } 7930 7931 @property int height(int w) @nogc pure @safe nothrow { 7932 return this._height = w; 7933 } 7934 7935 } 7936 7937 /++ 7938 FixedPosition is like [StaticPosition], but its coordinates 7939 are always relative to the viewport, meaning they do not scroll with 7940 the parent content. 7941 +/ 7942 class FixedPosition : StaticPosition { 7943 /// 7944 this(Widget parent) { super(parent); } 7945 } 7946 7947 version(win32_widgets) 7948 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 7949 if(true) { 7950 // cmd == 0 = menu, cmd == 1 = accelerator 7951 if(auto item = idm in Action.mapping) { 7952 foreach(handler; (*item).triggered) 7953 handler(); 7954 /* 7955 auto event = new Event("triggered", *item); 7956 event.button = idm; 7957 event.dispatch(); 7958 */ 7959 return 0; 7960 } 7961 } 7962 if(handle) 7963 if(auto widgetp = handle in Widget.nativeMapping) { 7964 (*widgetp).handleWmCommand(cmd, idm); 7965 return 0; 7966 } 7967 return 1; 7968 } 7969 7970 7971 /// 7972 class Window : Widget { 7973 int mouseCaptureCount = 0; 7974 Widget mouseCapturedBy; 7975 void captureMouse(Widget byWhom) { 7976 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 7977 mouseCaptureCount++; 7978 mouseCapturedBy = byWhom; 7979 win.grabInput(); 7980 } 7981 void releaseMouseCapture() { 7982 mouseCaptureCount--; 7983 mouseCapturedBy = null; 7984 win.releaseInputGrab(); 7985 } 7986 7987 /++ 7988 Sets the window icon which is often seen in title bars and taskbars. 7989 7990 History: 7991 Added April 5, 2022 (dub v10.8) 7992 +/ 7993 @property void icon(MemoryImage icon) { 7994 if(win && icon) 7995 win.icon = icon; 7996 } 7997 7998 /// 7999 @scriptable 8000 @property bool focused() { 8001 return win.focused; 8002 } 8003 8004 static class Style : Widget.Style { 8005 override WidgetBackground background() { 8006 version(custom_widgets) 8007 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 8008 else version(win32_widgets) 8009 return WidgetBackground(Color.transparent); 8010 else static assert(0); 8011 } 8012 } 8013 mixin OverrideStyle!Style; 8014 8015 /++ 8016 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. 8017 +/ 8018 deprecated("Use the non-static Widget.defaultLineHeight() instead") static int lineHeight() { 8019 return lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback(); 8020 } 8021 8022 private static int lineHeightNotDeprecatedButShouldBeSinceItIsJustAFallback() { 8023 OperatingSystemFont font; 8024 if(auto vt = WidgetPainter.visualTheme) { 8025 font = vt.defaultFontCached(96); // FIXME 8026 } 8027 8028 if(font is null) { 8029 static int defaultHeightCache; 8030 if(defaultHeightCache == 0) { 8031 font = new OperatingSystemFont; 8032 font.loadDefault; 8033 defaultHeightCache = font.height();// * 5 / 4; 8034 } 8035 return defaultHeightCache; 8036 } 8037 8038 return font.height();// * 5 / 4; 8039 } 8040 8041 Widget focusedWidget; 8042 8043 private SimpleWindow win_; 8044 8045 @property { 8046 /++ 8047 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 8048 8049 History: 8050 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 8051 +/ 8052 public SimpleWindow win() { 8053 return win_; 8054 } 8055 /// 8056 protected void win(SimpleWindow w) { 8057 win_ = w; 8058 } 8059 } 8060 8061 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 8062 this(Widget p) { 8063 tabStop = false; 8064 super(p); 8065 } 8066 8067 private void actualRedraw() { 8068 if(recomputeChildLayoutRequired) 8069 recomputeChildLayoutEntry(); 8070 if(!showing) return; 8071 8072 assert(parentWindow !is null); 8073 8074 auto w = drawableWindow; 8075 if(w is null) 8076 w = parentWindow.win; 8077 8078 if(w.closed()) 8079 return; 8080 8081 auto ugh = this.parent; 8082 int lox, loy; 8083 while(ugh) { 8084 lox += ugh.x; 8085 loy += ugh.y; 8086 ugh = ugh.parent; 8087 } 8088 auto painter = w.draw(true); 8089 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 8090 // RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN); 8091 } 8092 8093 8094 private bool skipNextChar = false; 8095 8096 /++ 8097 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 8098 8099 This constructor is intended primarily for internal use and may be changed to `protected` later. 8100 +/ 8101 this(SimpleWindow win) { 8102 8103 static if(UsingSimpledisplayX11) { 8104 win.discardAdditionalConnectionState = &discardXConnectionState; 8105 win.recreateAdditionalConnectionState = &recreateXConnectionState; 8106 } 8107 8108 tabStop = false; 8109 super(null); 8110 this.win = win; 8111 8112 win.addEventListener((Widget.RedrawEvent) { 8113 if(win.eventQueued!RecomputeEvent) { 8114 // writeln("skipping"); 8115 return; // let the recompute event do the actual redraw 8116 } 8117 this.actualRedraw(); 8118 }); 8119 8120 win.addEventListener((Widget.RecomputeEvent) { 8121 recomputeChildLayoutEntry(); 8122 if(win.eventQueued!RedrawEvent) 8123 return; // let the queued one do it 8124 else { 8125 // writeln("drawing"); 8126 this.actualRedraw(); // if not queued, it needs to be done now anyway 8127 } 8128 }); 8129 8130 this.width = win.width; 8131 this.height = win.height; 8132 this.parentWindow = this; 8133 8134 win.closeQuery = () { 8135 if(this.emit!ClosingEvent()) 8136 win.close(); 8137 }; 8138 win.onClosing = () { 8139 this.emit!ClosedEvent(); 8140 }; 8141 8142 win.windowResized = (int w, int h) { 8143 this.width = w; 8144 this.height = h; 8145 recomputeChildLayout(); 8146 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 8147 //version(win32_widgets) 8148 //InvalidateRect(hwnd, null, true); 8149 redraw(); 8150 }; 8151 8152 win.onFocusChange = (bool getting) { 8153 if(this.focusedWidget) { 8154 if(getting) { 8155 this.focusedWidget.emit!FocusEvent(); 8156 this.focusedWidget.emit!FocusInEvent(); 8157 } else { 8158 this.focusedWidget.emit!BlurEvent(); 8159 this.focusedWidget.emit!FocusOutEvent(); 8160 } 8161 } 8162 8163 if(getting) { 8164 this.emit!FocusEvent(); 8165 this.emit!FocusInEvent(); 8166 } else { 8167 this.emit!BlurEvent(); 8168 this.emit!FocusOutEvent(); 8169 } 8170 }; 8171 8172 win.onDpiChanged = { 8173 this.queueRecomputeChildLayout(); 8174 auto event = new DpiChangedEvent(this); 8175 event.sendDirectly(); 8176 8177 privateDpiChanged(); 8178 }; 8179 8180 win.setEventHandlers( 8181 (MouseEvent e) { 8182 dispatchMouseEvent(e); 8183 }, 8184 (KeyEvent e) { 8185 //writefln("%x %s", cast(uint) e.key, e.key); 8186 dispatchKeyEvent(e); 8187 }, 8188 (dchar e) { 8189 if(e == 13) e = 10; // hack? 8190 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8191 dispatchCharEvent(e); 8192 }, 8193 ); 8194 8195 addEventListener("char", (Widget, Event ev) { 8196 if(skipNextChar) { 8197 ev.preventDefault(); 8198 skipNextChar = false; 8199 } 8200 }); 8201 8202 version(win32_widgets) 8203 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 8204 if(hwnd !is this.win.impl.hwnd) 8205 return 1; // we don't care... pass it on 8206 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 8207 if(mustReturn) 8208 return ret; 8209 return 1; // pass it on 8210 }; 8211 8212 if(Window.newWindowCreated) 8213 Window.newWindowCreated(this); 8214 } 8215 8216 version(custom_widgets) 8217 override void defaultEventHandler_click(ClickEvent event) { 8218 if(event.target && event.target.tabStop) 8219 event.target.focus(); 8220 } 8221 8222 private static void delegate(Window) newWindowCreated; 8223 8224 version(win32_widgets) 8225 override void paint(WidgetPainter painter) { 8226 /* 8227 RECT rect; 8228 rect.right = this.width; 8229 rect.bottom = this.height; 8230 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 8231 */ 8232 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 8233 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 8234 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 8235 // since the pen is null, to fill the whole space, we need the +1 on both. 8236 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 8237 SelectObject(painter.impl.hdc, p); 8238 SelectObject(painter.impl.hdc, b); 8239 } 8240 version(custom_widgets) 8241 override void paint(WidgetPainter painter) { 8242 auto cs = getComputedStyle(); 8243 painter.fillColor = cs.windowBackgroundColor; 8244 painter.outlineColor = cs.windowBackgroundColor; 8245 painter.drawRectangle(Point(0, 0), this.width, this.height); 8246 } 8247 8248 8249 override void defaultEventHandler_keydown(KeyDownEvent event) { 8250 Widget _this = event.target; 8251 8252 if(event.key == Key.Tab) { 8253 /* Window tab ordering is a recursive thingy with each group */ 8254 8255 // FIXME inefficient 8256 Widget[] helper(Widget p) { 8257 if(p.hidden) 8258 return null; 8259 Widget[] childOrdering; 8260 8261 auto children = p.children.dup; 8262 8263 while(true) { 8264 // UIs should be generally small, so gonna brute force it a little 8265 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 8266 8267 Widget smallestTab; 8268 foreach(ref c; children) { 8269 if(c is null) continue; 8270 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 8271 smallestTab = c; 8272 c = null; 8273 } 8274 } 8275 if(smallestTab !is null) { 8276 if(smallestTab.tabStop && !smallestTab.hidden) 8277 childOrdering ~= smallestTab; 8278 if(!smallestTab.hidden) 8279 childOrdering ~= helper(smallestTab); 8280 } else 8281 break; 8282 8283 } 8284 8285 return childOrdering; 8286 } 8287 8288 Widget[] tabOrdering = helper(this); 8289 8290 Widget recipient; 8291 8292 if(tabOrdering.length) { 8293 bool seenThis = false; 8294 Widget previous; 8295 foreach(idx, child; tabOrdering) { 8296 if(child is focusedWidget) { 8297 8298 if(event.shiftKey) { 8299 if(idx == 0) 8300 recipient = tabOrdering[$-1]; 8301 else 8302 recipient = tabOrdering[idx - 1]; 8303 break; 8304 } 8305 8306 seenThis = true; 8307 if(idx + 1 == tabOrdering.length) { 8308 // we're at the end, either move to the next group 8309 // or start back over 8310 recipient = tabOrdering[0]; 8311 } 8312 continue; 8313 } 8314 if(seenThis) { 8315 recipient = child; 8316 break; 8317 } 8318 previous = child; 8319 } 8320 } 8321 8322 if(recipient !is null) { 8323 // writeln(typeid(recipient)); 8324 recipient.focus(); 8325 8326 skipNextChar = true; 8327 } 8328 } 8329 8330 debug if(event.key == Key.F12) { 8331 if(devTools) { 8332 devTools.close(); 8333 devTools = null; 8334 } else { 8335 devTools = new DevToolWindow(this); 8336 devTools.show(); 8337 } 8338 } 8339 } 8340 8341 debug DevToolWindow devTools; 8342 8343 8344 /++ 8345 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 8346 8347 History: 8348 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 8349 8350 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 8351 +/ 8352 this(int width = 500, int height = 500, string title = null) { 8353 if(title is null) { 8354 import core.runtime; 8355 if(Runtime.args.length) 8356 title = Runtime.args[0]; 8357 } 8358 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus); 8359 8360 static if(UsingSimpledisplayX11) { 8361 ///+ 8362 // for input proxy 8363 auto display = XDisplayConnection.get; 8364 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 8365 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 8366 XMapWindow(display, inputProxy); 8367 // writefln("input proxy: 0x%0x", inputProxy); 8368 this.inputProxy = new SimpleWindow(inputProxy); 8369 8370 XEvent lastEvent; 8371 this.inputProxy.handleNativeEvent = (XEvent ev) { 8372 lastEvent = ev; 8373 return 1; 8374 }; 8375 this.inputProxy.setEventHandlers( 8376 (MouseEvent e) { 8377 dispatchMouseEvent(e); 8378 }, 8379 (KeyEvent e) { 8380 //writefln("%x %s", cast(uint) e.key, e.key); 8381 if(dispatchKeyEvent(e)) { 8382 // FIXME: i should trap error 8383 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 8384 auto thing = nw.focusableWindow(); 8385 if(thing && thing.window) { 8386 lastEvent.xkey.window = thing.window; 8387 // writeln("sending event ", lastEvent.xkey); 8388 trapXErrors( { 8389 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 8390 }); 8391 } 8392 } 8393 } 8394 }, 8395 (dchar e) { 8396 if(e == 13) e = 10; // hack? 8397 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8398 dispatchCharEvent(e); 8399 }, 8400 ); 8401 8402 this.inputProxy.populateXic(); 8403 // done 8404 //+/ 8405 } 8406 8407 8408 8409 win.setRequestedInputFocus = &this.setRequestedInputFocus; 8410 8411 this(win); 8412 } 8413 8414 SimpleWindow inputProxy; 8415 8416 private SimpleWindow setRequestedInputFocus() { 8417 return inputProxy; 8418 } 8419 8420 /// ditto 8421 this(string title, int width = 500, int height = 500) { 8422 this(width, height, title); 8423 } 8424 8425 /// 8426 @property string title() { return parentWindow.win.title; } 8427 /// 8428 @property void title(string title) { parentWindow.win.title = title; } 8429 8430 /// 8431 @scriptable 8432 void close() { 8433 win.close(); 8434 // I synchronize here upon window closing to ensure all child windows 8435 // get updated too before the event loop. This avoids some random X errors. 8436 static if(UsingSimpledisplayX11) { 8437 runInGuiThread( { 8438 XSync(XDisplayConnection.get, false); 8439 }); 8440 } 8441 } 8442 8443 bool dispatchKeyEvent(KeyEvent ev) { 8444 auto wid = focusedWidget; 8445 if(wid is null) 8446 wid = this; 8447 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 8448 event.originalKeyEvent = ev; 8449 event.key = ev.key; 8450 event.state = ev.modifierState; 8451 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8452 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8453 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8454 event.dispatch(); 8455 8456 return !event.propagationStopped; 8457 } 8458 8459 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 8460 bool dispatchCharEvent(dchar ch) { 8461 if(focusedWidget) { 8462 auto event = new CharEvent(focusedWidget, ch); 8463 event.dispatch(); 8464 return !event.propagationStopped; 8465 } 8466 return true; 8467 } 8468 8469 Widget mouseLastOver; 8470 Widget mouseLastDownOn; 8471 bool lastWasDoubleClick; 8472 bool dispatchMouseEvent(MouseEvent ev) { 8473 auto eleR = widgetAtPoint(this, ev.x, ev.y); 8474 auto ele = eleR.widget; 8475 8476 auto captureEle = ele; 8477 8478 if(mouseCapturedBy !is null) { 8479 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 8480 captureEle = mouseCapturedBy; 8481 } 8482 8483 // a hack to get it relative to the widget. 8484 eleR.x = ev.x; 8485 eleR.y = ev.y; 8486 auto pain = captureEle; 8487 while(pain) { 8488 eleR.x -= pain.x; 8489 eleR.y -= pain.y; 8490 pain.addScrollPosition(eleR.x, eleR.y); 8491 pain = pain.parent; 8492 } 8493 8494 void populateMouseEventBase(MouseEventBase event) { 8495 event.button = ev.button; 8496 event.buttonLinear = ev.buttonLinear; 8497 event.state = ev.modifierState; 8498 event.clientX = eleR.x; 8499 event.clientY = eleR.y; 8500 8501 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8502 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8503 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8504 } 8505 8506 if(ev.type == MouseEventType.buttonPressed) { 8507 { 8508 auto event = new MouseDownEvent(captureEle); 8509 populateMouseEventBase(event); 8510 event.dispatch(); 8511 } 8512 8513 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 8514 auto event = new DoubleClickEvent(captureEle); 8515 populateMouseEventBase(event); 8516 event.dispatch(); 8517 lastWasDoubleClick = ev.doubleClick; 8518 } else { 8519 lastWasDoubleClick = false; 8520 } 8521 8522 mouseLastDownOn = ele; 8523 } else if(ev.type == MouseEventType.buttonReleased) { 8524 { 8525 auto event = new MouseUpEvent(captureEle); 8526 populateMouseEventBase(event); 8527 event.dispatch(); 8528 } 8529 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 8530 auto event = new ClickEvent(captureEle); 8531 populateMouseEventBase(event); 8532 event.dispatch(); 8533 } 8534 } else if(ev.type == MouseEventType.motion) { 8535 // motion 8536 { 8537 auto event = new MouseMoveEvent(captureEle); 8538 populateMouseEventBase(event); // fills in button which is meaningless but meh 8539 event.dispatch(); 8540 } 8541 8542 if(mouseLastOver !is ele) { 8543 if(ele !is null) { 8544 if(!isAParentOf(ele, mouseLastOver)) { 8545 ele.setDynamicState(DynamicState.hover, true); 8546 auto event = new MouseEnterEvent(ele); 8547 event.relatedTarget = mouseLastOver; 8548 event.sendDirectly(); 8549 8550 ele.useStyleProperties((scope Widget.Style s) { 8551 ele.parentWindow.win.cursor = s.cursor; 8552 }); 8553 } 8554 } 8555 8556 if(mouseLastOver !is null) { 8557 if(!isAParentOf(mouseLastOver, ele)) { 8558 mouseLastOver.setDynamicState(DynamicState.hover, false); 8559 auto event = new MouseLeaveEvent(mouseLastOver); 8560 event.relatedTarget = ele; 8561 event.sendDirectly(); 8562 } 8563 } 8564 8565 if(ele !is null) { 8566 auto event = new MouseOverEvent(ele); 8567 event.relatedTarget = mouseLastOver; 8568 event.dispatch(); 8569 } 8570 8571 if(mouseLastOver !is null) { 8572 auto event = new MouseOutEvent(mouseLastOver); 8573 event.relatedTarget = ele; 8574 event.dispatch(); 8575 } 8576 8577 mouseLastOver = ele; 8578 } 8579 } 8580 8581 return true; // FIXME: the event default prevented? 8582 } 8583 8584 /++ 8585 Shows the window and runs the application event loop. 8586 8587 Blocks until this window is closed. 8588 8589 Bugs: 8590 8591 $(PITFALL 8592 You should always have one event loop live for your application. 8593 If you make two windows in sequence, the second call to loop (or 8594 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 8595 might fail: 8596 8597 --- 8598 // don't do this! 8599 auto window = new Window(); 8600 window.loop(); 8601 8602 // or new Window or new MainWindow, all the same 8603 auto window2 = new SimpleWindow(); 8604 window2.eventLoop(0); // problematic! might crash 8605 --- 8606 8607 simpledisplay's current implementation assumes that final cleanup is 8608 done when the event loop refcount reaches zero. So after the first 8609 eventLoop returns, when there isn't already another one active, it assumes 8610 the program will exit soon and cleans up. 8611 8612 This is arguably a bug that it doesn't reinitialize, and I'll probably change 8613 it eventually, but in the mean time, there's an easy solution: 8614 8615 --- 8616 // do this 8617 EventLoop mainEventLoop = EventLoop.get; // just add this line 8618 8619 auto window = new Window(); 8620 window.loop(); 8621 8622 // or any other type of Window etc. 8623 auto window2 = new Window(); 8624 window2.loop(); // perfectly fine since mainEventLoop still alive 8625 --- 8626 8627 By adding a top-level reference to the event loop, it ensures the final cleanup 8628 is not performed until it goes out of scope too, letting the individual window loops 8629 work without trouble despite the bug. 8630 ) 8631 8632 History: 8633 The [BlockingMode] parameter was added on December 8, 2021. 8634 The default behavior is to block until the application quits 8635 (so all windows have been closed), unless another minigui or 8636 simpledisplay event loop is already running, in which case it 8637 will block until this window closes specifically. 8638 +/ 8639 @scriptable 8640 void loop(BlockingMode bm = BlockingMode.automatic) { 8641 if(win.closed) 8642 return; // otherwise show will throw 8643 show(); 8644 win.eventLoopWithBlockingMode(bm, 0); 8645 } 8646 8647 private bool firstShow = true; 8648 8649 @scriptable 8650 override void show() { 8651 bool rd = false; 8652 if(firstShow) { 8653 firstShow = false; 8654 recomputeChildLayout(); 8655 auto f = getFirstFocusable(this); // FIXME: autofocus? 8656 if(f) 8657 f.focus(); 8658 redraw(); 8659 } 8660 win.show(); 8661 super.show(); 8662 } 8663 @scriptable 8664 override void hide() { 8665 win.hide(); 8666 super.hide(); 8667 } 8668 8669 static Widget getFirstFocusable(Widget start) { 8670 if(start is null) 8671 return null; 8672 8673 foreach(widget; &start.focusableWidgets) { 8674 return widget; 8675 } 8676 8677 return null; 8678 } 8679 8680 static Widget getLastFocusable(Widget start) { 8681 if(start is null) 8682 return null; 8683 8684 Widget last; 8685 foreach(widget; &start.focusableWidgets) { 8686 last = widget; 8687 } 8688 8689 return last; 8690 } 8691 8692 8693 mixin Emits!ClosingEvent; 8694 mixin Emits!ClosedEvent; 8695 } 8696 8697 /++ 8698 History: 8699 Added January 12, 2022 8700 +/ 8701 class DpiChangedEvent : Event { 8702 enum EventString = "dpichanged"; 8703 8704 this(Widget target) { 8705 super(EventString, target); 8706 } 8707 } 8708 8709 debug private class DevToolWindow : Window { 8710 Window p; 8711 8712 TextEdit parentList; 8713 TextEdit logWindow; 8714 TextLabel clickX, clickY; 8715 8716 this(Window p) { 8717 this.p = p; 8718 super(400, 300, "Developer Toolbox"); 8719 8720 logWindow = new TextEdit(this); 8721 parentList = new TextEdit(this); 8722 8723 auto hl = new HorizontalLayout(this); 8724 clickX = new TextLabel("", TextAlignment.Right, hl); 8725 clickY = new TextLabel("", TextAlignment.Right, hl); 8726 8727 parentListeners ~= p.addEventListener("*", (Event ev) { 8728 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 8729 }); 8730 8731 parentListeners ~= p.addEventListener((ClickEvent ev) { 8732 auto s = ev.srcElement; 8733 8734 string list; 8735 8736 void addInfo(Widget s) { 8737 list ~= s.toString(); 8738 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 8739 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 8740 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 8741 list ~= "\n\theight: " ~ toInternal!string(s.height); 8742 list ~= "\n\tminWidth: " ~ toInternal!string(s.minWidth); 8743 list ~= "\n\tmaxWidth: " ~ toInternal!string(s.maxWidth); 8744 list ~= "\n\twidthStretchiness: " ~ toInternal!string(s.widthStretchiness); 8745 list ~= "\n\twidth: " ~ toInternal!string(s.width); 8746 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 8747 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 8748 } 8749 8750 addInfo(s); 8751 8752 s = s.parent; 8753 while(s) { 8754 list ~= "\n"; 8755 addInfo(s); 8756 s = s.parent; 8757 } 8758 parentList.content = list; 8759 8760 clickX.label = toInternal!string(ev.clientX); 8761 clickY.label = toInternal!string(ev.clientY); 8762 }); 8763 } 8764 8765 EventListener[] parentListeners; 8766 8767 override void close() { 8768 assert(p !is null); 8769 foreach(p; parentListeners) 8770 p.disconnect(); 8771 parentListeners = null; 8772 p.devTools = null; 8773 p = null; 8774 super.close(); 8775 } 8776 8777 override void defaultEventHandler_keydown(KeyDownEvent ev) { 8778 if(ev.key == Key.F12) { 8779 this.close(); 8780 if(p) 8781 p.devTools = null; 8782 } else { 8783 super.defaultEventHandler_keydown(ev); 8784 } 8785 } 8786 8787 void log(T...)(T t) { 8788 string str; 8789 import std.conv; 8790 foreach(i; t) 8791 str ~= to!string(i); 8792 str ~= "\n"; 8793 logWindow.addText(str); 8794 8795 //version(custom_widgets) 8796 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 8797 } 8798 } 8799 8800 /++ 8801 A dialog is a transient window that intends to get information from 8802 the user before being dismissed. 8803 +/ 8804 abstract class Dialog : Window { 8805 /// 8806 this(int width, int height, string title = null) { 8807 super(width, height, title); 8808 } 8809 8810 /// 8811 abstract void OK(); 8812 8813 /// 8814 void Cancel() { 8815 this.close(); 8816 } 8817 } 8818 8819 /++ 8820 A custom widget similar to the HTML5 <details> tag. 8821 +/ 8822 version(none) 8823 class DetailsView : Widget { 8824 8825 } 8826 8827 // FIXME: maybe i should expose the other list views Windows offers too 8828 8829 /++ 8830 A TableView is a widget made to display a table of data strings. 8831 8832 8833 Future_Directions: 8834 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 8835 8836 I will add a selection changed event at some point, as well as item clicked events. 8837 History: 8838 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 8839 See_Also: 8840 [ListWidget] which displays a list of strings without additional columns. 8841 +/ 8842 class TableView : Widget { 8843 /++ 8844 8845 +/ 8846 this(Widget parent) { 8847 super(parent); 8848 8849 version(win32_widgets) { 8850 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 8851 } else version(custom_widgets) { 8852 auto smw = new ScrollMessageWidget(this); 8853 smw.addDefaultKeyboardListeners(); 8854 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 8855 tvwi = new TableViewWidgetInner(this, smw); 8856 } 8857 } 8858 8859 // FIXME: auto-size columns on double click of header thing like in Windows 8860 // it need only make the currently displayed things fit well. 8861 8862 8863 private ColumnInfo[] columns; 8864 private int itemCount; 8865 8866 version(custom_widgets) private { 8867 TableViewWidgetInner tvwi; 8868 } 8869 8870 /// Passed to [setColumnInfo] 8871 static struct ColumnInfo { 8872 const(char)[] name; /// the name displayed in the header 8873 /++ 8874 The default width, in pixels. As a special case, you can set this to -1 8875 if you want the system to try to automatically size the width to fit visible 8876 content. If it can't, it will try to pick a sensible default size. 8877 8878 Any other negative value is not allowed and may lead to unpredictable results. 8879 8880 History: 8881 The -1 behavior was specified on December 3, 2021. It actually worked before 8882 anyway on Win32 but now it is a formal feature with partial Linux support. 8883 8884 Bugs: 8885 It doesn't actually attempt to calculate a best-fit width on Linux as of 8886 December 3, 2021. I do plan to fix this in the future, but Windows is the 8887 priority right now. At least it doesn't break things when you use it now. 8888 +/ 8889 int width; 8890 8891 /++ 8892 Alignment of the text in the cell. Applies to the header as well as all data in this 8893 column. 8894 8895 Bugs: 8896 On Windows, the first column ignores this member and is always left aligned. 8897 You can work around this by inserting a dummy first column with width = 0 8898 then putting your actual data in the second column, which does respect the 8899 alignment. 8900 8901 This is a quirk of the operating system's implementation going back a very 8902 long time and is unlikely to ever be fixed. 8903 +/ 8904 TextAlignment alignment; 8905 8906 /++ 8907 After all the pixel widths have been assigned, any left over 8908 space is divided up among all columns and distributed to according 8909 to the widthPercent field. 8910 8911 8912 For example, if you have two fields, both with width 50 and one with 8913 widthPercent of 25 and the other with widthPercent of 75, and the 8914 container is 200 pixels wide, first both get their width of 50. 8915 then the 100 remaining pixels are split up, so the one gets a total 8916 of 75 pixels and the other gets a total of 125. 8917 8918 This is automatically applied as the window is resized. 8919 8920 If there is not enough space - that is, when a horizontal scrollbar 8921 needs to appear - there are 0 pixels divided up, and thus everyone 8922 gets 0. This can cause a column to shrink out of proportion when 8923 passing the scroll threshold. 8924 8925 It is important to still set a fixed width (that is, to populate the 8926 `width` field) even if you use the percents because that will be the 8927 default minimum in the event of a scroll bar appearing. 8928 8929 The percents total in the column can never exceed 100 or be less than 0. 8930 Doing this will trigger an assert error. 8931 8932 Implementation note: 8933 8934 Please note that percentages are only recalculated 1) upon original 8935 construction and 2) upon resizing the control. If the user adjusts the 8936 width of a column, the percentage items will not be updated. 8937 8938 On the other hand, if the user adjusts the width of a percentage column 8939 then resizes the window, it is recalculated, meaning their hand adjustment 8940 is discarded. This specific behavior may change in the future as it is 8941 arguably a bug, but I'm not certain yet. 8942 8943 History: 8944 Added November 10, 2021 (dub v10.4) 8945 +/ 8946 int widthPercent; 8947 8948 8949 private int calculatedWidth; 8950 } 8951 /++ 8952 Sets the number of columns along with information about the headers. 8953 8954 Please note: on Windows, the first column ignores your alignment preference 8955 and is always left aligned. 8956 +/ 8957 void setColumnInfo(ColumnInfo[] columns...) { 8958 8959 foreach(ref c; columns) { 8960 c.name = c.name.idup; 8961 } 8962 this.columns = columns.dup; 8963 8964 updateCalculatedWidth(false); 8965 8966 version(custom_widgets) { 8967 tvwi.header.updateHeaders(); 8968 tvwi.updateScrolls(); 8969 } else version(win32_widgets) 8970 foreach(i, column; this.columns) { 8971 LVCOLUMN lvColumn; 8972 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 8973 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 8974 8975 auto bfr = WCharzBuffer(column.name); 8976 lvColumn.pszText = bfr.ptr; 8977 8978 if(column.alignment & TextAlignment.Center) 8979 lvColumn.fmt = LVCFMT_CENTER; 8980 else if(column.alignment & TextAlignment.Right) 8981 lvColumn.fmt = LVCFMT_RIGHT; 8982 else 8983 lvColumn.fmt = LVCFMT_LEFT; 8984 8985 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 8986 throw new WindowsApiException("Insert Column Fail", GetLastError()); 8987 } 8988 } 8989 8990 private int getActualSetSize(size_t i, bool askWindows) { 8991 version(win32_widgets) 8992 if(askWindows) 8993 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 8994 auto w = columns[i].width; 8995 if(w == -1) 8996 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 8997 return w; 8998 } 8999 9000 private void updateCalculatedWidth(bool informWindows) { 9001 int padding; 9002 version(win32_widgets) 9003 padding = 4; 9004 int remaining = this.width; 9005 foreach(i, column; columns) 9006 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 9007 remaining -= padding; 9008 if(remaining < 0) 9009 remaining = 0; 9010 9011 int percentTotal; 9012 foreach(i, ref column; columns) { 9013 percentTotal += column.widthPercent; 9014 9015 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 9016 9017 column.calculatedWidth = c; 9018 9019 version(win32_widgets) 9020 if(informWindows) 9021 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 9022 } 9023 9024 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 9025 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)."); 9026 9027 9028 } 9029 9030 override void registerMovement() { 9031 super.registerMovement(); 9032 9033 updateCalculatedWidth(true); 9034 } 9035 9036 /++ 9037 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. 9038 +/ 9039 void setItemCount(int count) { 9040 this.itemCount = count; 9041 version(custom_widgets) { 9042 tvwi.updateScrolls(); 9043 redraw(); 9044 } else version(win32_widgets) { 9045 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 9046 } 9047 } 9048 9049 /++ 9050 Clears all items; 9051 +/ 9052 void clear() { 9053 this.itemCount = 0; 9054 this.columns = null; 9055 version(custom_widgets) { 9056 tvwi.header.updateHeaders(); 9057 tvwi.updateScrolls(); 9058 redraw(); 9059 } else version(win32_widgets) { 9060 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 9061 } 9062 } 9063 9064 /+ 9065 version(win32_widgets) 9066 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 9067 auto itemId = dis.itemID; 9068 auto hdc = dis.hDC; 9069 auto rect = dis.rcItem; 9070 switch(dis.itemAction) { 9071 case ODA_DRAWENTIRE: 9072 9073 // FIXME: do other items 9074 // FIXME: do the focus rectangle i guess 9075 // FIXME: alignment 9076 // FIXME: column width 9077 // FIXME: padding left 9078 // FIXME: check dpi scaling 9079 // FIXME: don't owner draw unless it is necessary. 9080 9081 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 9082 RECT itemRect; 9083 itemRect.top = 1; // subitem idx, 1-based 9084 itemRect.left = LVIR_BOUNDS; 9085 9086 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 9087 itemRect.left += padding; 9088 9089 getData(itemId, 0, (in char[] data) { 9090 auto wdata = WCharzBuffer(data); 9091 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 9092 9093 }); 9094 goto case; 9095 case ODA_FOCUS: 9096 if(dis.itemState & ODS_FOCUS) 9097 DrawFocusRect(hdc, &rect); 9098 break; 9099 case ODA_SELECT: 9100 // itemState & ODS_SELECTED 9101 break; 9102 default: 9103 } 9104 return 1; 9105 } 9106 +/ 9107 9108 version(win32_widgets) { 9109 CellStyle last; 9110 COLORREF defaultColor; 9111 COLORREF defaultBackground; 9112 } 9113 9114 version(win32_widgets) 9115 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 9116 switch(code) { 9117 case NM_CUSTOMDRAW: 9118 auto s = cast(NMLVCUSTOMDRAW*) hdr; 9119 switch(s.nmcd.dwDrawStage) { 9120 case CDDS_PREPAINT: 9121 if(getCellStyle is null) 9122 return 0; 9123 9124 mustReturn = true; 9125 return CDRF_NOTIFYITEMDRAW; 9126 case CDDS_ITEMPREPAINT: 9127 mustReturn = true; 9128 return CDRF_NOTIFYSUBITEMDRAW; 9129 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 9130 mustReturn = true; 9131 9132 if(getCellStyle is null) // this SHOULD never happen... 9133 return 0; 9134 9135 if(s.iSubItem == 0) { 9136 // Windows resets it per row so we'll use item 0 as a chance 9137 // to capture these for later 9138 defaultColor = s.clrText; 9139 defaultBackground = s.clrTextBk; 9140 } 9141 9142 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 9143 // if no special style and no reset needed... 9144 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 9145 return 0; // allow default processing to continue 9146 9147 last = style; 9148 9149 // might still need to reset or use the preference. 9150 9151 if(style.flags & CellStyle.Flags.textColorSet) 9152 s.clrText = style.textColor.asWindowsColorRef; 9153 else 9154 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 9155 if(style.flags & CellStyle.Flags.backgroundColorSet) 9156 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 9157 else 9158 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 9159 9160 return CDRF_NEWFONT; 9161 default: 9162 return 0; 9163 9164 } 9165 case NM_RETURN: // no need since i subclass keydown 9166 break; 9167 case LVN_COLUMNCLICK: 9168 auto info = cast(LPNMLISTVIEW) hdr; 9169 this.emit!HeaderClickedEvent(info.iSubItem); 9170 break; 9171 case NM_CLICK: 9172 case NM_DBLCLK: 9173 case NM_RCLICK: 9174 case NM_RDBLCLK: 9175 // the item/subitem is set here and that can be a useful notification 9176 // even beyond the normal click notification 9177 break; 9178 case LVN_GETDISPINFO: 9179 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 9180 if(info.item.mask & LVIF_TEXT) { 9181 if(getData) { 9182 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 9183 auto bfr = WCharzBuffer(dataReceived); 9184 auto len = info.item.cchTextMax; 9185 if(bfr.length < len) 9186 len = cast(typeof(len)) bfr.length; 9187 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 9188 info.item.pszText[len] = 0; 9189 }); 9190 } else { 9191 info.item.pszText[0] = 0; 9192 } 9193 //info.item.iItem 9194 //if(info.item.iSubItem) 9195 } 9196 break; 9197 default: 9198 } 9199 return 0; 9200 } 9201 9202 override bool encapsulatedChildren() { 9203 return true; 9204 } 9205 9206 /++ 9207 Informs the control that content has changed. 9208 9209 History: 9210 Added November 10, 2021 (dub v10.4) 9211 +/ 9212 void update() { 9213 version(custom_widgets) 9214 redraw(); 9215 else { 9216 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 9217 UpdateWindow(hwnd); 9218 } 9219 9220 9221 } 9222 9223 /++ 9224 Called by the system to request the text content of an individual cell. You 9225 should pass the text into the provided `sink` delegate. This function will be 9226 called for each visible cell as-needed when drawing. 9227 +/ 9228 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 9229 9230 /++ 9231 Available per-cell style customization options. Use one of the constructors 9232 provided to set the values conveniently, or default construct it and set individual 9233 values yourself. Just remember to set the `flags` so your values are actually used. 9234 If the flag isn't set, the field is ignored and the system default is used instead. 9235 9236 This is returned by the [getCellStyle] delegate. 9237 9238 Examples: 9239 --- 9240 // assumes you have a variables called `my_data` which is an array of arrays of numbers 9241 auto table = new TableView(window); 9242 // snip: you would set up columns here 9243 9244 // this is how you provide data to the table view class 9245 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 9246 import std.conv; 9247 sink(to!string(my_data[row][column])); 9248 }; 9249 9250 // and this is how you customize the colors 9251 table.getCellStyle = delegate(int row, int column) { 9252 return (my_data[row][column] < 0) ? 9253 TableView.CellStyle(Color.red); // make negative numbers red 9254 : TableView.CellStyle.init; // leave the rest alone 9255 }; 9256 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 9257 --- 9258 9259 History: 9260 Added November 27, 2021 (dub v10.4) 9261 +/ 9262 struct CellStyle { 9263 /// 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. 9264 this(Color textColor) { 9265 this.textColor = textColor; 9266 this.flags |= Flags.textColorSet; 9267 } 9268 /// Sets a custom text and background color. 9269 this(Color textColor, Color backgroundColor) { 9270 this.textColor = textColor; 9271 this.backgroundColor = backgroundColor; 9272 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 9273 } 9274 9275 Color textColor; 9276 Color backgroundColor; 9277 int flags; /// bitmask of [Flags] 9278 /// available options to combine into [flags] 9279 enum Flags { 9280 textColorSet = 1 << 0, 9281 backgroundColorSet = 1 << 1, 9282 } 9283 } 9284 /++ 9285 Companion delegate to [getData] that allows you to custom style each 9286 cell of the table. 9287 9288 Returns: 9289 A [CellStyle] structure that describes the desired style for the 9290 given cell. `return CellStyle.init` if you want the default style. 9291 9292 History: 9293 Added November 27, 2021 (dub v10.4) 9294 +/ 9295 CellStyle delegate(int row, int column) getCellStyle; 9296 9297 // i want to be able to do things like draw little colored things to show red for negative numbers 9298 // or background color indicators or even in-cell charts 9299 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 9300 9301 /++ 9302 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 9303 +/ 9304 mixin Emits!HeaderClickedEvent; 9305 } 9306 9307 /++ 9308 This is emitted by the [TableView] when a user clicks on a column header. 9309 9310 Its member `columnIndex` has the zero-based index of the column that was clicked. 9311 9312 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 9313 9314 History: 9315 Added November 27, 2021 (dub v10.4) 9316 +/ 9317 class HeaderClickedEvent : Event { 9318 enum EventString = "HeaderClicked"; 9319 this(Widget target, int columnIndex) { 9320 this.columnIndex = columnIndex; 9321 super(EventString, target); 9322 } 9323 9324 /// The index of the column 9325 int columnIndex; 9326 9327 /// 9328 override @property int intValue() { 9329 return columnIndex; 9330 } 9331 } 9332 9333 version(custom_widgets) 9334 private class TableViewWidgetInner : Widget { 9335 9336 // wrap this thing in a ScrollMessageWidget 9337 9338 TableView tvw; 9339 ScrollMessageWidget smw; 9340 HeaderWidget header; 9341 9342 this(TableView tvw, ScrollMessageWidget smw) { 9343 this.tvw = tvw; 9344 this.smw = smw; 9345 super(smw); 9346 9347 this.tabStop = true; 9348 9349 header = new HeaderWidget(this, smw.getHeader()); 9350 9351 smw.addEventListener("scroll", () { 9352 this.redraw(); 9353 header.redraw(); 9354 }); 9355 9356 9357 // I need headers outside the scroll area but rendered on the same line as the up arrow 9358 // FIXME: add a fixed header to the SMW 9359 } 9360 9361 enum padding = 3; 9362 9363 void updateScrolls() { 9364 int w; 9365 foreach(idx, column; tvw.columns) { 9366 if(column.width == 0) continue; 9367 w += tvw.getActualSetSize(idx, false);// + padding; 9368 } 9369 smw.setTotalArea(w, tvw.itemCount); 9370 columnsWidth = w; 9371 } 9372 9373 private int columnsWidth; 9374 9375 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 9376 9377 override void registerMovement() { 9378 super.registerMovement(); 9379 // FIXME: actual column width. it might need to be done per-pixel instead of per-colum 9380 smw.setViewableArea(this.width, this.height / lh); 9381 } 9382 9383 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 9384 int x; 9385 int y; 9386 9387 int row = smw.position.y; 9388 9389 foreach(lol; 0 .. this.height / lh) { 9390 if(row >= tvw.itemCount) 9391 break; 9392 x = 0; 9393 foreach(columnNumber, column; tvw.columns) { 9394 auto x2 = x + column.calculatedWidth; 9395 auto smwx = smw.position.x; 9396 9397 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 9398 auto startX = x; 9399 auto endX = x + column.calculatedWidth; 9400 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 9401 case TextAlignment.Left: startX += padding; break; 9402 case TextAlignment.Center: startX += padding; endX -= padding; break; 9403 case TextAlignment.Right: endX -= padding; break; 9404 default: /* broken */ break; 9405 } 9406 if(column.width != 0) // no point drawing an invisible column 9407 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 9408 // auto clip = painter.setClipRectangle( 9409 9410 void dotext(WidgetPainter painter) { 9411 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 9412 } 9413 9414 if(tvw.getCellStyle !is null) { 9415 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 9416 9417 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 9418 auto tempPainter = painter; 9419 tempPainter.fillColor = style.backgroundColor; 9420 tempPainter.outlineColor = style.backgroundColor; 9421 9422 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 9423 Point(endX - smw.position.x, y + lh)); 9424 } 9425 auto tempPainter = painter; 9426 if(style.flags & TableView.CellStyle.Flags.textColorSet) 9427 tempPainter.outlineColor = style.textColor; 9428 9429 dotext(tempPainter); 9430 } else { 9431 dotext(painter); 9432 } 9433 }); 9434 } 9435 9436 x += column.calculatedWidth; 9437 } 9438 row++; 9439 y += lh; 9440 } 9441 return bounds; 9442 } 9443 9444 static class Style : Widget.Style { 9445 override WidgetBackground background() { 9446 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 9447 } 9448 } 9449 mixin OverrideStyle!Style; 9450 9451 private static class HeaderWidget : Widget { 9452 this(TableViewWidgetInner tvw, Widget parent) { 9453 super(parent); 9454 this.tvw = tvw; 9455 9456 this.remainder = new Button("", this); 9457 9458 this.addEventListener((scope ClickEvent ev) { 9459 int header = -1; 9460 foreach(idx, child; this.children[1 .. $]) { 9461 if(child is ev.target) { 9462 header = cast(int) idx; 9463 break; 9464 } 9465 } 9466 9467 if(header != -1) { 9468 auto hce = new HeaderClickedEvent(tvw.tvw, header); 9469 hce.dispatch(); 9470 } 9471 9472 }); 9473 } 9474 9475 void updateHeaders() { 9476 foreach(child; children[1 .. $]) 9477 child.removeWidget(); 9478 9479 foreach(column; tvw.tvw.columns) { 9480 // the cast is ok because I dup it above, just the type is never changed. 9481 // all this is private so it should never get messed up. 9482 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 9483 } 9484 } 9485 9486 Button remainder; 9487 TableViewWidgetInner tvw; 9488 9489 override void recomputeChildLayout() { 9490 registerMovement(); 9491 int pos; 9492 foreach(idx, child; children[1 .. $]) { 9493 if(idx >= tvw.tvw.columns.length) 9494 continue; 9495 child.x = pos; 9496 child.y = 0; 9497 child.width = tvw.tvw.columns[idx].calculatedWidth; 9498 child.height = scaleWithDpi(16);// this.height; 9499 pos += child.width; 9500 9501 child.recomputeChildLayout(); 9502 } 9503 9504 if(remainder is null) 9505 return; 9506 9507 remainder.x = pos; 9508 remainder.y = 0; 9509 if(pos < this.width) 9510 remainder.width = this.width - pos;// + 4; 9511 else 9512 remainder.width = 0; 9513 remainder.height = scaleWithDpi(16); 9514 9515 remainder.recomputeChildLayout(); 9516 } 9517 9518 // for the scrollable children mixin 9519 Point scrollOrigin() { 9520 return Point(tvw.smw.position.x, 0); 9521 } 9522 void paintFrameAndBackground(WidgetPainter painter) { } 9523 9524 mixin ScrollableChildren; 9525 } 9526 } 9527 9528 /+ 9529 9530 // given struct / array / number / string / etc, make it viewable and editable 9531 class DataViewerWidget : Widget { 9532 9533 } 9534 +/ 9535 9536 /++ 9537 A line edit box with an associated label. 9538 9539 History: 9540 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 9541 9542 ``` 9543 Old: ________ 9544 9545 New: 9546 ____________ 9547 ``` 9548 9549 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 9550 9551 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 9552 horizontal label but left aligned. You may also consider a [GridLayout]. 9553 +/ 9554 alias LabeledLineEdit = Labeled!LineEdit; 9555 9556 /++ 9557 History: 9558 Added May 19, 2021 9559 +/ 9560 class Labeled(T) : Widget { 9561 /// 9562 this(string label, Widget parent) { 9563 super(parent); 9564 initialize!VerticalLayout(label, TextAlignment.Left, parent); 9565 } 9566 9567 /++ 9568 History: 9569 The alignment parameter was added May 17, 2021 9570 +/ 9571 this(string label, TextAlignment alignment, Widget parent) { 9572 super(parent); 9573 initialize!HorizontalLayout(label, alignment, parent); 9574 } 9575 9576 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 9577 tabStop = false; 9578 horizontal = is(L == HorizontalLayout); 9579 auto hl = new L(this); 9580 if(horizontal) { 9581 static class SpecialTextLabel : TextLabel { 9582 this(string label, TextAlignment alignment, Widget parent) { 9583 super(label, alignment, parent); 9584 } 9585 9586 override int paddingTop() { return 6; } 9587 } 9588 this.label = new SpecialTextLabel(label, alignment, hl); 9589 } else 9590 this.label = new TextLabel(label, alignment, hl); 9591 this.lineEdit = new T(hl); 9592 9593 this.label.labelFor = this.lineEdit; 9594 } 9595 9596 private bool horizontal; 9597 9598 TextLabel label; /// 9599 T lineEdit; /// 9600 9601 override int flexBasisWidth() { return 250; } 9602 9603 override int minHeight() { 9604 return this.children[0].minHeight; 9605 } 9606 override int maxHeight() { return minHeight(); } 9607 override int marginTop() { return 4; } 9608 override int marginBottom() { return 4; } 9609 9610 // FIXME: i should prolly call it value as well as content tbh 9611 9612 /// 9613 @property string content() { 9614 return lineEdit.content; 9615 } 9616 /// 9617 @property void content(string c) { 9618 return lineEdit.content(c); 9619 } 9620 9621 /// 9622 void selectAll() { 9623 lineEdit.selectAll(); 9624 } 9625 9626 override void focus() { 9627 lineEdit.focus(); 9628 } 9629 } 9630 9631 /++ 9632 A labeled password edit. 9633 9634 History: 9635 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 9636 9637 The default parameters for the constructors were also removed on May 19, 2021 9638 +/ 9639 alias LabeledPasswordEdit = Labeled!PasswordEdit; 9640 9641 private string toMenuLabel(string s) { 9642 string n; 9643 n.reserve(s.length); 9644 foreach(c; s) 9645 if(c == '_') 9646 n ~= ' '; 9647 else 9648 n ~= c; 9649 return n; 9650 } 9651 9652 private void autoExceptionHandler(Exception e) { 9653 messageBox(e.msg); 9654 } 9655 9656 private void delegate() makeAutomaticHandler(alias fn, T)(T t) { 9657 static if(is(T : void delegate())) { 9658 return () { 9659 try 9660 t(); 9661 catch(Exception e) 9662 autoExceptionHandler(e); 9663 }; 9664 } else static if(is(typeof(fn) Params == __parameters)) { 9665 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 9666 return () { 9667 void onOK(string s) { 9668 member = s; 9669 try 9670 t(Params[0](s)); 9671 catch(Exception e) 9672 autoExceptionHandler(e); 9673 } 9674 9675 if( 9676 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 9677 || type == FileDialogType.Save) 9678 { 9679 getSaveFileName(&onOK, member, filters, null); 9680 } else 9681 getOpenFileName(&onOK, member, filters, null); 9682 }; 9683 } else { 9684 struct S { 9685 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 9686 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 9687 } else mixin(q{ 9688 static foreach(idx, ignore; Params) { 9689 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 9690 } 9691 }); 9692 } 9693 return () { 9694 dialog((S s) { 9695 try { 9696 static if(is(typeof(t) Ret == return)) { 9697 static if(is(Ret == void)) { 9698 t(s.tupleof); 9699 } else { 9700 auto ret = t(s.tupleof); 9701 import std.conv; 9702 messageBox(to!string(ret), "Returned Value"); 9703 } 9704 } 9705 } catch(Exception e) 9706 autoExceptionHandler(e); 9707 }, null, __traits(identifier, fn)); 9708 }; 9709 } 9710 } 9711 } 9712 9713 private template hasAnyRelevantAnnotations(a...) { 9714 bool helper() { 9715 bool any; 9716 foreach(attr; a) { 9717 static if(is(typeof(attr) == .menu)) 9718 any = true; 9719 else static if(is(typeof(attr) == .toolbar)) 9720 any = true; 9721 else static if(is(attr == .separator)) 9722 any = true; 9723 else static if(is(typeof(attr) == .accelerator)) 9724 any = true; 9725 else static if(is(typeof(attr) == .hotkey)) 9726 any = true; 9727 else static if(is(typeof(attr) == .icon)) 9728 any = true; 9729 else static if(is(typeof(attr) == .label)) 9730 any = true; 9731 else static if(is(typeof(attr) == .tip)) 9732 any = true; 9733 } 9734 return any; 9735 } 9736 9737 enum bool hasAnyRelevantAnnotations = helper(); 9738 } 9739 9740 /++ 9741 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. 9742 +/ 9743 class MainWindow : Window { 9744 /// 9745 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 9746 super(initialWidth, initialHeight, title); 9747 9748 _clientArea = new ClientAreaWidget(); 9749 _clientArea.x = 0; 9750 _clientArea.y = 0; 9751 _clientArea.width = this.width; 9752 _clientArea.height = this.height; 9753 _clientArea.tabStop = false; 9754 9755 super.addChild(_clientArea); 9756 9757 statusBar = new StatusBar(this); 9758 } 9759 9760 /++ 9761 Adds a menu and toolbar from annotated functions. 9762 9763 --- 9764 struct Commands { 9765 @menu("File") { 9766 void New() {} 9767 void Open() {} 9768 void Save() {} 9769 @separator 9770 void Exit() @accelerator("Alt+F4") @hotkey('x') { 9771 window.close(); 9772 } 9773 } 9774 9775 @menu("Edit") { 9776 void Undo() { 9777 undo(); 9778 } 9779 @separator 9780 void Cut() {} 9781 void Copy() {} 9782 void Paste() {} 9783 } 9784 9785 @menu("Help") { 9786 void About() {} 9787 } 9788 } 9789 9790 Commands commands; 9791 9792 window.setMenuAndToolbarFromAnnotatedCode(commands); 9793 --- 9794 9795 Note that you can call this function multiple times and it will add the items in order to the given items. 9796 9797 +/ 9798 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 9799 setMenuAndToolbarFromAnnotatedCode_internal(t); 9800 } 9801 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 9802 setMenuAndToolbarFromAnnotatedCode_internal(t); 9803 } 9804 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 9805 Action[] toolbarActions; 9806 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 9807 Menu[string] mcs; 9808 9809 foreach(menu; menuBar.subMenus) { 9810 mcs[menu.label] = menu; 9811 } 9812 9813 foreach(memberName; __traits(derivedMembers, T)) { 9814 static if(memberName != "this") 9815 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 9816 .menu menu; 9817 .toolbar toolbar; 9818 bool separator; 9819 .accelerator accelerator; 9820 .hotkey hotkey; 9821 .icon icon; 9822 string label; 9823 string tip; 9824 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 9825 static if(is(typeof(attr) == .menu)) 9826 menu = attr; 9827 else static if(is(typeof(attr) == .toolbar)) 9828 toolbar = attr; 9829 else static if(is(attr == .separator)) 9830 separator = true; 9831 else static if(is(typeof(attr) == .accelerator)) 9832 accelerator = attr; 9833 else static if(is(typeof(attr) == .hotkey)) 9834 hotkey = attr; 9835 else static if(is(typeof(attr) == .icon)) 9836 icon = attr; 9837 else static if(is(typeof(attr) == .label)) 9838 label = attr.label; 9839 else static if(is(typeof(attr) == .tip)) 9840 tip = attr.tip; 9841 } 9842 9843 if(menu !is .menu.init || toolbar !is .toolbar.init) { 9844 ushort correctIcon = icon.id; // FIXME 9845 if(label.length == 0) 9846 label = memberName.toMenuLabel; 9847 9848 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName)); 9849 9850 auto action = new Action(label, correctIcon, handler); 9851 9852 if(accelerator.keyString.length) { 9853 auto ke = KeyEvent.parse(accelerator.keyString); 9854 action.accelerator = ke; 9855 accelerators[ke.toStr] = handler; 9856 } 9857 9858 if(toolbar !is .toolbar.init) 9859 toolbarActions ~= action; 9860 if(menu !is .menu.init) { 9861 Menu mc; 9862 if(menu.name in mcs) { 9863 mc = mcs[menu.name]; 9864 } else { 9865 mc = new Menu(menu.name, this); 9866 menuBar.addItem(mc); 9867 mcs[menu.name] = mc; 9868 } 9869 9870 if(separator) 9871 mc.addSeparator(); 9872 mc.addItem(new MenuItem(action)); 9873 } 9874 } 9875 } 9876 } 9877 9878 this.menuBar = menuBar; 9879 9880 if(toolbarActions.length) { 9881 auto tb = new ToolBar(toolbarActions, this); 9882 } 9883 } 9884 9885 void delegate()[string] accelerators; 9886 9887 override void defaultEventHandler_keydown(KeyDownEvent event) { 9888 auto str = event.originalKeyEvent.toStr; 9889 if(auto acl = str in accelerators) 9890 (*acl)(); 9891 super.defaultEventHandler_keydown(event); 9892 } 9893 9894 override void defaultEventHandler_mouseover(MouseOverEvent event) { 9895 super.defaultEventHandler_mouseover(event); 9896 if(this.statusBar !is null && event.target.statusTip.length) 9897 this.statusBar.parts[0].content = event.target.statusTip; 9898 else if(this.statusBar !is null && this.statusTip.length) 9899 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 9900 } 9901 9902 override void addChild(Widget c, int position = int.max) { 9903 if(auto tb = cast(ToolBar) c) 9904 version(win32_widgets) 9905 super.addChild(c, 0); 9906 else version(custom_widgets) 9907 super.addChild(c, menuBar ? 1 : 0); 9908 else static assert(0); 9909 else 9910 clientArea.addChild(c, position); 9911 } 9912 9913 ToolBar _toolBar; 9914 /// 9915 ToolBar toolBar() { return _toolBar; } 9916 /// 9917 ToolBar toolBar(ToolBar t) { 9918 _toolBar = t; 9919 foreach(child; this.children) 9920 if(child is t) 9921 return t; 9922 version(win32_widgets) 9923 super.addChild(t, 0); 9924 else version(custom_widgets) 9925 super.addChild(t, menuBar ? 1 : 0); 9926 else static assert(0); 9927 return t; 9928 } 9929 9930 MenuBar _menu; 9931 /// 9932 MenuBar menuBar() { return _menu; } 9933 /// 9934 MenuBar menuBar(MenuBar m) { 9935 if(m is _menu) { 9936 version(custom_widgets) 9937 recomputeChildLayout(); 9938 return m; 9939 } 9940 9941 if(_menu !is null) { 9942 // make sure it is sanely removed 9943 // FIXME 9944 } 9945 9946 _menu = m; 9947 9948 version(win32_widgets) { 9949 SetMenu(parentWindow.win.impl.hwnd, m.handle); 9950 } else version(custom_widgets) { 9951 super.addChild(m, 0); 9952 9953 // clientArea.y = menu.height; 9954 // clientArea.height = this.height - menu.height; 9955 9956 recomputeChildLayout(); 9957 } else static assert(false); 9958 9959 return _menu; 9960 } 9961 private Widget _clientArea; 9962 /// 9963 @property Widget clientArea() { return _clientArea; } 9964 protected @property void clientArea(Widget wid) { 9965 _clientArea = wid; 9966 } 9967 9968 private StatusBar _statusBar; 9969 /++ 9970 Returns the window's [StatusBar]. Be warned it may be `null`. 9971 +/ 9972 @property StatusBar statusBar() { return _statusBar; } 9973 /// ditto 9974 @property void statusBar(StatusBar bar) { 9975 if(_statusBar !is null) 9976 _statusBar.removeWidget(); 9977 _statusBar = bar; 9978 if(bar !is null) 9979 super.addChild(_statusBar); 9980 } 9981 } 9982 9983 /+ 9984 This is really an implementation detail of [MainWindow] 9985 +/ 9986 private class ClientAreaWidget : Widget { 9987 this() { 9988 this.tabStop = false; 9989 super(null); 9990 //sa = new ScrollableWidget(this); 9991 } 9992 /* 9993 ScrollableWidget sa; 9994 override void addChild(Widget w, int position) { 9995 if(sa is null) 9996 super.addChild(w, position); 9997 else { 9998 sa.addChild(w, position); 9999 sa.setContentSize(this.minWidth + 1, this.minHeight); 10000 writeln(sa.contentWidth, "x", sa.contentHeight); 10001 } 10002 } 10003 */ 10004 } 10005 10006 /** 10007 Toolbars are lists of buttons (typically icons) that appear under the menu. 10008 Each button ought to correspond to a menu item, represented by [Action] objects. 10009 */ 10010 class ToolBar : Widget { 10011 version(win32_widgets) { 10012 private int idealHeight; 10013 override int minHeight() { return idealHeight; } 10014 override int maxHeight() { return idealHeight; } 10015 } else version(custom_widgets) { 10016 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 10017 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 10018 } else static assert(false); 10019 override int heightStretchiness() { return 0; } 10020 10021 version(win32_widgets) { 10022 HIMAGELIST imageListSmall; 10023 HIMAGELIST imageListLarge; 10024 } 10025 10026 this(Widget parent) { 10027 this(null, parent); 10028 } 10029 10030 version(win32_widgets) 10031 void changeIconSize(bool useLarge) { 10032 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall)); 10033 10034 /+ 10035 SIZE size; 10036 import core.sys.windows.commctrl; 10037 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 10038 idealHeight = size.cy + 4; // the plus 4 is a hack 10039 +/ 10040 10041 idealHeight = useLarge ? 34 : 26; 10042 10043 if(parent) { 10044 parent.recomputeChildLayout(); 10045 parent.redraw(); 10046 } 10047 10048 SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4)); 10049 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 10050 } 10051 10052 /// 10053 this(Action[] actions, Widget parent) { 10054 super(parent); 10055 10056 tabStop = false; 10057 10058 version(win32_widgets) { 10059 // so i like how the flat thing looks on windows, but not on wine 10060 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 10061 // leave it commented 10062 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 10063 10064 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 10065 10066 imageListSmall = ImageList_Create( 10067 // width, height 10068 16, 16, 10069 ILC_COLOR16 | ILC_MASK, 10070 16 /*numberOfButtons*/, 0); 10071 10072 imageListLarge = ImageList_Create( 10073 // width, height 10074 24, 24, 10075 ILC_COLOR16 | ILC_MASK, 10076 16 /*numberOfButtons*/, 0); 10077 10078 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall); 10079 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 10080 10081 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge); 10082 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL); 10083 10084 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 10085 10086 TBBUTTON[] buttons; 10087 10088 // FIXME: I_IMAGENONE is if here is no icon 10089 foreach(action; actions) 10090 buttons ~= TBBUTTON( 10091 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 10092 action.id, 10093 TBSTATE_ENABLED, // state 10094 0, // style 10095 0, // reserved array, just zero it out 10096 0, // dwData 10097 cast(size_t) toWstringzInternal(action.label) // INT_PTR 10098 ); 10099 10100 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 10101 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 10102 10103 /* 10104 RECT rect; 10105 GetWindowRect(hwnd, &rect); 10106 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 10107 */ 10108 10109 dpiChanged(); // to load the things calling changeIconSize the first time 10110 10111 assert(idealHeight); 10112 } else version(custom_widgets) { 10113 foreach(action; actions) 10114 new ToolButton(action, this); 10115 } else static assert(false); 10116 } 10117 10118 override void recomputeChildLayout() { 10119 .recomputeChildLayout!"width"(this); 10120 } 10121 10122 10123 version(win32_widgets) 10124 override protected void dpiChanged() { 10125 auto sz = scaleWithDpi(16); 10126 if(sz >= 20) 10127 changeIconSize(true); 10128 else 10129 changeIconSize(false); 10130 } 10131 } 10132 10133 enum toolbarIconSize = 24; 10134 10135 /// 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. 10136 class ToolButton : Button { 10137 /// 10138 this(string label, Widget parent) { 10139 super(label, parent); 10140 tabStop = false; 10141 } 10142 /// 10143 this(Action action, Widget parent) { 10144 super(action.label, parent); 10145 tabStop = false; 10146 this.action = action; 10147 } 10148 10149 version(custom_widgets) 10150 override void defaultEventHandler_click(ClickEvent event) { 10151 foreach(handler; action.triggered) 10152 handler(); 10153 } 10154 10155 Action action; 10156 10157 override int maxWidth() { return toolbarIconSize; } 10158 override int minWidth() { return toolbarIconSize; } 10159 override int maxHeight() { return toolbarIconSize; } 10160 override int minHeight() { return toolbarIconSize; } 10161 10162 version(custom_widgets) 10163 override void paint(WidgetPainter painter) { 10164 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 10165 painter.outlineColor = Color.black; 10166 10167 // I want to get from 16 to 24. that's * 3 / 2 10168 static assert(toolbarIconSize >= 16); 10169 enum multiplier = toolbarIconSize / 8; 10170 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 10171 switch(action.iconId) { 10172 case GenericIcons.New: 10173 painter.fillColor = Color.white; 10174 painter.drawPolygon( 10175 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 10176 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 10177 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 10178 ); 10179 break; 10180 case GenericIcons.Save: 10181 painter.fillColor = Color.white; 10182 painter.outlineColor = Color.black; 10183 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10184 10185 // the label 10186 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 10187 10188 // the slider 10189 painter.fillColor = Color.black; 10190 painter.outlineColor = Color.black; 10191 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 10192 10193 painter.fillColor = Color.white; 10194 painter.outlineColor = Color.white; 10195 // the disc window 10196 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 10197 break; 10198 case GenericIcons.Open: 10199 painter.fillColor = Color.white; 10200 painter.drawPolygon( 10201 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 10202 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 10203 painter.drawPolygon( 10204 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 10205 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 10206 Point(2, 6) * multiplier / divisor); 10207 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 10208 break; 10209 case GenericIcons.Copy: 10210 painter.fillColor = Color.white; 10211 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 10212 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 10213 break; 10214 case GenericIcons.Cut: 10215 painter.fillColor = Color.transparent; 10216 painter.outlineColor = getComputedStyle.foregroundColor(); 10217 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 10218 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 10219 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 10220 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 10221 break; 10222 case GenericIcons.Paste: 10223 painter.fillColor = Color.white; 10224 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 10225 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10226 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 10227 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 10228 painter.fillColor = Color.black; 10229 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 10230 break; 10231 case GenericIcons.Help: 10232 painter.outlineColor = getComputedStyle.foregroundColor(); 10233 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10234 break; 10235 case GenericIcons.Undo: 10236 painter.fillColor = Color.transparent; 10237 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10238 painter.outlineColor = Color.black; 10239 painter.fillColor = Color.black; 10240 painter.drawPolygon( 10241 Point(4, 4) * multiplier / divisor, 10242 Point(8, 2) * multiplier / divisor, 10243 Point(8, 6) * multiplier / divisor, 10244 Point(4, 4) * multiplier / divisor, 10245 ); 10246 break; 10247 case GenericIcons.Redo: 10248 painter.fillColor = Color.transparent; 10249 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10250 painter.outlineColor = Color.black; 10251 painter.fillColor = Color.black; 10252 painter.drawPolygon( 10253 Point(10, 4) * multiplier / divisor, 10254 Point(6, 2) * multiplier / divisor, 10255 Point(6, 6) * multiplier / divisor, 10256 Point(10, 4) * multiplier / divisor, 10257 ); 10258 break; 10259 default: 10260 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10261 } 10262 return bounds; 10263 }); 10264 } 10265 10266 } 10267 10268 10269 /++ 10270 You can make one of thse yourself but it is generally easer to use [MainWindow.setMenuAndToolbarFromAnnotatedCode]. 10271 +/ 10272 class MenuBar : Widget { 10273 MenuItem[] items; 10274 Menu[] subMenus; 10275 10276 version(win32_widgets) { 10277 HMENU handle; 10278 /// 10279 this(Widget parent = null) { 10280 super(parent); 10281 10282 handle = CreateMenu(); 10283 tabStop = false; 10284 } 10285 } else version(custom_widgets) { 10286 /// 10287 this(Widget parent = null) { 10288 tabStop = false; // these are selected some other way 10289 super(parent); 10290 } 10291 10292 mixin Padding!q{2}; 10293 } else static assert(false); 10294 10295 version(custom_widgets) 10296 override void paint(WidgetPainter painter) { 10297 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 10298 } 10299 10300 /// 10301 MenuItem addItem(MenuItem item) { 10302 this.addChild(item); 10303 items ~= item; 10304 version(win32_widgets) { 10305 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10306 } 10307 return item; 10308 } 10309 10310 10311 /// 10312 Menu addItem(Menu item) { 10313 10314 subMenus ~= item; 10315 10316 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 10317 10318 addChild(mbItem); 10319 items ~= mbItem; 10320 10321 version(win32_widgets) { 10322 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 10323 } else version(custom_widgets) { 10324 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 10325 item.popup(mbItem); 10326 }; 10327 } else static assert(false); 10328 10329 return item; 10330 } 10331 10332 override void recomputeChildLayout() { 10333 .recomputeChildLayout!"width"(this); 10334 } 10335 10336 override int maxHeight() { return defaultLineHeight + 4; } 10337 override int minHeight() { return defaultLineHeight + 4; } 10338 } 10339 10340 10341 /** 10342 Status bars appear at the bottom of a MainWindow. 10343 They are made out of Parts, with a width and content. 10344 10345 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 10346 10347 10348 sb.parts[0].content = "Status bar text!"; 10349 */ 10350 class StatusBar : Widget { 10351 private Part[] partsArray; 10352 /// 10353 struct Parts { 10354 @disable this(); 10355 this(StatusBar owner) { this.owner = owner; } 10356 //@disable this(this); 10357 /// 10358 @property int length() { return cast(int) owner.partsArray.length; } 10359 private StatusBar owner; 10360 private this(StatusBar owner, Part[] parts) { 10361 this.owner.partsArray = parts; 10362 this.owner = owner; 10363 } 10364 /// 10365 Part opIndex(int p) { 10366 if(owner.partsArray.length == 0) 10367 this ~= new StatusBar.Part(0); 10368 return owner.partsArray[p]; 10369 } 10370 10371 /// 10372 Part opOpAssign(string op : "~" )(Part p) { 10373 assert(owner.partsArray.length < 255); 10374 p.owner = this.owner; 10375 p.idx = cast(int) owner.partsArray.length; 10376 owner.partsArray ~= p; 10377 10378 owner.recomputeChildLayout(); 10379 10380 version(win32_widgets) { 10381 int[256] pos; 10382 int cpos; 10383 foreach(idx, part; owner.partsArray) { 10384 if(idx + 1 == owner.partsArray.length) 10385 pos[idx] = -1; 10386 else { 10387 cpos += part.currentlyAssignedWidth; 10388 pos[idx] = cpos; 10389 } 10390 } 10391 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 10392 } else version(custom_widgets) { 10393 owner.redraw(); 10394 } else static assert(false); 10395 10396 return p; 10397 } 10398 } 10399 10400 private Parts _parts; 10401 /// 10402 final @property Parts parts() { 10403 return _parts; 10404 } 10405 10406 /++ 10407 10408 +/ 10409 static class Part { 10410 /++ 10411 History: 10412 Added September 1, 2023 (dub v11.1) 10413 +/ 10414 enum WidthUnits { 10415 /++ 10416 Unscaled pixels as they appear on screen. 10417 10418 If you pass 0, it will treat it as a [Proportional] unit for compatibility with code written against older versions of minigui. 10419 +/ 10420 DeviceDependentPixels, 10421 /++ 10422 Pixels at the assumed DPI, but will be automatically scaled with the rest of the ui. 10423 +/ 10424 DeviceIndependentPixels, 10425 /++ 10426 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`). 10427 +/ 10428 ApproximateCharacters, 10429 /++ 10430 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. 10431 10432 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. 10433 +/ 10434 Proportional 10435 } 10436 private WidthUnits units; 10437 private int width; 10438 private StatusBar owner; 10439 10440 private int currentlyAssignedWidth; 10441 10442 /++ 10443 History: 10444 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. 10445 10446 It now allows you to provide your own value for [WidthUnits]. 10447 10448 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`. 10449 +/ 10450 this(int w, WidthUnits units = WidthUnits.Proportional) { 10451 this.units = units; 10452 this.width = w; 10453 } 10454 10455 /// ditto 10456 this(int w = 0) { 10457 if(w == 0) 10458 this(w, WidthUnits.Proportional); 10459 else 10460 this(w, WidthUnits.DeviceDependentPixels); 10461 } 10462 10463 private int idx; 10464 private string _content; 10465 /// 10466 @property string content() { return _content; } 10467 /// 10468 @property void content(string s) { 10469 version(win32_widgets) { 10470 _content = s; 10471 WCharzBuffer bfr = WCharzBuffer(s); 10472 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 10473 } else version(custom_widgets) { 10474 if(_content != s) { 10475 _content = s; 10476 owner.redraw(); 10477 } 10478 } else static assert(false); 10479 } 10480 } 10481 string simpleModeContent; 10482 bool inSimpleMode; 10483 10484 10485 /// 10486 this(Widget parent) { 10487 super(null); // FIXME 10488 _parts = Parts(this); 10489 tabStop = false; 10490 version(win32_widgets) { 10491 parentWindow = parent.parentWindow; 10492 createWin32Window(this, "msctls_statusbar32"w, "", 0); 10493 10494 RECT rect; 10495 GetWindowRect(hwnd, &rect); 10496 idealHeight = rect.bottom - rect.top; 10497 assert(idealHeight); 10498 } else version(custom_widgets) { 10499 } else static assert(false); 10500 } 10501 10502 override void recomputeChildLayout() { 10503 int remainingLength = this.width; 10504 10505 int proportionalSum; 10506 int proportionalCount; 10507 foreach(idx, part; this.partsArray) { 10508 with(Part.WidthUnits) 10509 final switch(part.units) { 10510 case DeviceDependentPixels: 10511 part.currentlyAssignedWidth = part.width; 10512 remainingLength -= part.currentlyAssignedWidth; 10513 break; 10514 case DeviceIndependentPixels: 10515 part.currentlyAssignedWidth = scaleWithDpi(part.width); 10516 remainingLength -= part.currentlyAssignedWidth; 10517 break; 10518 case ApproximateCharacters: 10519 auto cs = getComputedStyle(); 10520 auto font = cs.font; 10521 10522 part.currentlyAssignedWidth = font.averageWidth * this.width; 10523 remainingLength -= part.currentlyAssignedWidth; 10524 break; 10525 case Proportional: 10526 proportionalSum += part.width; 10527 proportionalCount ++; 10528 break; 10529 } 10530 } 10531 10532 foreach(part; this.partsArray) { 10533 if(part.units == Part.WidthUnits.Proportional) { 10534 auto proportion = part.width == 0 ? proportionalSum / proportionalCount : part.width; 10535 if(proportion == 0) 10536 proportion = 1; 10537 10538 if(proportionalSum == 0) 10539 proportionalSum = proportionalCount; 10540 10541 part.currentlyAssignedWidth = remainingLength * proportion / proportionalSum; 10542 } 10543 } 10544 10545 super.recomputeChildLayout(); 10546 } 10547 10548 version(win32_widgets) 10549 override protected void dpiChanged() { 10550 RECT rect; 10551 GetWindowRect(hwnd, &rect); 10552 idealHeight = rect.bottom - rect.top; 10553 assert(idealHeight); 10554 } 10555 10556 version(custom_widgets) 10557 override void paint(WidgetPainter painter) { 10558 auto cs = getComputedStyle(); 10559 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10560 int cpos = 0; 10561 foreach(idx, part; this.partsArray) { 10562 auto partWidth = part.currentlyAssignedWidth; 10563 // part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 10564 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 10565 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 10566 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 10567 10568 painter.outlineColor = cs.foregroundColor(); 10569 painter.fillColor = cs.foregroundColor(); 10570 10571 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 10572 cpos += partWidth; 10573 } 10574 } 10575 10576 10577 version(win32_widgets) { 10578 private int idealHeight; 10579 override int maxHeight() { return idealHeight; } 10580 override int minHeight() { return idealHeight; } 10581 } else version(custom_widgets) { 10582 override int maxHeight() { return defaultLineHeight + 4; } 10583 override int minHeight() { return defaultLineHeight + 4; } 10584 } else static assert(false); 10585 } 10586 10587 /// Displays an in-progress indicator without known values 10588 version(none) 10589 class IndefiniteProgressBar : Widget { 10590 version(win32_widgets) 10591 this(Widget parent) { 10592 super(parent); 10593 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 10594 tabStop = false; 10595 } 10596 override int minHeight() { return 10; } 10597 } 10598 10599 /// A progress bar with a known endpoint and completion amount 10600 class ProgressBar : Widget { 10601 /++ 10602 History: 10603 Added March 16, 2022 (dub v10.7) 10604 +/ 10605 this(int min, int max, Widget parent) { 10606 this(parent); 10607 setRange(cast(ushort) min, cast(ushort) max); // FIXME 10608 } 10609 this(Widget parent) { 10610 version(win32_widgets) { 10611 super(parent); 10612 createWin32Window(this, "msctls_progress32"w, "", 0); 10613 tabStop = false; 10614 } else version(custom_widgets) { 10615 super(parent); 10616 max = 100; 10617 step = 10; 10618 tabStop = false; 10619 } else static assert(0); 10620 } 10621 10622 version(custom_widgets) 10623 override void paint(WidgetPainter painter) { 10624 auto cs = getComputedStyle(); 10625 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10626 painter.fillColor = cs.progressBarColor; 10627 painter.drawRectangle(Point(0, 0), width * current / max, height); 10628 } 10629 10630 10631 version(custom_widgets) { 10632 int current; 10633 int max; 10634 int step; 10635 } 10636 10637 /// 10638 void advanceOneStep() { 10639 version(win32_widgets) 10640 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 10641 else version(custom_widgets) 10642 addToPosition(step); 10643 else static assert(false); 10644 } 10645 10646 /// 10647 void setStepIncrement(int increment) { 10648 version(win32_widgets) 10649 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 10650 else version(custom_widgets) 10651 step = increment; 10652 else static assert(false); 10653 } 10654 10655 /// 10656 void addToPosition(int amount) { 10657 version(win32_widgets) 10658 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 10659 else version(custom_widgets) 10660 setPosition(current + amount); 10661 else static assert(false); 10662 } 10663 10664 /// 10665 void setPosition(int pos) { 10666 version(win32_widgets) 10667 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 10668 else version(custom_widgets) { 10669 current = pos; 10670 if(current > max) 10671 current = max; 10672 redraw(); 10673 } 10674 else static assert(false); 10675 } 10676 10677 /// 10678 void setRange(ushort min, ushort max) { 10679 version(win32_widgets) 10680 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 10681 else version(custom_widgets) { 10682 this.max = max; 10683 } 10684 else static assert(false); 10685 } 10686 10687 override int minHeight() { return 10; } 10688 } 10689 10690 version(custom_widgets) 10691 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 10692 thisLabel.reserve(label.length); 10693 bool justSawAmpersand; 10694 foreach(ch; label) { 10695 if(justSawAmpersand) { 10696 justSawAmpersand = false; 10697 if(ch == '&') { 10698 goto plain; 10699 } 10700 thisAccelerator = ch; 10701 } else { 10702 if(ch == '&') { 10703 justSawAmpersand = true; 10704 continue; 10705 } 10706 plain: 10707 thisLabel ~= ch; 10708 } 10709 } 10710 } 10711 10712 /++ 10713 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. 10714 10715 10716 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 10717 10718 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10719 10720 History: 10721 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. 10722 +/ 10723 class Fieldset : Widget { 10724 // FIXME: on Windows,it doesn't draw the background on the label 10725 // on X, it doesn't fix the clipping rectangle for it 10726 version(win32_widgets) 10727 override int paddingTop() { return defaultLineHeight; } 10728 else version(custom_widgets) 10729 override int paddingTop() { return defaultLineHeight + 2; } 10730 else static assert(false); 10731 override int paddingBottom() { return 6; } 10732 override int paddingLeft() { return 6; } 10733 override int paddingRight() { return 6; } 10734 10735 override int marginLeft() { return 6; } 10736 override int marginRight() { return 6; } 10737 override int marginTop() { return 2; } 10738 override int marginBottom() { return 2; } 10739 10740 string legend; 10741 10742 version(custom_widgets) private dchar accelerator; 10743 10744 this(string legend, Widget parent) { 10745 version(win32_widgets) { 10746 super(parent); 10747 this.legend = legend; 10748 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 10749 tabStop = false; 10750 } else version(custom_widgets) { 10751 super(parent); 10752 tabStop = false; 10753 10754 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 10755 } else static assert(0); 10756 } 10757 10758 version(custom_widgets) 10759 override void paint(WidgetPainter painter) { 10760 auto dlh = defaultLineHeight; 10761 10762 painter.fillColor = Color.transparent; 10763 auto cs = getComputedStyle(); 10764 painter.pen = Pen(cs.foregroundColor, 1); 10765 painter.drawRectangle(Point(0, dlh / 2), width, height - dlh / 2); 10766 10767 auto tx = painter.textSize(legend); 10768 painter.outlineColor = Color.transparent; 10769 10770 version(Windows) { 10771 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 10772 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 10773 SelectObject(painter.impl.hdc, b); 10774 } else static if(UsingSimpledisplayX11) { 10775 painter.fillColor = getComputedStyle().windowBackgroundColor; 10776 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 10777 } 10778 painter.outlineColor = cs.foregroundColor; 10779 painter.drawText(Point(8, 0), legend); 10780 } 10781 10782 override int maxHeight() { 10783 auto m = paddingTop() + paddingBottom(); 10784 foreach(child; children) { 10785 auto mh = child.maxHeight(); 10786 if(mh == int.max) 10787 return int.max; 10788 m += mh; 10789 m += child.marginBottom(); 10790 m += child.marginTop(); 10791 } 10792 m += 6; 10793 if(m < minHeight) 10794 return minHeight; 10795 return m; 10796 } 10797 10798 override int minHeight() { 10799 auto m = paddingTop() + paddingBottom(); 10800 foreach(child; children) { 10801 m += child.minHeight(); 10802 m += child.marginBottom(); 10803 m += child.marginTop(); 10804 } 10805 return m + 6; 10806 } 10807 10808 override int minWidth() { 10809 return 6 + cast(int) this.legend.length * 7; 10810 } 10811 } 10812 10813 /++ 10814 $(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") 10815 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 10816 +/ 10817 version(minigui_screenshots) 10818 @Screenshot("Fieldset") 10819 unittest { 10820 auto window = new Window(200, 100); 10821 auto set = new Fieldset("Baby will", window); 10822 auto option1 = new Radiobox("Eat", set); 10823 auto option2 = new Radiobox("Cry", set); 10824 auto option3 = new Radiobox("Sleep", set); 10825 window.loop(); 10826 } 10827 10828 /// Draws a line 10829 class HorizontalRule : Widget { 10830 mixin Margin!q{ 2 }; 10831 override int minHeight() { return 2; } 10832 override int maxHeight() { return 2; } 10833 10834 /// 10835 this(Widget parent) { 10836 super(parent); 10837 } 10838 10839 override void paint(WidgetPainter painter) { 10840 auto cs = getComputedStyle(); 10841 painter.outlineColor = cs.darkAccentColor; 10842 painter.drawLine(Point(0, 0), Point(width, 0)); 10843 painter.outlineColor = cs.lightAccentColor; 10844 painter.drawLine(Point(0, 1), Point(width, 1)); 10845 } 10846 } 10847 10848 version(minigui_screenshots) 10849 @Screenshot("HorizontalRule") 10850 /++ 10851 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 10852 10853 +/ 10854 unittest { 10855 auto window = new Window(200, 100); 10856 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 10857 new HorizontalRule(window); 10858 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 10859 window.loop(); 10860 } 10861 10862 /// ditto 10863 class VerticalRule : Widget { 10864 mixin Margin!q{ 2 }; 10865 override int minWidth() { return 2; } 10866 override int maxWidth() { return 2; } 10867 10868 /// 10869 this(Widget parent) { 10870 super(parent); 10871 } 10872 10873 override void paint(WidgetPainter painter) { 10874 auto cs = getComputedStyle(); 10875 painter.outlineColor = cs.darkAccentColor; 10876 painter.drawLine(Point(0, 0), Point(0, height)); 10877 painter.outlineColor = cs.lightAccentColor; 10878 painter.drawLine(Point(1, 0), Point(1, height)); 10879 } 10880 } 10881 10882 10883 /// 10884 class Menu : Window { 10885 void remove() { 10886 foreach(i, child; parentWindow.children) 10887 if(child is this) { 10888 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 10889 break; 10890 } 10891 parentWindow.redraw(); 10892 10893 parentWindow.releaseMouseCapture(); 10894 } 10895 10896 /// 10897 void addSeparator() { 10898 version(win32_widgets) 10899 AppendMenu(handle, MF_SEPARATOR, 0, null); 10900 else version(custom_widgets) 10901 auto hr = new HorizontalRule(this); 10902 else static assert(0); 10903 } 10904 10905 override int paddingTop() { return 4; } 10906 override int paddingBottom() { return 4; } 10907 override int paddingLeft() { return 2; } 10908 override int paddingRight() { return 2; } 10909 10910 version(win32_widgets) {} 10911 else version(custom_widgets) { 10912 SimpleWindow dropDown; 10913 Widget menuParent; 10914 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 10915 this.menuParent = parent; 10916 10917 int w = 150; 10918 int h = paddingTop + paddingBottom; 10919 if(this.children.length) { 10920 // hacking it to get the ideal height out of recomputeChildLayout 10921 this.width = w; 10922 this.height = h; 10923 this.recomputeChildLayout(); 10924 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 10925 h += paddingBottom; 10926 10927 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 10928 } 10929 10930 if(offsetY == int.min) 10931 offsetY = parent.defaultLineHeight; 10932 10933 auto coord = parent.globalCoordinates(); 10934 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 10935 this.x = 0; 10936 this.y = 0; 10937 this.width = dropDown.width; 10938 this.height = dropDown.height; 10939 this.drawableWindow = dropDown; 10940 this.recomputeChildLayout(); 10941 10942 static if(UsingSimpledisplayX11) 10943 XSync(XDisplayConnection.get, 0); 10944 10945 dropDown.visibilityChanged = (bool visible) { 10946 if(visible) { 10947 this.redraw(); 10948 dropDown.grabInput(); 10949 } else { 10950 dropDown.releaseInputGrab(); 10951 } 10952 }; 10953 10954 dropDown.show(); 10955 10956 clickListener = this.addEventListener((scope ClickEvent ev) { 10957 unpopup(); 10958 // need to unlock asap just in case other user handlers block... 10959 static if(UsingSimpledisplayX11) 10960 flushGui(); 10961 }, true /* again for asap action */); 10962 } 10963 10964 EventListener clickListener; 10965 } 10966 else static assert(false); 10967 10968 version(custom_widgets) 10969 void unpopup() { 10970 mouseLastOver = mouseLastDownOn = null; 10971 dropDown.hide(); 10972 if(!menuParent.parentWindow.win.closed) { 10973 if(auto maw = cast(MouseActivatedWidget) menuParent) { 10974 maw.setDynamicState(DynamicState.depressed, false); 10975 maw.setDynamicState(DynamicState.hover, false); 10976 maw.redraw(); 10977 } 10978 // menuParent.parentWindow.win.focus(); 10979 } 10980 clickListener.disconnect(); 10981 } 10982 10983 MenuItem[] items; 10984 10985 /// 10986 MenuItem addItem(MenuItem item) { 10987 addChild(item); 10988 items ~= item; 10989 version(win32_widgets) { 10990 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10991 } 10992 return item; 10993 } 10994 10995 string label; 10996 10997 version(win32_widgets) { 10998 HMENU handle; 10999 /// 11000 this(string label, Widget parent) { 11001 // not actually passing the parent since it effs up the drawing 11002 super(cast(Widget) null);// parent); 11003 this.label = label; 11004 handle = CreatePopupMenu(); 11005 } 11006 } else version(custom_widgets) { 11007 /// 11008 this(string label, Widget parent) { 11009 11010 if(dropDown) { 11011 dropDown.close(); 11012 } 11013 dropDown = new SimpleWindow( 11014 150, 4, 11015 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 11016 11017 this.label = label; 11018 11019 super(dropDown); 11020 } 11021 } else static assert(false); 11022 11023 override int maxHeight() { return defaultLineHeight; } 11024 override int minHeight() { return defaultLineHeight; } 11025 11026 version(custom_widgets) 11027 override void paint(WidgetPainter painter) { 11028 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 11029 } 11030 } 11031 11032 /++ 11033 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 11034 +/ 11035 class MenuItem : MouseActivatedWidget { 11036 Menu submenu; 11037 11038 Action action; 11039 string label; 11040 11041 override int paddingLeft() { return 4; } 11042 11043 override int maxHeight() { return defaultLineHeight + 4; } 11044 override int minHeight() { return defaultLineHeight + 4; } 11045 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 11046 override int maxWidth() { 11047 if(cast(MenuBar) parent) { 11048 return minWidth(); 11049 } 11050 return int.max; 11051 } 11052 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 11053 this(string lbl, Widget parent = null) { 11054 super(parent); 11055 //label = lbl; // FIXME 11056 foreach(char ch; lbl) // FIXME 11057 if(ch != '&') // FIXME 11058 label ~= ch; // FIXME 11059 tabStop = false; // these are selected some other way 11060 } 11061 11062 /// 11063 this(Action action, Widget parent = null) { 11064 assert(action !is null); 11065 this(action.label, parent); 11066 this.action = action; 11067 tabStop = false; // these are selected some other way 11068 } 11069 11070 version(custom_widgets) 11071 override void paint(WidgetPainter painter) { 11072 auto cs = getComputedStyle(); 11073 if(dynamicState & DynamicState.depressed) 11074 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 11075 if(dynamicState & DynamicState.hover) 11076 painter.outlineColor = cs.activeMenuItemColor; 11077 else 11078 painter.outlineColor = cs.foregroundColor; 11079 painter.fillColor = Color.transparent; 11080 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11081 if(action && action.accelerator !is KeyEvent.init) { 11082 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 11083 11084 } 11085 } 11086 11087 static class Style : Widget.Style { 11088 override bool variesWithState(ulong dynamicStateFlags) { 11089 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11090 } 11091 } 11092 mixin OverrideStyle!Style; 11093 11094 override void defaultEventHandler_triggered(Event event) { 11095 if(action) 11096 foreach(handler; action.triggered) 11097 handler(); 11098 11099 if(auto pmenu = cast(Menu) this.parent) 11100 pmenu.remove(); 11101 11102 super.defaultEventHandler_triggered(event); 11103 } 11104 } 11105 11106 version(win32_widgets) 11107 /// A "mouse activiated widget" is really just an abstract variant of button. 11108 class MouseActivatedWidget : Widget { 11109 @property bool isChecked() { 11110 assert(hwnd); 11111 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 11112 11113 } 11114 @property void isChecked(bool state) { 11115 assert(hwnd); 11116 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 11117 11118 } 11119 11120 override void handleWmCommand(ushort cmd, ushort id) { 11121 if(cmd == 0) { 11122 auto event = new Event(EventType.triggered, this); 11123 event.dispatch(); 11124 } 11125 } 11126 11127 this(Widget parent) { 11128 super(parent); 11129 } 11130 } 11131 else version(custom_widgets) 11132 /// ditto 11133 class MouseActivatedWidget : Widget { 11134 @property bool isChecked() { return isChecked_; } 11135 @property bool isChecked(bool b) { return isChecked_ = b; } 11136 11137 private bool isChecked_; 11138 11139 this(Widget parent) { 11140 super(parent); 11141 11142 addEventListener((MouseDownEvent ev) { 11143 if(ev.button == MouseButton.left) { 11144 setDynamicState(DynamicState.depressed, true); 11145 setDynamicState(DynamicState.hover, true); 11146 redraw(); 11147 } 11148 }); 11149 11150 addEventListener((MouseUpEvent ev) { 11151 if(ev.button == MouseButton.left) { 11152 setDynamicState(DynamicState.depressed, false); 11153 setDynamicState(DynamicState.hover, false); 11154 redraw(); 11155 } 11156 }); 11157 11158 addEventListener((MouseMoveEvent mme) { 11159 if(!(mme.state & ModifierState.leftButtonDown)) { 11160 if(dynamicState_ & DynamicState.depressed) { 11161 setDynamicState(DynamicState.depressed, false); 11162 redraw(); 11163 } 11164 } 11165 }); 11166 } 11167 11168 override void defaultEventHandler_focus(Event ev) { 11169 super.defaultEventHandler_focus(ev); 11170 this.redraw(); 11171 } 11172 override void defaultEventHandler_blur(Event ev) { 11173 super.defaultEventHandler_blur(ev); 11174 setDynamicState(DynamicState.depressed, false); 11175 this.redraw(); 11176 } 11177 override void defaultEventHandler_keydown(KeyDownEvent ev) { 11178 super.defaultEventHandler_keydown(ev); 11179 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 11180 setDynamicState(DynamicState.depressed, true); 11181 setDynamicState(DynamicState.hover, true); 11182 this.redraw(); 11183 } 11184 } 11185 override void defaultEventHandler_keyup(KeyUpEvent ev) { 11186 super.defaultEventHandler_keyup(ev); 11187 if(!(dynamicState & DynamicState.depressed)) 11188 return; 11189 setDynamicState(DynamicState.depressed, false); 11190 setDynamicState(DynamicState.hover, false); 11191 this.redraw(); 11192 11193 auto event = new Event(EventType.triggered, this); 11194 event.sendDirectly(); 11195 } 11196 override void defaultEventHandler_click(ClickEvent ev) { 11197 super.defaultEventHandler_click(ev); 11198 if(ev.button == MouseButton.left) { 11199 auto event = new Event(EventType.triggered, this); 11200 event.sendDirectly(); 11201 } 11202 } 11203 11204 } 11205 else static assert(false); 11206 11207 /* 11208 /++ 11209 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 11210 11211 Basically the same as a checkbox. 11212 +/ 11213 class OnOffSwitch : MouseActivatedWidget { 11214 11215 } 11216 */ 11217 11218 /++ 11219 History: 11220 Added June 15, 2021 (dub v10.1) 11221 +/ 11222 struct ImageLabel { 11223 /++ 11224 Defines a label+image combo used by some widgets. 11225 11226 If you provide just a text label, that is all the widget will try to 11227 display. Or just an image will display just that. If you provide both, 11228 it may display both text and image side by side or display the image 11229 and offer text on an input event depending on the widget. 11230 11231 History: 11232 The `alignment` parameter was added on September 27, 2021 11233 +/ 11234 this(string label, TextAlignment alignment = TextAlignment.Center) { 11235 this.label = label; 11236 this.displayFlags = DisplayFlags.displayText; 11237 this.alignment = alignment; 11238 } 11239 11240 /// ditto 11241 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11242 this.label = label; 11243 this.image = image; 11244 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11245 this.alignment = alignment; 11246 } 11247 11248 /// ditto 11249 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11250 this.image = image; 11251 this.displayFlags = DisplayFlags.displayImage; 11252 this.alignment = alignment; 11253 } 11254 11255 /// ditto 11256 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 11257 this.label = label; 11258 this.image = image; 11259 this.alignment = alignment; 11260 this.displayFlags = displayFlags; 11261 } 11262 11263 string label; 11264 MemoryImage image; 11265 11266 enum DisplayFlags { 11267 displayText = 1 << 0, 11268 displayImage = 1 << 1, 11269 } 11270 11271 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11272 11273 TextAlignment alignment; 11274 } 11275 11276 /++ 11277 A basic checked or not checked box with an attached label. 11278 11279 11280 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 11281 11282 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11283 11284 History: 11285 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. 11286 +/ 11287 class Checkbox : MouseActivatedWidget { 11288 version(win32_widgets) { 11289 override int maxHeight() { return scaleWithDpi(16); } 11290 override int minHeight() { return scaleWithDpi(16); } 11291 } else version(custom_widgets) { 11292 private enum buttonSize = 16; 11293 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 11294 override int minHeight() { return maxHeight(); } 11295 } else static assert(0); 11296 11297 override int marginLeft() { return 4; } 11298 11299 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 11300 11301 /++ 11302 Just an alias because I keep typing checked out of web habit. 11303 11304 History: 11305 Added May 31, 2021 11306 +/ 11307 alias checked = isChecked; 11308 11309 private string label; 11310 private dchar accelerator; 11311 11312 /++ 11313 +/ 11314 this(string label, Widget parent) { 11315 this(ImageLabel(label), Appearance.checkbox, parent); 11316 } 11317 11318 /// ditto 11319 this(string label, Appearance appearance, Widget parent) { 11320 this(ImageLabel(label), appearance, parent); 11321 } 11322 11323 /++ 11324 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 11325 11326 History: 11327 Added June 29, 2021 (dub v10.2) 11328 +/ 11329 enum Appearance { 11330 checkbox, /// a normal checkbox 11331 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 11332 //sliderswitch, 11333 } 11334 private Appearance appearance; 11335 11336 /// ditto 11337 private this(ImageLabel label, Appearance appearance, Widget parent) { 11338 super(parent); 11339 version(win32_widgets) { 11340 this.label = label.label; 11341 11342 uint extraStyle; 11343 final switch(appearance) { 11344 case Appearance.checkbox: 11345 break; 11346 case Appearance.pushbutton: 11347 extraStyle |= BS_PUSHLIKE; 11348 break; 11349 } 11350 11351 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 11352 } else version(custom_widgets) { 11353 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 11354 } else static assert(0); 11355 } 11356 11357 version(custom_widgets) 11358 override void paint(WidgetPainter painter) { 11359 auto cs = getComputedStyle(); 11360 if(isFocused()) { 11361 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11362 painter.fillColor = cs.windowBackgroundColor; 11363 painter.drawRectangle(Point(0, 0), width, height); 11364 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11365 } else { 11366 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 11367 painter.fillColor = cs.windowBackgroundColor; 11368 painter.drawRectangle(Point(0, 0), width, height); 11369 } 11370 11371 11372 painter.outlineColor = Color.black; 11373 painter.fillColor = Color.white; 11374 enum rectOffset = 2; 11375 painter.drawRectangle(scaleWithDpi(Point(rectOffset, rectOffset)), scaleWithDpi(buttonSize - rectOffset - rectOffset), scaleWithDpi(buttonSize - rectOffset - rectOffset)); 11376 11377 if(isChecked) { 11378 auto size = scaleWithDpi(2); 11379 painter.pen = Pen(Color.black, size); 11380 // I'm using height so the checkbox is square 11381 enum padding = 3; 11382 painter.drawLine( 11383 scaleWithDpi(Point(rectOffset + padding, rectOffset + padding)), 11384 scaleWithDpi(Point(buttonSize - padding - rectOffset, buttonSize - padding - rectOffset)) - Point(1 - size % 2, 1 - size % 2) 11385 ); 11386 painter.drawLine( 11387 scaleWithDpi(Point(buttonSize - padding - rectOffset, padding + rectOffset)) - Point(1 - size % 2, 0), 11388 scaleWithDpi(Point(padding + rectOffset, buttonSize - padding - rectOffset)) - Point(0,1 - size % 2) 11389 ); 11390 11391 painter.pen = Pen(Color.black, 1); 11392 } 11393 11394 if(label !is null) { 11395 painter.outlineColor = cs.foregroundColor(); 11396 painter.fillColor = cs.foregroundColor(); 11397 11398 // i want the centerline of the text to be aligned with the centerline of the checkbox 11399 /+ 11400 auto font = cs.font(); 11401 auto y = scaleWithDpi(rectOffset + buttonSize / 2) - font.height / 2; 11402 painter.drawText(Point(scaleWithDpi(buttonSize + 4), y), label); 11403 +/ 11404 painter.drawText(scaleWithDpi(Point(buttonSize + 4, rectOffset)), label, Point(width, height - scaleWithDpi(rectOffset)), TextAlignment.Left | TextAlignment.VerticalCenter); 11405 } 11406 } 11407 11408 override void defaultEventHandler_triggered(Event ev) { 11409 isChecked = !isChecked; 11410 11411 this.emit!(ChangeEvent!bool)(&isChecked); 11412 11413 redraw(); 11414 } 11415 11416 /// Emits a change event with the checked state 11417 mixin Emits!(ChangeEvent!bool); 11418 } 11419 11420 /// Adds empty space to a layout. 11421 class VerticalSpacer : Widget { 11422 /// 11423 this(Widget parent) { 11424 super(parent); 11425 } 11426 } 11427 11428 /// ditto 11429 class HorizontalSpacer : Widget { 11430 /// 11431 this(Widget parent) { 11432 super(parent); 11433 this.tabStop = false; 11434 } 11435 } 11436 11437 11438 /++ 11439 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 11440 11441 11442 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 11443 11444 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11445 11446 History: 11447 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. 11448 +/ 11449 class Radiobox : MouseActivatedWidget { 11450 11451 version(win32_widgets) { 11452 override int maxHeight() { return scaleWithDpi(16); } 11453 override int minHeight() { return scaleWithDpi(16); } 11454 } else version(custom_widgets) { 11455 private enum buttonSize = 16; 11456 override int maxHeight() { return mymax(defaultLineHeight, scaleWithDpi(buttonSize)); } 11457 override int minHeight() { return maxHeight(); } 11458 } else static assert(0); 11459 11460 override int marginLeft() { return 4; } 11461 11462 // FIXME: make a label getter 11463 private string label; 11464 private dchar accelerator; 11465 11466 /++ 11467 11468 +/ 11469 this(string label, Widget parent) { 11470 super(parent); 11471 version(win32_widgets) { 11472 this.label = label; 11473 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 11474 } else version(custom_widgets) { 11475 label.extractWindowsStyleLabel(this.label, this.accelerator); 11476 height = 16; 11477 width = height + 4 + cast(int) label.length * 16; 11478 } 11479 } 11480 11481 version(custom_widgets) 11482 override void paint(WidgetPainter painter) { 11483 auto cs = getComputedStyle(); 11484 11485 if(isFocused) { 11486 painter.fillColor = cs.windowBackgroundColor; 11487 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11488 } else { 11489 painter.fillColor = cs.windowBackgroundColor; 11490 painter.outlineColor = cs.windowBackgroundColor; 11491 } 11492 painter.drawRectangle(Point(0, 0), width, height); 11493 11494 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11495 11496 painter.outlineColor = Color.black; 11497 painter.fillColor = Color.white; 11498 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 11499 if(isChecked) { 11500 painter.outlineColor = Color.black; 11501 painter.fillColor = Color.black; 11502 // I'm using height so the checkbox is square 11503 auto size = scaleWithDpi(2); 11504 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5)) + Point(size % 2, size % 2)); 11505 } 11506 11507 painter.outlineColor = cs.foregroundColor(); 11508 painter.fillColor = cs.foregroundColor(); 11509 11510 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11511 } 11512 11513 11514 override void defaultEventHandler_triggered(Event ev) { 11515 isChecked = true; 11516 11517 if(this.parent) { 11518 foreach(child; this.parent.children) { 11519 if(child is this) continue; 11520 if(auto rb = cast(Radiobox) child) { 11521 rb.isChecked = false; 11522 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 11523 rb.redraw(); 11524 } 11525 } 11526 } 11527 11528 this.emit!(ChangeEvent!bool)(&this.isChecked); 11529 11530 redraw(); 11531 } 11532 11533 /// 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. 11534 mixin Emits!(ChangeEvent!bool); 11535 } 11536 11537 11538 /++ 11539 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 11540 11541 11542 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 11543 11544 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11545 11546 History: 11547 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. 11548 +/ 11549 class Button : MouseActivatedWidget { 11550 override int heightStretchiness() { return 3; } 11551 override int widthStretchiness() { return 3; } 11552 11553 /++ 11554 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. 11555 11556 History: 11557 Added July 2, 2021 11558 +/ 11559 public bool triggersOnMultiClick; 11560 11561 private string label_; 11562 private TextAlignment alignment; 11563 private dchar accelerator; 11564 11565 /// 11566 string label() { return label_; } 11567 /// 11568 void label(string l) { 11569 label_ = l; 11570 version(win32_widgets) { 11571 WCharzBuffer bfr = WCharzBuffer(l); 11572 SetWindowTextW(hwnd, bfr.ptr); 11573 } else version(custom_widgets) { 11574 redraw(); 11575 } 11576 } 11577 11578 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 11579 super.defaultEventHandler_dblclick(ev); 11580 if(triggersOnMultiClick) { 11581 if(ev.button == MouseButton.left) { 11582 auto event = new Event(EventType.triggered, this); 11583 event.sendDirectly(); 11584 } 11585 } 11586 } 11587 11588 private Sprite sprite; 11589 private int displayFlags; 11590 11591 /++ 11592 Creates a push button with the given label, which may be an image or some text. 11593 11594 Bugs: 11595 If the image is bigger than the button, it may not be displayed in the right position on Linux. 11596 11597 History: 11598 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 11599 11600 The button with label and image will respect requests to show both on Windows as 11601 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 11602 +/ 11603 this(ImageLabel label, Widget parent) { 11604 version(win32_widgets) { 11605 // FIXME: use ideal button size instead 11606 width = 50; 11607 height = 30; 11608 super(parent); 11609 11610 // BS_BITMAP is set when we want image only, so checking for exactly that combination 11611 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 11612 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 11613 11614 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 11615 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 11616 11617 if(label.image) { 11618 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 11619 11620 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 11621 } 11622 11623 this.label = label.label; 11624 } else version(custom_widgets) { 11625 width = 50; 11626 height = 30; 11627 super(parent); 11628 11629 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 11630 11631 if(label.image) { 11632 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 11633 this.displayFlags = label.displayFlags; 11634 } 11635 11636 this.alignment = label.alignment; 11637 } 11638 } 11639 11640 /// 11641 this(string label, Widget parent) { 11642 this(ImageLabel(label), parent); 11643 } 11644 11645 override int minHeight() { return defaultLineHeight + 4; } 11646 11647 static class Style : Widget.Style { 11648 override WidgetBackground background() { 11649 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 11650 11651 auto pressed = DynamicState.depressed | DynamicState.hover; 11652 if((widget.dynamicState & pressed) == pressed) { 11653 return WidgetBackground(cs.depressedButtonColor()); 11654 } else if(widget.dynamicState & DynamicState.hover) { 11655 return WidgetBackground(cs.hoveringColor()); 11656 } else { 11657 return WidgetBackground(cs.buttonColor()); 11658 } 11659 } 11660 11661 override FrameStyle borderStyle() { 11662 auto pressed = DynamicState.depressed | DynamicState.hover; 11663 if((widget.dynamicState & pressed) == pressed) { 11664 return FrameStyle.sunk; 11665 } else { 11666 return FrameStyle.risen; 11667 } 11668 11669 } 11670 11671 override bool variesWithState(ulong dynamicStateFlags) { 11672 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11673 } 11674 } 11675 mixin OverrideStyle!Style; 11676 11677 version(custom_widgets) 11678 override void paint(WidgetPainter painter) { 11679 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 11680 if(sprite) { 11681 sprite.drawAt( 11682 painter, 11683 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 11684 Point(0, 0) 11685 ); 11686 } else { 11687 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 11688 } 11689 return bounds; 11690 }); 11691 } 11692 11693 override int flexBasisWidth() { 11694 version(win32_widgets) { 11695 SIZE size; 11696 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11697 if(size.cx == 0) 11698 goto fallback; 11699 return size.cx + scaleWithDpi(16); 11700 } 11701 fallback: 11702 return scaleWithDpi(cast(int) label.length * 8 + 16); 11703 } 11704 11705 override int flexBasisHeight() { 11706 version(win32_widgets) { 11707 SIZE size; 11708 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11709 if(size.cy == 0) 11710 goto fallback; 11711 return size.cy + scaleWithDpi(6); 11712 } 11713 fallback: 11714 return defaultLineHeight + 4; 11715 } 11716 } 11717 11718 /++ 11719 A button with a consistent size, suitable for user commands like OK and CANCEL. 11720 +/ 11721 class CommandButton : Button { 11722 this(string label, Widget parent) { 11723 super(label, parent); 11724 } 11725 11726 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 11727 11728 override int maxHeight() { 11729 return defaultLineHeight + 4; 11730 } 11731 11732 override int maxWidth() { 11733 return defaultLineHeight * 4; 11734 } 11735 11736 override int marginLeft() { return 12; } 11737 override int marginRight() { return 12; } 11738 override int marginTop() { return 12; } 11739 override int marginBottom() { return 12; } 11740 } 11741 11742 /// 11743 enum ArrowDirection { 11744 left, /// 11745 right, /// 11746 up, /// 11747 down /// 11748 } 11749 11750 /// 11751 version(custom_widgets) 11752 class ArrowButton : Button { 11753 /// 11754 this(ArrowDirection direction, Widget parent) { 11755 super("", parent); 11756 this.direction = direction; 11757 triggersOnMultiClick = true; 11758 } 11759 11760 private ArrowDirection direction; 11761 11762 override int minHeight() { return scaleWithDpi(16); } 11763 override int maxHeight() { return scaleWithDpi(16); } 11764 override int minWidth() { return scaleWithDpi(16); } 11765 override int maxWidth() { return scaleWithDpi(16); } 11766 11767 override void paint(WidgetPainter painter) { 11768 super.paint(painter); 11769 11770 auto cs = getComputedStyle(); 11771 11772 painter.outlineColor = cs.foregroundColor; 11773 painter.fillColor = cs.foregroundColor; 11774 11775 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 11776 11777 final switch(direction) { 11778 case ArrowDirection.up: 11779 painter.drawPolygon( 11780 scaleWithDpi(Point(2, 10) + offset), 11781 scaleWithDpi(Point(7, 5) + offset), 11782 scaleWithDpi(Point(12, 10) + offset), 11783 scaleWithDpi(Point(2, 10) + offset) 11784 ); 11785 break; 11786 case ArrowDirection.down: 11787 painter.drawPolygon( 11788 scaleWithDpi(Point(2, 6) + offset), 11789 scaleWithDpi(Point(7, 11) + offset), 11790 scaleWithDpi(Point(12, 6) + offset), 11791 scaleWithDpi(Point(2, 6) + offset) 11792 ); 11793 break; 11794 case ArrowDirection.left: 11795 painter.drawPolygon( 11796 scaleWithDpi(Point(10, 2) + offset), 11797 scaleWithDpi(Point(5, 7) + offset), 11798 scaleWithDpi(Point(10, 12) + offset), 11799 scaleWithDpi(Point(10, 2) + offset) 11800 ); 11801 break; 11802 case ArrowDirection.right: 11803 painter.drawPolygon( 11804 scaleWithDpi(Point(6, 2) + offset), 11805 scaleWithDpi(Point(11, 7) + offset), 11806 scaleWithDpi(Point(6, 12) + offset), 11807 scaleWithDpi(Point(6, 2) + offset) 11808 ); 11809 break; 11810 } 11811 } 11812 } 11813 11814 private 11815 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 11816 int x, y; 11817 Widget par = c; 11818 while(par) { 11819 x += par.x; 11820 y += par.y; 11821 par = par.parent; 11822 } 11823 return [x, y]; 11824 } 11825 11826 version(win32_widgets) 11827 private 11828 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 11829 // MapWindowPoints? 11830 int x, y; 11831 Widget par = c; 11832 while(par) { 11833 x += par.x; 11834 y += par.y; 11835 par = par.parent; 11836 if(par !is null && par.useNativeDrawing()) 11837 break; 11838 } 11839 return [x, y]; 11840 } 11841 11842 /// 11843 class ImageBox : Widget { 11844 private MemoryImage image_; 11845 11846 override int widthStretchiness() { return 1; } 11847 override int heightStretchiness() { return 1; } 11848 override int widthShrinkiness() { return 1; } 11849 override int heightShrinkiness() { return 1; } 11850 11851 override int flexBasisHeight() { 11852 return image_.height; 11853 } 11854 11855 override int flexBasisWidth() { 11856 return image_.width; 11857 } 11858 11859 /// 11860 public void setImage(MemoryImage image){ 11861 this.image_ = image; 11862 if(this.parentWindow && this.parentWindow.win) { 11863 if(sprite) 11864 sprite.dispose(); 11865 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11866 } 11867 redraw(); 11868 } 11869 11870 /// How to fit the image in the box if they aren't an exact match in size? 11871 enum HowToFit { 11872 center, /// centers the image, cropping around all the edges as needed 11873 crop, /// always draws the image in the upper left, cropping the lower right if needed 11874 // stretch, /// not implemented 11875 } 11876 11877 private Sprite sprite; 11878 private HowToFit howToFit_; 11879 11880 private Color backgroundColor_; 11881 11882 /// 11883 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 11884 this.image_ = image; 11885 this.tabStop = false; 11886 this.howToFit_ = howToFit; 11887 this.backgroundColor_ = backgroundColor; 11888 super(parent); 11889 updateSprite(); 11890 } 11891 11892 /// ditto 11893 this(MemoryImage image, HowToFit howToFit, Widget parent) { 11894 this(image, howToFit, Color.transparent, parent); 11895 } 11896 11897 private void updateSprite() { 11898 if(sprite is null && this.parentWindow && this.parentWindow.win) { 11899 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11900 } 11901 } 11902 11903 override void paint(WidgetPainter painter) { 11904 updateSprite(); 11905 if(backgroundColor_.a) { 11906 painter.fillColor = backgroundColor_; 11907 painter.drawRectangle(Point(0, 0), width, height); 11908 } 11909 if(howToFit_ == HowToFit.crop) 11910 sprite.drawAt(painter, Point(0, 0)); 11911 else if(howToFit_ == HowToFit.center) { 11912 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 11913 } 11914 } 11915 } 11916 11917 /// 11918 class TextLabel : Widget { 11919 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight()))).height; } 11920 override int maxHeight() { return minHeight; } 11921 override int minWidth() { return 32; } 11922 11923 override int flexBasisHeight() { return minHeight(); } 11924 override int flexBasisWidth() { return defaultTextWidth(label); } 11925 11926 string label_; 11927 11928 /++ 11929 Indicates which other control this label is here for. Similar to HTML `for` attribute. 11930 11931 In practice this means a click on the label will focus the `labelFor`. In future versions 11932 it will also set screen reader hints but that is not yet implemented. 11933 11934 History: 11935 Added October 3, 2021 (dub v10.4) 11936 +/ 11937 Widget labelFor; 11938 11939 /// 11940 @scriptable 11941 string label() { return label_; } 11942 11943 /// 11944 @scriptable 11945 void label(string l) { 11946 label_ = l; 11947 version(win32_widgets) { 11948 WCharzBuffer bfr = WCharzBuffer(l); 11949 SetWindowTextW(hwnd, bfr.ptr); 11950 } else version(custom_widgets) 11951 redraw(); 11952 } 11953 11954 override void defaultEventHandler_click(scope ClickEvent ce) { 11955 if(this.labelFor !is null) 11956 this.labelFor.focus(); 11957 } 11958 11959 /++ 11960 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 11961 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 11962 +/ 11963 this(string label, TextAlignment alignment, Widget parent) { 11964 this.label_ = label; 11965 this.alignment = alignment; 11966 this.tabStop = false; 11967 super(parent); 11968 11969 version(win32_widgets) 11970 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 11971 } 11972 11973 /// ditto 11974 this(string label, Widget parent) { 11975 this(label, TextAlignment.Right, parent); 11976 } 11977 11978 TextAlignment alignment; 11979 11980 version(custom_widgets) 11981 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 11982 painter.outlineColor = getComputedStyle().foregroundColor; 11983 painter.drawText(bounds.upperLeft, this.label, bounds.lowerRight, alignment); 11984 return bounds; 11985 } 11986 11987 } 11988 11989 version(custom_widgets) 11990 private struct etc { 11991 mixin ExperimentalTextComponent; 11992 } 11993 11994 version(win32_widgets) 11995 alias EditableTextWidgetParent = Widget; /// 11996 else version(custom_widgets) { 11997 version(trash_text) { 11998 alias EditableTextWidgetParent = ScrollableWidget; /// 11999 } else { 12000 alias EditableTextWidgetParent = Widget; 12001 version=use_new_text_system; 12002 import arsd.textlayouter; 12003 } 12004 } else static assert(0); 12005 12006 version(use_new_text_system) 12007 class TextDisplayHelper : Widget { 12008 protected TextLayouter l; 12009 protected ScrollMessageWidget smw; 12010 12011 private const(TextLayouter.State)*[] undoStack; 12012 private const(TextLayouter.State)*[] redoStack; 12013 12014 bool readonly; 12015 bool caretNavigation; // scroll lock can flip this 12016 bool singleLine; 12017 bool acceptsTabInput; 12018 12019 private Menu ctx; 12020 override Menu contextMenu(int x, int y) { 12021 if(ctx is null) { 12022 ctx = new Menu("Actions", this); 12023 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 12024 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 12025 ctx.addSeparator(); 12026 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 12027 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 12028 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 12029 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 12030 ctx.addSeparator(); 12031 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 12032 } 12033 return ctx; 12034 } 12035 12036 override void defaultEventHandler_blur(Event ev) { 12037 super.defaultEventHandler_blur(ev); 12038 if(l.wasMutated()) { 12039 auto evt = new ChangeEvent!string(this, &this.content); 12040 evt.dispatch(); 12041 l.clearWasMutatedFlag(); 12042 } 12043 } 12044 12045 private string content() { 12046 return l.getTextString(); 12047 } 12048 12049 void undo() { 12050 if(undoStack.length) { 12051 auto state = undoStack[$-1]; 12052 undoStack = undoStack[0 .. $-1]; 12053 undoStack.assumeSafeAppend(); 12054 redoStack ~= l.saveState(); 12055 l.restoreState(state); 12056 adjustScrollbarSizes(); 12057 scrollForCaret(); 12058 redraw(); 12059 stateCheckpoint = true; 12060 } 12061 } 12062 12063 void redo() { 12064 if(redoStack.length) { 12065 doStateCheckpoint(); 12066 auto state = redoStack[$-1]; 12067 redoStack = redoStack[0 .. $-1]; 12068 redoStack.assumeSafeAppend(); 12069 l.restoreState(state); 12070 adjustScrollbarSizes(); 12071 scrollForCaret(); 12072 redraw(); 12073 stateCheckpoint = true; 12074 } 12075 } 12076 12077 void cut() { 12078 with(l.selection()) { 12079 if(!isEmpty()) { 12080 setClipboardText(parentWindow.win, getContentString()); 12081 doStateCheckpoint(); 12082 replaceContent(""); 12083 adjustScrollbarSizes(); 12084 scrollForCaret(); 12085 this.redraw(); 12086 } 12087 } 12088 12089 } 12090 12091 void copy() { 12092 with(l.selection()) { 12093 if(!isEmpty()) { 12094 setClipboardText(parentWindow.win, getContentString()); 12095 this.redraw(); 12096 } 12097 } 12098 } 12099 12100 void paste() { 12101 getClipboardText(parentWindow.win, (txt) { 12102 doStateCheckpoint(); 12103 l.selection.replaceContent(txt); 12104 adjustScrollbarSizes(); 12105 scrollForCaret(); 12106 this.redraw(); 12107 }); 12108 } 12109 12110 void deleteContentOfSelection() { 12111 doStateCheckpoint(); 12112 l.selection.replaceContent(""); 12113 l.selection.setUserXCoordinate(); 12114 adjustScrollbarSizes(); 12115 scrollForCaret(); 12116 redraw(); 12117 } 12118 12119 void selectAll() { 12120 with(l.selection) { 12121 moveToStartOfDocument(); 12122 setAnchor(); 12123 moveToEndOfDocument(); 12124 setFocus(); 12125 } 12126 redraw(); 12127 } 12128 12129 protected bool stateCheckpoint = true; 12130 12131 protected void doStateCheckpoint() { 12132 if(stateCheckpoint) { 12133 undoStack ~= l.saveState(); 12134 stateCheckpoint = false; 12135 } 12136 } 12137 12138 protected void adjustScrollbarSizes() { 12139 // FIXME: will want a content area helper function instead of doing all these subtractions myself 12140 auto borderWidth = 2; 12141 this.smw.setTotalArea(l.width, l.height); 12142 this.smw.setViewableArea( 12143 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 12144 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 12145 } 12146 12147 protected void scrollForCaret() { 12148 // writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 12149 smw.scrollIntoView(l.selection.focusBoundingBox()); 12150 } 12151 12152 // FIXME: this should be a theme changed event listener instead 12153 private BaseVisualTheme currentTheme; 12154 override void recomputeChildLayout() { 12155 if(currentTheme is null) 12156 currentTheme = WidgetPainter.visualTheme; 12157 if(WidgetPainter.visualTheme !is currentTheme) { 12158 currentTheme = WidgetPainter.visualTheme; 12159 auto ds = this.l.defaultStyle; 12160 if(auto ms = cast(MyTextStyle) ds) { 12161 auto cs = getComputedStyle(); 12162 auto font = cs.font(); 12163 if(font !is null) 12164 ms.font_ = font; 12165 else { 12166 auto osc = new OperatingSystemFont(); 12167 osc.loadDefault; 12168 ms.font_ = osc; 12169 } 12170 } 12171 } 12172 super.recomputeChildLayout(); 12173 } 12174 12175 private Point adjustForSingleLine(Point p) { 12176 if(singleLine) 12177 return Point(p.x, this.height / 2); 12178 else 12179 return p; 12180 } 12181 12182 private bool wordWrapEnabled_; 12183 12184 this(TextLayouter l, ScrollMessageWidget parent) { 12185 this.smw = parent; 12186 12187 smw.addDefaultWheelListeners(16, 16, 8); 12188 smw.movementPerButtonClick(16, 16); 12189 12190 this.defaultPadding = Rectangle(2, 2, 2, 2); 12191 12192 this.l = l; 12193 super(parent); 12194 12195 smw.addEventListener((scope ScrollEvent se) { 12196 this.redraw(); 12197 }); 12198 12199 bool mouseDown; 12200 12201 this.addEventListener((scope ResizeEvent re) { 12202 // FIXME: I should add a method to give this client area width thing 12203 if(wordWrapEnabled_) 12204 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 12205 12206 adjustScrollbarSizes(); 12207 scrollForCaret(); 12208 12209 this.redraw(); 12210 }); 12211 12212 this.addEventListener((scope KeyDownEvent kde) { 12213 switch(kde.key) { 12214 case Key.Up, Key.Down, Key.Left, Key.Right: 12215 case Key.Home, Key.End: 12216 stateCheckpoint = true; 12217 bool setPosition = false; 12218 switch(kde.key) { 12219 case Key.Up: l.selection.moveUp(); break; 12220 case Key.Down: l.selection.moveDown(); break; 12221 case Key.Left: l.selection.moveLeft(); setPosition = true; break; 12222 case Key.Right: l.selection.moveRight(); setPosition = true; break; 12223 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 12224 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 12225 default: assert(0); 12226 } 12227 12228 if(kde.shiftKey) 12229 l.selection.setFocus(); 12230 else 12231 l.selection.setAnchor(); 12232 if(setPosition) 12233 l.selection.setUserXCoordinate(); 12234 scrollForCaret(); 12235 redraw(); 12236 break; 12237 case Key.PageUp, Key.PageDown: 12238 // FIXME 12239 scrollForCaret(); 12240 break; 12241 case Key.Delete: 12242 if(l.selection.isEmpty()) { 12243 l.selection.setAnchor(); 12244 l.selection.moveRight(); 12245 l.selection.setFocus(); 12246 } 12247 deleteContentOfSelection(); 12248 adjustScrollbarSizes(); 12249 scrollForCaret(); 12250 break; 12251 case Key.Insert: 12252 break; 12253 case Key.A: 12254 if(kde.ctrlKey) 12255 selectAll(); 12256 break; 12257 case Key.F: 12258 // find 12259 break; 12260 case Key.Z: 12261 if(kde.ctrlKey) 12262 undo(); 12263 break; 12264 case Key.R: 12265 if(kde.ctrlKey) 12266 redo(); 12267 break; 12268 case Key.X: 12269 if(kde.ctrlKey) 12270 cut(); 12271 break; 12272 case Key.C: 12273 if(kde.ctrlKey) 12274 copy(); 12275 break; 12276 case Key.V: 12277 if(kde.ctrlKey) 12278 paste(); 12279 break; 12280 case Key.F1: 12281 with(l.selection()) { 12282 moveToStartOfLine(); 12283 setAnchor(); 12284 moveToEndOfLine(); 12285 moveToIncludeAdjacentEndOfLineMarker(); 12286 setFocus(); 12287 replaceContent(""); 12288 } 12289 12290 redraw(); 12291 break; 12292 /* 12293 case Key.F2: 12294 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 12295 //(cast(MyTextStyle) old).font, 12296 font2, 12297 Color.red))); 12298 redraw(); 12299 break; 12300 */ 12301 case Key.Tab: 12302 // we process the char event, so don't want to change focus on it 12303 if(acceptsTabInput) 12304 kde.preventDefault(); 12305 break; 12306 default: 12307 } 12308 }); 12309 12310 Point downAt; 12311 12312 static if(UsingSimpledisplayX11) 12313 this.addEventListener((scope ClickEvent ce) { 12314 if(ce.button == MouseButton.middle) { 12315 parentWindow.win.getPrimarySelection((txt) { 12316 l.selection.replaceContent(txt); 12317 redraw(); 12318 }); 12319 } 12320 }); 12321 12322 this.addEventListener((scope MouseDownEvent ce) { 12323 if(ce.button == MouseButton.left) { 12324 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12325 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 12326 l.selection.setAnchor(); 12327 mouseDown = true; 12328 parentWindow.captureMouse(this); 12329 this.redraw(); 12330 } else if(ce.button == MouseButton.right) { 12331 this.showContextMenu(ce.clientX, ce.clientY); 12332 } 12333 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12334 }); 12335 12336 Timer autoscrollTimer; 12337 int autoscrollDirection; 12338 int autoscrollAmount; 12339 12340 void autoscroll() { 12341 switch(autoscrollDirection) { 12342 case 0: smw.scrollUp(autoscrollAmount); break; 12343 case 1: smw.scrollDown(autoscrollAmount); break; 12344 case 2: smw.scrollLeft(autoscrollAmount); break; 12345 case 3: smw.scrollRight(autoscrollAmount); break; 12346 default: assert(0); 12347 } 12348 12349 this.redraw(); 12350 } 12351 12352 void setAutoscrollTimer(int direction, int amount) { 12353 if(autoscrollTimer is null) { 12354 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 12355 } 12356 12357 autoscrollDirection = direction; 12358 autoscrollAmount = amount; 12359 } 12360 12361 void stopAutoscrollTimer() { 12362 if(autoscrollTimer !is null) { 12363 autoscrollTimer.dispose(); 12364 autoscrollTimer = null; 12365 } 12366 autoscrollAmount = 0; 12367 autoscrollDirection = 0; 12368 } 12369 12370 this.addEventListener((scope MouseMoveEvent ce) { 12371 if(mouseDown) { 12372 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12373 12374 // FIXME: when scrolling i actually do want a timer. 12375 // i also want a zone near the sides of the window where i can auto scroll 12376 12377 auto scrollMultiplier = scaleWithDpi(16); 12378 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 12379 12380 if(!singleLine && movedTo.y < 4) { 12381 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 12382 } else 12383 if(!singleLine && (movedTo.y + 6) > this.height) { 12384 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 12385 } else 12386 if(movedTo.x < 4) { 12387 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 12388 } else 12389 if((movedTo.x + 6) > this.width) { 12390 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 12391 } else 12392 stopAutoscrollTimer(); 12393 12394 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 12395 l.selection.setFocus(); 12396 this.redraw(); 12397 } 12398 }); 12399 12400 this.addEventListener((scope MouseUpEvent ce) { 12401 // FIXME: assert primary selection 12402 if(mouseDown && ce.button == MouseButton.left) { 12403 stateCheckpoint = true; 12404 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 12405 //l.selection.setFocus(); 12406 mouseDown = false; 12407 parentWindow.releaseMouseCapture(); 12408 stopAutoscrollTimer(); 12409 this.redraw(); 12410 } 12411 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12412 }); 12413 12414 this.addEventListener((scope CharEvent ce) { 12415 if(readonly) 12416 return; 12417 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 12418 return; // skip the ctrl+x characters we don't care about as plain text 12419 12420 if(singleLine && ce.character == '\n') 12421 return; 12422 if(!acceptsTabInput && ce.character == '\t') 12423 return; 12424 12425 doStateCheckpoint(); 12426 12427 char[4] buffer; 12428 import std.utf; // FIXME: i should remove this. compile time not significant but the logs get spammed with phobos' import web 12429 auto stride = encode(buffer, ce.character); 12430 l.selection.replaceContent(buffer[0 .. stride]); 12431 l.selection.setUserXCoordinate(); 12432 adjustScrollbarSizes(); 12433 scrollForCaret(); 12434 redraw(); 12435 }); 12436 } 12437 12438 static class Style : Widget.Style { 12439 override WidgetBackground background() { 12440 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 12441 } 12442 12443 override Color foregroundColor() { 12444 return WidgetPainter.visualTheme.foregroundColor; 12445 } 12446 12447 override FrameStyle borderStyle() { 12448 return FrameStyle.sunk; 12449 } 12450 12451 override MouseCursor cursor() { 12452 return GenericCursor.Text; 12453 } 12454 } 12455 mixin OverrideStyle!Style; 12456 12457 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultTextHeight))).height; } 12458 override int maxHeight() { 12459 if(singleLine) 12460 return minHeight; 12461 else 12462 return super.maxHeight(); 12463 } 12464 12465 void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 12466 painter.drawText(upperLeft, text); 12467 } 12468 12469 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 12470 //painter.setFont(font); 12471 12472 auto cs = getComputedStyle(); 12473 auto defaultColor = cs.foregroundColor; 12474 12475 auto old = painter.setClipRectangle(bounds); 12476 scope(exit) painter.setClipRectangle(old); 12477 12478 l.getDrawableText(delegate bool(txt, style, info, carets...) { 12479 //writeln("Segment: ", txt); 12480 assert(style !is null); 12481 12482 auto myStyle = cast(MyTextStyle) style; 12483 assert(myStyle !is null); 12484 12485 painter.setFont(myStyle.font); 12486 // defaultColor = myStyle.color; // FIXME: so wrong 12487 12488 if(info.selections && info.boundingBox.width > 0) { 12489 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 12490 painter.fillColor = color; 12491 painter.outlineColor = color; 12492 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 12493 painter.outlineColor = cs.selectionForegroundColor; 12494 //painter.fillColor = Color.white; 12495 } else { 12496 painter.outlineColor = defaultColor; 12497 } 12498 12499 if(this.isFocused) 12500 foreach(idx, caret; carets) { 12501 if(idx == 0) 12502 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 12503 painter.drawLine( 12504 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 12505 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 12506 ); 12507 } 12508 12509 if(txt.stripInternal.length) { 12510 drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 12511 } 12512 12513 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) { 12514 return false; 12515 } else { 12516 return true; 12517 } 12518 }, Rectangle(smw.position(), bounds.size)); 12519 12520 /+ 12521 int place = 0; 12522 int y = 75; 12523 foreach(width; widths) { 12524 painter.fillColor = Color.red; 12525 painter.drawRectangle(Point(place, y), Size(width, 75)); 12526 //y += 15; 12527 place += width; 12528 } 12529 +/ 12530 12531 return bounds; 12532 } 12533 12534 static class MyTextStyle : TextStyle { 12535 OperatingSystemFont font_; 12536 this(OperatingSystemFont font, bool passwordMode = false) { 12537 this.font_ = font; 12538 } 12539 12540 override OperatingSystemFont font() { 12541 return font_; 12542 } 12543 } 12544 } 12545 12546 /+ 12547 version(use_new_text_system) 12548 class TextWidget : Widget { 12549 TextLayouter l; 12550 ScrollMessageWidget smw; 12551 TextDisplayHelper helper; 12552 this(TextLayouter l, Widget parent) { 12553 this.l = l; 12554 super(parent); 12555 12556 smw = new ScrollMessageWidget(this); 12557 //smw.horizontalScrollBar.hide; 12558 //smw.verticalScrollBar.hide; 12559 smw.addDefaultWheelListeners(16, 16, 8); 12560 smw.movementPerButtonClick(16, 16); 12561 helper = new TextDisplayHelper(l, smw); 12562 12563 // no need to do this here since there's gonna be a resize 12564 // event immediately before any drawing 12565 // smw.setTotalArea(l.width, l.height); 12566 smw.setViewableArea( 12567 this.width - this.paddingLeft - this.paddingRight, 12568 this.height - this.paddingTop - this.paddingBottom); 12569 12570 /+ 12571 writeln(l.width, "x", l.height); 12572 +/ 12573 } 12574 } 12575 +/ 12576 12577 12578 12579 12580 /+ 12581 This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too 12582 +/ 12583 12584 /// Contains the implementation of text editing 12585 abstract class EditableTextWidget : EditableTextWidgetParent { 12586 this(Widget parent) { 12587 super(parent); 12588 12589 version(custom_widgets) 12590 setupCustomTextEditing(); 12591 } 12592 12593 private bool wordWrapEnabled_; 12594 void wordWrapEnabled(bool enabled) { 12595 version(win32_widgets) { 12596 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 12597 } else version(custom_widgets) { 12598 wordWrapEnabled_ = enabled; 12599 version(use_new_text_system) 12600 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 12601 } else static assert(false); 12602 } 12603 12604 override int minWidth() { return scaleWithDpi(16); } 12605 override int widthStretchiness() { return 7; } 12606 12607 version(use_new_text_system) 12608 override int maxHeight() { return tdh.maxHeight; } 12609 12610 version(use_new_text_system) 12611 override void focus() { if(tdh) tdh.focus(); else super.focus(); } 12612 12613 void selectAll() { 12614 version(win32_widgets) 12615 SendMessage(hwnd, EM_SETSEL, 0, -1); 12616 else version(custom_widgets) { 12617 version(use_new_text_system) 12618 tdh.selectAll(); 12619 else 12620 textLayout.selectAll(); 12621 redraw(); 12622 } 12623 } 12624 12625 version(use_new_text_system) 12626 TextDisplayHelper tdh; 12627 12628 @property string content() { 12629 version(win32_widgets) { 12630 wchar[4096] bufferstack; 12631 wchar[] buffer; 12632 auto len = GetWindowTextLength(hwnd); 12633 if(len < bufferstack.length) 12634 buffer = bufferstack[0 .. len + 1]; 12635 else 12636 buffer = new wchar[](len + 1); 12637 12638 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 12639 if(l >= 0) 12640 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 12641 else 12642 return null; 12643 } else version(custom_widgets) { 12644 version(use_new_text_system) { 12645 return textLayout.getTextString(); 12646 } else 12647 return textLayout.getPlainText(); 12648 } else static assert(false); 12649 } 12650 @property void content(string s) { 12651 version(win32_widgets) { 12652 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 12653 SetWindowTextW(hwnd, bfr.ptr); 12654 } else version(custom_widgets) { 12655 version(use_new_text_system) { 12656 selectAll(); 12657 textLayout.selection.replaceContent(s); 12658 12659 tdh.adjustScrollbarSizes(); 12660 // these don't seem to help 12661 // tdh.smw.setPosition(0, 0); 12662 // tdh.scrollForCaret(); 12663 12664 redraw(); 12665 } else { 12666 textLayout.clear(); 12667 textLayout.addText(s); 12668 12669 { 12670 // FIXME: it should be able to get this info easier 12671 auto painter = draw(); 12672 textLayout.redoLayout(painter); 12673 } 12674 auto cbb = textLayout.contentBoundingBox(); 12675 setContentSize(cbb.width, cbb.height); 12676 /* 12677 textLayout.addText(ForegroundColor.red, s); 12678 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 12679 textLayout.addText(" is the best!"); 12680 */ 12681 redraw(); 12682 } 12683 } 12684 else static assert(false); 12685 } 12686 12687 void addText(string txt) { 12688 version(custom_widgets) { 12689 version(use_new_text_system) { 12690 textLayout.appendText(txt); 12691 tdh.adjustScrollbarSizes(); 12692 redraw(); 12693 } else { 12694 textLayout.addText(txt); 12695 12696 { 12697 // FIXME: it should be able to get this info easier 12698 auto painter = draw(); 12699 textLayout.redoLayout(painter); 12700 } 12701 auto cbb = textLayout.contentBoundingBox(); 12702 setContentSize(cbb.width, cbb.height); 12703 } 12704 } else version(win32_widgets) { 12705 // get the current selection 12706 DWORD StartPos, EndPos; 12707 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 12708 12709 // move the caret to the end of the text 12710 int outLength = GetWindowTextLengthW(hwnd); 12711 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 12712 12713 // insert the text at the new caret position 12714 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 12715 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 12716 12717 // restore the previous selection 12718 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 12719 } else static assert(0); 12720 } 12721 12722 version(custom_widgets) 12723 version(trash_text) 12724 override void paintFrameAndBackground(WidgetPainter painter) { 12725 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 12726 } 12727 12728 version(use_new_text_system) 12729 TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 12730 return new TextDisplayHelper(textLayout, smw); 12731 } 12732 12733 version(use_new_text_system) 12734 TextStyle defaultTextStyle() { 12735 return new TextDisplayHelper.MyTextStyle(getUsedFont()); 12736 } 12737 12738 version(use_new_text_system) 12739 private OperatingSystemFont getUsedFont() { 12740 auto cs = getComputedStyle(); 12741 auto font = cs.font; 12742 if(font is null) { 12743 font = new OperatingSystemFont; 12744 font.loadDefault(); 12745 } 12746 return font; 12747 } 12748 12749 version(win32_widgets) { /* will do it with Windows calls in the classes */ } 12750 else version(custom_widgets) { 12751 // FIXME 12752 version(use_new_text_system) { 12753 TextLayouter textLayout; 12754 12755 void setupCustomTextEditing() { 12756 textLayout = new TextLayouter(defaultTextStyle()); 12757 auto smw = new ScrollMessageWidget(this); 12758 if(!showingHorizontalScroll) 12759 smw.horizontalScrollBar.hide(); 12760 if(!showingVerticalScroll) 12761 smw.verticalScrollBar.hide(); 12762 this.tabStop = false; 12763 smw.tabStop = false; 12764 tdh = textDisplayHelperFactory(textLayout, smw); 12765 12766 this.parentWindow.addEventListener((scope DpiChangedEvent dce) { 12767 if(textLayout) { 12768 if(auto style = cast(TextDisplayHelper.MyTextStyle) textLayout.defaultStyle()) { 12769 // the dpi change can change the font, so this informs the layouter that it has changed too 12770 style.font_ = getUsedFont(); 12771 12772 // arsd.core.writeln(this.parentWindow.win.actualDpi); 12773 } 12774 } 12775 }); 12776 } 12777 12778 } else { 12779 12780 static if(SimpledisplayTimerAvailable) 12781 Timer caretTimer; 12782 etc.TextLayout textLayout; 12783 12784 void setupCustomTextEditing() { 12785 textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4)); 12786 textLayout.selectionXorColor = getComputedStyle().activeListXorColor; 12787 } 12788 12789 override void paint(WidgetPainter painter) { 12790 if(parentWindow.win.closed) return; 12791 12792 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 12793 12794 /* 12795 painter.outlineColor = Color.white; 12796 painter.fillColor = Color.white; 12797 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 12798 */ 12799 12800 painter.outlineColor = Color.black; 12801 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 12802 12803 textLayout.caretShowingOnScreen = false; 12804 12805 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 12806 } 12807 } 12808 12809 static class Style : Widget.Style { 12810 override FrameStyle borderStyle() { 12811 return FrameStyle.sunk; 12812 } 12813 override MouseCursor cursor() { 12814 return GenericCursor.Text; 12815 } 12816 } 12817 mixin OverrideStyle!Style; 12818 } 12819 else static assert(false); 12820 12821 version(trash_text) 12822 version(custom_widgets) 12823 override void defaultEventHandler_mousedown(MouseDownEvent ev) { 12824 super.defaultEventHandler_mousedown(ev); 12825 if(parentWindow.win.closed) return; 12826 if(ev.button == MouseButton.left) { 12827 if(textLayout.selectNone()) 12828 redraw(); 12829 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 12830 this.focus(); 12831 //this.parentWindow.win.grabInput(); 12832 } else if(ev.button == MouseButton.middle) { 12833 static if(UsingSimpledisplayX11) { 12834 getPrimarySelection(parentWindow.win, (in char[] txt) { 12835 textLayout.insert(txt); 12836 redraw(); 12837 12838 auto cbb = textLayout.contentBoundingBox(); 12839 setContentSize(cbb.width, cbb.height); 12840 }); 12841 } 12842 } 12843 } 12844 12845 version(trash_text) 12846 version(custom_widgets) 12847 override void defaultEventHandler_mouseup(MouseUpEvent ev) { 12848 //this.parentWindow.win.releaseInputGrab(); 12849 super.defaultEventHandler_mouseup(ev); 12850 } 12851 12852 version(trash_text) 12853 version(custom_widgets) 12854 override void defaultEventHandler_mousemove(MouseMoveEvent ev) { 12855 super.defaultEventHandler_mousemove(ev); 12856 if(ev.state & ModifierState.leftButtonDown) { 12857 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 12858 redraw(); 12859 } 12860 } 12861 12862 version(trash_text) 12863 version(custom_widgets) 12864 override void defaultEventHandler_focus(Event ev) { 12865 super.defaultEventHandler_focus(ev); 12866 if(parentWindow.win.closed) return; 12867 auto painter = this.draw(); 12868 textLayout.drawCaret(painter); 12869 12870 static if(SimpledisplayTimerAvailable) 12871 if(caretTimer) { 12872 caretTimer.destroy(); 12873 caretTimer = null; 12874 } 12875 12876 bool blinkingCaret = true; 12877 static if(UsingSimpledisplayX11) 12878 if(!Image.impl.xshmAvailable) 12879 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 12880 12881 if(blinkingCaret) 12882 static if(SimpledisplayTimerAvailable) 12883 caretTimer = new Timer(500, { 12884 if(parentWindow.win.closed) { 12885 caretTimer.destroy(); 12886 return; 12887 } 12888 if(isFocused()) { 12889 auto painter = this.draw(); 12890 textLayout.drawCaret(painter); 12891 } else if(textLayout.caretShowingOnScreen) { 12892 auto painter = this.draw(); 12893 textLayout.eraseCaret(painter); 12894 } 12895 }); 12896 } 12897 12898 version(trash_text) { 12899 private string lastContentBlur; 12900 12901 override void defaultEventHandler_blur(Event ev) { 12902 super.defaultEventHandler_blur(ev); 12903 if(parentWindow.win.closed) return; 12904 version(custom_widgets) { 12905 auto painter = this.draw(); 12906 textLayout.eraseCaret(painter); 12907 static if(SimpledisplayTimerAvailable) 12908 if(caretTimer) { 12909 caretTimer.destroy(); 12910 caretTimer = null; 12911 } 12912 } 12913 12914 if(this.content != lastContentBlur) { 12915 auto evt = new ChangeEvent!string(this, &this.content); 12916 evt.dispatch(); 12917 lastContentBlur = this.content; 12918 } 12919 } 12920 } 12921 12922 version(win32_widgets) { 12923 private string lastContentBlur; 12924 12925 override void defaultEventHandler_blur(Event ev) { 12926 super.defaultEventHandler_blur(ev); 12927 12928 if(this.content != lastContentBlur) { 12929 auto evt = new ChangeEvent!string(this, &this.content); 12930 evt.dispatch(); 12931 lastContentBlur = this.content; 12932 } 12933 } 12934 } 12935 12936 12937 version(trash_text) 12938 version(custom_widgets) 12939 override void defaultEventHandler_char(CharEvent ev) { 12940 super.defaultEventHandler_char(ev); 12941 textLayout.insert(ev.character); 12942 redraw(); 12943 12944 // FIXME: too inefficient 12945 auto cbb = textLayout.contentBoundingBox(); 12946 setContentSize(cbb.width, cbb.height); 12947 } 12948 version(trash_text) 12949 version(custom_widgets) 12950 override void defaultEventHandler_keydown(KeyDownEvent ev) { 12951 //super.defaultEventHandler_keydown(ev); 12952 switch(ev.key) { 12953 case Key.Delete: 12954 textLayout.delete_(); 12955 redraw(); 12956 break; 12957 case Key.Left: 12958 textLayout.moveLeft(); 12959 redraw(); 12960 break; 12961 case Key.Right: 12962 textLayout.moveRight(); 12963 redraw(); 12964 break; 12965 case Key.Up: 12966 textLayout.moveUp(); 12967 redraw(); 12968 break; 12969 case Key.Down: 12970 textLayout.moveDown(); 12971 redraw(); 12972 break; 12973 case Key.Home: 12974 textLayout.moveHome(); 12975 redraw(); 12976 break; 12977 case Key.End: 12978 textLayout.moveEnd(); 12979 redraw(); 12980 break; 12981 case Key.PageUp: 12982 foreach(i; 0 .. 32) 12983 textLayout.moveUp(); 12984 redraw(); 12985 break; 12986 case Key.PageDown: 12987 foreach(i; 0 .. 32) 12988 textLayout.moveDown(); 12989 redraw(); 12990 break; 12991 12992 default: 12993 {} // intentionally blank, let "char" handle it 12994 } 12995 /* 12996 if(ev.key == Key.Backspace) { 12997 textLayout.backspace(); 12998 redraw(); 12999 } 13000 */ 13001 ensureVisibleInScroll(textLayout.caretBoundingBox()); 13002 } 13003 13004 version(use_new_text_system) { 13005 bool showingVerticalScroll() { return true; } 13006 bool showingHorizontalScroll() { return true; } 13007 } 13008 } 13009 13010 /// 13011 class LineEdit : EditableTextWidget { 13012 // FIXME: hack 13013 version(custom_widgets) { 13014 override bool showingVerticalScroll() { return false; } 13015 override bool showingHorizontalScroll() { return false; } 13016 } 13017 13018 override int flexBasisWidth() { return 250; } 13019 13020 /// 13021 this(Widget parent) { 13022 super(parent); 13023 version(win32_widgets) { 13024 createWin32Window(this, "edit"w, "", 13025 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 13026 } else version(custom_widgets) { 13027 version(trash_text) { 13028 setupCustomTextEditing(); 13029 addEventListener(delegate(CharEvent ev) { 13030 if(ev.character == '\n') 13031 ev.preventDefault(); 13032 }); 13033 } 13034 } else static assert(false); 13035 } 13036 13037 version(use_new_text_system) 13038 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13039 auto tdh = new TextDisplayHelper(textLayout, smw); 13040 tdh.singleLine = true; 13041 return tdh; 13042 } 13043 13044 version(win32_widgets) { 13045 mixin Padding!q{2}; 13046 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 13047 override int maxHeight() { return minHeight; } 13048 } 13049 13050 /+ 13051 @property void passwordMode(bool p) { 13052 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 13053 } 13054 +/ 13055 } 13056 13057 /++ 13058 A [LineEdit] that displays `*` in place of the actual characters. 13059 13060 Alas, Windows requires the window to be created differently to use this style, 13061 so it had to be a new class instead of a toggle on and off on an existing object. 13062 13063 FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now. 13064 13065 History: 13066 Added January 24, 2021 13067 +/ 13068 class PasswordEdit : EditableTextWidget { 13069 version(custom_widgets) { 13070 override bool showingVerticalScroll() { return false; } 13071 override bool showingHorizontalScroll() { return false; } 13072 } 13073 13074 override int flexBasisWidth() { return 250; } 13075 13076 version(use_new_text_system) 13077 override TextStyle defaultTextStyle() { 13078 auto cs = getComputedStyle(); 13079 13080 auto osf = new class OperatingSystemFont { 13081 this() { 13082 super(cs.font); 13083 } 13084 override int stringWidth(scope const(char)[] text, SimpleWindow window = null) { 13085 int count = 0; 13086 foreach(dchar ch; text) 13087 count++; 13088 return count * super.stringWidth("*", window); 13089 } 13090 }; 13091 13092 return new TextDisplayHelper.MyTextStyle(osf); 13093 } 13094 13095 version(use_new_text_system) 13096 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 13097 static class TDH : TextDisplayHelper { 13098 this(TextLayouter textLayout, ScrollMessageWidget smw) { 13099 singleLine = true; 13100 super(textLayout, smw); 13101 } 13102 13103 override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 13104 char[256] buffer = void; 13105 int bufferLength = 0; 13106 foreach(dchar ch; text) 13107 buffer[bufferLength++] = '*'; 13108 painter.drawText(upperLeft, buffer[0..bufferLength]); 13109 } 13110 } 13111 13112 return new TDH(textLayout, smw); 13113 } 13114 13115 /// 13116 this(Widget parent) { 13117 super(parent); 13118 version(win32_widgets) { 13119 createWin32Window(this, "edit"w, "", 13120 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 13121 } else version(custom_widgets) { 13122 version(trash_text) 13123 setupCustomTextEditing(); 13124 addEventListener(delegate(CharEvent ev) { 13125 if(ev.character == '\n') 13126 ev.preventDefault(); 13127 }); 13128 } else static assert(false); 13129 } 13130 version(win32_widgets) { 13131 mixin Padding!q{2}; 13132 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 13133 override int maxHeight() { return minHeight; } 13134 } 13135 } 13136 13137 13138 /// 13139 class TextEdit : EditableTextWidget { 13140 /// 13141 this(Widget parent) { 13142 super(parent); 13143 version(win32_widgets) { 13144 createWin32Window(this, "edit"w, "", 13145 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 13146 } else version(custom_widgets) { 13147 version(trash_text) 13148 setupCustomTextEditing(); 13149 } else static assert(false); 13150 } 13151 override int maxHeight() { return int.max; } 13152 override int heightStretchiness() { return 7; } 13153 13154 override int flexBasisWidth() { return 250; } 13155 override int flexBasisHeight() { return 25; } 13156 } 13157 13158 13159 /++ 13160 13161 +/ 13162 version(none) 13163 class RichTextDisplay : Widget { 13164 @property void content(string c) {} 13165 void appendContent(string c) {} 13166 } 13167 13168 /// 13169 class MessageBox : Window { 13170 private string message; 13171 MessageBoxButton buttonPressed = MessageBoxButton.None; 13172 /// 13173 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 13174 super(300, 100); 13175 13176 assert(buttons.length); 13177 assert(buttons.length == buttonIds.length); 13178 13179 this.message = message; 13180 13181 auto label = new TextLabel(message, TextAlignment.Center, this); 13182 13183 auto hl = new HorizontalLayout(this); 13184 auto spacer = new HorizontalSpacer(hl); // to right align 13185 13186 foreach(idx, buttonText; buttons) { 13187 auto button = new CommandButton(buttonText, hl); 13188 13189 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 13190 this.buttonPressed = buttonIds[idx]; 13191 win.close(); 13192 }; })(idx)); 13193 13194 if(idx == 0) 13195 button.focus(); 13196 } 13197 13198 if(buttons.length == 1) 13199 auto spacer2 = new HorizontalSpacer(hl); // to center it 13200 13201 win.resize(scaleWithDpi(300), this.minHeight()); 13202 13203 win.show(); 13204 redraw(); 13205 } 13206 13207 mixin Padding!q{16}; 13208 } 13209 13210 /// 13211 enum MessageBoxStyle { 13212 OK, /// 13213 OKCancel, /// 13214 RetryCancel, /// 13215 YesNo, /// 13216 YesNoCancel, /// 13217 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. 13218 } 13219 13220 /// 13221 enum MessageBoxIcon { 13222 None, /// 13223 Info, /// 13224 Warning, /// 13225 Error /// 13226 } 13227 13228 /// Identifies the button the user pressed on a message box. 13229 enum MessageBoxButton { 13230 None, /// The user closed the message box without clicking any of the buttons. 13231 OK, /// 13232 Cancel, /// 13233 Retry, /// 13234 Yes, /// 13235 No, /// 13236 Continue /// 13237 } 13238 13239 13240 /++ 13241 Displays a modal message box, blocking until the user dismisses it. 13242 13243 Returns: the button pressed. 13244 +/ 13245 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13246 version(win32_widgets) { 13247 WCharzBuffer t = WCharzBuffer(title); 13248 WCharzBuffer m = WCharzBuffer(message); 13249 UINT type; 13250 with(MessageBoxStyle) 13251 final switch(style) { 13252 case OK: type |= MB_OK; break; 13253 case OKCancel: type |= MB_OKCANCEL; break; 13254 case RetryCancel: type |= MB_RETRYCANCEL; break; 13255 case YesNo: type |= MB_YESNO; break; 13256 case YesNoCancel: type |= MB_YESNOCANCEL; break; 13257 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 13258 } 13259 with(MessageBoxIcon) 13260 final switch(icon) { 13261 case None: break; 13262 case Info: type |= MB_ICONINFORMATION; break; 13263 case Warning: type |= MB_ICONWARNING; break; 13264 case Error: type |= MB_ICONERROR; break; 13265 } 13266 switch(MessageBoxW(null, m.ptr, t.ptr, type)) { 13267 case IDOK: return MessageBoxButton.OK; 13268 case IDCANCEL: return MessageBoxButton.Cancel; 13269 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 13270 case IDYES: return MessageBoxButton.Yes; 13271 case IDNO: return MessageBoxButton.No; 13272 case IDCONTINUE: return MessageBoxButton.Continue; 13273 default: return MessageBoxButton.None; 13274 } 13275 } else { 13276 string[] buttons; 13277 MessageBoxButton[] buttonIds; 13278 with(MessageBoxStyle) 13279 final switch(style) { 13280 case OK: 13281 buttons = ["OK"]; 13282 buttonIds = [MessageBoxButton.OK]; 13283 break; 13284 case OKCancel: 13285 buttons = ["OK", "Cancel"]; 13286 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 13287 break; 13288 case RetryCancel: 13289 buttons = ["Retry", "Cancel"]; 13290 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 13291 break; 13292 case YesNo: 13293 buttons = ["Yes", "No"]; 13294 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 13295 break; 13296 case YesNoCancel: 13297 buttons = ["Yes", "No", "Cancel"]; 13298 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 13299 break; 13300 case RetryCancelContinue: 13301 buttons = ["Try Again", "Cancel", "Continue"]; 13302 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 13303 break; 13304 } 13305 auto mb = new MessageBox(message, buttons, buttonIds); 13306 EventLoop el = EventLoop.get; 13307 el.run(() { return !mb.win.closed; }); 13308 return mb.buttonPressed; 13309 } 13310 } 13311 13312 /// ditto 13313 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13314 return messageBox(null, message, style, icon); 13315 } 13316 13317 13318 13319 /// 13320 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 13321 13322 /++ 13323 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 13324 13325 History: 13326 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. 13327 +/ 13328 struct EventListener { 13329 private Widget widget; 13330 private string event; 13331 private EventHandler handler; 13332 private bool useCapture; 13333 13334 /// 13335 void disconnect() { 13336 widget.removeEventListener(this); 13337 } 13338 } 13339 13340 /++ 13341 The purpose of this enum was to give a compile-time checked version of various standard event strings. 13342 13343 Now, I recommend you use a statically typed event object instead. 13344 13345 See_Also: [Event] 13346 +/ 13347 enum EventType : string { 13348 click = "click", /// 13349 13350 mouseenter = "mouseenter", /// 13351 mouseleave = "mouseleave", /// 13352 mousein = "mousein", /// 13353 mouseout = "mouseout", /// 13354 mouseup = "mouseup", /// 13355 mousedown = "mousedown", /// 13356 mousemove = "mousemove", /// 13357 13358 keydown = "keydown", /// 13359 keyup = "keyup", /// 13360 char_ = "char", /// 13361 13362 focus = "focus", /// 13363 blur = "blur", /// 13364 13365 triggered = "triggered", /// 13366 13367 change = "change", /// 13368 } 13369 13370 /++ 13371 Represents an event that is currently being processed. 13372 13373 13374 Minigui's event model is based on the web browser. An event has a name, a target, 13375 and an associated data object. It starts from the window and works its way down through 13376 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 13377 then goes back up again all the way back to the window, triggering bubble phase handlers. At 13378 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 13379 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 13380 was called; that just stops it from calling other handlers in the widget tree, but the default happens 13381 whenever propagation is done, not only if it gets to the end of the chain). 13382 13383 This model has several nice points: 13384 13385 $(LIST 13386 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 13387 with event handlers set, then add/remove children as much as you want without needing 13388 to manage the event handlers on them - the parent alone can manage everything. 13389 13390 * It is easy to create new custom events in your application. 13391 13392 * It is familiar to many web developers. 13393 ) 13394 13395 There's a few downsides though: 13396 13397 $(LIST 13398 * There's not a lot of type safety. 13399 13400 * You don't get a static list of what events a widget can emit. 13401 13402 * Tracing where an event got cancelled along the chain can get difficult; the downside of 13403 the central delegation benefit is it can be lead to debugging of action at a distance. 13404 ) 13405 13406 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 13407 while keeping the benefits - and most compatibility with - the existing model. The main idea is 13408 to simply use a D object type which provides a static interface as well as a built-in event name. 13409 Then, a new static interface allows you to see what an event can emit and attach handlers to it 13410 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 13411 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 13412 to having a little more help from the D compiler and documentation generator. 13413 13414 Your code would change like this: 13415 13416 --- 13417 // old 13418 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 13419 13420 // new 13421 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 13422 --- 13423 13424 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. 13425 13426 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 13427 13428 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 13429 13430 Thus the family of functions are: 13431 13432 [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. 13433 13434 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 13435 13436 [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. 13437 13438 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 13439 13440 --- 13441 class MyCheckbox : Widget { 13442 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 13443 /// It is NOT actually required but should be used whenever possible. 13444 mixin Emits!(ChangeEvent!bool); 13445 13446 this(Widget parent) { 13447 super(parent); 13448 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 13449 } 13450 13451 private bool _checked; 13452 @property bool checked() { return _checked; } 13453 @property void checked(bool set) { 13454 _checked = set; 13455 emit!(ChangeEvent!bool)(&checked); 13456 } 13457 } 13458 --- 13459 13460 ## Creating Your Own Events 13461 13462 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. 13463 13464 --- 13465 class MyEvent : Event { 13466 this(Widget target) { super(EventString, target); } 13467 mixin Register; // adds EventString and other reflection information 13468 } 13469 --- 13470 13471 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 13472 13473 History: 13474 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. 13475 13476 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. 13477 +/ 13478 /+ 13479 13480 ## General Conventions 13481 13482 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. 13483 13484 13485 ## Qt-style signals and slots 13486 13487 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. 13488 13489 The intention is for events to be used when 13490 13491 --- 13492 class Demo : Widget { 13493 this() { 13494 myPropertyChanged = Signal!int(this); 13495 } 13496 @property myProperty(int v) { 13497 myPropertyChanged.emit(v); 13498 } 13499 13500 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 13501 // but it can just genuinely not care about `this` since that's not really passed. 13502 } 13503 13504 class Foo : Widget { 13505 // the slot uda is not necessary, but it helps the script and ui builder find it. 13506 @slot void setValue(int v) { ... } 13507 } 13508 13509 demo.myPropertyChanged.connect(&foo.setValue); 13510 --- 13511 13512 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 13513 13514 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 13515 13516 class StringChangeEvent : ChangeEvent, Signal!string { 13517 mixin SignalImpl 13518 } 13519 13520 +/ 13521 class Event : ReflectableProperties { 13522 /// Creates an event without populating any members and without sending it. See [dispatch] 13523 this(string eventName, Widget emittedBy) { 13524 this.eventName = eventName; 13525 this.srcElement = emittedBy; 13526 } 13527 13528 13529 /// Implementations for the [ReflectableProperties] interface/ 13530 void getPropertiesList(scope void delegate(string name) sink) const {} 13531 /// ditto 13532 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 13533 /// ditto 13534 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 13535 return SetPropertyResult.notPermitted; 13536 } 13537 13538 13539 /+ 13540 /++ 13541 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 13542 13543 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. 13544 +/ 13545 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 13546 if(value.length == 0) { 13547 finalSink(memberName, `""`); 13548 return; 13549 } 13550 13551 char[1024] bufferBacking; 13552 char[] buffer = bufferBacking; 13553 int bufferPosition; 13554 13555 void sink(char ch) { 13556 if(bufferPosition >= buffer.length) 13557 buffer.length = buffer.length + 1024; 13558 buffer[bufferPosition++] = ch; 13559 } 13560 13561 sink('"'); 13562 13563 foreach(ch; value) { 13564 switch(ch) { 13565 case '\\': 13566 sink('\\'); sink('\\'); 13567 break; 13568 case '"': 13569 sink('\\'); sink('"'); 13570 break; 13571 case '\n': 13572 sink('\\'); sink('n'); 13573 break; 13574 case '\r': 13575 sink('\\'); sink('r'); 13576 break; 13577 case '\t': 13578 sink('\\'); sink('t'); 13579 break; 13580 default: 13581 sink(ch); 13582 } 13583 } 13584 13585 sink('"'); 13586 13587 finalSink(memberName, buffer[0 .. bufferPosition]); 13588 } 13589 +/ 13590 13591 /+ 13592 enum EventInitiator { 13593 system, 13594 minigui, 13595 user 13596 } 13597 13598 immutable EventInitiator; initiatedBy; 13599 +/ 13600 13601 /++ 13602 Events should generally follow the propagation model, but there's some exceptions 13603 to that rule. If so, they should override this to return false. In that case, only 13604 bubbling event handlers on the target itself and capturing event handlers on the containing 13605 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 13606 capture -> target -> bubble process.) 13607 13608 History: 13609 Added May 12, 2021 13610 +/ 13611 bool propagates() const pure nothrow @nogc @safe { 13612 return true; 13613 } 13614 13615 /++ 13616 hints as to whether preventDefault will actually do anything. not entirely reliable. 13617 13618 History: 13619 Added May 14, 2021 13620 +/ 13621 bool cancelable() const pure nothrow @nogc @safe { 13622 return true; 13623 } 13624 13625 /++ 13626 You can mix this into child class to register some boilerplate. It includes the `EventString` 13627 member, a constructor, and implementations of the dynamic get data interfaces. 13628 13629 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 13630 13631 13632 You can override the default EventString by simply providing your own in the form of 13633 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 13634 which provides some namespace protection against conflicts in other libraries while still being fairly 13635 easy to use. 13636 13637 If you provide your own constructor, it will override the default constructor provided here. A constructor 13638 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 13639 first argument to your constructor. 13640 13641 History: 13642 Added May 13, 2021. 13643 +/ 13644 protected static mixin template Register() { 13645 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 13646 this(Widget target) { super(EventString, target); } 13647 13648 mixin ReflectableProperties.RegisterGetters; 13649 } 13650 13651 /++ 13652 This is the widget that emitted the event. 13653 13654 13655 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 13656 13657 History: 13658 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 13659 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 13660 so I don't intend to remove these aliases. 13661 +/ 13662 Widget source; 13663 /// ditto 13664 alias source target; 13665 /// ditto 13666 alias source srcElement; 13667 13668 Widget relatedTarget; /// Note: likely to be deprecated at some point. 13669 13670 /// Prevents the default event handler (if there is one) from being called 13671 void preventDefault() { 13672 lastDefaultPrevented = true; 13673 defaultPrevented = true; 13674 } 13675 13676 /// Stops the event propagation immediately. 13677 void stopPropagation() { 13678 propagationStopped = true; 13679 } 13680 13681 private bool defaultPrevented; 13682 private bool propagationStopped; 13683 private string eventName; 13684 13685 private bool isBubbling; 13686 13687 /// 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. 13688 protected void adjustScrolling() { } 13689 /// ditto 13690 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 13691 13692 /++ 13693 this sends it only to the target. If you want propagation, use dispatch() instead. 13694 13695 This should be made private!!! 13696 13697 +/ 13698 void sendDirectly() { 13699 if(srcElement is null) 13700 return; 13701 13702 // 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. 13703 13704 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 13705 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 13706 13707 adjustScrolling(); 13708 13709 if(auto e = target.parentWindow) { 13710 if(auto handlers = "*" in e.capturingEventHandlers) 13711 foreach(handler; *handlers) 13712 if(handler) handler(e, this); 13713 if(auto handlers = eventName in e.capturingEventHandlers) 13714 foreach(handler; *handlers) 13715 if(handler) handler(e, this); 13716 } 13717 13718 auto e = srcElement; 13719 13720 if(auto handlers = eventName in e.bubblingEventHandlers) 13721 foreach(handler; *handlers) 13722 if(handler) handler(e, this); 13723 13724 if(auto handlers = "*" in e.bubblingEventHandlers) 13725 foreach(handler; *handlers) 13726 if(handler) handler(e, this); 13727 13728 // there's never a default for a catch-all event 13729 if(!defaultPrevented) 13730 if(eventName in e.defaultEventHandlers) 13731 e.defaultEventHandlers[eventName](e, this); 13732 } 13733 13734 /// this dispatches the element using the capture -> target -> bubble process 13735 void dispatch() { 13736 if(srcElement is null) 13737 return; 13738 13739 if(!propagates) { 13740 sendDirectly; 13741 return; 13742 } 13743 13744 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 13745 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 13746 13747 adjustScrolling(); 13748 // first capture, then bubble 13749 13750 Widget[] chain; 13751 Widget curr = srcElement; 13752 while(curr) { 13753 auto l = curr; 13754 chain ~= l; 13755 curr = curr.parent; 13756 } 13757 13758 isBubbling = false; 13759 13760 foreach_reverse(e; chain) { 13761 if(auto handlers = "*" in e.capturingEventHandlers) 13762 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13763 13764 if(propagationStopped) 13765 break; 13766 13767 if(auto handlers = eventName in e.capturingEventHandlers) 13768 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13769 13770 // the default on capture should really be to always do nothing 13771 13772 //if(!defaultPrevented) 13773 // if(eventName in e.defaultEventHandlers) 13774 // e.defaultEventHandlers[eventName](e.element, this); 13775 13776 if(propagationStopped) 13777 break; 13778 } 13779 13780 int adjustX; 13781 int adjustY; 13782 13783 isBubbling = true; 13784 if(!propagationStopped) 13785 foreach(e; chain) { 13786 if(auto handlers = eventName in e.bubblingEventHandlers) 13787 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13788 13789 if(propagationStopped) 13790 break; 13791 13792 if(auto handlers = "*" in e.bubblingEventHandlers) 13793 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13794 13795 if(propagationStopped) 13796 break; 13797 13798 if(e.encapsulatedChildren()) { 13799 adjustClientCoordinates(adjustX, adjustY); 13800 target = e; 13801 } else { 13802 adjustX += e.x; 13803 adjustY += e.y; 13804 } 13805 } 13806 13807 if(!defaultPrevented) 13808 foreach(e; chain) { 13809 if(eventName in e.defaultEventHandlers) 13810 e.defaultEventHandlers[eventName](e, this); 13811 } 13812 } 13813 13814 13815 /* old compatibility things */ 13816 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 13817 final @property { 13818 Key key() { return (cast(KeyEventBase) this).key; } 13819 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 13820 13821 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 13822 bool altKey() { return (cast(KeyEventBase) this).altKey; } 13823 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 13824 } 13825 13826 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 13827 final @property { 13828 int clientX() { return (cast(MouseEventBase) this).clientX; } 13829 int clientY() { return (cast(MouseEventBase) this).clientY; } 13830 13831 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 13832 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 13833 13834 int button() { return (cast(MouseEventBase) this).button; } 13835 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 13836 } 13837 13838 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 13839 final @property { 13840 int state() { 13841 if(auto meb = cast(MouseEventBase) this) 13842 return meb.state; 13843 if(auto keb = cast(KeyEventBase) this) 13844 return keb.state; 13845 assert(0); 13846 } 13847 } 13848 13849 deprecated("Use a CharEvent instead of Event in your handler going forward") 13850 final @property { 13851 dchar character() { 13852 if(auto ce = cast(CharEvent) this) 13853 return ce.character; 13854 return dchar.init; 13855 } 13856 } 13857 13858 // for change events 13859 @property { 13860 /// 13861 int intValue() { return 0; } 13862 /// 13863 string stringValue() { return null; } 13864 } 13865 } 13866 13867 /++ 13868 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 13869 13870 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 13871 dynamic and custom events, but the static list helps ensure you get them right. 13872 13873 If this is declared, you can use [Widget.emit] to send the event. 13874 13875 All events work the same way though, following the capture->widget->bubble model described under [Event]. 13876 13877 History: 13878 Added May 4, 2021 13879 +/ 13880 mixin template Emits(EventType) { 13881 import arsd.minigui : EventString; 13882 static if(is(EventType : Event) && !is(EventType == Event)) 13883 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 13884 else 13885 static assert(0, "You can only emit subclasses of Event"); 13886 } 13887 13888 /// ditto 13889 mixin template Emits(string eventString) { 13890 mixin("private Event[0] emits_" ~ eventString ~";"); 13891 } 13892 13893 /* 13894 class SignalEvent(string name) : Event { 13895 13896 } 13897 */ 13898 13899 /++ 13900 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". 13901 13902 13903 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. 13904 13905 History: 13906 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 13907 +/ 13908 class CommandEvent : Event { 13909 enum EventString = "command"; 13910 this(Widget source, string CommandString = EventString) { 13911 super(CommandString, source); 13912 } 13913 } 13914 13915 /++ 13916 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 13917 +/ 13918 class CommandEventWithArgs(Args...) : CommandEvent { 13919 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 13920 Args args; 13921 } 13922 13923 /++ 13924 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. 13925 13926 See [CommandEvent] for more information. 13927 13928 Returns: 13929 The [EventListener] you can use to remove the handler. 13930 +/ 13931 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 13932 return w.addEventListener(CommandString, (Event ev) { 13933 if(ev.target is w) 13934 return; // it does not consume its own commands! 13935 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 13936 handler(cev.args); 13937 ev.stopPropagation(); 13938 } 13939 }); 13940 } 13941 13942 /++ 13943 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. 13944 +/ 13945 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 13946 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 13947 event.dispatch(); 13948 } 13949 13950 class ResizeEvent : Event { 13951 enum EventString = "resize"; 13952 13953 this(Widget target) { super(EventString, target); } 13954 13955 override bool propagates() const { return false; } 13956 } 13957 13958 /++ 13959 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 13960 13961 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. 13962 13963 History: 13964 Added June 21, 2021 (dub v10.1) 13965 +/ 13966 class ClosingEvent : Event { 13967 enum EventString = "closing"; 13968 13969 this(Widget target) { super(EventString, target); } 13970 13971 override bool propagates() const { return false; } 13972 override bool cancelable() const { return true; } 13973 } 13974 13975 /// ditto 13976 class ClosedEvent : Event { 13977 enum EventString = "closed"; 13978 13979 this(Widget target) { super(EventString, target); } 13980 13981 override bool propagates() const { return false; } 13982 override bool cancelable() const { return false; } 13983 } 13984 13985 /// 13986 class BlurEvent : Event { 13987 enum EventString = "blur"; 13988 13989 // FIXME: related target? 13990 this(Widget target) { super(EventString, target); } 13991 13992 override bool propagates() const { return false; } 13993 } 13994 13995 /// 13996 class FocusEvent : Event { 13997 enum EventString = "focus"; 13998 13999 // FIXME: related target? 14000 this(Widget target) { super(EventString, target); } 14001 14002 override bool propagates() const { return false; } 14003 } 14004 14005 /++ 14006 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 14007 14008 History: 14009 Added July 3, 2021 14010 +/ 14011 class FocusInEvent : Event { 14012 enum EventString = "focusin"; 14013 14014 // FIXME: related target? 14015 this(Widget target) { super(EventString, target); } 14016 14017 override bool cancelable() const { return false; } 14018 } 14019 14020 /// ditto 14021 class FocusOutEvent : Event { 14022 enum EventString = "focusout"; 14023 14024 // FIXME: related target? 14025 this(Widget target) { super(EventString, target); } 14026 14027 override bool cancelable() const { return false; } 14028 } 14029 14030 /// 14031 class ScrollEvent : Event { 14032 enum EventString = "scroll"; 14033 this(Widget target) { super(EventString, target); } 14034 14035 override bool cancelable() const { return false; } 14036 } 14037 14038 /++ 14039 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 14040 14041 History: 14042 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 14043 +/ 14044 class CharEvent : Event { 14045 enum EventString = "char"; 14046 this(Widget target, dchar ch) { 14047 character = ch; 14048 super(EventString, target); 14049 } 14050 14051 immutable dchar character; 14052 } 14053 14054 /++ 14055 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 14056 +/ 14057 abstract class ChangeEventBase : Event { 14058 enum EventString = "change"; 14059 this(Widget target) { 14060 super(EventString, target); 14061 } 14062 14063 /+ 14064 // idk where or how exactly i want to do this. 14065 // i might come back to it later. 14066 14067 // If a widget itself broadcasts one of theses itself, it stops propagation going down 14068 // this way the source doesn't get too confused (think of a nested scroll widget) 14069 // 14070 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 14071 // then you consume that command and change you scroll x position to whatever. then you do 14072 // some kind of change event that is broadcast back to the children and any horizontal scroll 14073 // listeners are now able to update, without having an explicit connection between them. 14074 void broadcastToChildren(string fieldName) { 14075 14076 } 14077 +/ 14078 } 14079 14080 /++ 14081 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. 14082 14083 14084 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). 14085 14086 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);` 14087 14088 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 14089 14090 History: 14091 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. 14092 +/ 14093 class ChangeEvent(T) : ChangeEventBase { 14094 this(Widget target, T delegate() getNewValue) { 14095 assert(getNewValue !is null); 14096 this.getNewValue = getNewValue; 14097 super(target); 14098 } 14099 14100 private T delegate() getNewValue; 14101 14102 /++ 14103 Gets the new value that just changed. 14104 +/ 14105 @property T value() { 14106 return getNewValue(); 14107 } 14108 14109 /// compatibility method for old generic Events 14110 static if(is(immutable T == immutable int)) 14111 override int intValue() { return value; } 14112 /// ditto 14113 static if(is(immutable T == immutable string)) 14114 override string stringValue() { return value; } 14115 } 14116 14117 /++ 14118 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 14119 14120 14121 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14122 14123 History: 14124 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14125 +/ 14126 abstract class KeyEventBase : Event { 14127 this(string name, Widget target) { 14128 super(name, target); 14129 } 14130 14131 // for key events 14132 Key key; /// 14133 14134 KeyEvent originalKeyEvent; 14135 14136 /++ 14137 Indicates the current state of the given keyboard modifier keys. 14138 14139 History: 14140 Added to events on April 15, 2020. 14141 +/ 14142 bool ctrlKey; 14143 14144 /// ditto 14145 bool altKey; 14146 14147 /// ditto 14148 bool shiftKey; 14149 14150 /++ 14151 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 14152 14153 See [arsd.simpledisplay.ModifierState] for other possible flags. 14154 +/ 14155 int state; 14156 14157 mixin Register; 14158 } 14159 14160 /++ 14161 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]. 14162 14163 14164 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14165 14166 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. 14167 14168 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. 14169 14170 See_Also: [KeyUpEvent], [CharEvent] 14171 14172 History: 14173 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 14174 +/ 14175 class KeyDownEvent : KeyEventBase { 14176 enum EventString = "keydown"; 14177 this(Widget target) { super(EventString, target); } 14178 } 14179 14180 /++ 14181 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 14182 14183 14184 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14185 14186 See_Also: [KeyDownEvent], [CharEvent] 14187 14188 History: 14189 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 14190 +/ 14191 class KeyUpEvent : KeyEventBase { 14192 enum EventString = "keyup"; 14193 this(Widget target) { super(EventString, target); } 14194 } 14195 14196 /++ 14197 Contains shared properties for various mouse events; 14198 14199 14200 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14201 14202 History: 14203 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14204 +/ 14205 abstract class MouseEventBase : Event { 14206 this(string name, Widget target) { 14207 super(name, target); 14208 } 14209 14210 // for mouse events 14211 int clientX; /// The mouse event location relative to the target widget 14212 int clientY; /// ditto 14213 14214 int viewportX; /// The mouse event location relative to the window origin 14215 int viewportY; /// ditto 14216 14217 int button; /// See: [MouseEvent.button] 14218 int buttonLinear; /// See: [MouseEvent.buttonLinear] 14219 14220 /++ 14221 Indicates the current state of the given keyboard modifier keys. 14222 14223 History: 14224 Added to mouse events on September 28, 2010. 14225 +/ 14226 bool ctrlKey; 14227 14228 /// ditto 14229 bool altKey; 14230 14231 /// ditto 14232 bool shiftKey; 14233 14234 14235 14236 int state; /// 14237 14238 /++ 14239 for consistent names with key event. 14240 14241 History: 14242 Added September 28, 2021 (dub v10.3) 14243 +/ 14244 alias modifierState = state; 14245 14246 /++ 14247 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 14248 14249 History: 14250 Added May 15, 2021 14251 +/ 14252 bool isMouseWheel() { 14253 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 14254 } 14255 14256 // private 14257 override void adjustClientCoordinates(int deltaX, int deltaY) { 14258 clientX += deltaX; 14259 clientY += deltaY; 14260 } 14261 14262 override void adjustScrolling() { 14263 version(custom_widgets) { // TEMP 14264 viewportX = clientX; 14265 viewportY = clientY; 14266 if(auto se = cast(ScrollableWidget) srcElement) { 14267 clientX += se.scrollOrigin.x; 14268 clientY += se.scrollOrigin.y; 14269 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 14270 //clientX += se.scrollX_; 14271 //clientY += se.scrollY_; 14272 } 14273 } 14274 } 14275 14276 mixin Register; 14277 } 14278 14279 /++ 14280 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 14281 14282 14283 $(WARNING 14284 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 14285 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 14286 behavior. 14287 ) 14288 14289 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 14290 14291 [MouseUpEvent] is sent when the user releases a mouse button. 14292 14293 [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.) 14294 14295 [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. 14296 14297 [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. 14298 14299 [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. 14300 14301 [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. 14302 14303 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 14304 14305 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 14306 14307 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14308 14309 Rationale: 14310 14311 If you only want to do drag, mousedown/up works just fine being consistently sent. 14312 14313 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). 14314 14315 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. 14316 14317 History: 14318 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. 14319 +/ 14320 class MouseUpEvent : MouseEventBase { 14321 enum EventString = "mouseup"; /// 14322 this(Widget target) { super(EventString, target); } 14323 } 14324 /// ditto 14325 class MouseDownEvent : MouseEventBase { 14326 enum EventString = "mousedown"; /// 14327 this(Widget target) { super(EventString, target); } 14328 } 14329 /// ditto 14330 class MouseMoveEvent : MouseEventBase { 14331 enum EventString = "mousemove"; /// 14332 this(Widget target) { super(EventString, target); } 14333 } 14334 /// ditto 14335 class ClickEvent : MouseEventBase { 14336 enum EventString = "click"; /// 14337 this(Widget target) { super(EventString, target); } 14338 } 14339 /// ditto 14340 class DoubleClickEvent : MouseEventBase { 14341 enum EventString = "dblclick"; /// 14342 this(Widget target) { super(EventString, target); } 14343 } 14344 /// ditto 14345 class MouseOverEvent : Event { 14346 enum EventString = "mouseover"; /// 14347 this(Widget target) { super(EventString, target); } 14348 } 14349 /// ditto 14350 class MouseOutEvent : Event { 14351 enum EventString = "mouseout"; /// 14352 this(Widget target) { super(EventString, target); } 14353 } 14354 /// ditto 14355 class MouseEnterEvent : Event { 14356 enum EventString = "mouseenter"; /// 14357 this(Widget target) { super(EventString, target); } 14358 14359 override bool propagates() const { return false; } 14360 } 14361 /// ditto 14362 class MouseLeaveEvent : Event { 14363 enum EventString = "mouseleave"; /// 14364 this(Widget target) { super(EventString, target); } 14365 14366 override bool propagates() const { return false; } 14367 } 14368 14369 private bool isAParentOf(Widget a, Widget b) { 14370 if(a is null || b is null) 14371 return false; 14372 14373 while(b !is null) { 14374 if(a is b) 14375 return true; 14376 b = b.parent; 14377 } 14378 14379 return false; 14380 } 14381 14382 private struct WidgetAtPointResponse { 14383 Widget widget; 14384 14385 // x, y relative to the widget in the response. 14386 int x; 14387 int y; 14388 } 14389 14390 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 14391 assert(starting !is null); 14392 14393 starting.addScrollPosition(x, y); 14394 14395 auto child = starting.getChildAtPosition(x, y); 14396 while(child) { 14397 if(child.hidden) 14398 continue; 14399 starting = child; 14400 x -= child.x; 14401 y -= child.y; 14402 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 14403 child = r.widget; 14404 if(child is starting) 14405 break; 14406 } 14407 return WidgetAtPointResponse(starting, x, y); 14408 } 14409 14410 version(win32_widgets) { 14411 private: 14412 import core.sys.windows.commctrl; 14413 14414 pragma(lib, "comctl32"); 14415 shared static this() { 14416 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 14417 INITCOMMONCONTROLSEX ic; 14418 ic.dwSize = cast(DWORD) ic.sizeof; 14419 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 14420 if(!InitCommonControlsEx(&ic)) { 14421 //writeln("ICC failed"); 14422 } 14423 } 14424 14425 14426 // everything from here is just win32 headers copy pasta 14427 private: 14428 extern(Windows): 14429 14430 alias HANDLE HMENU; 14431 HMENU CreateMenu(); 14432 bool SetMenu(HWND, HMENU); 14433 HMENU CreatePopupMenu(); 14434 enum MF_POPUP = 0x10; 14435 enum MF_STRING = 0; 14436 14437 14438 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 14439 struct INITCOMMONCONTROLSEX { 14440 DWORD dwSize; 14441 DWORD dwICC; 14442 } 14443 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 14444 enum { 14445 IDB_STD_SMALL_COLOR, 14446 IDB_STD_LARGE_COLOR, 14447 IDB_VIEW_SMALL_COLOR = 4, 14448 IDB_VIEW_LARGE_COLOR = 5 14449 } 14450 enum { 14451 STD_CUT, 14452 STD_COPY, 14453 STD_PASTE, 14454 STD_UNDO, 14455 STD_REDOW, 14456 STD_DELETE, 14457 STD_FILENEW, 14458 STD_FILEOPEN, 14459 STD_FILESAVE, 14460 STD_PRINTPRE, 14461 STD_PROPERTIES, 14462 STD_HELP, 14463 STD_FIND, 14464 STD_REPLACE, 14465 STD_PRINT // = 14 14466 } 14467 14468 alias HANDLE HIMAGELIST; 14469 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 14470 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 14471 BOOL ImageList_Destroy(HIMAGELIST); 14472 14473 uint MAKELONG(ushort a, ushort b) { 14474 return cast(uint) ((b << 16) | a); 14475 } 14476 14477 14478 struct TBBUTTON { 14479 int iBitmap; 14480 int idCommand; 14481 BYTE fsState; 14482 BYTE fsStyle; 14483 version(Win64) 14484 BYTE[6] bReserved; 14485 else 14486 BYTE[2] bReserved; 14487 DWORD dwData; 14488 INT_PTR iString; 14489 } 14490 14491 enum { 14492 TB_ADDBUTTONSA = WM_USER + 20, 14493 TB_INSERTBUTTONA = WM_USER + 21, 14494 TB_GETIDEALSIZE = WM_USER + 99, 14495 } 14496 14497 struct SIZE { 14498 LONG cx; 14499 LONG cy; 14500 } 14501 14502 14503 enum { 14504 TBSTATE_CHECKED = 1, 14505 TBSTATE_PRESSED = 2, 14506 TBSTATE_ENABLED = 4, 14507 TBSTATE_HIDDEN = 8, 14508 TBSTATE_INDETERMINATE = 16, 14509 TBSTATE_WRAP = 32 14510 } 14511 14512 14513 14514 enum { 14515 ILC_COLOR = 0, 14516 ILC_COLOR4 = 4, 14517 ILC_COLOR8 = 8, 14518 ILC_COLOR16 = 16, 14519 ILC_COLOR24 = 24, 14520 ILC_COLOR32 = 32, 14521 ILC_COLORDDB = 254, 14522 ILC_MASK = 1, 14523 ILC_PALETTE = 2048 14524 } 14525 14526 14527 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 14528 14529 14530 enum { 14531 TB_ENABLEBUTTON = WM_USER + 1, 14532 TB_CHECKBUTTON, 14533 TB_PRESSBUTTON, 14534 TB_HIDEBUTTON, 14535 TB_INDETERMINATE, // = WM_USER + 5, 14536 TB_ISBUTTONENABLED = WM_USER + 9, 14537 TB_ISBUTTONCHECKED, 14538 TB_ISBUTTONPRESSED, 14539 TB_ISBUTTONHIDDEN, 14540 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 14541 TB_SETSTATE = WM_USER + 17, 14542 TB_GETSTATE = WM_USER + 18, 14543 TB_ADDBITMAP = WM_USER + 19, 14544 TB_DELETEBUTTON = WM_USER + 22, 14545 TB_GETBUTTON, 14546 TB_BUTTONCOUNT, 14547 TB_COMMANDTOINDEX, 14548 TB_SAVERESTOREA, 14549 TB_CUSTOMIZE, 14550 TB_ADDSTRINGA, 14551 TB_GETITEMRECT, 14552 TB_BUTTONSTRUCTSIZE, 14553 TB_SETBUTTONSIZE, 14554 TB_SETBITMAPSIZE, 14555 TB_AUTOSIZE, // = WM_USER + 33, 14556 TB_GETTOOLTIPS = WM_USER + 35, 14557 TB_SETTOOLTIPS = WM_USER + 36, 14558 TB_SETPARENT = WM_USER + 37, 14559 TB_SETROWS = WM_USER + 39, 14560 TB_GETROWS, 14561 TB_GETBITMAPFLAGS, 14562 TB_SETCMDID, 14563 TB_CHANGEBITMAP, 14564 TB_GETBITMAP, 14565 TB_GETBUTTONTEXTA, 14566 TB_REPLACEBITMAP, // = WM_USER + 46, 14567 TB_GETBUTTONSIZE = WM_USER + 58, 14568 TB_SETBUTTONWIDTH = WM_USER + 59, 14569 TB_GETBUTTONTEXTW = WM_USER + 75, 14570 TB_SAVERESTOREW = WM_USER + 76, 14571 TB_ADDSTRINGW = WM_USER + 77, 14572 } 14573 14574 extern(Windows) 14575 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 14576 14577 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 14578 14579 14580 enum { 14581 TB_SETINDENT = WM_USER + 47, 14582 TB_SETIMAGELIST, 14583 TB_GETIMAGELIST, 14584 TB_LOADIMAGES, 14585 TB_GETRECT, 14586 TB_SETHOTIMAGELIST, 14587 TB_GETHOTIMAGELIST, 14588 TB_SETDISABLEDIMAGELIST, 14589 TB_GETDISABLEDIMAGELIST, 14590 TB_SETSTYLE, 14591 TB_GETSTYLE, 14592 //TB_GETBUTTONSIZE, 14593 //TB_SETBUTTONWIDTH, 14594 TB_SETMAXTEXTROWS, 14595 TB_GETTEXTROWS // = WM_USER + 61 14596 } 14597 14598 enum { 14599 CCM_FIRST = 0x2000, 14600 CCM_LAST = CCM_FIRST + 0x200, 14601 CCM_SETBKCOLOR = 8193, 14602 CCM_SETCOLORSCHEME = 8194, 14603 CCM_GETCOLORSCHEME = 8195, 14604 CCM_GETDROPTARGET = 8196, 14605 CCM_SETUNICODEFORMAT = 8197, 14606 CCM_GETUNICODEFORMAT = 8198, 14607 CCM_SETVERSION = 0x2007, 14608 CCM_GETVERSION = 0x2008, 14609 CCM_SETNOTIFYWINDOW = 0x2009 14610 } 14611 14612 14613 enum { 14614 PBM_SETRANGE = WM_USER + 1, 14615 PBM_SETPOS, 14616 PBM_DELTAPOS, 14617 PBM_SETSTEP, 14618 PBM_STEPIT, // = WM_USER + 5 14619 PBM_SETRANGE32 = 1030, 14620 PBM_GETRANGE, 14621 PBM_GETPOS, 14622 PBM_SETBARCOLOR, // = 1033 14623 PBM_SETBKCOLOR = CCM_SETBKCOLOR 14624 } 14625 14626 enum { 14627 PBS_SMOOTH = 1, 14628 PBS_VERTICAL = 4 14629 } 14630 14631 enum { 14632 ICC_LISTVIEW_CLASSES = 1, 14633 ICC_TREEVIEW_CLASSES = 2, 14634 ICC_BAR_CLASSES = 4, 14635 ICC_TAB_CLASSES = 8, 14636 ICC_UPDOWN_CLASS = 16, 14637 ICC_PROGRESS_CLASS = 32, 14638 ICC_HOTKEY_CLASS = 64, 14639 ICC_ANIMATE_CLASS = 128, 14640 ICC_WIN95_CLASSES = 255, 14641 ICC_DATE_CLASSES = 256, 14642 ICC_USEREX_CLASSES = 512, 14643 ICC_COOL_CLASSES = 1024, 14644 ICC_STANDARD_CLASSES = 0x00004000, 14645 } 14646 14647 enum WM_USER = 1024; 14648 } 14649 14650 version(win32_widgets) 14651 pragma(lib, "comdlg32"); 14652 14653 14654 /// 14655 enum GenericIcons : ushort { 14656 None, /// 14657 // these happen to match the win32 std icons numerically if you just subtract one from the value 14658 Cut, /// 14659 Copy, /// 14660 Paste, /// 14661 Undo, /// 14662 Redo, /// 14663 Delete, /// 14664 New, /// 14665 Open, /// 14666 Save, /// 14667 PrintPreview, /// 14668 Properties, /// 14669 Help, /// 14670 Find, /// 14671 Replace, /// 14672 Print, /// 14673 } 14674 14675 enum FileDialogType { 14676 Automatic, 14677 Open, 14678 Save 14679 } 14680 string previousFileReferenced; 14681 14682 /++ 14683 Used in automatic menu functions to indicate that the user should be able to browse for a file. 14684 14685 Params: 14686 storage = an alias to a `static string` variable that stores the last file referenced. It will 14687 use this to pre-fill the dialog with a suggestion. 14688 14689 Please note that it MUST be `static` or you will get compile errors. 14690 14691 filters = the filters param to [getFileName] 14692 14693 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 14694 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 14695 a save dialog box. Otherwise, it will show an open dialog box. 14696 +/ 14697 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 14698 string name; 14699 alias name this; 14700 } 14701 14702 /++ 14703 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. 14704 14705 History: 14706 onCancel was added November 6, 2021. 14707 14708 The dialog itself on Linux was modified on December 2, 2021 to include 14709 a directory picker in addition to the command line completion view. 14710 14711 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 14712 Future_directions: 14713 I want to add some kind of custom preview and maybe thumbnail thing in the future, 14714 at least on Linux, maybe on Windows too. 14715 +/ 14716 void getOpenFileName( 14717 void delegate(string) onOK, 14718 string prefilledName = null, 14719 string[] filters = null, 14720 void delegate() onCancel = null, 14721 string initialDirectory = null, 14722 ) 14723 { 14724 return getFileName(true, onOK, prefilledName, filters, onCancel, initialDirectory); 14725 } 14726 14727 /// ditto 14728 void getSaveFileName( 14729 void delegate(string) onOK, 14730 string prefilledName = null, 14731 string[] filters = null, 14732 void delegate() onCancel = null, 14733 string initialDirectory = null, 14734 ) 14735 { 14736 return getFileName(false, onOK, prefilledName, filters, onCancel, initialDirectory); 14737 } 14738 14739 void getFileName( 14740 bool openOrSave, 14741 void delegate(string) onOK, 14742 string prefilledName = null, 14743 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 14744 void delegate() onCancel = null, 14745 string initialDirectory = null, 14746 ) 14747 { 14748 14749 version(win32_widgets) { 14750 import core.sys.windows.commdlg; 14751 /* 14752 Ofn.lStructSize = sizeof(OPENFILENAME); 14753 Ofn.hwndOwner = hWnd; 14754 Ofn.lpstrFilter = szFilter; 14755 Ofn.lpstrFile= szFile; 14756 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 14757 Ofn.lpstrFileTitle = szFileTitle; 14758 Ofn.nMaxFileTitle = sizeof(szFileTitle); 14759 Ofn.lpstrInitialDir = (LPSTR)NULL; 14760 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 14761 Ofn.lpstrTitle = szTitle; 14762 */ 14763 14764 14765 wchar[1024] file = 0; 14766 wchar[1024] filterBuffer = 0; 14767 makeWindowsString(prefilledName, file[]); 14768 OPENFILENAME ofn; 14769 ofn.lStructSize = ofn.sizeof; 14770 if(filters.length) { 14771 string filter; 14772 foreach(i, f; filters) { 14773 filter ~= f; 14774 filter ~= "\0"; 14775 } 14776 filter ~= "\0"; 14777 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 14778 } 14779 ofn.lpstrFile = file.ptr; 14780 ofn.nMaxFile = file.length; 14781 14782 wchar[1024] initialDir = 0; 14783 if(initialDirectory !is null) { 14784 makeWindowsString(initialDirectory, initialDir[]); 14785 ofn.lpstrInitialDir = file.ptr; 14786 } 14787 14788 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 14789 { 14790 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 14791 if(okString.length && okString[$-1] == '\0') 14792 okString = okString[0..$-1]; 14793 onOK(okString); 14794 } else { 14795 if(onCancel) 14796 onCancel(); 14797 } 14798 } else version(custom_widgets) { 14799 if(filters.length == 0) 14800 filters = ["All Files\0*.*"]; 14801 auto picker = new FilePicker(prefilledName, filters, initialDirectory); 14802 picker.onOK = onOK; 14803 picker.onCancel = onCancel; 14804 picker.show(); 14805 } 14806 } 14807 14808 version(custom_widgets) 14809 private 14810 class FilePicker : Dialog { 14811 void delegate(string) onOK; 14812 void delegate() onCancel; 14813 LineEdit lineEdit; 14814 14815 // returns common prefix 14816 string loadFiles(string cwd, string[] filters...) { 14817 string[] files; 14818 string[] dirs; 14819 14820 string commonPrefix; 14821 14822 getFiles(cwd, (string name, bool isDirectory) { 14823 if(name == ".") 14824 return; // skip this as unnecessary 14825 if(isDirectory) 14826 dirs ~= name; 14827 else { 14828 foreach(filter; filters) 14829 if( 14830 filter.length <= 1 || 14831 filter == "*.*" || 14832 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 14833 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 14834 ) 14835 { 14836 files ~= name; 14837 14838 if(filter.length > 0 && filter[$-1] == '*') { 14839 if(commonPrefix is null) { 14840 commonPrefix = name; 14841 } else { 14842 foreach(idx, char i; name) { 14843 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 14844 commonPrefix = commonPrefix[0 .. idx]; 14845 break; 14846 } 14847 } 14848 } 14849 } 14850 14851 break; 14852 } 14853 } 14854 }); 14855 14856 extern(C) static int comparator(scope const void* a, scope const void* b) { 14857 auto sa = *cast(string*) a; 14858 auto sb = *cast(string*) b; 14859 14860 for(int i = 0; i < sa.length; i++) { 14861 if(i == sb.length) 14862 return 1; 14863 return sa[i] - sb[i]; 14864 } 14865 14866 return 0; 14867 } 14868 14869 nonPhobosSort(files, &comparator); 14870 nonPhobosSort(dirs, &comparator); 14871 14872 listWidget.clear(); 14873 dirWidget.clear(); 14874 foreach(name; dirs) 14875 dirWidget.addOption(name); 14876 foreach(name; files) 14877 listWidget.addOption(name); 14878 14879 return commonPrefix; 14880 } 14881 14882 ListWidget listWidget; 14883 ListWidget dirWidget; 14884 14885 string currentDirectory; 14886 string[] processedFilters; 14887 14888 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"] 14889 this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) { 14890 super(300, 200, "Choose File..."); // owner); 14891 14892 foreach(filter; filters) { 14893 while(filter.length && filter[0] != 0) { 14894 filter = filter[1 .. $]; 14895 } 14896 if(filter.length) 14897 filter = filter[1 .. $]; // trim off the 0 14898 14899 while(filter.length) { 14900 int idx = 0; 14901 while(idx < filter.length && filter[idx] != ';') { 14902 idx++; 14903 } 14904 14905 processedFilters ~= filter[0 .. idx]; 14906 if(idx < filter.length) 14907 idx++; // skip the ; 14908 filter = filter[idx .. $]; 14909 } 14910 } 14911 14912 currentDirectory = initialDirectory is null ? "." : initialDirectory; 14913 14914 { 14915 auto hl = new HorizontalLayout(this); 14916 dirWidget = new ListWidget(hl); 14917 listWidget = new ListWidget(hl); 14918 14919 // double click events normally trigger something else but 14920 // here user might be clicking kinda fast and we'd rather just 14921 // keep it 14922 dirWidget.addEventListener((scope DoubleClickEvent dev) { 14923 auto ce = new ChangeEvent!void(dirWidget, () {}); 14924 ce.dispatch(); 14925 }); 14926 14927 dirWidget.addEventListener((scope ChangeEvent!void sce) { 14928 string v; 14929 foreach(o; dirWidget.options) 14930 if(o.selected) { 14931 v = o.label; 14932 break; 14933 } 14934 if(v.length) { 14935 currentDirectory ~= "/" ~ v; 14936 loadFiles(currentDirectory, processedFilters); 14937 } 14938 }); 14939 14940 // double click here, on the other hand, selects the file 14941 // and moves on 14942 listWidget.addEventListener((scope DoubleClickEvent dev) { 14943 OK(); 14944 }); 14945 } 14946 14947 lineEdit = new LineEdit(this); 14948 lineEdit.focus(); 14949 lineEdit.addEventListener(delegate(CharEvent event) { 14950 if(event.character == '\t' || event.character == '\n') 14951 event.preventDefault(); 14952 }); 14953 14954 listWidget.addEventListener(EventType.change, () { 14955 foreach(o; listWidget.options) 14956 if(o.selected) 14957 lineEdit.content = o.label; 14958 }); 14959 14960 loadFiles(currentDirectory, processedFilters); 14961 14962 lineEdit.addEventListener((KeyDownEvent event) { 14963 if(event.key == Key.Tab) { 14964 14965 auto current = lineEdit.content; 14966 if(current.length >= 2 && current[0 ..2] == "./") 14967 current = current[2 .. $]; 14968 14969 auto commonPrefix = loadFiles(".", current ~ "*"); 14970 14971 if(commonPrefix.length) 14972 lineEdit.content = commonPrefix; 14973 14974 // FIXME: if that is a directory, add the slash? or even go inside? 14975 14976 event.preventDefault(); 14977 } 14978 }); 14979 14980 lineEdit.content = prefilledName; 14981 14982 auto hl = new HorizontalLayout(60, this); 14983 auto cancelButton = new Button("Cancel", hl); 14984 auto okButton = new Button("OK", hl); 14985 14986 cancelButton.addEventListener(EventType.triggered, &Cancel); 14987 okButton.addEventListener(EventType.triggered, &OK); 14988 14989 this.addEventListener((KeyDownEvent event) { 14990 if(event.key == Key.Enter || event.key == Key.PadEnter) { 14991 event.preventDefault(); 14992 OK(); 14993 } 14994 if(event.key == Key.Escape) 14995 Cancel(); 14996 }); 14997 14998 } 14999 15000 override void OK() { 15001 if(lineEdit.content.length) { 15002 string accepted; 15003 auto c = lineEdit.content; 15004 if(c.length && c[0] == '/') 15005 accepted = c; 15006 else 15007 accepted = currentDirectory ~ "/" ~ lineEdit.content; 15008 15009 if(isDir(accepted)) { 15010 // FIXME: would be kinda nice to support ~ and collapse these paths too 15011 // FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later. 15012 currentDirectory = accepted; 15013 loadFiles(currentDirectory, processedFilters); 15014 lineEdit.content = ""; 15015 return; 15016 } 15017 15018 if(onOK) 15019 onOK(accepted); 15020 } 15021 close(); 15022 } 15023 15024 override void Cancel() { 15025 if(onCancel) 15026 onCancel(); 15027 close(); 15028 } 15029 } 15030 15031 private bool isDir(string name) { 15032 version(Windows) { 15033 auto ws = WCharzBuffer(name); 15034 auto ret = GetFileAttributesW(ws.ptr); 15035 if(ret == INVALID_FILE_ATTRIBUTES) 15036 return false; 15037 return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0; 15038 } else version(Posix) { 15039 import core.sys.posix.sys.stat; 15040 stat_t buf; 15041 auto ret = stat((name ~ '\0').ptr, &buf); 15042 if(ret == -1) 15043 return false; // I could probably check more specific errors tbh 15044 return (buf.st_mode & S_IFMT) == S_IFDIR; 15045 } else return false; 15046 } 15047 15048 /* 15049 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 15050 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 15051 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 15052 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 15053 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 15054 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 15055 http://www.sbin.org/doc/Xlib/chapt_03.html 15056 15057 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 15058 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 15059 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 15060 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 15061 */ 15062 15063 15064 // These are all for setMenuAndToolbarFromAnnotatedCode 15065 /// This item in the menu will be preceded by a separator line 15066 /// Group: generating_from_code 15067 struct separator {} 15068 deprecated("It was misspelled, use separator instead") alias seperator = separator; 15069 /// Program-wide keyboard shortcut to trigger the action 15070 /// Group: generating_from_code 15071 struct accelerator { string keyString; } 15072 /// tells which menu the action will be on 15073 /// Group: generating_from_code 15074 struct menu { string name; } 15075 /// Describes which toolbar section the action appears on 15076 /// Group: generating_from_code 15077 struct toolbar { string groupName; } 15078 /// 15079 /// Group: generating_from_code 15080 struct icon { ushort id; } 15081 /// 15082 /// Group: generating_from_code 15083 struct label { string label; } 15084 /// 15085 /// Group: generating_from_code 15086 struct hotkey { dchar ch; } 15087 /// 15088 /// Group: generating_from_code 15089 struct tip { string tip; } 15090 15091 15092 /++ 15093 Observes and allows inspection of an object via automatic gui 15094 +/ 15095 /// Group: generating_from_code 15096 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 15097 return new ObjectInspectionWindowImpl!(T)(t); 15098 } 15099 15100 class ObjectInspectionWindow : Window { 15101 this(int a, int b, string c) { 15102 super(a, b, c); 15103 } 15104 15105 abstract void readUpdatesFromObject(); 15106 } 15107 15108 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 15109 T t; 15110 this(T t) { 15111 this.t = t; 15112 15113 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 15114 15115 foreach(memberName; __traits(derivedMembers, T)) {{ 15116 alias member = I!(__traits(getMember, t, memberName))[0]; 15117 alias type = typeof(member); 15118 static if(is(type == int)) { 15119 auto le = new LabeledLineEdit(memberName ~ ": ", this); 15120 //le.addEventListener("char", (Event ev) { 15121 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 15122 //ev.preventDefault(); 15123 //}); 15124 le.addEventListener(EventType.change, (Event ev) { 15125 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 15126 }); 15127 15128 updateMemberDelegates[memberName] = () { 15129 le.content = toInternal!string(__traits(getMember, t, memberName)); 15130 }; 15131 } 15132 }} 15133 } 15134 15135 void delegate()[string] updateMemberDelegates; 15136 15137 override void readUpdatesFromObject() { 15138 foreach(k, v; updateMemberDelegates) 15139 v(); 15140 } 15141 } 15142 15143 /++ 15144 Creates a dialog based on a data structure. 15145 15146 --- 15147 dialog((YourStructure value) { 15148 // the user filled in the struct and clicked OK, 15149 // you can check the members now 15150 }); 15151 --- 15152 15153 Params: 15154 initialData = the initial value to show in the dialog. It will not modify this unless 15155 it is a class then it might, no promises. 15156 15157 History: 15158 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 15159 +/ 15160 /// Group: generating_from_code 15161 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15162 dialog(T.init, onOK, onCancel, title); 15163 } 15164 /// ditto 15165 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 15166 auto dg = new AutomaticDialog!T(initialData, onOK, onCancel, title); 15167 dg.show(); 15168 } 15169 15170 private static template I(T...) { alias I = T; } 15171 15172 15173 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 15174 if(name == "id") 15175 return allLowerCase ? name : "ID"; 15176 15177 char[160] buffer; 15178 int bufferIndex = 0; 15179 bool shouldCap = true; 15180 bool shouldSpace; 15181 bool lastWasCap; 15182 foreach(idx, char ch; name) { 15183 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15184 15185 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 15186 if(lastWasCap) { 15187 // two caps in a row, don't change. Prolly acronym. 15188 } else { 15189 if(idx) 15190 shouldSpace = true; // new word, add space 15191 } 15192 15193 lastWasCap = true; 15194 } else { 15195 lastWasCap = false; 15196 } 15197 15198 if(shouldSpace) { 15199 buffer[bufferIndex++] = space; 15200 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15201 shouldSpace = false; 15202 } 15203 if(shouldCap) { 15204 if(ch >= 'a' && ch <= 'z') 15205 ch -= 32; 15206 shouldCap = false; 15207 } 15208 if(allLowerCase && ch >= 'A' && ch <= 'Z') 15209 ch += 32; 15210 buffer[bufferIndex++] = ch; 15211 } 15212 return buffer[0 .. bufferIndex].idup; 15213 } 15214 15215 /++ 15216 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. 15217 +/ 15218 class AutomaticDialog(T) : Dialog { 15219 T t; 15220 15221 void delegate(T) onOK; 15222 void delegate() onCancel; 15223 15224 override int paddingTop() { return defaultLineHeight; } 15225 override int paddingBottom() { return defaultLineHeight; } 15226 override int paddingRight() { return defaultLineHeight; } 15227 override int paddingLeft() { return defaultLineHeight; } 15228 15229 this(T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 15230 assert(onOK !is null); 15231 15232 t = initialData; 15233 15234 static if(is(T == class)) { 15235 if(t is null) 15236 t = new T(); 15237 } 15238 this.onOK = onOK; 15239 this.onCancel = onCancel; 15240 super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + scaleWithDpi(4 + 2)) + defaultLineHeight + scaleWithDpi(56), title); 15241 15242 static if(is(T == class)) 15243 this.addDataControllerWidget(t); 15244 else 15245 this.addDataControllerWidget(&t); 15246 15247 auto hl = new HorizontalLayout(this); 15248 auto stretch = new HorizontalSpacer(hl); // to right align 15249 auto ok = new CommandButton("OK", hl); 15250 auto cancel = new CommandButton("Cancel", hl); 15251 ok.addEventListener(EventType.triggered, &OK); 15252 cancel.addEventListener(EventType.triggered, &Cancel); 15253 15254 this.addEventListener((KeyDownEvent ev) { 15255 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 15256 ok.focus(); 15257 OK(); 15258 ev.preventDefault(); 15259 } 15260 if(ev.key == Key.Escape) { 15261 Cancel(); 15262 ev.preventDefault(); 15263 } 15264 }); 15265 15266 this.addEventListener((scope ClosedEvent ce) { 15267 if(onCancel) 15268 onCancel(); 15269 }); 15270 15271 //this.children[0].focus(); 15272 } 15273 15274 override void OK() { 15275 onOK(t); 15276 close(); 15277 } 15278 15279 override void Cancel() { 15280 if(onCancel) 15281 onCancel(); 15282 close(); 15283 } 15284 } 15285 15286 private template baseClassCount(Class) { 15287 private int helper() { 15288 int count = 0; 15289 static if(is(Class bases == super)) { 15290 foreach(base; bases) 15291 static if(is(base == class)) 15292 count += 1 + baseClassCount!base; 15293 } 15294 return count; 15295 } 15296 15297 enum int baseClassCount = helper(); 15298 } 15299 15300 private long stringToLong(string s) { 15301 long ret; 15302 if(s.length == 0) 15303 return ret; 15304 bool negative = s[0] == '-'; 15305 if(negative) 15306 s = s[1 .. $]; 15307 foreach(ch; s) { 15308 if(ch >= '0' && ch <= '9') { 15309 ret *= 10; 15310 ret += ch - '0'; 15311 } 15312 } 15313 if(negative) 15314 ret = -ret; 15315 return ret; 15316 } 15317 15318 15319 interface ReflectableProperties { 15320 /++ 15321 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 15322 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 15323 json in the current implementation. 15324 15325 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 15326 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 15327 as of the June 2, 2021 release. 15328 15329 History: 15330 Added June 2, 2021. 15331 15332 See_Also: [getPropertyAsString], [setPropertyFromString] 15333 +/ 15334 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 15335 /++ 15336 Requests a property to be delivered to you as a string, through your `sink` delegate. 15337 15338 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 15339 be interpreted as json, otherwise, it is just a plain string. 15340 15341 The sink should always be called exactly once for each call (it is basically a return value, but it might 15342 use a local buffer it maintains instead of allocating a return value). 15343 15344 History: 15345 Added June 2, 2021. 15346 15347 See_Also: [getPropertiesList], [setPropertyFromString] 15348 +/ 15349 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 15350 /++ 15351 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. 15352 15353 History: 15354 Added June 2, 2021. 15355 15356 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 15357 +/ 15358 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 15359 15360 /// [setPropertyFromString] possible return values 15361 enum SetPropertyResult { 15362 success = 0, /// the property has been successfully set to the request value 15363 notPermitted = -1, /// the property exists but it cannot be changed at this time 15364 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 15365 noSuchProperty = -3, /// there is no property by that name 15366 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 15367 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) 15368 } 15369 15370 /++ 15371 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 15372 15373 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15374 15375 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15376 rarely need to use these building blocks directly. 15377 +/ 15378 mixin template RegisterSetters() { 15379 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 15380 switch(name) { 15381 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15382 case memberName: 15383 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15384 if(value != "true" && value != "false") 15385 return SetPropertyResult.wrongFormat; 15386 __traits(getMember, this, memberName) = value == "true" ? true : false; 15387 return SetPropertyResult.success; 15388 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15389 import core.stdc.stdlib; 15390 char[128] zero = 0; 15391 if(buffer.length + 1 >= zero.length) 15392 return SetPropertyResult.wrongFormat; 15393 zero[0 .. buffer.length] = buffer[]; 15394 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 15395 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15396 import core.stdc.stdlib; 15397 char[128] zero = 0; 15398 if(buffer.length + 1 >= zero.length) 15399 return SetPropertyResult.wrongFormat; 15400 zero[0 .. buffer.length] = buffer[]; 15401 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 15402 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15403 __traits(getMember, this, memberName) = value.idup; 15404 } else { 15405 return SetPropertyResult.notImplemented; 15406 } 15407 15408 } 15409 default: 15410 return super.setPropertyFromString(name, value, valueIsJson); 15411 } 15412 } 15413 } 15414 15415 /++ 15416 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 15417 15418 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15419 15420 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15421 rarely need to use these building blocks directly. 15422 +/ 15423 mixin template RegisterGetters() { 15424 override void getPropertiesList(scope void delegate(string name) sink) const { 15425 super.getPropertiesList(sink); 15426 15427 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15428 sink(memberName); 15429 } 15430 } 15431 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 15432 switch(name) { 15433 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15434 case memberName: 15435 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15436 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 15437 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15438 import core.stdc.stdio; 15439 char[32] buffer; 15440 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 15441 sink(name, buffer[0 .. len], true); 15442 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15443 import core.stdc.stdio; 15444 char[32] buffer; 15445 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 15446 sink(name, buffer[0 .. len], true); 15447 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15448 sink(name, __traits(getMember, this, memberName), false); 15449 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 15450 } else { 15451 sink(name, null, true); 15452 } 15453 15454 return; 15455 } 15456 default: 15457 return super.getPropertyAsString(name, sink); 15458 } 15459 } 15460 } 15461 } 15462 15463 private struct Stack(T) { 15464 this(int maxSize) { 15465 internalLength = 0; 15466 arr = initialBuffer[]; 15467 } 15468 15469 ///. 15470 void push(T t) { 15471 if(internalLength >= arr.length) { 15472 auto oldarr = arr; 15473 if(arr.length < 4096) 15474 arr = new T[arr.length * 2]; 15475 else 15476 arr = new T[arr.length + 4096]; 15477 arr[0 .. oldarr.length] = oldarr[]; 15478 } 15479 15480 arr[internalLength] = t; 15481 internalLength++; 15482 } 15483 15484 ///. 15485 T pop() { 15486 assert(internalLength); 15487 internalLength--; 15488 return arr[internalLength]; 15489 } 15490 15491 ///. 15492 T peek() { 15493 assert(internalLength); 15494 return arr[internalLength - 1]; 15495 } 15496 15497 ///. 15498 @property bool empty() { 15499 return internalLength ? false : true; 15500 } 15501 15502 ///. 15503 private T[] arr; 15504 private size_t internalLength; 15505 private T[64] initialBuffer; 15506 // 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), 15507 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 15508 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 15509 } 15510 15511 /// 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. 15512 private struct WidgetStream { 15513 15514 ///. 15515 @property Widget front() { 15516 return current.widget; 15517 } 15518 15519 /// Use Widget.tree instead. 15520 this(Widget start) { 15521 current.widget = start; 15522 current.childPosition = -1; 15523 isEmpty = false; 15524 stack = typeof(stack)(0); 15525 } 15526 15527 /* 15528 Handle it 15529 handle its children 15530 15531 */ 15532 15533 ///. 15534 void popFront() { 15535 more: 15536 if(isEmpty) return; 15537 15538 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 15539 15540 current.childPosition++; 15541 if(current.childPosition >= current.widget.children.length) { 15542 if(stack.empty()) 15543 isEmpty = true; 15544 else { 15545 current = stack.pop(); 15546 goto more; 15547 } 15548 } else { 15549 stack.push(current); 15550 current.widget = current.widget.children[current.childPosition]; 15551 current.childPosition = -1; 15552 } 15553 } 15554 15555 ///. 15556 @property bool empty() { 15557 return isEmpty; 15558 } 15559 15560 private: 15561 15562 struct Current { 15563 Widget widget; 15564 int childPosition; 15565 } 15566 15567 Current current; 15568 15569 Stack!(Current) stack; 15570 15571 bool isEmpty; 15572 } 15573 15574 15575 /+ 15576 15577 I could fix up the hierarchy kinda like this 15578 15579 class Widget { 15580 Widget[] children() { return null; } 15581 } 15582 interface WidgetContainer { 15583 Widget asWidget(); 15584 void addChild(Widget w); 15585 15586 // alias asWidget this; // but meh 15587 } 15588 15589 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 15590 15591 class Layout : Widget, WidgetContainer {} 15592 15593 class Window : WidgetContainer {} 15594 15595 15596 All constructors that previously took Widgets should now take WidgetContainers instead 15597 15598 15599 15600 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". 15601 +/ 15602 15603 /+ 15604 LAYOUTS 2.0 15605 15606 can just be assigned as a function. assigning a new one will cause it to be immediately called. 15607 15608 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 15609 15610 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 15611 15612 and even Paint can just use computedStyle... 15613 15614 background color 15615 font 15616 border color and style 15617 15618 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 15619 please note that many widgets and in some modes will completely ignore properties as they will. 15620 they are just hints you set, not promises. 15621 15622 15623 15624 15625 15626 So generally the existing virtual functions are just the default for the class. But individual objects 15627 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 15628 +/ 15629 15630 /++ 15631 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. 15632 15633 History: 15634 Added May 24, 2021. 15635 +/ 15636 struct WidgetBackground { 15637 /++ 15638 A background with the given solid color. 15639 +/ 15640 this(Color color) { 15641 this.color = color; 15642 } 15643 15644 this(WidgetBackground bg) { 15645 this = bg; 15646 } 15647 15648 /++ 15649 Creates a widget from the string. 15650 15651 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 15652 +/ 15653 static WidgetBackground fromString(string s) { 15654 return WidgetBackground(Color.fromString(s)); 15655 } 15656 15657 /++ 15658 The background is not necessarily a solid color, but you can always specify a color as a fallback. 15659 15660 History: 15661 Made `public` on December 18, 2022 (dub v10.10). 15662 +/ 15663 Color color; 15664 } 15665 15666 /++ 15667 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!) 15668 15669 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. 15670 15671 You should not inherit from this directly, but instead use [VisualTheme]. 15672 15673 History: 15674 Added May 8, 2021 15675 +/ 15676 abstract class BaseVisualTheme { 15677 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 15678 abstract void doPaint(Widget widget, WidgetPainter painter); 15679 15680 /+ 15681 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 15682 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 15683 +/ 15684 15685 /++ 15686 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 15687 where the interpretation of the string varies for each property and may include things like measurement units. 15688 +/ 15689 abstract string getPropertyString(Widget widget, string propertyName); 15690 15691 /++ 15692 Default background color of the window. Widgets also use this to simulate transparency. 15693 15694 Probably some shade of grey. 15695 +/ 15696 abstract Color windowBackgroundColor(); 15697 abstract Color widgetBackgroundColor(); 15698 abstract Color foregroundColor(); 15699 abstract Color lightAccentColor(); 15700 abstract Color darkAccentColor(); 15701 15702 /++ 15703 Colors used to indicate active selections in lists and text boxes, etc. 15704 +/ 15705 abstract Color selectionForegroundColor(); 15706 /// ditto 15707 abstract Color selectionBackgroundColor(); 15708 15709 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 15710 15711 /++ 15712 If you return `null` it will use simpledisplay's default. Otherwise, you return what font you want and it will cache it internally. 15713 +/ 15714 abstract OperatingSystemFont defaultFont(int dpi); 15715 15716 private OperatingSystemFont[int] defaultFontCache_; 15717 private OperatingSystemFont defaultFontCached(int dpi) { 15718 if(dpi !in defaultFontCache_) { 15719 // FIXME: set this to false if X disconnect or if visual theme changes 15720 defaultFontCache_[dpi] = defaultFont(dpi); 15721 } 15722 return defaultFontCache_[dpi]; 15723 } 15724 } 15725 15726 /+ 15727 A widget should have: 15728 classList 15729 dataset 15730 attributes 15731 computedStyles 15732 state (persistent) 15733 dynamic state (focused, hover, etc) 15734 +/ 15735 15736 // visualTheme.computedStyle(this).paddingLeft 15737 15738 15739 /++ 15740 This is your entry point to create your own visual theme for custom widgets. 15741 15742 You will want to inherit from this with a `final` class, passing your own class as the `CRTP` argument, then define the necessary methods. 15743 15744 Compatibility note: future versions of minigui may add new methods here. You will likely need to implement them when updating. 15745 +/ 15746 abstract class VisualTheme(CRTP) : BaseVisualTheme { 15747 override string getPropertyString(Widget widget, string propertyName) { 15748 return null; 15749 } 15750 15751 /+ 15752 mixin StyleOverride!Widget 15753 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 15754 w.useStyleProperties(dg); 15755 } 15756 +/ 15757 15758 final override void doPaint(Widget widget, WidgetPainter painter) { 15759 auto derived = cast(CRTP) cast(void*) this; 15760 15761 scope void delegate(Widget, WidgetPainter) bestMatch; 15762 int bestMatchScore; 15763 15764 static if(__traits(hasMember, CRTP, "paint")) 15765 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 15766 static if(is(typeof(overload) Params == __parameters)) { 15767 static assert(Params.length == 2); 15768 static assert(is(Params[0] : Widget)); 15769 static assert(is(Params[1] == WidgetPainter)); 15770 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); 15771 15772 alias type = Params[0]; 15773 if(cast(type) widget) { 15774 auto score = baseClassCount!type; 15775 15776 if(score > bestMatchScore) { 15777 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 15778 bestMatchScore = score; 15779 } 15780 } 15781 } else static assert(0, "paint should be a method."); 15782 } 15783 15784 if(bestMatch) 15785 bestMatch(widget, painter); 15786 else 15787 widget.paint(painter); 15788 } 15789 15790 deprecated("Add an `int dpi` argument to your override now.") OperatingSystemFont defaultFont() { return null; } 15791 15792 // 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 15793 // mixin Beautiful95Theme; 15794 mixin DefaultLightTheme; 15795 15796 private static struct Cached { 15797 // i prolly want to do this 15798 } 15799 } 15800 15801 /// ditto 15802 mixin template Beautiful95Theme() { 15803 override Color windowBackgroundColor() { return Color(212, 212, 212); } 15804 override Color widgetBackgroundColor() { return Color.white; } 15805 override Color foregroundColor() { return Color.black; } 15806 override Color darkAccentColor() { return Color(172, 172, 172); } 15807 override Color lightAccentColor() { return Color(223, 223, 223); } 15808 override Color selectionForegroundColor() { return Color.white; } 15809 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 15810 override OperatingSystemFont defaultFont(int dpi) { return null; } // will just use the default out of simpledisplay's xfontstr 15811 } 15812 15813 /// ditto 15814 mixin template DefaultLightTheme() { 15815 override Color windowBackgroundColor() { return Color(232, 232, 232); } 15816 override Color widgetBackgroundColor() { return Color.white; } 15817 override Color foregroundColor() { return Color.black; } 15818 override Color darkAccentColor() { return Color(172, 172, 172); } 15819 override Color lightAccentColor() { return Color(223, 223, 223); } 15820 override Color selectionForegroundColor() { return Color.white; } 15821 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 15822 override OperatingSystemFont defaultFont(int dpi) { 15823 version(Windows) 15824 return new OperatingSystemFont("Segoe UI"); 15825 else { 15826 // FIXME: undo xft's scaling so we don't end up double scaled 15827 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 15828 } 15829 } 15830 } 15831 15832 /// ditto 15833 mixin template DefaultDarkTheme() { 15834 override Color windowBackgroundColor() { return Color(64, 64, 64); } 15835 override Color widgetBackgroundColor() { return Color.black; } 15836 override Color foregroundColor() { return Color.white; } 15837 override Color darkAccentColor() { return Color(20, 20, 20); } 15838 override Color lightAccentColor() { return Color(80, 80, 80); } 15839 override Color selectionForegroundColor() { return Color.white; } 15840 override Color selectionBackgroundColor() { return Color(128, 0, 128); } 15841 override OperatingSystemFont defaultFont(int dpi) { 15842 version(Windows) 15843 return new OperatingSystemFont("Segoe UI", 12); 15844 else 15845 return new OperatingSystemFont("DejaVu Sans", 9 * dpi / 96); 15846 } 15847 } 15848 15849 /// ditto 15850 alias DefaultTheme = DefaultLightTheme; 15851 15852 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 15853 /+ 15854 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 15855 Color windowBackgroundColor() { return Color(242, 242, 242); } 15856 Color darkAccentColor() { return windowBackgroundColor; } 15857 Color lightAccentColor() { return windowBackgroundColor; } 15858 +/ 15859 } 15860 15861 /++ 15862 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 15863 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 15864 15865 History: 15866 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 15867 +/ 15868 class StateChanged(alias field) : Event { 15869 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 15870 override bool cancelable() const { return false; } 15871 this(Widget target, typeof(field) newValue) { 15872 this.newValue = newValue; 15873 super(EventString, target); 15874 } 15875 15876 typeof(field) newValue; 15877 } 15878 15879 /++ 15880 Convenience function to add a `triggered` event listener. 15881 15882 Its implementation is simply `w.addEventListener("triggered", dg);` 15883 15884 History: 15885 Added November 27, 2021 (dub v10.4) 15886 +/ 15887 void addWhenTriggered(Widget w, void delegate() dg) { 15888 w.addEventListener("triggered", dg); 15889 } 15890 15891 /++ 15892 Observable varables can be added to widgets and when they are changed, it fires 15893 off a [StateChanged] event so you can react to it. 15894 15895 It is implemented as a getter and setter property, along with another helper you 15896 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 15897 event through the usual means. Just give the name of the variable. See [StateChanged] for an 15898 example. 15899 15900 History: 15901 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 15902 +/ 15903 mixin template Observable(T, string name) { 15904 private T backing; 15905 15906 mixin(q{ 15907 void } ~ name ~ q{_changed (void delegate(T) dg) { 15908 this.addEventListener((StateChanged!this_thing ev) { 15909 dg(ev.newValue); 15910 }); 15911 } 15912 15913 @property T } ~ name ~ q{ () { 15914 return backing; 15915 } 15916 15917 @property void } ~ name ~ q{ (T t) { 15918 backing = t; 15919 auto event = new StateChanged!this_thing(this, t); 15920 event.dispatch(); 15921 } 15922 }); 15923 15924 mixin("private alias this_thing = " ~ name ~ ";"); 15925 } 15926 15927 15928 private bool startsWith(string test, string thing) { 15929 if(test.length < thing.length) 15930 return false; 15931 return test[0 .. thing.length] == thing; 15932 } 15933 15934 private bool endsWith(string test, string thing) { 15935 if(test.length < thing.length) 15936 return false; 15937 return test[$ - thing.length .. $] == thing; 15938 } 15939 15940 // still do layout delegation 15941 // and... split off Window from Widget. 15942 15943 version(minigui_screenshots) 15944 struct Screenshot { 15945 string name; 15946 } 15947 15948 version(minigui_screenshots) 15949 static if(__VERSION__ > 2092) 15950 mixin(q{ 15951 shared static this() { 15952 import core.runtime; 15953 15954 static UnitTestResult screenshotMagic() { 15955 string name; 15956 15957 import arsd.png; 15958 15959 auto results = new Window(); 15960 auto button = new Button("do it", results); 15961 15962 Window.newWindowCreated = delegate(Window w) { 15963 Timer timer; 15964 timer = new Timer(250, { 15965 auto img = w.win.takeScreenshot(); 15966 timer.destroy(); 15967 15968 version(Windows) 15969 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 15970 else 15971 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 15972 15973 w.close(); 15974 }); 15975 }; 15976 15977 button.addWhenTriggered( { 15978 15979 foreach(test; __traits(getUnitTests, mixin(__MODULE__))) { 15980 name = null; 15981 static foreach(attr; __traits(getAttributes, test)) { 15982 static if(is(typeof(attr) == Screenshot)) 15983 name = attr.name; 15984 } 15985 if(name.length) { 15986 test(); 15987 } 15988 } 15989 15990 }); 15991 15992 results.loop(); 15993 15994 return UnitTestResult(0, 0, false, false); 15995 } 15996 15997 15998 Runtime.extendedModuleUnitTester = &screenshotMagic; 15999 } 16000 }); 16001 version(minigui_screenshots) { 16002 version(unittest) 16003 void main() {} 16004 else static assert(0, "dont forget the -unittest flag to dmd"); 16005 } 16006 16007 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 16008 // FIXME: make multiple accelerators disambiguate based ona rgs 16009 // FIXME: MainWindow ctor should have same arg order as Window 16010 // FIXME: mainwindow ctor w/ client area size instead of total size. 16011 // 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. 16012 // FIXME: tri-state checkbox 16013 // FIXME: subordinate controls grouping...