1 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx 2 3 // if doing nested menus, make sure the straight line from where it pops up to any destination on the new popup is not going to disappear the menu until at least a delay 4 5 // me@arsd:~/.kde/share/config$ vim kdeglobals 6 7 // FIXME: i kinda like how you can show find locations in scrollbars in the chrome browisers i wanna support that here too. 8 9 // https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/ 10 11 // for responsive design, a collapsible widget that if it doesn't have enough room, it just automatically becomes a "more" button or whatever. 12 13 // responsive minigui, menu search, and file open with a preview hook on the side. 14 15 // FIXME: add menu checkbox and menu icon eventually 16 17 /* 18 19 im tempted to add some css kind of thing to minigui. i've not done in the past cuz i have a lot of virtual functins i use but i think i have an evil plan 20 21 the virtual functions remain as the default calculated values. then the reads go through some proxy object that can override it... 22 */ 23 24 // FIXME: a popup with slightly shaped window pointing at the mouse might eb useful in places 25 26 // FIXME: text label must be copyable to the clipboard, at least as a full chunk. 27 28 // FIXME: opt-in file picker widget with image support 29 30 // FIXME: number widget 31 32 // https://www.codeguru.com/cpp/controls/buttonctrl/advancedbuttons/article.php/c5161/Native-Win32-ThemeAware-OwnerDraw-Controls-No-MFC.htm 33 // https://docs.microsoft.com/en-us/windows/win32/controls/using-visual-styles 34 35 // osx style menu search. 36 37 // would be cool for a scroll bar to have marking capabilities 38 // kinda like vim's marks just on clicks etc and visual representation 39 // generically. may be cool to add an up arrow to the bottom too 40 // 41 // leave a shadow of where you last were for going back easily 42 43 // So a window needs to have a selection, and that can be represented by a type. This is manipulated by various 44 // functions like cut, copy, paste. Widgets can have a selection and that would assert teh selection ownership for 45 // the window. 46 47 // so what about context menus? 48 49 // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw 50 51 // FIXME: make the scroll thing go to bottom when the content changes. 52 53 // add a knob slider view... you click and go up and down so basically same as a vertical slider, just presented as a round image 54 55 // FIXME: the scroll area MUST be fixed to use the proper apis under the hood. 56 57 58 // FIXME: add a command search thingy built in and implement tip. 59 // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! 60 61 // On Windows: 62 // FIXME: various labels look broken in high contrast mode 63 // FIXME: changing themes while the program is upen doesn't trigger a redraw 64 65 // add note about manifest to documentation. also icons. 66 67 // a pager control is just a horizontal scroll area just with arrows on the sides instead of a scroll bar 68 // FIXME: clear the corner of scrollbars if they pop up 69 70 // minigui needs to have a stdout redirection for gui mode on windows writeln 71 72 // I kinda wanna do state reacting. sort of. idk tho 73 74 // need a viewer widget that works like a web page - arrows scroll down consistently 75 76 // I want a nanovega widget, and a svg widget with some kind of event handlers attached to the inside. 77 78 // FIXME: the menus should be a bit more discoverable, at least a single click to open the others instead of two. 79 // and help info about menu items. 80 // and search in menus? 81 82 // FIXME: a scroll area event signaling when a thing comes into view might be good 83 // FIXME: arrow key navigation and accelerators in dialog boxes will be a must 84 85 // FIXME: unify Windows style line endings 86 87 /* 88 TODO: 89 90 pie menu 91 92 class Form with submit behavior -- see AutomaticDialog 93 94 disabled widgets and menu items 95 96 event cleanup 97 tooltips. 98 api improvements 99 100 margins are kinda broken, they don't collapse like they should. at least. 101 102 a table form btw would be a horizontal layout of vertical layouts holding each column 103 that would give the same width things 104 */ 105 106 /* 107 108 1(15:19:48) NotSpooky: Menus, text entry, label, notebook, box, frame, file dialogs and layout (this one is very useful because I can draw lines between its child widgets 109 */ 110 111 /++ 112 minigui is a smallish GUI widget library, aiming to be on par with at least 113 HTML4 forms and a few other expected gui components. It uses native controls 114 on Windows and does its own thing on Linux (Mac is not currently supported but 115 may be later, and should use native controls) to keep size down. The Linux 116 appearance is similar to Windows 95 and avoids using images to maintain network 117 efficiency on remote X connections, though you can customize that. 118 119 120 minigui's only required dependencies are [arsd.simpledisplay] and [arsd.color], 121 on which it is built. simpledisplay provides the low-level interfaces and minigui 122 builds the concept of widgets inside the windows on top of it. 123 124 Its #1 goal is to be useful without being large and complicated like GTK and Qt. 125 It isn't hugely concerned with appearance - on Windows, it just uses the native 126 controls and native theme, and on Linux, it keeps it simple and I may change that 127 at any time, though after May 2021, you can customize some things with css-inspired 128 [Widget.Style] classes. (On Windows, if you compile with `-version=custom_widgets`, 129 you can use the custom implementation there too, but... you shouldn't.) 130 131 The event model is similar to what you use in the browser with Javascript and the 132 layout engine tries to automatically fit things in, similar to a css flexbox. 133 134 FOR BEST RESULTS: be sure to link with the appropriate subsystem command 135 `-L/SUBSYSTEM:WINDOWS:5.0`, for example, because otherwise you'll get a 136 console and other visual bugs. 137 138 HTML_To_Classes: 139 $(SMALL_TABLE 140 HTML Code | Minigui Class 141 142 `<input type="text">` | [LineEdit] 143 `<textarea>` | [TextEdit] 144 `<select>` | [DropDownSelection] 145 `<input type="checkbox">` | [Checkbox] 146 `<input type="radio">` | [Radiobox] 147 `<button>` | [Button] 148 ) 149 150 151 Stretchiness: 152 The default is 4. You can use larger numbers for things that should 153 consume a lot of space, and lower numbers for ones that are better at 154 smaller sizes. 155 156 Overlapped_input: 157 COMING EVENTUALLY: 158 minigui will include a little bit of I/O functionality that just works 159 with the event loop. If you want to get fancy, I suggest spinning up 160 another thread and posting events back and forth. 161 162 $(H2 Add ons) 163 See the `minigui_addons` directory in the arsd repo for some add on widgets 164 you can import separately too. 165 166 $(H3 XML definitions) 167 If you use [arsd.minigui_xml], you can create widget trees from XML at runtime. 168 169 $(H3 Scriptability) 170 minigui is compatible with [arsd.script]. If you see `@scriptable` on a method 171 in this documentation, it means you can call it from the script language. 172 173 Tip: to allow easy creation of widget trees from script, import [arsd.minigui_xml] 174 and make [arsd.minigui_xml.makeWidgetFromString] available to your script: 175 176 --- 177 import arsd.minigui_xml; 178 import arsd.script; 179 180 var globals = var.emptyObject; 181 globals.makeWidgetFromString = &makeWidgetFromString; 182 183 // this now works 184 interpret(`var window = makeWidgetFromString("<MainWindow />");`, globals); 185 --- 186 187 More to come. 188 189 History: 190 Minigui had mostly additive changes or bug fixes since its inception until May 2021. 191 192 In May 2021 (dub v10.0), minigui got an overhaul. If it was versioned independently, I'd 193 tag this as version 2.0. 194 195 Among the changes: 196 $(LIST 197 * The event model changed to prefer strongly-typed events, though the Javascript string style ones still work, using properties off them is deprecated. It will still compile and function, but you should change the handler to use the classes in its argument list. I adapted my code to use the new model in just a few minutes, so it shouldn't too hard. 198 199 See [Event] for details. 200 201 * A [DoubleClickEvent] was added. Previously, you'd get two rapidly repeated click events. Now, you get one click event followed by a double click event. If you must recreate the old way exactly, you can listen for a DoubleClickEvent, set a flag upon receiving one, then send yourself a synthetic ClickEvent on the next MouseUpEvent, but your program might be better served just working with [MouseDownEvent]s instead. 202 203 See [DoubleClickEvent] for details. 204 205 * Styling hints were added, and the few that existed before have been moved to a new helper class. Deprecated forwarders exist for the (few) old properties to help you transition. Note that most of these only affect a `custom_events` build, which is the default on Linux, but opt in only on Windows. 206 207 See [Widget.Style] for details. 208 209 // * A widget must now opt in to receiving keyboard focus, rather than opting out. 210 211 * Widgets now draw their keyboard focus by default instead of opt in. You may wish to set `tabStop = false;` if it wasn't supposed to receive it. 212 213 * Most Widget constructors no longer have a default `parent` argument. You must pass the parent to almost all widgets, or in rare cases, an explict `null`, but more often than not, you need the parent so the default argument was not very useful at best and misleading to a crash at worst. 214 215 * [LabeledLineEdit] changed its default layout to vertical instead of horizontal. You can restore the old behavior by passing a `TextAlignment` argument to the constructor. 216 217 * Several conversions of public fields to properties, deprecated, or made private. It is unlikely this will affect you, but the compiler will tell you if it does. 218 219 * Various non-breaking additions. 220 ) 221 +/ 222 module arsd.minigui; 223 224 import arsd.core; 225 226 /++ 227 This hello world sample will have an oversized button, but that's ok, you see your first window! 228 +/ 229 version(Demo) 230 unittest { 231 import arsd.minigui; 232 233 void main() { 234 auto window = new MainWindow(); 235 236 auto hello = new TextLabel("Hello, world!", TextAlignment.Center, window); 237 auto button = new Button("Close", window); 238 button.addWhenTriggered({ 239 window.close(); 240 }); 241 242 window.loop(); 243 } 244 245 main(); // exclude from docs 246 } 247 248 /++ 249 This example shows one way you can partition your window into a header 250 and sidebar. Here, the header and sidebar have a fixed width, while the 251 rest of the content sizes with the window. 252 253 It might be a new way of thinking about window layout to do things this 254 way - perhaps [GridLayout] more matches your style of thought - but the 255 concept here is to partition the window into sub-boxes with a particular 256 size, then partition those boxes into further boxes. 257 258 $(IMG //arsdnet.net/minigui-screenshots/windows/layout.png, The example window has a header across the top, then below it a sidebar to the left and a content area to the right.) 259 260 So to make the header, start with a child layout that has a max height. 261 It will use that space from the top, then the remaining children will 262 split the remaining area, meaning you can think of is as just being another 263 box you can split again. Keep splitting until you have the look you desire. 264 +/ 265 // https://github.com/adamdruppe/arsd/issues/310 266 version(minigui_screenshots) 267 @Screenshot("layout") 268 unittest { 269 import arsd.minigui; 270 271 // This helper class is just to help make the layout boxes visible. 272 // think of it like a <div style="background-color: whatever;"></div> in HTML. 273 class ColorWidget : Widget { 274 this(Color color, Widget parent) { 275 this.color = color; 276 super(parent); 277 } 278 Color color; 279 class Style : Widget.Style { 280 override WidgetBackground background() { return WidgetBackground(color); } 281 } 282 mixin OverrideStyle!Style; 283 } 284 285 void main() { 286 auto window = new Window; 287 288 // the key is to give it a max height. This is one way to do it: 289 auto header = new class HorizontalLayout { 290 this() { super(window); } 291 override int maxHeight() { return 50; } 292 }; 293 // this next line is a shortcut way of doing it too, but it only works 294 // for HorizontalLayout and VerticalLayout, and is less explicit, so it 295 // is good to know how to make a new class like above anyway. 296 // auto header = new HorizontalLayout(50, window); 297 298 auto bar = new HorizontalLayout(window); 299 300 // or since this is so common, VerticalLayout and HorizontalLayout both 301 // can just take an argument in their constructor for max width/height respectively 302 303 // (could have tone this above too, but I wanted to demo both techniques) 304 auto left = new VerticalLayout(100, bar); 305 306 // and this is the main section's container. A plain Widget instance is good enough here. 307 auto container = new Widget(bar); 308 309 // and these just add color to the containers we made above for the screenshot. 310 // in a real application, you can just add your actual controls instead of these. 311 auto headerColorBox = new ColorWidget(Color.teal, header); 312 auto leftColorBox = new ColorWidget(Color.green, left); 313 auto rightColorBox = new ColorWidget(Color.purple, container); 314 315 window.loop(); 316 } 317 318 main(); // exclude from docs 319 } 320 321 322 public import arsd.simpledisplay; 323 /++ 324 Convenience import to override the Windows GDI Rectangle function (you can still use it through fully-qualified imports) 325 326 History: 327 Was private until May 15, 2021. 328 +/ 329 public alias Rectangle = arsd.color.Rectangle; // I specifically want this in here, not the win32 GDI Rectangle() 330 331 version(Windows) { 332 import core.sys.windows.winnls; 333 import core.sys.windows.windef; 334 import core.sys.windows.basetyps; 335 import core.sys.windows.winbase; 336 import core.sys.windows.winuser; 337 import core.sys.windows.wingdi; 338 static import gdi = core.sys.windows.wingdi; 339 } 340 341 version(Windows) { 342 version(minigui_manifest) {} else version=minigui_no_manifest; 343 344 version(minigui_no_manifest) {} else 345 static if(__VERSION__ >= 2_083) 346 version(CRuntime_Microsoft) { // FIXME: mingw? 347 // assume we want commctrl6 whenever possible since there's really no reason not to 348 // and this avoids some of the manifest hassle 349 pragma(linkerDirective, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\""); 350 } 351 } 352 353 // this is a hack to call the original window procedure on native win32 widgets if our event listener thing prevents default. 354 private bool lastDefaultPrevented; 355 356 /// Methods marked with this are available from scripts if added to the [arsd.script] engine. 357 alias scriptable = arsd_jsvar_compatible; 358 359 version(Windows) { 360 // use native widgets when available unless specifically asked otherwise 361 version(custom_widgets) { 362 enum bool UsingCustomWidgets = true; 363 enum bool UsingWin32Widgets = false; 364 } else { 365 version = win32_widgets; 366 enum bool UsingCustomWidgets = false; 367 enum bool UsingWin32Widgets = true; 368 } 369 // and native theming when needed 370 //version = win32_theming; 371 } else { 372 enum bool UsingCustomWidgets = true; 373 enum bool UsingWin32Widgets = false; 374 version=custom_widgets; 375 } 376 377 378 379 /* 380 381 The main goals of minigui.d are to: 382 1) Provide basic widgets that just work in a lightweight lib. 383 I basically want things comparable to a plain HTML form, 384 plus the easy and obvious things you expect from Windows 385 apps like a menu. 386 2) Use native things when possible for best functionality with 387 least library weight. 388 3) Give building blocks to provide easy extension for your 389 custom widgets, or hooking into additional native widgets 390 I didn't wrap. 391 4) Provide interfaces for easy interaction between third 392 party minigui extensions. (event model, perhaps 393 signals/slots, drop-in ease of use bits.) 394 5) Zero non-system dependencies, including Phobos as much as 395 I reasonably can. It must only import arsd.color and 396 my simpledisplay.d. If you need more, it will have to be 397 an extension module. 398 6) An easy layout system that generally works. 399 400 A stretch goal is to make it easy to make gui forms with code, 401 some kind of resource file (xml?) and even a wysiwyg designer. 402 403 Another stretch goal is to make it easy to hook data into the gui, 404 including from reflection. So like auto-generate a form from a 405 function signature or struct definition, or show a list from an 406 array that automatically updates as the array is changed. Then, 407 your program focuses on the data more than the gui interaction. 408 409 410 411 STILL NEEDED: 412 * combo box. (this is diff than select because you can free-form edit too. more like a lineedit with autoselect) 413 * slider 414 * listbox 415 * spinner 416 * label? 417 * rich text 418 */ 419 420 421 /+ 422 enum LayoutMethods { 423 verticalFlex, 424 horizontalFlex, 425 inlineBlock, // left to right, no stretch, goes to next line as needed 426 static, // just set to x, y 427 verticalNoStretch, // browser style default 428 429 inlineBlockFlex, // goes left to right, flexing, but when it runs out of space, it spills into next line 430 431 grid, // magic 432 } 433 +/ 434 435 /++ 436 The `Widget` is the base class for minigui's functionality, ranging from UI components like checkboxes or text displays to abstract groupings of other widgets like a layout container or a html `<div>`. You will likely want to use pre-made widgets as well as creating your own. 437 438 439 To create your own widget, you must inherit from it and create a constructor that passes a parent to `super`. Everything else after that is optional. 440 441 --- 442 class MinimalWidget : Widget { 443 this(Widget parent) { 444 super(parent); 445 } 446 } 447 --- 448 449 $(SIDEBAR 450 I'm not entirely happy with leaf, container, and windows all coming from the same base Widget class, but I so far haven't thought of a better solution that's good enough to justify the breakage of a transition. It hasn't been a major problem in practice anyway. 451 ) 452 453 Broadly, there's two kinds of widgets: leaf widgets, which are intended to be the direct user-interactive components, and container widgets, which organize, lay out, and aggregate other widgets in the object tree. A special case of a container widget is [Window], which represents a separate top-level window on the screen. Both leaf and container widgets inherit from `Widget`, so this distinction is more conventional than formal. 454 455 Among the things you'll most likely want to change in your custom widget: 456 457 $(LIST 458 * In your constructor, set `tabStop = false;` if the widget is not supposed to receive keyboard focus. (Please note its childen still can, so `tabStop = false;` is appropriate on most container widgets.) 459 460 You may explicitly set `tabStop = true;` to ensure you get it, even against future changes to the library, though that's the default right now. 461 462 Do this $(I after) calling the `super` constructor. 463 464 * Override [paint] if you want full control of the widget's drawing area (except the area obscured by children!), or [paintContent] if you want to participate in the styling engine's system. You'll also possibly want to make a subclass of [Style] and use [OverrideStyle] to change the default hints given to the styling engine for widget. 465 466 Generally, painting is a job for leaf widgets, since child widgets would obscure your drawing area anyway. However, it is your decision. 467 468 * Override default event handlers with your behavior. For example [defaultEventHandler_click] may be overridden to make clicks do something. Again, this is generally a job for leaf widgets rather than containers; most events are dispatched to the lowest leaf on the widget tree, but they also pass through all their parents. See [Event] for more details about the event model. 469 470 * You may also want to override the various layout hints like [minWidth], [maxHeight], etc. In particular [Padding] and [Margin] are often relevant for both container and leaf widgets and the default values of 0 are often not what you want. 471 ) 472 473 On Microsoft Windows, many widgets are also based on native controls. You can also do this if `static if(UsingWin32Widgets)` passes. You should use the helper function [createWin32Window] to create the window and let minigui do what it needs to do to create its bridge structures. This will populate [Widget.hwnd] which you can access later for communcating with the native window. You may also consider overriding [Widget.handleWmCommand] and [Widget.handleWmNotify] for the widget to translate those messages into appropriate minigui [Event]s. 474 475 It is also possible to embed a [SimpleWindow]-based native window inside a widget. See [OpenGlWidget]'s source code as an example. 476 477 Your own custom-drawn and native system controls can exist side-by-side. 478 479 Later I'll add more complete examples, but for now [TextLabel] and [LabeledPasswordEdit] are both simple widgets you can view implementation to get some ideas. 480 +/ 481 class Widget : ReflectableProperties { 482 483 private bool willDraw() { 484 return true; 485 } 486 487 /+ 488 /++ 489 Calling this directly after constructor can give you a reflectable object as-needed so you don't pay for what you don't need. 490 491 History: 492 Added September 15, 2021 493 implemented.... ??? 494 +/ 495 void prepareReflection(this This)() { 496 497 } 498 +/ 499 500 private bool _enabled = true; 501 502 /++ 503 Determines whether the control is marked enabled. Disabled controls are generally displayed as greyed out and clicking on them does nothing. It is also possible for a control to be disabled because its parent is disabled, in which case this will still return `true`, but setting `enabled = true` may have no effect. Check [disabledBy] to see which parent caused it to be disabled. 504 505 I also recommend you set a [disabledReason] if you chose to set `enabled = false` to tell the user why the control does not work and what they can do to enable it. 506 507 History: 508 Added November 23, 2021 (dub v10.4) 509 510 Warning: the specific behavior of disabling with parents may change in the future. 511 Bugs: 512 Currently only implemented for widgets backed by native Windows controls. 513 514 See_Also: [disabledReason], [disabledBy] 515 +/ 516 @property bool enabled() { 517 return disabledBy() is null; 518 } 519 520 /// ditto 521 @property void enabled(bool yes) { 522 _enabled = yes; 523 version(win32_widgets) { 524 if(hwnd) 525 EnableWindow(hwnd, yes); 526 } 527 setDynamicState(DynamicState.disabled, yes); 528 } 529 530 private string disabledReason_; 531 532 /++ 533 If the widget is not [enabled] this string may be presented to the user when they try to use it. The exact manner and time it gets displayed is up to the implementation of the control. 534 535 Setting this does NOT disable the widget. You need to call `enabled = false;` separately. It does set the data though. 536 537 History: 538 Added November 23, 2021 (dub v10.4) 539 See_Also: [enabled], [disabledBy] 540 +/ 541 @property string disabledReason() { 542 auto w = disabledBy(); 543 return (w is null) ? null : w.disabledReason_; 544 } 545 546 /// ditto 547 @property void disabledReason(string reason) { 548 disabledReason_ = reason; 549 } 550 551 /++ 552 Returns the widget that disabled this. It might be this or one of its parents all the way up the chain, or `null` if the widget is not disabled by anything. You can check [disabledReason] on the return value (after the null check!) to get a hint to display to the user. 553 554 History: 555 Added November 25, 2021 (dub v10.4) 556 See_Also: [enabled], [disabledReason] 557 +/ 558 Widget disabledBy() { 559 Widget p = this; 560 while(p) { 561 if(!p._enabled) 562 return p; 563 p = p.parent; 564 } 565 return null; 566 } 567 568 /// Implementations of [ReflectableProperties] interface. See the interface for details. 569 SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 570 if(valueIsJson) 571 return SetPropertyResult.wrongFormat; 572 switch(name) { 573 case "name": 574 this.name = value.idup; 575 return SetPropertyResult.success; 576 case "statusTip": 577 this.statusTip = value.idup; 578 return SetPropertyResult.success; 579 default: 580 return SetPropertyResult.noSuchProperty; 581 } 582 } 583 /// ditto 584 void getPropertiesList(scope void delegate(string name) sink) const { 585 sink("name"); 586 sink("statusTip"); 587 } 588 /// ditto 589 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 590 switch(name) { 591 case "name": 592 sink(name, this.name, false); 593 return; 594 case "statusTip": 595 sink(name, this.statusTip, false); 596 return; 597 default: 598 sink(name, null, true); 599 } 600 } 601 602 /++ 603 Scales the given value to the system-reported DPI for the monitor on which the widget resides. 604 605 History: 606 Added November 25, 2021 (dub v10.5) 607 `Point` overload added January 12, 2022 (dub v10.6) 608 +/ 609 int scaleWithDpi(int value, int assumedDpi = 96) { 610 // avoid potential overflow with common special values 611 if(value == int.max) 612 return int.max; 613 if(value == int.min) 614 return int.min; 615 if(value == 0) 616 return 0; 617 618 auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi; 619 //divide = 138; 620 // 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. 621 // this also covers the case when actualDpi returns 0. 622 if(divide < 96) 623 divide = 96; 624 return value * divide / assumedDpi; 625 } 626 627 /// ditto 628 Point scaleWithDpi(Point value, int assumedDpi = 96) { 629 return Point(scaleWithDpi(value.x, assumedDpi), scaleWithDpi(value.y, assumedDpi)); 630 } 631 632 // avoid this it just forwards to a soon-to-be-deprecated function and is not remotely stable 633 // I'll think up something better eventually 634 protected final int defaultLineHeight() { 635 auto cs = getComputedStyle(); 636 if(cs.font && !cs.font.isNull) 637 return cs.font.height() * 5 / 4; 638 else 639 return scaleWithDpi(Window.lineHeight * 5/4); 640 } 641 642 protected final int defaultTextWidth(const(char)[] text) { 643 auto cs = getComputedStyle(); 644 if(cs.font && !cs.font.isNull) 645 return cs.font.stringWidth(text); 646 else 647 return scaleWithDpi(Window.lineHeight * cast(int) text.length / 2); 648 } 649 650 /++ 651 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. 652 653 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. 654 655 History: 656 Added May 22, 2021 657 +/ 658 protected bool encapsulatedChildren() { 659 return false; 660 } 661 662 private void privateDpiChanged() { 663 dpiChanged(); 664 foreach(child; children) 665 child.privateDpiChanged(); 666 } 667 668 /++ 669 Virtual hook to update any caches or fonts you need on the event of a dpi scaling change. 670 671 History: 672 Added January 12, 2022 (dub v10.6) 673 +/ 674 protected void dpiChanged() { 675 676 } 677 678 // Default layout properties { 679 680 int minWidth() { return 0; } 681 int minHeight() { 682 // default widgets have a vertical layout, therefore the minimum height is the sum of the contents 683 int sum = this.paddingTop + this.paddingBottom; 684 foreach(child; children) { 685 if(child.hidden) 686 continue; 687 sum += child.minHeight(); 688 sum += child.marginTop(); 689 sum += child.marginBottom(); 690 } 691 692 return sum; 693 } 694 int maxWidth() { return int.max; } 695 int maxHeight() { return int.max; } 696 int widthStretchiness() { return 4; } 697 int heightStretchiness() { return 4; } 698 699 /++ 700 Where stretchiness will grow from the flex basis, this shrinkiness will let it get smaller if needed to make room for other items. 701 702 History: 703 Added June 15, 2021 (dub v10.1) 704 +/ 705 int widthShrinkiness() { return 0; } 706 /// ditto 707 int heightShrinkiness() { return 0; } 708 709 /++ 710 The initial size of the widget for layout calculations. Default is 0. 711 712 See_Also: [https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis|CSS flex-basis] 713 714 History: 715 Added June 15, 2021 (dub v10.1) 716 +/ 717 int flexBasisWidth() { return 0; } 718 /// ditto 719 int flexBasisHeight() { return 0; } 720 721 /++ 722 Not stable. 723 724 Values are scaled with dpi after assignment. If you override the virtual functions, this may be ignored. 725 726 So if you set defaultPadding to 4 and the user is on 150% zoom, it will multiply to return 6. 727 728 History: 729 Added January 5, 2023 730 +/ 731 Rectangle defaultMargin; 732 /// ditto 733 Rectangle defaultPadding; 734 735 int marginLeft() { return scaleWithDpi(defaultMargin.left); } 736 int marginRight() { return scaleWithDpi(defaultMargin.right); } 737 int marginTop() { return scaleWithDpi(defaultMargin.top); } 738 int marginBottom() { return scaleWithDpi(defaultMargin.bottom); } 739 int paddingLeft() { return scaleWithDpi(defaultPadding.left); } 740 int paddingRight() { return scaleWithDpi(defaultPadding.right); } 741 int paddingTop() { return scaleWithDpi(defaultPadding.top); } 742 int paddingBottom() { return scaleWithDpi(defaultPadding.bottom); } 743 //LinePreference linePreference() { return LinePreference.PreferOwnLine; } 744 745 private bool recomputeChildLayoutRequired = true; 746 private static class RecomputeEvent {} 747 private __gshared rce = new RecomputeEvent(); 748 protected final void queueRecomputeChildLayout() { 749 recomputeChildLayoutRequired = true; 750 751 if(this.parentWindow) { 752 auto sw = this.parentWindow.win; 753 assert(sw !is null); 754 if(!sw.eventQueued!RecomputeEvent) { 755 sw.postEvent(rce); 756 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 757 } 758 } 759 760 } 761 762 protected final void recomputeChildLayoutEntry() { 763 if(recomputeChildLayoutRequired) { 764 recomputeChildLayout(); 765 recomputeChildLayoutRequired = false; 766 redraw(); 767 } else { 768 // I still need to check the tree just in case one of them was queued up 769 // and the event came up here instead of there. 770 foreach(child; children) 771 child.recomputeChildLayoutEntry(); 772 } 773 } 774 775 // this function should (almost) never be called directly anymore... call recomputeChildLayoutEntry when executing it and queueRecomputeChildLayout if you just want it done soon 776 void recomputeChildLayout() { 777 .recomputeChildLayout!"height"(this); 778 } 779 780 // } 781 782 783 /++ 784 Returns the style's tag name string this object uses. 785 786 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. 787 788 This tag may never be used, it is just available for the [VisualTheme.getPropertyString] if it chooses to do something like CSS. 789 790 History: 791 Added May 10, 2021 792 +/ 793 string styleTagName() const { 794 string n = typeid(this).name; 795 foreach_reverse(idx, ch; n) 796 if(ch == '.') { 797 n = n[idx + 1 .. $]; 798 break; 799 } 800 return n; 801 } 802 803 /// API for the [styleClassList] 804 static struct ClassList { 805 private Widget widget; 806 807 /// 808 void add(string s) { 809 widget.styleClassList_ ~= s; 810 } 811 812 /// 813 void remove(string s) { 814 foreach(idx, s1; widget.styleClassList_) 815 if(s1 == s) { 816 widget.styleClassList_[idx] = widget.styleClassList_[$-1]; 817 widget.styleClassList_ = widget.styleClassList_[0 .. $-1]; 818 widget.styleClassList_.assumeSafeAppend(); 819 return; 820 } 821 } 822 823 /// Returns true if it was added, false if it was removed. 824 bool toggle(string s) { 825 if(contains(s)) { 826 remove(s); 827 return false; 828 } else { 829 add(s); 830 return true; 831 } 832 } 833 834 /// 835 bool contains(string s) const { 836 foreach(s1; widget.styleClassList_) 837 if(s1 == s) 838 return true; 839 return false; 840 841 } 842 } 843 844 private string[] styleClassList_; 845 846 /++ 847 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. 848 849 It has no inherent meaning, it is really just a place to put some metadata tags on individual objects. 850 851 History: 852 Added May 10, 2021 853 +/ 854 inout(ClassList) styleClassList() inout { 855 return cast(inout(ClassList)) ClassList(cast() this); 856 } 857 858 /++ 859 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. 860 861 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. 862 863 The upper 32 bits are available for your own extensions. 864 865 History: 866 Added May 10, 2021 867 +/ 868 enum DynamicState : ulong { 869 focus = (1 << 0), /// the widget currently has the keyboard focus 870 hover = (1 << 1), /// the mouse is currently hovering over the widget (may not always be updated) 871 valid = (1 << 2), /// the widget's content has been validated and it passed (do not set if not validation has been performed!) 872 invalid = (1 << 3), /// the widget's content has been validated and it failed (do not set if not validation has been performed!) 873 checked = (1 << 4), /// the widget is toggleable and currently toggled on 874 selected = (1 << 5), /// the widget represents one option of many and is currently selected, but is not necessarily focused nor checked. 875 disabled = (1 << 6), /// the widget is currently unable to perform its designated task 876 indeterminate = (1 << 7), /// the widget has tri-state and is between checked and not checked 877 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. 878 879 USER_BEGIN = (1UL << 32), 880 } 881 882 // I want to add the primary and cancel styles to buttons at least at some point somehow. 883 884 /// ditto 885 @property ulong dynamicState() { return dynamicState_; } 886 /// ditto 887 @property ulong dynamicState(ulong newValue) { 888 if(dynamicState != newValue) { 889 auto old = dynamicState_; 890 dynamicState_ = newValue; 891 892 useStyleProperties((scope Widget.Style s) { 893 if(s.variesWithState(old ^ newValue)) 894 redraw(); 895 }); 896 } 897 return dynamicState_; 898 } 899 900 /// ditto 901 void setDynamicState(ulong flags, bool state) { 902 auto ds = dynamicState_; 903 if(state) 904 ds |= flags; 905 else 906 ds &= ~flags; 907 908 dynamicState = ds; 909 } 910 911 private ulong dynamicState_; 912 913 deprecated("Use dynamic styles instead now") { 914 Color backgroundColor() { return backgroundColor_; } 915 void backgroundColor(Color c){ this.backgroundColor_ = c; } 916 917 MouseCursor cursor() { return GenericCursor.Default; } 918 } private Color backgroundColor_ = Color.transparent; 919 920 921 /++ 922 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). 923 924 It is here so there can be a specificity switch. 925 926 See [OverrideStyle] for a helper function to use your own. 927 928 History: 929 Added May 11, 2021 930 +/ 931 static class Style/* : StyleProperties*/ { 932 public Widget widget; // public because the mixin template needs access to it 933 934 /++ 935 You must override this to trigger automatic redraws if you ever uses the `dynamicState` flag in your style. 936 937 History: 938 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. 939 +/ 940 bool variesWithState(ulong dynamicStateFlags) { 941 version(win32_widgets) { 942 if(widget.hwnd) 943 return false; 944 } 945 return widget.tabStop && ((dynamicStateFlags & DynamicState.focus) ? true : false); 946 } 947 948 /// 949 Color foregroundColor() { 950 return WidgetPainter.visualTheme.foregroundColor; 951 } 952 953 /// 954 WidgetBackground background() { 955 // the default is a "transparent" background, which means 956 // it goes as far up as it can to get the color 957 if (widget.backgroundColor_ != Color.transparent) 958 return WidgetBackground(widget.backgroundColor_); 959 if (widget.parent) 960 return widget.parent.getComputedStyle.background; 961 return WidgetBackground(widget.backgroundColor_); 962 } 963 964 private static OperatingSystemFont fontCached_; 965 private OperatingSystemFont fontCached() { 966 if(fontCached_ is null) 967 fontCached_ = font(); 968 return fontCached_; 969 } 970 971 /++ 972 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. 973 +/ 974 OperatingSystemFont font() { 975 return null; 976 } 977 978 /++ 979 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. 980 981 You can return a member of [GenericCursor] or your own [MouseCursor] instance. 982 983 History: 984 Was previously a method directly on [Widget], moved to [Widget.Style] on May 12, 2021 985 +/ 986 MouseCursor cursor() { 987 return GenericCursor.Default; 988 } 989 990 FrameStyle borderStyle() { 991 return FrameStyle.none; 992 } 993 994 /++ 995 +/ 996 Color borderColor() { 997 return Color.transparent; 998 } 999 1000 FrameStyle outlineStyle() { 1001 if(widget.dynamicState & DynamicState.focus) 1002 return FrameStyle.dotted; 1003 else 1004 return FrameStyle.none; 1005 } 1006 1007 Color outlineColor() { 1008 return foregroundColor; 1009 } 1010 } 1011 1012 /++ 1013 This mixin overrides the [useStyleProperties] method to direct it toward your own style class. 1014 The basic usage is simple: 1015 1016 --- 1017 static class Style : YourParentClass.Style { /* YourParentClass is frequently Widget, of course, but not always */ 1018 // override style hints as-needed here 1019 } 1020 OverrideStyle!Style; // add the method 1021 --- 1022 1023 $(TIP 1024 While the class is not forced to be `static`, for best results, it should be. A non-static class 1025 can not be inherited by other objects whereas the static one can. A property on the base class, 1026 called [Widget.Style.widget|widget], is available for you to access its properties. 1027 ) 1028 1029 This exists just because [useStyleProperties] has a somewhat convoluted signature and its overrides must 1030 repeat them. Moreover, its implementation uses a stack class to optimize GC pressure from small fetches 1031 and that's a little tedious to repeat in your child classes too when you only care about changing the type. 1032 1033 1034 It also has a further facility to pick a wholly differnet class based on the [DynamicState] of the Widget. 1035 You may also just override `variesWithState` when you use this flag. 1036 1037 --- 1038 mixin OverrideStyle!( 1039 DynamicState.focus, YourFocusedStyle, 1040 DynamicState.hover, YourHoverStyle, 1041 YourDefaultStyle 1042 ) 1043 --- 1044 1045 It checks if `dynamicState` matches the state and if so, returns the object given. 1046 1047 If there is no state mask given, the next one matches everything. The first match given is used. 1048 1049 However, since in most cases you'll want check state inside your individual methods, you probably won't 1050 find much use for this whole-class swap out. 1051 1052 History: 1053 Added May 16, 2021 1054 +/ 1055 static protected mixin template OverrideStyle(S...) { 1056 static import amg = arsd.minigui; 1057 override void useStyleProperties(scope void delegate(scope amg.Widget.Style props) dg) { 1058 ulong mask = 0; 1059 foreach(idx, thing; S) { 1060 static if(is(typeof(thing) : ulong)) { 1061 mask = thing; 1062 } else { 1063 if(!(idx & 1) || (this.dynamicState & mask) == mask) { 1064 //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."); 1065 scope amg.Widget.Style s = new thing(); 1066 s.widget = this; 1067 dg(s); 1068 return; 1069 } 1070 } 1071 } 1072 } 1073 } 1074 /++ 1075 You can override this by hand, or use the [OverrideStyle] helper which is a bit less verbose. 1076 +/ 1077 void useStyleProperties(scope void delegate(scope Style props) dg) { 1078 scope Style s = new Style(); 1079 s.widget = this; 1080 dg(s); 1081 } 1082 1083 1084 protected void sendResizeEvent() { 1085 this.emit!ResizeEvent(); 1086 } 1087 1088 Menu contextMenu(int x, int y) { return null; } 1089 1090 final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) { 1091 if(parentWindow is null || parentWindow.win is null) return false; 1092 1093 auto menu = this.contextMenu(x, y); 1094 if(menu is null) 1095 return false; 1096 1097 version(win32_widgets) { 1098 // FIXME: if it is -1, -1, do it at the current selection location instead 1099 // tho the corner of the window, whcih it does now, isn't the literal worst. 1100 1101 if(screenX < 0 && screenY < 0) { 1102 auto p = this.globalCoordinates(); 1103 if(screenX == -2) 1104 p.x += x; 1105 if(screenY == -2) 1106 p.y += y; 1107 1108 screenX = p.x; 1109 screenY = p.y; 1110 } 1111 1112 if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null)) 1113 throw new Exception("TrackContextMenuEx"); 1114 } else version(custom_widgets) { 1115 menu.popup(this, x, y); 1116 } 1117 1118 return true; 1119 } 1120 1121 /++ 1122 Removes this widget from its parent. 1123 1124 History: 1125 `removeWidget` was made `final` on May 11, 2021. 1126 +/ 1127 @scriptable 1128 final void removeWidget() { 1129 auto p = this.parent; 1130 if(p) { 1131 int item; 1132 for(item = 0; item < p._children.length; item++) 1133 if(p._children[item] is this) 1134 break; 1135 auto idx = item; 1136 for(; item < p._children.length - 1; item++) 1137 p._children[item] = p._children[item + 1]; 1138 p._children = p._children[0 .. $-1]; 1139 1140 this.parent.widgetRemoved(idx, this); 1141 //this.parent = null; 1142 1143 p.queueRecomputeChildLayout(); 1144 } 1145 version(win32_widgets) { 1146 removeAllChildren(); 1147 if(hwnd) { 1148 DestroyWindow(hwnd); 1149 hwnd = null; 1150 } 1151 } 1152 } 1153 1154 /++ 1155 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. 1156 1157 History: 1158 Added September 19, 2021 1159 +/ 1160 protected void widgetRemoved(size_t oldIndex, Widget oldReference) { } 1161 1162 /++ 1163 Removes all child widgets from `this`. You should not use the removed widgets again. 1164 1165 Note that on Windows, it also destroys the native handles for the removed children recursively. 1166 1167 History: 1168 Added July 1, 2021 (dub v10.2) 1169 +/ 1170 void removeAllChildren() { 1171 version(win32_widgets) 1172 foreach(child; _children) { 1173 child.removeAllChildren(); 1174 if(child.hwnd) { 1175 DestroyWindow(child.hwnd); 1176 child.hwnd = null; 1177 } 1178 } 1179 auto orig = this._children; 1180 this._children = null; 1181 foreach(idx, w; orig) 1182 this.widgetRemoved(idx, w); 1183 1184 queueRecomputeChildLayout(); 1185 } 1186 1187 /++ 1188 Calls [getByName] with the generic type of Widget. Meant for script interop where instantiating a template is impossible. 1189 +/ 1190 @scriptable 1191 Widget getChildByName(string name) { 1192 return getByName(name); 1193 } 1194 /++ 1195 Finds the nearest descendant with the requested type and [name]. May return `this`. 1196 +/ 1197 final WidgetClass getByName(WidgetClass = Widget)(string name) { 1198 if(this.name == name) 1199 if(auto c = cast(WidgetClass) this) 1200 return c; 1201 foreach(child; children) { 1202 auto w = child.getByName(name); 1203 if(auto c = cast(WidgetClass) w) 1204 return c; 1205 } 1206 return null; 1207 } 1208 1209 /++ 1210 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. 1211 Names should be unique in a window. 1212 1213 See_Also: [getByName], [getChildByName] 1214 +/ 1215 @scriptable string name; 1216 1217 private EventHandler[][string] bubblingEventHandlers; 1218 private EventHandler[][string] capturingEventHandlers; 1219 1220 /++ 1221 Default event handlers. These are called on the appropriate 1222 event unless [Event.preventDefault] is called on the event at 1223 some point through the bubbling process. 1224 1225 1226 If you are implementing your own widget and want to add custom 1227 events, you should follow the same pattern here: create a virtual 1228 function named `defaultEventHandler_eventname` with the implementation, 1229 then, override [setupDefaultEventHandlers] and add a wrapped caller to 1230 `defaultEventHandlers["eventname"]`. It should be wrapped like so: 1231 `defaultEventHandlers["eventname"] = (Widget t, Event event) { t.defaultEventHandler_name(event); };`. 1232 This ensures virtual dispatch based on the correct subclass. 1233 1234 Also, don't forget to call `super.setupDefaultEventHandlers();` too in your 1235 overridden version. 1236 1237 You only need to do that on parent classes adding NEW event types. If you 1238 just want to change the default behavior of an existing event type in a subclass, 1239 you override the function (and optionally call `super.method_name`) like normal. 1240 1241 +/ 1242 protected EventHandler[string] defaultEventHandlers; 1243 1244 /// ditto 1245 void setupDefaultEventHandlers() { 1246 defaultEventHandlers["click"] = (Widget t, Event event) { t.defaultEventHandler_click(cast(ClickEvent) event); }; 1247 defaultEventHandlers["dblclick"] = (Widget t, Event event) { t.defaultEventHandler_dblclick(cast(DoubleClickEvent) event); }; 1248 defaultEventHandlers["keydown"] = (Widget t, Event event) { t.defaultEventHandler_keydown(cast(KeyDownEvent) event); }; 1249 defaultEventHandlers["keyup"] = (Widget t, Event event) { t.defaultEventHandler_keyup(cast(KeyUpEvent) event); }; 1250 defaultEventHandlers["mouseover"] = (Widget t, Event event) { t.defaultEventHandler_mouseover(cast(MouseOverEvent) event); }; 1251 defaultEventHandlers["mouseout"] = (Widget t, Event event) { t.defaultEventHandler_mouseout(cast(MouseOutEvent) event); }; 1252 defaultEventHandlers["mousedown"] = (Widget t, Event event) { t.defaultEventHandler_mousedown(cast(MouseDownEvent) event); }; 1253 defaultEventHandlers["mouseup"] = (Widget t, Event event) { t.defaultEventHandler_mouseup(cast(MouseUpEvent) event); }; 1254 defaultEventHandlers["mouseenter"] = (Widget t, Event event) { t.defaultEventHandler_mouseenter(cast(MouseEnterEvent) event); }; 1255 defaultEventHandlers["mouseleave"] = (Widget t, Event event) { t.defaultEventHandler_mouseleave(cast(MouseLeaveEvent) event); }; 1256 defaultEventHandlers["mousemove"] = (Widget t, Event event) { t.defaultEventHandler_mousemove(cast(MouseMoveEvent) event); }; 1257 defaultEventHandlers["char"] = (Widget t, Event event) { t.defaultEventHandler_char(cast(CharEvent) event); }; 1258 defaultEventHandlers["triggered"] = (Widget t, Event event) { t.defaultEventHandler_triggered(event); }; 1259 defaultEventHandlers["change"] = (Widget t, Event event) { t.defaultEventHandler_change(event); }; 1260 defaultEventHandlers["focus"] = (Widget t, Event event) { t.defaultEventHandler_focus(event); }; 1261 defaultEventHandlers["blur"] = (Widget t, Event event) { t.defaultEventHandler_blur(event); }; 1262 defaultEventHandlers["focusin"] = (Widget t, Event event) { t.defaultEventHandler_focusin(event); }; 1263 defaultEventHandlers["focusout"] = (Widget t, Event event) { t.defaultEventHandler_focusout(event); }; 1264 } 1265 1266 /// ditto 1267 void defaultEventHandler_click(ClickEvent event) {} 1268 /// ditto 1269 void defaultEventHandler_dblclick(DoubleClickEvent event) {} 1270 /// ditto 1271 void defaultEventHandler_keydown(KeyDownEvent event) {} 1272 /// ditto 1273 void defaultEventHandler_keyup(KeyUpEvent event) {} 1274 /// ditto 1275 void defaultEventHandler_mousedown(MouseDownEvent event) { 1276 if(event.button == MouseButton.left) { 1277 if(this.tabStop) 1278 this.focus(); 1279 } 1280 } 1281 /// ditto 1282 void defaultEventHandler_mouseover(MouseOverEvent event) {} 1283 /// ditto 1284 void defaultEventHandler_mouseout(MouseOutEvent event) {} 1285 /// ditto 1286 void defaultEventHandler_mouseup(MouseUpEvent event) {} 1287 /// ditto 1288 void defaultEventHandler_mousemove(MouseMoveEvent event) {} 1289 /// ditto 1290 void defaultEventHandler_mouseenter(MouseEnterEvent event) {} 1291 /// ditto 1292 void defaultEventHandler_mouseleave(MouseLeaveEvent event) {} 1293 /// ditto 1294 void defaultEventHandler_char(CharEvent event) {} 1295 /// ditto 1296 void defaultEventHandler_triggered(Event event) {} 1297 /// ditto 1298 void defaultEventHandler_change(Event event) {} 1299 /// ditto 1300 void defaultEventHandler_focus(Event event) {} 1301 /// ditto 1302 void defaultEventHandler_blur(Event event) {} 1303 /// ditto 1304 void defaultEventHandler_focusin(Event event) {} 1305 /// ditto 1306 void defaultEventHandler_focusout(Event event) {} 1307 1308 /++ 1309 [Event]s use a Javascript-esque model. See more details on the [Event] page. 1310 1311 [addEventListener] returns an opaque handle that you can later pass to [removeEventListener]. 1312 1313 addDirectEventListener just inserts a check `if(e.target !is this) return;` meaning it opts out 1314 of participating in handler delegation. 1315 1316 $(TIP 1317 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. 1318 ) 1319 +/ 1320 EventListener addDirectEventListener(string event, void delegate() handler, bool useCapture = false) { 1321 return addEventListener(event, (Widget, scope Event e) { 1322 if(e.srcElement is this) 1323 handler(); 1324 }, useCapture); 1325 } 1326 1327 /// ditto 1328 EventListener addDirectEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1329 return addEventListener(event, (Widget, Event e) { 1330 if(e.srcElement is this) 1331 handler(e); 1332 }, useCapture); 1333 } 1334 1335 /// ditto 1336 EventListener addDirectEventListener(Handler)(Handler handler, bool useCapture = false) { 1337 static if(is(Handler Fn == delegate)) { 1338 static if(is(Fn Params == __parameters)) { 1339 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1340 if(e.srcElement !is this) 1341 return; 1342 auto ty = cast(Params[0]) e; 1343 if(ty !is null) 1344 handler(ty); 1345 }, useCapture); 1346 } else static assert(0); 1347 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1348 } 1349 1350 /// ditto 1351 @scriptable 1352 EventListener addEventListener(string event, void delegate() handler, bool useCapture = false) { 1353 return addEventListener(event, (Widget, scope Event) { handler(); }, useCapture); 1354 } 1355 1356 /// ditto 1357 EventListener addEventListener(Handler)(Handler handler, bool useCapture = false) { 1358 static if(is(Handler Fn == delegate)) { 1359 static if(is(Fn Params == __parameters)) { 1360 return addEventListener(EventString!(Params[0]), (Widget, Event e) { 1361 auto ty = cast(Params[0]) e; 1362 if(ty !is null) 1363 handler(ty); 1364 }, useCapture); 1365 } else static assert(0); 1366 } else static assert(0, "Your handler wasn't usable because it wasn't passed a delegate. Use the delegate keyword at the call site."); 1367 } 1368 1369 /// ditto 1370 EventListener addEventListener(string event, void delegate(Event) handler, bool useCapture = false) { 1371 return addEventListener(event, (Widget, Event e) { handler(e); }, useCapture); 1372 } 1373 1374 /// ditto 1375 EventListener addEventListener(string event, EventHandler handler, bool useCapture = false) { 1376 if(event.length > 2 && event[0..2] == "on") 1377 event = event[2 .. $]; 1378 1379 if(useCapture) 1380 capturingEventHandlers[event] ~= handler; 1381 else 1382 bubblingEventHandlers[event] ~= handler; 1383 1384 return EventListener(this, event, handler, useCapture); 1385 } 1386 1387 /// ditto 1388 void removeEventListener(string event, EventHandler handler, bool useCapture = false) { 1389 if(event.length > 2 && event[0..2] == "on") 1390 event = event[2 .. $]; 1391 1392 if(useCapture) { 1393 if(event in capturingEventHandlers) 1394 foreach(ref evt; capturingEventHandlers[event]) 1395 if(evt is handler) evt = null; 1396 } else { 1397 if(event in bubblingEventHandlers) 1398 foreach(ref evt; bubblingEventHandlers[event]) 1399 if(evt is handler) evt = null; 1400 } 1401 } 1402 1403 /// ditto 1404 void removeEventListener(EventListener listener) { 1405 removeEventListener(listener.event, listener.handler, listener.useCapture); 1406 } 1407 1408 static if(UsingSimpledisplayX11) { 1409 void discardXConnectionState() { 1410 foreach(child; children) 1411 child.discardXConnectionState(); 1412 } 1413 1414 void recreateXConnectionState() { 1415 foreach(child; children) 1416 child.recreateXConnectionState(); 1417 redraw(); 1418 } 1419 } 1420 1421 /++ 1422 Returns the coordinates of this widget on the screen, relative to the upper left corner of the whole screen. 1423 1424 History: 1425 `globalCoordinates` was made `final` on May 11, 2021. 1426 +/ 1427 Point globalCoordinates() { 1428 int x = this.x; 1429 int y = this.y; 1430 auto p = this.parent; 1431 while(p) { 1432 x += p.x; 1433 y += p.y; 1434 p = p.parent; 1435 } 1436 1437 static if(UsingSimpledisplayX11) { 1438 auto dpy = XDisplayConnection.get; 1439 arsd.simpledisplay.Window dummyw; 1440 XTranslateCoordinates(dpy, this.parentWindow.win.impl.window, RootWindow(dpy, DefaultScreen(dpy)), x, y, &x, &y, &dummyw); 1441 } else { 1442 POINT pt; 1443 pt.x = x; 1444 pt.y = y; 1445 MapWindowPoints(this.parentWindow.win.impl.hwnd, null, &pt, 1); 1446 x = pt.x; 1447 y = pt.y; 1448 } 1449 1450 return Point(x, y); 1451 } 1452 1453 version(win32_widgets) 1454 int handleWmDrawItem(DRAWITEMSTRUCT* dis) { return 0; } 1455 1456 version(win32_widgets) 1457 /// Called when a WM_COMMAND is sent to the associated hwnd. 1458 void handleWmCommand(ushort cmd, ushort id) {} 1459 1460 version(win32_widgets) 1461 /++ 1462 Called when a WM_NOTIFY is sent to the associated hwnd. 1463 1464 History: 1465 +/ 1466 int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { return 0; } 1467 1468 version(win32_widgets) 1469 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); } 1470 1471 /++ 1472 This tip is displayed in the status bar (if there is one in the containing window) when the mouse moves over this widget. 1473 1474 Updates to this variable will only be made visible on the next mouse enter event. 1475 +/ 1476 @scriptable string statusTip; 1477 // string toolTip; 1478 // string helpText; 1479 1480 /++ 1481 If true, this widget can be focused via keyboard control with the tab key. 1482 1483 If false, it is assumed the widget itself does will never receive the keyboard focus (though its childen are free to). 1484 +/ 1485 bool tabStop = true; 1486 /++ 1487 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.) 1488 +/ 1489 int tabOrder; 1490 1491 version(win32_widgets) { 1492 static Widget[HWND] nativeMapping; 1493 /// The native handle, if there is one. 1494 HWND hwnd; 1495 WNDPROC originalWindowProcedure; 1496 1497 SimpleWindow simpleWindowWrappingHwnd; 1498 1499 // please note it IGNORES your return value and does NOT forward it to Windows! 1500 int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) { 1501 return 0; 1502 } 1503 } 1504 private bool implicitlyCreated; 1505 1506 /// 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. 1507 int x; 1508 /// ditto 1509 int y; 1510 private int _width; 1511 private int _height; 1512 private Widget[] _children; 1513 private Widget _parent; 1514 private Window _parentWindow; 1515 1516 /++ 1517 Returns the window to which this widget is attached. 1518 1519 History: 1520 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. 1521 +/ 1522 final @property inout(Window) parentWindow() inout @nogc nothrow pure { return _parentWindow; } 1523 private @property void parentWindow(Window parent) { 1524 _parentWindow = parent; 1525 foreach(child; children) 1526 child.parentWindow = parent; // please note that this is recursive 1527 } 1528 1529 /++ 1530 Returns the list of the widget's children. 1531 1532 History: 1533 Prior to May 11, 2021, the `Widget[] children` was directly available. Now, only this property getter is available and the actual store is private. 1534 1535 Children should be added by the constructor most the time, but if that's impossible, use [addChild] and [removeWidget] to manage the list. 1536 +/ 1537 final @property inout(Widget)[] children() inout @nogc nothrow pure { return _children; } 1538 1539 /++ 1540 Returns the widget's parent. 1541 1542 History: 1543 Prior to May 11, 2021, the `Widget parent` variable was directly available. Now, only this property getter is permitted. 1544 1545 The parent should only be managed by the [addChild] and [removeWidget] method. 1546 +/ 1547 final @property inout(Widget) parent() inout nothrow @nogc pure @safe return { return _parent; } 1548 1549 /// The widget's current size. 1550 final @scriptable public @property int width() const nothrow @nogc pure @safe { return _width; } 1551 /// ditto 1552 final @scriptable public @property int height() const nothrow @nogc pure @safe { return _height; } 1553 1554 /// Only the layout manager should be calling these. 1555 final protected @property int width(int a) @safe { return _width = a; } 1556 /// ditto 1557 final protected @property int height(int a) @safe { return _height = a; } 1558 1559 /++ 1560 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. 1561 1562 It is also responsible for calling [sendResizeEvent] to notify other listeners that the widget has changed size. 1563 +/ 1564 protected void registerMovement() { 1565 version(win32_widgets) { 1566 if(hwnd) { 1567 auto pos = getChildPositionRelativeToParentHwnd(this); 1568 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 1569 } 1570 } 1571 sendResizeEvent(); 1572 } 1573 1574 /// Creates the widget and adds it to the parent. 1575 this(Widget parent) { 1576 if(parent !is null) 1577 parent.addChild(this); 1578 setupDefaultEventHandlers(); 1579 } 1580 1581 /// 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. 1582 @scriptable 1583 bool isFocused() { 1584 return parentWindow && parentWindow.focusedWidget is this; 1585 } 1586 1587 private bool showing_ = true; 1588 /// 1589 bool showing() { return showing_; } 1590 /// 1591 bool hidden() { return !showing_; } 1592 /++ 1593 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. 1594 +/ 1595 void showing(bool s, bool recalculate = true) { 1596 auto so = showing_; 1597 showing_ = s; 1598 if(s != so) { 1599 version(win32_widgets) 1600 if(hwnd) 1601 ShowWindow(hwnd, s ? SW_SHOW : SW_HIDE); 1602 1603 if(parent && recalculate) { 1604 parent.queueRecomputeChildLayout(); 1605 parent.redraw(); 1606 } 1607 1608 foreach(child; children) 1609 child.showing(s, false); 1610 1611 } 1612 queueRecomputeChildLayout(); 1613 redraw(); 1614 } 1615 /// Convenience method for `showing = true` 1616 @scriptable 1617 void show() { 1618 showing = true; 1619 } 1620 /// Convenience method for `showing = false` 1621 @scriptable 1622 void hide() { 1623 showing = false; 1624 } 1625 1626 /// 1627 @scriptable 1628 void focus() { 1629 assert(parentWindow !is null); 1630 if(isFocused()) 1631 return; 1632 1633 if(parentWindow.focusedWidget) { 1634 // FIXME: more details here? like from and to 1635 auto from = parentWindow.focusedWidget; 1636 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, false); 1637 parentWindow.focusedWidget = null; 1638 from.emit!BlurEvent(); 1639 this.emit!FocusOutEvent(); 1640 } 1641 1642 1643 version(win32_widgets) { 1644 if(this.hwnd !is null) 1645 SetFocus(this.hwnd); 1646 } 1647 //else static if(UsingSimpledisplayX11) 1648 //this.parentWindow.win.focus(); 1649 1650 parentWindow.focusedWidget = this; 1651 parentWindow.focusedWidget.setDynamicState(DynamicState.focus, true); 1652 this.emit!FocusEvent(); 1653 this.emit!FocusInEvent(); 1654 } 1655 1656 /+ 1657 /++ 1658 Unfocuses the widget. This may reset 1659 +/ 1660 @scriptable 1661 void blur() { 1662 1663 } 1664 +/ 1665 1666 1667 /++ 1668 This is called when the widget is added to a window. It gives you a chance to set up event hooks. 1669 1670 Update on May 11, 2021: I'm considering removing this method. You can usually achieve these things through looser-coupled methods. 1671 +/ 1672 void attachedToWindow(Window w) {} 1673 /++ 1674 Callback when the widget is added to another widget. 1675 1676 Update on May 11, 2021: I'm considering removing this method since I've never actually found it useful. 1677 +/ 1678 void addedTo(Widget w) {} 1679 1680 /++ 1681 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. 1682 1683 This is available primarily to be overridden. For example, [MainWindow] overrides it to redirect its children into a central widget. 1684 +/ 1685 protected void addChild(Widget w, int position = int.max) { 1686 assert(w._parent !is this, "Child cannot be added twice to the same parent"); 1687 assert(w !is this, "Child cannot be its own parent!"); 1688 w._parent = this; 1689 if(position == int.max || position == children.length) { 1690 _children ~= w; 1691 } else { 1692 assert(position < _children.length); 1693 _children.length = _children.length + 1; 1694 for(int i = cast(int) _children.length - 1; i > position; i--) 1695 _children[i] = _children[i - 1]; 1696 _children[position] = w; 1697 } 1698 1699 this.parentWindow = this._parentWindow; 1700 1701 w.addedTo(this); 1702 1703 if(this.hidden) 1704 w.showing = false; 1705 1706 if(parentWindow !is null) { 1707 w.attachedToWindow(parentWindow); 1708 parentWindow.queueRecomputeChildLayout(); 1709 parentWindow.redraw(); 1710 } 1711 } 1712 1713 /++ 1714 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. 1715 +/ 1716 Widget getChildAtPosition(int x, int y) { 1717 // it goes backward so the last one to show gets picked first 1718 // might use z-index later 1719 foreach_reverse(child; children) { 1720 if(child.hidden) 1721 continue; 1722 if(child.x <= x && child.y <= y 1723 && ((x - child.x) < child.width) 1724 && ((y - child.y) < child.height)) 1725 { 1726 return child; 1727 } 1728 } 1729 1730 return null; 1731 } 1732 1733 /++ 1734 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. 1735 1736 History: 1737 Added July 2, 2021 (v10.2) 1738 +/ 1739 protected void addScrollPosition(ref int x, ref int y) {}; 1740 1741 /++ 1742 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. 1743 1744 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. 1745 1746 [paint] is not called for system widgets as the OS library draws them instead. 1747 1748 1749 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. 1750 1751 You should also look at [WidgetPainter.visualTheme] to be theme aware. 1752 1753 History: 1754 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. 1755 +/ 1756 void paint(WidgetPainter painter) { 1757 version(win32_widgets) 1758 if(hwnd) { 1759 return; 1760 } 1761 painter.drawThemed(&paintContent); // note this refers to the following overload 1762 } 1763 1764 /++ 1765 Responsible for drawing the content as the theme engine is responsible for other elements. 1766 1767 $(WARNING If you override [paint], this method may never be used as it is only called from inside the default implementation of `paint`.) 1768 1769 Params: 1770 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. 1771 1772 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. 1773 1774 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. 1775 1776 Returns: 1777 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. 1778 1779 History: 1780 Added May 15, 2021 1781 +/ 1782 Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 1783 return bounds; 1784 } 1785 1786 deprecated("Change ScreenPainter to WidgetPainter") 1787 final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); } 1788 1789 /// I don't actually like the name of this 1790 /// this draws a background on it 1791 void erase(WidgetPainter painter) { 1792 version(win32_widgets) 1793 if(hwnd) return; // Windows will do it. I think. 1794 1795 auto c = getComputedStyle().background.color; 1796 painter.fillColor = c; 1797 painter.outlineColor = c; 1798 1799 version(win32_widgets) { 1800 HANDLE b, p; 1801 if(c.a == 0 && parent is parentWindow) { 1802 // I don't remember why I had this really... 1803 b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 1804 p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 1805 } 1806 } 1807 painter.drawRectangle(Point(0, 0), width, height); 1808 version(win32_widgets) { 1809 if(c.a == 0 && parent is parentWindow) { 1810 SelectObject(painter.impl.hdc, p); 1811 SelectObject(painter.impl.hdc, b); 1812 } 1813 } 1814 } 1815 1816 /// 1817 WidgetPainter draw() { 1818 int x = this.x, y = this.y; 1819 auto parent = this.parent; 1820 while(parent) { 1821 x += parent.x; 1822 y += parent.y; 1823 parent = parent.parent; 1824 } 1825 1826 auto painter = parentWindow.win.draw(true); 1827 painter.originX = x; 1828 painter.originY = y; 1829 painter.setClipRectangle(Point(0, 0), width, height); 1830 return WidgetPainter(painter, this); 1831 } 1832 1833 /// 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. 1834 protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 1835 if(hidden) 1836 return; 1837 1838 int paintX = x; 1839 int paintY = y; 1840 if(this.useNativeDrawing()) { 1841 paintX = 0; 1842 paintY = 0; 1843 lox = 0; 1844 loy = 0; 1845 containment = Rectangle(0, 0, int.max, int.max); 1846 } 1847 1848 painter.originX = lox + paintX; 1849 painter.originY = loy + paintY; 1850 1851 bool actuallyPainted = false; 1852 1853 const clip = containment.intersectionOf(Rectangle(Point(lox + paintX, loy + paintY), Size(width, height))); 1854 if(clip == Rectangle.init) { 1855 // writeln(this, " clipped out"); 1856 return; 1857 } 1858 1859 bool invalidateChildren = invalidate; 1860 1861 if(redrawRequested || force) { 1862 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 1863 1864 painter.drawingUpon = this; 1865 1866 erase(painter); 1867 if(painter.visualTheme) 1868 painter.visualTheme.doPaint(this, painter); 1869 else 1870 paint(painter); 1871 1872 if(invalidate) { 1873 // sdpyPrintDebugString("invalidate " ~ typeid(this).name); 1874 auto region = Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height)); 1875 painter.invalidateRect(region); 1876 // children are contained inside this, so no need to do extra work 1877 invalidateChildren = false; 1878 } 1879 1880 redrawRequested = false; 1881 actuallyPainted = true; 1882 } 1883 1884 foreach(child; children) { 1885 version(win32_widgets) 1886 if(child.useNativeDrawing()) continue; 1887 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidateChildren); 1888 } 1889 1890 version(win32_widgets) 1891 foreach(child; children) { 1892 if(child.useNativeDrawing) { 1893 painter = WidgetPainter(child.simpleWindowWrappingHwnd.draw(true), child); 1894 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 1895 } 1896 } 1897 } 1898 1899 protected bool useNativeDrawing() nothrow { 1900 version(win32_widgets) 1901 return hwnd !is null; 1902 else 1903 return false; 1904 } 1905 1906 private static class RedrawEvent {} 1907 private __gshared re = new RedrawEvent(); 1908 1909 private bool redrawRequested; 1910 /// 1911 final void redraw(string file = __FILE__, size_t line = __LINE__) { 1912 redrawRequested = true; 1913 1914 if(this.parentWindow) { 1915 auto sw = this.parentWindow.win; 1916 assert(sw !is null); 1917 if(!sw.eventQueued!RedrawEvent) { 1918 sw.postEvent(re); 1919 // writeln("redraw requested from ", file,":",line," ", this.parentWindow.win.impl.window); 1920 } 1921 } 1922 } 1923 1924 private SimpleWindow drawableWindow; 1925 1926 /++ 1927 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. 1928 1929 Returns: 1930 `true` if you should do your default behavior. 1931 1932 History: 1933 Added May 5, 2021 1934 1935 Bugs: 1936 It does not do the static checks on gdc right now. 1937 +/ 1938 final protected bool emit(EventType, this This, Args...)(Args args) { 1939 version(GNU) {} else 1940 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1941 auto e = new EventType(this, args); 1942 e.dispatch(); 1943 return !e.defaultPrevented; 1944 } 1945 /// ditto 1946 final protected bool emit(string eventString, this This)() { 1947 auto e = new Event(eventString, this); 1948 e.dispatch(); 1949 return !e.defaultPrevented; 1950 } 1951 1952 /++ 1953 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. 1954 1955 History: 1956 Added May 5, 2021 1957 +/ 1958 final public EventListener subscribe(EventType, this This)(void delegate(EventType) handler) { 1959 static assert(classStaticallyEmits!(This, EventType), "The " ~ This.stringof ~ " class is not declared to emit " ~ EventType.stringof); 1960 return addEventListener(handler); 1961 } 1962 1963 /++ 1964 Gets the computed style properties from the visual theme. 1965 1966 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].) 1967 1968 History: 1969 Added May 8, 2021 1970 +/ 1971 final StyleInformation getComputedStyle() { 1972 return StyleInformation(this); 1973 } 1974 1975 int focusableWidgets(scope int delegate(Widget) dg) { 1976 foreach(widget; WidgetStream(this)) { 1977 if(widget.tabStop && !widget.hidden) { 1978 int result = dg(widget); 1979 if (result) 1980 return result; 1981 } 1982 } 1983 return 0; 1984 } 1985 1986 /++ 1987 Calculates the border box (that is, the full width/height of the widget, from border edge to border edge) 1988 for the given content box (the area between the padding) 1989 1990 History: 1991 Added January 4, 2023 (dub v11.0) 1992 +/ 1993 Rectangle borderBoxForContentBox(Rectangle contentBox) { 1994 auto cs = getComputedStyle(); 1995 1996 auto borderWidth = getBorderWidth(cs.borderStyle); 1997 1998 auto rect = contentBox; 1999 2000 rect.left -= borderWidth; 2001 rect.right += borderWidth; 2002 rect.top -= borderWidth; 2003 rect.bottom += borderWidth; 2004 2005 auto insideBorderRect = rect; 2006 2007 rect.left -= cs.paddingLeft; 2008 rect.right += cs.paddingRight; 2009 rect.top -= cs.paddingTop; 2010 rect.bottom += cs.paddingBottom; 2011 2012 return rect; 2013 } 2014 2015 2016 // FIXME: I kinda want to hide events from implementation widgets 2017 // so it just catches them all and stops propagation... 2018 // i guess i can do it with a event listener on star. 2019 2020 mixin Emits!KeyDownEvent; /// 2021 mixin Emits!KeyUpEvent; /// 2022 mixin Emits!CharEvent; /// 2023 2024 mixin Emits!MouseDownEvent; /// 2025 mixin Emits!MouseUpEvent; /// 2026 mixin Emits!ClickEvent; /// 2027 mixin Emits!DoubleClickEvent; /// 2028 mixin Emits!MouseMoveEvent; /// 2029 mixin Emits!MouseOverEvent; /// 2030 mixin Emits!MouseOutEvent; /// 2031 mixin Emits!MouseEnterEvent; /// 2032 mixin Emits!MouseLeaveEvent; /// 2033 2034 mixin Emits!ResizeEvent; /// 2035 2036 mixin Emits!BlurEvent; /// 2037 mixin Emits!FocusEvent; /// 2038 2039 mixin Emits!FocusInEvent; /// 2040 mixin Emits!FocusOutEvent; /// 2041 } 2042 2043 /+ 2044 /++ 2045 Interface to indicate that the widget has a simple value property. 2046 2047 History: 2048 Added August 26, 2021 2049 +/ 2050 interface HasValue!T { 2051 /// Getter 2052 @property T value(); 2053 /// Setter 2054 @property void value(T); 2055 } 2056 2057 /++ 2058 Interface to indicate that the widget has a range of possible values for its simple value property. 2059 This would be present on something like a slider or possibly a number picker. 2060 2061 History: 2062 Added September 11, 2021 2063 +/ 2064 interface HasRangeOfValues!T : HasValue!T { 2065 /// The minimum and maximum values in the range, inclusive. 2066 @property T minValue(); 2067 @property void minValue(T); /// ditto 2068 @property T maxValue(); /// ditto 2069 @property void maxValue(T); /// ditto 2070 2071 /// The smallest step the user interface allows. User may still type in values without this limitation. 2072 @property void step(T); 2073 @property T step(); /// ditto 2074 } 2075 2076 /++ 2077 Interface to indicate that the widget has a list of possible values the user can choose from. 2078 This would be present on something like a drop-down selector. 2079 2080 The value is NOT necessarily one of the items on the list. Consider the case of a free-entry 2081 combobox. 2082 2083 History: 2084 Added September 11, 2021 2085 +/ 2086 interface HasListOfValues!T : HasValue!T { 2087 @property T[] values; 2088 @property void values(T[]); 2089 2090 @property int selectedIndex(); // note it may return -1! 2091 @property void selectedIndex(int); 2092 } 2093 +/ 2094 2095 /++ 2096 History: 2097 Added September 2021 (dub v10.4) 2098 +/ 2099 class GridLayout : Layout { 2100 2101 // 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. 2102 2103 /++ 2104 If a widget is too small to fill a grid cell, the graviy tells where it "sticks" to. 2105 +/ 2106 enum Gravity { 2107 Center = 0, 2108 NorthWest = North | West, 2109 North = 0b10_00, 2110 NorthEast = North | East, 2111 West = 0b00_10, 2112 East = 0b00_01, 2113 SouthWest = South | West, 2114 South = 0b01_00, 2115 SouthEast = South | East, 2116 } 2117 2118 /++ 2119 The width and height are in some proportional units and can often just be 12. 2120 +/ 2121 this(int width, int height, Widget parent) { 2122 this.gridWidth = width; 2123 this.gridHeight = height; 2124 super(parent); 2125 } 2126 2127 /++ 2128 Sets the position of the given child. 2129 2130 The units of these arguments are in the proportional grid units you set in the constructor. 2131 +/ 2132 Widget setChildPosition(return Widget child, int x, int y, int width, int height, Gravity gravity = Gravity.Center) { 2133 // ensure it is in bounds 2134 // then ensure no overlaps 2135 2136 ChildPosition p = ChildPosition(child, x, y, width, height, gravity); 2137 2138 foreach(ref position; positions) { 2139 if(position.widget is child) { 2140 position = p; 2141 goto set; 2142 } 2143 } 2144 2145 positions ~= p; 2146 2147 set: 2148 2149 // FIXME: should this batch? 2150 queueRecomputeChildLayout(); 2151 2152 return child; 2153 } 2154 2155 override void addChild(Widget w, int position = int.max) { 2156 super.addChild(w, position); 2157 //positions ~= ChildPosition(w); 2158 if(position != int.max) { 2159 // FIXME: align it so they actually match. 2160 } 2161 } 2162 2163 override void widgetRemoved(size_t idx, Widget w) { 2164 // FIXME: keep the positions array aligned 2165 // positions[idx].widget = null; 2166 } 2167 2168 override void recomputeChildLayout() { 2169 registerMovement(); 2170 int onGrid = cast(int) positions.length; 2171 c: foreach(child; children) { 2172 // just snap it to the grid 2173 if(onGrid) 2174 foreach(position; positions) 2175 if(position.widget is child) { 2176 child.x = this.width * position.x / this.gridWidth; 2177 child.y = this.height * position.y / this.gridHeight; 2178 child.width = this.width * position.width / this.gridWidth; 2179 child.height = this.height * position.height / this.gridHeight; 2180 2181 auto diff = child.width - child.maxWidth(); 2182 // FIXME: gravity? 2183 if(diff > 0) { 2184 child.width = child.width - diff; 2185 2186 if(position.gravity & Gravity.West) { 2187 // nothing needed, already aligned 2188 } else if(position.gravity & Gravity.East) { 2189 child.x += diff; 2190 } else { 2191 child.x += diff / 2; 2192 } 2193 } 2194 2195 diff = child.height - child.maxHeight(); 2196 // FIXME: gravity? 2197 if(diff > 0) { 2198 child.height = child.height - diff; 2199 2200 if(position.gravity & Gravity.North) { 2201 // nothing needed, already aligned 2202 } else if(position.gravity & Gravity.South) { 2203 child.y += diff; 2204 } else { 2205 child.y += diff / 2; 2206 } 2207 } 2208 2209 2210 child.recomputeChildLayout(); 2211 onGrid--; 2212 continue c; 2213 } 2214 // the position isn't given on the grid array, we'll just fill in from where the explicit ones left off. 2215 } 2216 } 2217 2218 private struct ChildPosition { 2219 Widget widget; 2220 int x; 2221 int y; 2222 int width; 2223 int height; 2224 Gravity gravity; 2225 } 2226 private ChildPosition[] positions; 2227 2228 int gridWidth = 12; 2229 int gridHeight = 12; 2230 } 2231 2232 /// 2233 abstract class ComboboxBase : Widget { 2234 // if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN 2235 // or to always show the list, we want CBS_SIMPLE == 1 2236 version(win32_widgets) 2237 this(uint style, Widget parent) { 2238 super(parent); 2239 createWin32Window(this, "ComboBox"w, null, style); 2240 } 2241 else version(custom_widgets) 2242 this(Widget parent) { 2243 super(parent); 2244 2245 addEventListener((KeyDownEvent event) { 2246 if(event.key == Key.Up) { 2247 if(selection_ > -1) { // -1 means select blank 2248 selection_--; 2249 fireChangeEvent(); 2250 } 2251 event.preventDefault(); 2252 } 2253 if(event.key == Key.Down) { 2254 if(selection_ + 1 < options.length) { 2255 selection_++; 2256 fireChangeEvent(); 2257 } 2258 event.preventDefault(); 2259 } 2260 2261 }); 2262 2263 } 2264 else static assert(false); 2265 2266 /++ 2267 Returns the current list of options in the selection. 2268 2269 History: 2270 Property accessor added March 1, 2022 (dub v10.7). Prior to that, it was private. 2271 +/ 2272 final @property string[] options() const { 2273 return cast(string[]) options_; 2274 } 2275 2276 private string[] options_; 2277 private int selection_ = -1; 2278 2279 /++ 2280 Adds an option to the end of options array. 2281 +/ 2282 void addOption(string s) { 2283 options_ ~= s; 2284 version(win32_widgets) 2285 SendMessageW(hwnd, 323 /*CB_ADDSTRING*/, 0, cast(LPARAM) toWstringzInternal(s)); 2286 } 2287 2288 /++ 2289 Gets the current selection as an index into the [options] array. Returns -1 if nothing is selected. 2290 +/ 2291 int getSelection() { 2292 return selection_; 2293 } 2294 2295 /++ 2296 Returns the current selection as a string. 2297 2298 History: 2299 Added November 17, 2021 2300 +/ 2301 string getSelectionString() { 2302 return selection_ == -1 ? null : options[selection_]; 2303 } 2304 2305 /++ 2306 Sets the current selection to an index in the options array, or to the given option if present. 2307 Please note that the string version may do a linear lookup. 2308 2309 Returns: 2310 the index you passed in 2311 2312 History: 2313 The `string` based overload was added on March 1, 2022 (dub v10.7). 2314 2315 The return value was `void` prior to March 1, 2022. 2316 +/ 2317 int setSelection(int idx) { 2318 selection_ = idx; 2319 version(win32_widgets) 2320 SendMessageW(hwnd, 334 /*CB_SETCURSEL*/, idx, 0); 2321 2322 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2323 t.dispatch(); 2324 2325 return idx; 2326 } 2327 2328 /// ditto 2329 int setSelection(string s) { 2330 if(s !is null) 2331 foreach(idx, item; options) 2332 if(item == s) { 2333 return setSelection(cast(int) idx); 2334 } 2335 return setSelection(-1); 2336 } 2337 2338 /++ 2339 This event is fired when the selection changes. Note it inherits 2340 from ChangeEvent!string, meaning you can use that as well, and it also 2341 fills in [Event.intValue]. 2342 +/ 2343 static class SelectionChangedEvent : ChangeEvent!string { 2344 this(Widget target, int iv, string sv) { 2345 super(target, &stringValue); 2346 this.iv = iv; 2347 this.sv = sv; 2348 } 2349 immutable int iv; 2350 immutable string sv; 2351 2352 override @property string stringValue() { return sv; } 2353 override @property int intValue() { return iv; } 2354 } 2355 2356 version(win32_widgets) 2357 override void handleWmCommand(ushort cmd, ushort id) { 2358 if(cmd == CBN_SELCHANGE) { 2359 selection_ = cast(int) SendMessageW(hwnd, 327 /* CB_GETCURSEL */, 0, 0); 2360 fireChangeEvent(); 2361 } 2362 } 2363 2364 private void fireChangeEvent() { 2365 if(selection_ >= options.length) 2366 selection_ = -1; 2367 2368 auto t = new SelectionChangedEvent(this, selection_, selection_ == -1 ? null : options[selection_]); 2369 t.dispatch(); 2370 } 2371 2372 version(win32_widgets) { 2373 override int minHeight() { return defaultLineHeight + 6; } 2374 override int maxHeight() { return defaultLineHeight + 6; } 2375 } else { 2376 override int minHeight() { return defaultLineHeight + 4; } 2377 override int maxHeight() { return defaultLineHeight + 4; } 2378 } 2379 2380 version(custom_widgets) { 2381 2382 // FIXME: this should scroll if there's too many elements to reasonably fit on screen 2383 2384 SimpleWindow dropDown; 2385 void popup() { 2386 auto w = width; 2387 // FIXME: suggestedDropdownHeight see below 2388 auto h = cast(int) this.options.length * defaultLineHeight + 8; 2389 2390 auto coord = this.globalCoordinates(); 2391 auto dropDown = new SimpleWindow( 2392 w, h, 2393 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parentWindow ? parentWindow.win : null); 2394 2395 dropDown.move(coord.x, coord.y + this.height); 2396 2397 { 2398 auto cs = getComputedStyle(); 2399 auto painter = dropDown.draw(); 2400 draw3dFrame(0, 0, w, h, painter, FrameStyle.risen, getComputedStyle().background.color); 2401 auto p = Point(4, 4); 2402 painter.outlineColor = cs.foregroundColor; 2403 foreach(option; options) { 2404 painter.drawText(p, option); 2405 p.y += defaultLineHeight; 2406 } 2407 } 2408 2409 dropDown.setEventHandlers( 2410 (MouseEvent event) { 2411 if(event.type == MouseEventType.buttonReleased) { 2412 dropDown.close(); 2413 auto element = (event.y - 4) / defaultLineHeight; 2414 if(element >= 0 && element <= options.length) { 2415 selection_ = element; 2416 2417 fireChangeEvent(); 2418 } 2419 } 2420 } 2421 ); 2422 2423 dropDown.visibilityChanged = (bool visible) { 2424 if(visible) { 2425 this.redraw(); 2426 dropDown.grabInput(); 2427 } else { 2428 dropDown.releaseInputGrab(); 2429 } 2430 }; 2431 2432 dropDown.show(); 2433 } 2434 2435 } 2436 } 2437 2438 /++ 2439 A drop-down list where the user must select one of the 2440 given options. Like `<select>` in HTML. 2441 +/ 2442 class DropDownSelection : ComboboxBase { 2443 this(Widget parent) { 2444 version(win32_widgets) 2445 super(3 /* CBS_DROPDOWNLIST */ | WS_VSCROLL, parent); 2446 else version(custom_widgets) { 2447 super(parent); 2448 2449 addEventListener("focus", () { this.redraw; }); 2450 addEventListener("blur", () { this.redraw; }); 2451 addEventListener(EventType.change, () { this.redraw; }); 2452 addEventListener("mousedown", () { this.focus(); this.popup(); }); 2453 addEventListener((KeyDownEvent event) { 2454 if(event.key == Key.Space) 2455 popup(); 2456 }); 2457 } else static assert(false); 2458 } 2459 2460 mixin Padding!q{2}; 2461 static class Style : Widget.Style { 2462 override FrameStyle borderStyle() { return FrameStyle.risen; } 2463 } 2464 mixin OverrideStyle!Style; 2465 2466 version(custom_widgets) 2467 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 2468 auto cs = getComputedStyle(); 2469 2470 painter.drawText(bounds.upperLeft, selection_ == -1 ? "" : options[selection_]); 2471 2472 painter.outlineColor = cs.foregroundColor; 2473 painter.fillColor = cs.foregroundColor; 2474 Point[4] triangle; 2475 enum padding = 6; 2476 enum paddingV = 7; 2477 enum triangleWidth = 10; 2478 triangle[0] = Point(width - padding - triangleWidth, paddingV); 2479 triangle[1] = Point(width - padding - triangleWidth / 2, height - paddingV); 2480 triangle[2] = Point(width - padding - 0, paddingV); 2481 triangle[3] = triangle[0]; 2482 painter.drawPolygon(triangle[]); 2483 2484 return bounds; 2485 } 2486 2487 version(win32_widgets) 2488 override void registerMovement() { 2489 version(win32_widgets) { 2490 if(hwnd) { 2491 auto pos = getChildPositionRelativeToParentHwnd(this); 2492 // the height given to this from Windows' perspective is supposed 2493 // to include the drop down's height. so I add to it to give some 2494 // room for that. 2495 // FIXME: maybe make the subclass provide a suggestedDropdownHeight thing 2496 MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true); 2497 } 2498 } 2499 sendResizeEvent(); 2500 } 2501 } 2502 2503 /++ 2504 A text box with a drop down arrow listing selections. 2505 The user can choose from the list, or type their own. 2506 +/ 2507 class FreeEntrySelection : ComboboxBase { 2508 this(Widget parent) { 2509 version(win32_widgets) 2510 super(2 /* CBS_DROPDOWN */, parent); 2511 else version(custom_widgets) { 2512 super(parent); 2513 auto hl = new HorizontalLayout(this); 2514 lineEdit = new LineEdit(hl); 2515 2516 tabStop = false; 2517 2518 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2519 2520 auto btn = new class ArrowButton { 2521 this() { 2522 super(ArrowDirection.down, hl); 2523 } 2524 override int maxHeight() { 2525 return int.max; 2526 } 2527 }; 2528 //btn.addDirectEventListener("focus", &lineEdit.focus); 2529 btn.addEventListener("triggered", &this.popup); 2530 addEventListener(EventType.change, (Event event) { 2531 lineEdit.content = event.stringValue; 2532 lineEdit.focus(); 2533 redraw(); 2534 }); 2535 } 2536 else static assert(false); 2537 } 2538 2539 version(custom_widgets) { 2540 LineEdit lineEdit; 2541 } 2542 } 2543 2544 /++ 2545 A combination of free entry with a list below it. 2546 +/ 2547 class ComboBox : ComboboxBase { 2548 this(Widget parent) { 2549 version(win32_widgets) 2550 super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent); 2551 else version(custom_widgets) { 2552 super(parent); 2553 lineEdit = new LineEdit(this); 2554 listWidget = new ListWidget(this); 2555 listWidget.multiSelect = false; 2556 listWidget.addEventListener(EventType.change, delegate(Widget, Event) { 2557 string c = null; 2558 foreach(option; listWidget.options) 2559 if(option.selected) { 2560 c = option.label; 2561 break; 2562 } 2563 lineEdit.content = c; 2564 }); 2565 2566 listWidget.tabStop = false; 2567 this.tabStop = false; 2568 listWidget.addEventListener("focus", &lineEdit.focus); 2569 this.addEventListener("focus", &lineEdit.focus); 2570 2571 addDirectEventListener(EventType.change, { 2572 listWidget.setSelection(selection_); 2573 if(selection_ != -1) 2574 lineEdit.content = options[selection_]; 2575 lineEdit.focus(); 2576 redraw(); 2577 }); 2578 2579 lineEdit.addEventListener("focus", &lineEdit.selectAll); 2580 2581 listWidget.addDirectEventListener(EventType.change, { 2582 int set = -1; 2583 foreach(idx, opt; listWidget.options) 2584 if(opt.selected) { 2585 set = cast(int) idx; 2586 break; 2587 } 2588 if(set != selection_) 2589 this.setSelection(set); 2590 }); 2591 } else static assert(false); 2592 } 2593 2594 override int minHeight() { return defaultLineHeight * 3; } 2595 override int maxHeight() { return cast(int) options.length * defaultLineHeight + defaultLineHeight; } 2596 override int heightStretchiness() { return 5; } 2597 2598 version(custom_widgets) { 2599 LineEdit lineEdit; 2600 ListWidget listWidget; 2601 2602 override void addOption(string s) { 2603 listWidget.options ~= ListWidget.Option(s); 2604 ComboboxBase.addOption(s); 2605 } 2606 } 2607 } 2608 2609 /+ 2610 class Spinner : Widget { 2611 version(win32_widgets) 2612 this(Widget parent) { 2613 super(parent); 2614 parentWindow = parent.parentWindow; 2615 auto hlayout = new HorizontalLayout(this); 2616 lineEdit = new LineEdit(hlayout); 2617 upDownControl = new UpDownControl(hlayout); 2618 } 2619 2620 LineEdit lineEdit; 2621 UpDownControl upDownControl; 2622 } 2623 2624 class UpDownControl : Widget { 2625 version(win32_widgets) 2626 this(Widget parent) { 2627 super(parent); 2628 parentWindow = parent.parentWindow; 2629 createWin32Window(this, "msctls_updown32"w, null, 4/*UDS_ALIGNRIGHT*/| 2 /* UDS_SETBUDDYINT */ | 16 /* UDS_AUTOBUDDY */ | 32 /* UDS_ARROWKEYS */); 2630 } 2631 2632 override int minHeight() { return defaultLineHeight; } 2633 override int maxHeight() { return defaultLineHeight * 3/2; } 2634 2635 override int minWidth() { return defaultLineHeight * 3/2; } 2636 override int maxWidth() { return defaultLineHeight * 3/2; } 2637 } 2638 +/ 2639 2640 /+ 2641 class DataView : Widget { 2642 // this is the omnibus data viewer 2643 // the internal data layout is something like: 2644 // string[string][] but also each node can have parents 2645 } 2646 +/ 2647 2648 2649 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775491(v=vs.85).aspx#PROGRESS_CLASS 2650 2651 // http://svn.dsource.org/projects/bindings/trunk/win32/commctrl.d 2652 2653 // FIXME: menus should prolly capture the mouse. ugh i kno. 2654 /* 2655 TextEdit needs: 2656 2657 * caret manipulation 2658 * selection control 2659 * convenience functions for appendText, insertText, insertTextAtCaret, etc. 2660 2661 For example: 2662 2663 connect(paste, &textEdit.insertTextAtCaret); 2664 2665 would be nice. 2666 2667 2668 2669 I kinda want an omnibus dataview that combines list, tree, 2670 and table - it can be switched dynamically between them. 2671 2672 Flattening policy: only show top level, show recursive, show grouped 2673 List styles: plain list (e.g. <ul>), tiles (some details next to it), icons (like Windows explorer) 2674 2675 Single select, multi select, organization, drag+drop 2676 */ 2677 2678 //static if(UsingSimpledisplayX11) 2679 version(win32_widgets) {} 2680 else version(custom_widgets) { 2681 enum scrollClickRepeatInterval = 50; 2682 2683 deprecated("Get these properties off `Widget.getComputedStyle` instead. The defaults are now set in the `WidgetPainter.visualTheme`.") { 2684 enum windowBackgroundColor = Color(212, 212, 212); // used to be 192 2685 enum activeTabColor = lightAccentColor; 2686 enum hoveringColor = Color(228, 228, 228); 2687 enum buttonColor = windowBackgroundColor; 2688 enum depressedButtonColor = darkAccentColor; 2689 enum activeListXorColor = Color(255, 255, 127); 2690 enum progressBarColor = Color(0, 0, 128); 2691 enum activeMenuItemColor = Color(0, 0, 128); 2692 2693 }} 2694 else static assert(false); 2695 deprecated("Get these properties off the `visualTheme` instead.") { 2696 // these are used by horizontal rule so not just custom_widgets. for now at least. 2697 enum darkAccentColor = Color(172, 172, 172); 2698 enum lightAccentColor = Color(223, 223, 223); // used to be 223 2699 } 2700 2701 private const(wchar)* toWstringzInternal(in char[] s) { 2702 wchar[] str; 2703 str.reserve(s.length + 1); 2704 foreach(dchar ch; s) 2705 str ~= ch; 2706 str ~= '\0'; 2707 return str.ptr; 2708 } 2709 2710 static if(SimpledisplayTimerAvailable) 2711 void setClickRepeat(Widget w, int interval, int delay = 250) { 2712 Timer timer; 2713 int delayRemaining = delay / interval; 2714 if(delayRemaining <= 1) 2715 delayRemaining = 2; 2716 2717 immutable originalDelayRemaining = delayRemaining; 2718 2719 w.addDirectEventListener((scope MouseDownEvent ev) { 2720 if(ev.srcElement !is w) 2721 return; 2722 if(timer !is null) { 2723 timer.destroy(); 2724 timer = null; 2725 } 2726 delayRemaining = originalDelayRemaining; 2727 timer = new Timer(interval, () { 2728 if(delayRemaining > 0) 2729 delayRemaining--; 2730 else { 2731 auto ev = new Event("triggered", w); 2732 ev.sendDirectly(); 2733 } 2734 }); 2735 }); 2736 2737 w.addDirectEventListener((scope MouseUpEvent ev) { 2738 if(ev.srcElement !is w) 2739 return; 2740 if(timer !is null) { 2741 timer.destroy(); 2742 timer = null; 2743 } 2744 }); 2745 2746 w.addDirectEventListener((scope MouseLeaveEvent ev) { 2747 if(ev.srcElement !is w) 2748 return; 2749 if(timer !is null) { 2750 timer.destroy(); 2751 timer = null; 2752 } 2753 }); 2754 2755 } 2756 else 2757 void setClickRepeat(Widget w, int interval, int delay = 250) {} 2758 2759 enum FrameStyle { 2760 none, /// 2761 risen, /// a 3d pop-out effect (think Windows 95 button) 2762 sunk, /// a 3d sunken effect (think Windows 95 button as you click on it) 2763 solid, /// 2764 dotted, /// 2765 fantasy, /// a style based on a popular fantasy video game 2766 } 2767 2768 version(custom_widgets) 2769 deprecated 2770 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style) { 2771 draw3dFrame(0, 0, widget.width, widget.height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2772 } 2773 2774 version(custom_widgets) 2775 void draw3dFrame(Widget widget, ScreenPainter painter, FrameStyle style, Color background) { 2776 draw3dFrame(0, 0, widget.width, widget.height, painter, style, background); 2777 } 2778 2779 version(custom_widgets) 2780 deprecated 2781 void draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style) { 2782 draw3dFrame(x, y, width, height, painter, style, WidgetPainter.visualTheme.windowBackgroundColor); 2783 } 2784 2785 int getBorderWidth(FrameStyle style) { 2786 final switch(style) { 2787 case FrameStyle.sunk, FrameStyle.risen: 2788 return 2; 2789 case FrameStyle.none: 2790 return 0; 2791 case FrameStyle.solid: 2792 return 1; 2793 case FrameStyle.dotted: 2794 return 1; 2795 case FrameStyle.fantasy: 2796 return 3; 2797 } 2798 } 2799 2800 int draw3dFrame(int x, int y, int width, int height, ScreenPainter painter, FrameStyle style, Color background, Color border = Color.transparent) { 2801 int borderWidth = getBorderWidth(style); 2802 final switch(style) { 2803 case FrameStyle.sunk, FrameStyle.risen: 2804 // outer layer 2805 painter.outlineColor = style == FrameStyle.sunk ? Color.white : Color.black; 2806 break; 2807 case FrameStyle.none: 2808 painter.outlineColor = background; 2809 break; 2810 case FrameStyle.solid: 2811 painter.pen = Pen(border, 1); 2812 break; 2813 case FrameStyle.dotted: 2814 painter.pen = Pen(border, 1, Pen.Style.Dotted); 2815 break; 2816 case FrameStyle.fantasy: 2817 painter.pen = Pen(border, 3); 2818 break; 2819 } 2820 2821 painter.fillColor = background; 2822 painter.drawRectangle(Point(x + 0, y + 0), width, height); 2823 2824 2825 if(style == FrameStyle.sunk || style == FrameStyle.risen) { 2826 // 3d effect 2827 auto vt = WidgetPainter.visualTheme; 2828 2829 painter.outlineColor = (style == FrameStyle.sunk) ? vt.darkAccentColor : vt.lightAccentColor; 2830 painter.drawLine(Point(x + 0, y + 0), Point(x + width, y + 0)); 2831 painter.drawLine(Point(x + 0, y + 0), Point(x + 0, y + height - 1)); 2832 2833 // inner layer 2834 //right, bottom 2835 painter.outlineColor = (style == FrameStyle.sunk) ? vt.lightAccentColor : vt.darkAccentColor; 2836 painter.drawLine(Point(x + width - 2, y + 2), Point(x + width - 2, y + height - 2)); 2837 painter.drawLine(Point(x + 2, y + height - 2), Point(x + width - 2, y + height - 2)); 2838 // left, top 2839 painter.outlineColor = (style == FrameStyle.sunk) ? Color.black : Color.white; 2840 painter.drawLine(Point(x + 1, y + 1), Point(x + width, y + 1)); 2841 painter.drawLine(Point(x + 1, y + 1), Point(x + 1, y + height - 2)); 2842 } else if(style == FrameStyle.fantasy) { 2843 painter.pen = Pen(Color.white, 1, Pen.Style.Solid); 2844 painter.fillColor = Color.transparent; 2845 painter.drawRectangle(Point(x + 1, y + 1), Point(x + width - 1, y + height - 1)); 2846 } 2847 2848 return borderWidth; 2849 } 2850 2851 /++ 2852 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. 2853 2854 See_Also: 2855 [MenuItem] 2856 [ToolButton] 2857 [Menu.addItem] 2858 +/ 2859 class Action { 2860 version(win32_widgets) { 2861 private int id; 2862 private static int lastId = 9000; 2863 private static Action[int] mapping; 2864 } 2865 2866 KeyEvent accelerator; 2867 2868 // FIXME: disable message 2869 // and toggle thing? 2870 // ??? and trigger arguments too ??? 2871 2872 /++ 2873 Params: 2874 label = the textual label 2875 icon = icon ID. See [GenericIcons]. There is currently no way to do custom icons. 2876 triggered = initial handler, more can be added via the [triggered] member. 2877 +/ 2878 this(string label, ushort icon = 0, void delegate() triggered = null) { 2879 this.label = label; 2880 this.iconId = icon; 2881 if(triggered !is null) 2882 this.triggered ~= triggered; 2883 version(win32_widgets) { 2884 id = ++lastId; 2885 mapping[id] = this; 2886 } 2887 } 2888 2889 private string label; 2890 private ushort iconId; 2891 // icon 2892 2893 // when it is triggered, the triggered event is fired on the window 2894 /// The list of handlers when it is triggered. 2895 void delegate()[] triggered; 2896 } 2897 2898 /* 2899 plan: 2900 keyboard accelerators 2901 2902 * menus (and popups and tooltips) 2903 * status bar 2904 * toolbars and buttons 2905 2906 sortable table view 2907 2908 maybe notification area icons 2909 basic clipboard 2910 2911 * radio box 2912 splitter 2913 toggle buttons (optionally mutually exclusive, like in Paint) 2914 label, rich text display, multi line plain text (selectable) 2915 * fieldset 2916 * nestable grid layout 2917 single line text input 2918 * multi line text input 2919 slider 2920 spinner 2921 list box 2922 drop down 2923 combo box 2924 auto complete box 2925 * progress bar 2926 2927 terminal window/widget (on unix it might even be a pty but really idk) 2928 2929 ok button 2930 cancel button 2931 2932 keyboard hotkeys 2933 2934 scroll widget 2935 2936 event redirections and network transparency 2937 script integration 2938 */ 2939 2940 2941 /* 2942 MENUS 2943 2944 auto bar = new MenuBar(window); 2945 window.menuBar = bar; 2946 2947 auto fileMenu = bar.addItem(new Menu("&File")); 2948 fileMenu.addItem(new MenuItem("&Exit")); 2949 2950 2951 EVENTS 2952 2953 For controls, you should usually use "triggered" rather than "click", etc., because 2954 triggered handles both keyboard (focus and press as well as hotkeys) and mouse activation. 2955 This is the case on menus and pushbuttons. 2956 2957 "click", on the other hand, currently only fires when it is literally clicked by the mouse. 2958 */ 2959 2960 2961 /* 2962 enum LinePreference { 2963 AlwaysOnOwnLine, // always on its own line 2964 PreferOwnLine, // it will always start a new line, and if max width <= line width, it will expand all the way 2965 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 2966 } 2967 */ 2968 2969 /++ 2970 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. 2971 2972 --- 2973 class MyWidget : Widget { 2974 this(Widget parent) { super(parent); } 2975 2976 // set paddingLeft, paddingRight, paddingTop, and paddingBottom all to `return 4;` in one go: 2977 mixin Padding!q{4}; 2978 2979 // set marginLeft, marginRight, marginTop, and marginBottom all to `return 8;` in one go: 2980 mixin Margin!q{8}; 2981 2982 // but if I specify one outside, it overrides the override, so now marginLeft is 2, 2983 // while Top/Bottom/Right remain 8 from the mixin above. 2984 override int marginLeft() { return 2; } 2985 } 2986 --- 2987 2988 2989 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]). 2990 2991 Padding is the area inside a widget where its background is drawn, but the content avoids. 2992 2993 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!). 2994 2995 * 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. 2996 +/ 2997 mixin template Padding(string code) { 2998 override int paddingLeft() { return mixin(code);} 2999 override int paddingRight() { return mixin(code);} 3000 override int paddingTop() { return mixin(code);} 3001 override int paddingBottom() { return mixin(code);} 3002 } 3003 3004 /// ditto 3005 mixin template Margin(string code) { 3006 override int marginLeft() { return mixin(code);} 3007 override int marginRight() { return mixin(code);} 3008 override int marginTop() { return mixin(code);} 3009 override int marginBottom() { return mixin(code);} 3010 } 3011 3012 private 3013 void recomputeChildLayout(string relevantMeasure)(Widget parent) { 3014 enum calcingV = relevantMeasure == "height"; 3015 3016 parent.registerMovement(); 3017 3018 if(parent.children.length == 0) 3019 return; 3020 3021 auto parentStyle = parent.getComputedStyle(); 3022 3023 enum firstThingy = relevantMeasure == "height" ? "Top" : "Left"; 3024 enum secondThingy = relevantMeasure == "height" ? "Bottom" : "Right"; 3025 3026 enum otherFirstThingy = relevantMeasure == "height" ? "Left" : "Top"; 3027 enum otherSecondThingy = relevantMeasure == "height" ? "Right" : "Bottom"; 3028 3029 // my own width and height should already be set by the caller of this function... 3030 int spaceRemaining = mixin("parent." ~ relevantMeasure) - 3031 mixin("parentStyle.padding"~firstThingy~"()") - 3032 mixin("parentStyle.padding"~secondThingy~"()"); 3033 3034 int stretchinessSum; 3035 int stretchyChildSum; 3036 int lastMargin = 0; 3037 3038 int shrinkinessSum; 3039 int shrinkyChildSum; 3040 3041 // set initial size 3042 foreach(child; parent.children) { 3043 3044 auto childStyle = child.getComputedStyle(); 3045 3046 if(cast(StaticPosition) child) 3047 continue; 3048 if(child.hidden) 3049 continue; 3050 3051 const iw = child.flexBasisWidth(); 3052 const ih = child.flexBasisHeight(); 3053 3054 static if(calcingV) { 3055 child.width = parent.width - 3056 mixin("childStyle.margin"~otherFirstThingy~"()") - 3057 mixin("childStyle.margin"~otherSecondThingy~"()") - 3058 mixin("parentStyle.padding"~otherFirstThingy~"()") - 3059 mixin("parentStyle.padding"~otherSecondThingy~"()"); 3060 3061 if(child.width < 0) 3062 child.width = 0; 3063 if(child.width > childStyle.maxWidth()) 3064 child.width = childStyle.maxWidth(); 3065 3066 if(iw > 0) { 3067 auto totalPossible = child.width; 3068 if(child.width > iw && child.widthStretchiness() == 0) 3069 child.width = iw; 3070 } 3071 3072 child.height = mymax(childStyle.minHeight(), ih); 3073 } else { 3074 // set to take all the space 3075 child.height = parent.height - 3076 mixin("childStyle.margin"~firstThingy~"()") - 3077 mixin("childStyle.margin"~secondThingy~"()") - 3078 mixin("parentStyle.padding"~firstThingy~"()") - 3079 mixin("parentStyle.padding"~secondThingy~"()"); 3080 3081 // then clamp it 3082 if(child.height < 0) 3083 child.height = 0; 3084 if(child.height > childStyle.maxHeight()) 3085 child.height = childStyle.maxHeight(); 3086 3087 // and if possible, respect the ideal target 3088 if(ih > 0) { 3089 auto totalPossible = child.height; 3090 if(child.height > ih && child.heightStretchiness() == 0) 3091 child.height = ih; 3092 } 3093 3094 // if we have an ideal, try to respect it, otehrwise, just use the minimum 3095 child.width = mymax(childStyle.minWidth(), iw); 3096 } 3097 3098 spaceRemaining -= mixin("child." ~ relevantMeasure); 3099 3100 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3101 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3102 lastMargin = margin; 3103 spaceRemaining -= thisMargin + margin; 3104 3105 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3106 stretchinessSum += s; 3107 if(s > 0) 3108 stretchyChildSum++; 3109 3110 auto s2 = mixin("child." ~ relevantMeasure ~ "Shrinkiness()"); 3111 shrinkinessSum += s2; 3112 if(s2 > 0) 3113 shrinkyChildSum++; 3114 } 3115 3116 if(spaceRemaining < 0 && shrinkyChildSum) { 3117 // shrink to get into the space if it is possible 3118 auto toRemove = -spaceRemaining; 3119 auto removalPerItem = toRemove * shrinkinessSum / shrinkyChildSum; 3120 auto remainder = toRemove * shrinkinessSum % shrinkyChildSum; 3121 3122 // FIXME: wtf why am i shrinking things with no shrinkiness? 3123 3124 foreach(child; parent.children) { 3125 auto childStyle = child.getComputedStyle(); 3126 if(cast(StaticPosition) child) 3127 continue; 3128 if(child.hidden) 3129 continue; 3130 static if(calcingV) { 3131 auto maximum = childStyle.maxHeight(); 3132 } else { 3133 auto maximum = childStyle.maxWidth(); 3134 } 3135 3136 mixin("child._" ~ relevantMeasure) -= removalPerItem + remainder; // this is removing more than needed to trigger the next thing. ugh. 3137 3138 spaceRemaining += removalPerItem + remainder; 3139 } 3140 } 3141 3142 // stretch to fill space 3143 while(spaceRemaining > 0 && stretchinessSum && stretchyChildSum) { 3144 auto spacePerChild = spaceRemaining / stretchinessSum; 3145 bool spreadEvenly; 3146 bool giveToBiggest; 3147 if(spacePerChild <= 0) { 3148 spacePerChild = spaceRemaining / stretchyChildSum; 3149 spreadEvenly = true; 3150 } 3151 if(spacePerChild <= 0) { 3152 giveToBiggest = true; 3153 } 3154 int previousSpaceRemaining = spaceRemaining; 3155 stretchinessSum = 0; 3156 Widget mostStretchy; 3157 int mostStretchyS; 3158 foreach(child; parent.children) { 3159 auto childStyle = child.getComputedStyle(); 3160 if(cast(StaticPosition) child) 3161 continue; 3162 if(child.hidden) 3163 continue; 3164 static if(calcingV) { 3165 auto maximum = childStyle.maxHeight(); 3166 } else { 3167 auto maximum = childStyle.maxWidth(); 3168 } 3169 3170 if(mixin("child." ~ relevantMeasure) >= maximum) { 3171 auto adj = mixin("child." ~ relevantMeasure) - maximum; 3172 mixin("child._" ~ relevantMeasure) -= adj; 3173 spaceRemaining += adj; 3174 continue; 3175 } 3176 auto s = mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3177 if(s <= 0) 3178 continue; 3179 auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s); 3180 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3181 spaceRemaining -= spaceAdjustment; 3182 if(mixin("child." ~ relevantMeasure) > maximum) { 3183 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3184 mixin("child._" ~ relevantMeasure) -= diff; 3185 spaceRemaining += diff; 3186 } else if(mixin("child._" ~ relevantMeasure) < maximum) { 3187 stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()"); 3188 if(mostStretchy is null || s >= mostStretchyS) { 3189 mostStretchy = child; 3190 mostStretchyS = s; 3191 } 3192 } 3193 } 3194 3195 if(giveToBiggest && mostStretchy !is null) { 3196 auto child = mostStretchy; 3197 auto childStyle = child.getComputedStyle(); 3198 int spaceAdjustment = spaceRemaining; 3199 3200 static if(calcingV) 3201 auto maximum = childStyle.maxHeight(); 3202 else 3203 auto maximum = childStyle.maxWidth(); 3204 3205 mixin("child._" ~ relevantMeasure) += spaceAdjustment; 3206 spaceRemaining -= spaceAdjustment; 3207 if(mixin("child._" ~ relevantMeasure) > maximum) { 3208 auto diff = mixin("child." ~ relevantMeasure) - maximum; 3209 mixin("child._" ~ relevantMeasure) -= diff; 3210 spaceRemaining += diff; 3211 } 3212 } 3213 3214 if(spaceRemaining == previousSpaceRemaining) { 3215 if(mostStretchy !is null) { 3216 static if(calcingV) 3217 auto maximum = mostStretchy.maxHeight(); 3218 else 3219 auto maximum = mostStretchy.maxWidth(); 3220 3221 mixin("mostStretchy._" ~ relevantMeasure) += spaceRemaining; 3222 if(mixin("mostStretchy._" ~ relevantMeasure) > maximum) 3223 mixin("mostStretchy._" ~ relevantMeasure) = maximum; 3224 } 3225 break; // apparently nothing more we can do 3226 } 3227 3228 } 3229 3230 // position 3231 lastMargin = 0; 3232 int currentPos = mixin("parent.padding"~firstThingy~"()"); 3233 foreach(child; parent.children) { 3234 auto childStyle = child.getComputedStyle(); 3235 if(cast(StaticPosition) child) { 3236 child.recomputeChildLayout(); 3237 continue; 3238 } 3239 if(child.hidden) 3240 continue; 3241 auto margin = mixin("childStyle.margin" ~ secondThingy ~ "()"); 3242 int thisMargin = mymax(lastMargin, mixin("childStyle.margin"~firstThingy~"()")); 3243 currentPos += thisMargin; 3244 static if(calcingV) { 3245 child.x = parentStyle.paddingLeft() + childStyle.marginLeft(); 3246 child.y = currentPos; 3247 } else { 3248 child.x = currentPos; 3249 child.y = parentStyle.paddingTop() + childStyle.marginTop(); 3250 3251 } 3252 currentPos += mixin("child." ~ relevantMeasure); 3253 currentPos += margin; 3254 lastMargin = margin; 3255 3256 child.recomputeChildLayout(); 3257 } 3258 } 3259 3260 int mymax(int a, int b) { return a > b ? a : b; } 3261 int mymax(int a, int b, int c) { 3262 auto d = mymax(a, b); 3263 return c > d ? c : d; 3264 } 3265 3266 // OK so we need to make getting at the native window stuff possible in simpledisplay.d 3267 // and here, it must be integrable with the layout, the event system, and not be painted over. 3268 version(win32_widgets) { 3269 3270 // this function just does stuff that a parent window needs for redirection 3271 int WindowProcedureHelper(Widget this_, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 3272 this_.hookedWndProc(msg, wParam, lParam); 3273 3274 switch(msg) { 3275 3276 case WM_VSCROLL, WM_HSCROLL: 3277 auto pos = HIWORD(wParam); 3278 auto m = LOWORD(wParam); 3279 3280 auto scrollbarHwnd = cast(HWND) lParam; 3281 3282 if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) { 3283 3284 //auto smw = cast(ScrollMessageWidget) widgetp.parent; 3285 3286 switch(m) { 3287 /+ 3288 // I don't think those messages are ever actually sent normally by the widget itself, 3289 // they are more used for the keyboard interface. methinks. 3290 case SB_BOTTOM: 3291 // writeln("end"); 3292 auto event = new Event("scrolltoend", *widgetp); 3293 event.dispatch(); 3294 //if(!event.defaultPrevented) 3295 break; 3296 case SB_TOP: 3297 // writeln("top"); 3298 auto event = new Event("scrolltobeginning", *widgetp); 3299 event.dispatch(); 3300 break; 3301 case SB_ENDSCROLL: 3302 // idk 3303 break; 3304 +/ 3305 case SB_LINEDOWN: 3306 (*widgetp).emitCommand!"scrolltonextline"(); 3307 return 0; 3308 case SB_LINEUP: 3309 (*widgetp).emitCommand!"scrolltopreviousline"(); 3310 return 0; 3311 case SB_PAGEDOWN: 3312 (*widgetp).emitCommand!"scrolltonextpage"(); 3313 return 0; 3314 case SB_PAGEUP: 3315 (*widgetp).emitCommand!"scrolltopreviouspage"(); 3316 return 0; 3317 case SB_THUMBPOSITION: 3318 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3319 ev.dispatch(); 3320 return 0; 3321 case SB_THUMBTRACK: 3322 // eh kinda lying but i like the real time update display 3323 auto ev = new ScrollToPositionEvent(*widgetp, pos); 3324 ev.dispatch(); 3325 3326 // the event loop doesn't seem to carry on with a requested redraw.. 3327 // so we request it to get our dirty bit set... 3328 // then we need to immediately actually redraw it too for instant feedback to user 3329 SimpleWindow.processAllCustomEvents(); 3330 SimpleWindow.processAllCustomEvents(); 3331 //if(this_.parentWindow) 3332 //this_.parentWindow.actualRedraw(); 3333 3334 // and this ensures the WM_PAINT message is sent fairly quickly 3335 // still seems to lag a little in large windows but meh it basically works. 3336 if(this_.parentWindow) { 3337 // FIXME: if painting is slow, this does still lag 3338 // we probably will want to expose some user hook to ScrollWindowEx 3339 // or something. 3340 UpdateWindow(this_.parentWindow.hwnd); 3341 } 3342 return 0; 3343 default: 3344 } 3345 } 3346 break; 3347 3348 case WM_CONTEXTMENU: 3349 auto hwndFrom = cast(HWND) wParam; 3350 3351 auto xPos = cast(short) LOWORD(lParam); 3352 auto yPos = cast(short) HIWORD(lParam); 3353 3354 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3355 POINT p; 3356 p.x = xPos; 3357 p.y = yPos; 3358 ScreenToClient(hwnd, &p); 3359 auto clientX = cast(ushort) p.x; 3360 auto clientY = cast(ushort) p.y; 3361 3362 auto wap = widgetAtPoint(*widgetp, clientX, clientY); 3363 3364 if(wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos)) { 3365 return 0; 3366 } 3367 } 3368 break; 3369 3370 case WM_DRAWITEM: 3371 auto dis = cast(DRAWITEMSTRUCT*) lParam; 3372 if(auto widgetp = dis.hwndItem in Widget.nativeMapping) { 3373 return (*widgetp).handleWmDrawItem(dis); 3374 } 3375 break; 3376 3377 case WM_NOTIFY: 3378 auto hdr = cast(NMHDR*) lParam; 3379 auto hwndFrom = hdr.hwndFrom; 3380 auto code = hdr.code; 3381 3382 if(auto widgetp = hwndFrom in Widget.nativeMapping) { 3383 return (*widgetp).handleWmNotify(hdr, code, mustReturn); 3384 } 3385 break; 3386 case WM_COMMAND: 3387 auto handle = cast(HWND) lParam; 3388 auto cmd = HIWORD(wParam); 3389 return processWmCommand(hwnd, handle, cmd, LOWORD(wParam)); 3390 3391 default: 3392 // pass it on 3393 } 3394 return 0; 3395 } 3396 3397 3398 3399 extern(Windows) 3400 private 3401 // this is called by native child windows, whereas the other hook is done by simpledisplay windows 3402 // but can i merge them?! 3403 LRESULT HookedWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3404 // try { writeln(iMessage); } catch(Exception e) {}; 3405 3406 if(auto te = hWnd in Widget.nativeMapping) { 3407 try { 3408 3409 te.hookedWndProc(iMessage, wParam, lParam); 3410 3411 int mustReturn; 3412 auto ret = WindowProcedureHelper(*te, hWnd, iMessage, wParam, lParam, mustReturn); 3413 if(mustReturn) 3414 return ret; 3415 3416 if(iMessage == WM_SETFOCUS) { 3417 auto lol = *te; 3418 while(lol !is null && lol.implicitlyCreated) 3419 lol = lol.parent; 3420 lol.focus(); 3421 //(*te).parentWindow.focusedWidget = lol; 3422 } 3423 3424 3425 if(iMessage == WM_CTLCOLORBTN || iMessage == WM_CTLCOLORSTATIC) { 3426 SetBkMode(cast(HDC) wParam, TRANSPARENT); 3427 return cast(typeof(return)) GetSysColorBrush(COLOR_3DFACE); // this is the window background color... 3428 //GetStockObject(NULL_BRUSH); 3429 } 3430 3431 auto pos = getChildPositionRelativeToParentOrigin(*te); 3432 lastDefaultPrevented = false; 3433 // try { writeln(typeid(*te)); } catch(Exception e) {} 3434 if(SimpleWindow.triggerEvents(hWnd, iMessage, wParam, lParam, pos[0], pos[1], (*te).parentWindow.win) || !lastDefaultPrevented) 3435 return CallWindowProcW((*te).originalWindowProcedure, hWnd, iMessage, wParam, lParam); 3436 else { 3437 // it was something we recognized, should only call the window procedure if the default was not prevented 3438 } 3439 } catch(Exception e) { 3440 assert(0, e.toString()); 3441 } 3442 return 0; 3443 } 3444 assert(0, "shouldn't be receiving messages for this window...."); 3445 //assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen 3446 } 3447 3448 extern(Windows) 3449 private 3450 // see for info https://jeffpar.github.io/kbarchive/kb/079/Q79982/ 3451 LRESULT HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 3452 if(iMessage == WM_ERASEBKGND) { 3453 auto dc = GetDC(hWnd); 3454 auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE)); 3455 auto p = SelectObject(dc, GetStockObject(NULL_PEN)); 3456 RECT r; 3457 GetWindowRect(hWnd, &r); 3458 // since the pen is null, to fill the whole space, we need the +1 on both. 3459 gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1); 3460 SelectObject(dc, p); 3461 SelectObject(dc, b); 3462 ReleaseDC(hWnd, dc); 3463 InvalidateRect(hWnd, null, false); // redraw the border 3464 return 1; 3465 } 3466 return HookedWndProc(hWnd, iMessage, wParam, lParam); 3467 } 3468 3469 /++ 3470 Calls MS Windows' CreateWindowExW function to create a native backing for the given widget. It will create 3471 needed mappings, window procedure hooks, and other private member variables needed to tie it into the rest 3472 of minigui's expectations. 3473 3474 This should be called in your widget's constructor AFTER you call `super(parent);`. The parent window 3475 member MUST already be initialized for this function to succeed, which is done by [Widget]'s base constructor. 3476 3477 It assumes `className` is zero-terminated. It should come from a `"wide string literal"w`. 3478 3479 To check if you can use this, use `static if(UsingWin32Widgets)`. 3480 +/ 3481 void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) { 3482 assert(p.parentWindow !is null); 3483 assert(p.parentWindow.win.impl.hwnd !is null); 3484 3485 auto bsgroupbox = style == BS_GROUPBOX; 3486 3487 HWND phwnd; 3488 3489 auto wtf = p.parent; 3490 while(wtf) { 3491 if(wtf.hwnd !is null) { 3492 phwnd = wtf.hwnd; 3493 break; 3494 } 3495 wtf = wtf.parent; 3496 } 3497 3498 if(phwnd is null) 3499 phwnd = p.parentWindow.win.impl.hwnd; 3500 3501 assert(phwnd !is null); 3502 3503 WCharzBuffer wt = WCharzBuffer(windowText); 3504 3505 style |= WS_VISIBLE | WS_CHILD; 3506 //if(className != WC_TABCONTROL) 3507 style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 3508 p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style, 3509 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 3510 phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null); 3511 3512 assert(p.hwnd !is null); 3513 3514 3515 static HFONT font; 3516 if(font is null) { 3517 NONCLIENTMETRICS params; 3518 params.cbSize = params.sizeof; 3519 if(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, params.sizeof, ¶ms, 0)) { 3520 font = CreateFontIndirect(¶ms.lfMessageFont); 3521 } 3522 } 3523 3524 if(font) 3525 SendMessage(p.hwnd, WM_SETFONT, cast(uint) font, true); 3526 3527 p.simpleWindowWrappingHwnd = new SimpleWindow(p.hwnd); 3528 p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false; 3529 Widget.nativeMapping[p.hwnd] = p; 3530 3531 if(bsgroupbox) 3532 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK); 3533 else 3534 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3535 3536 EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p); 3537 3538 p.registerMovement(); 3539 } 3540 } 3541 3542 version(win32_widgets) 3543 private 3544 extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) { 3545 if(hwnd is null || hwnd in Widget.nativeMapping) 3546 return true; 3547 auto parent = cast(Widget) cast(void*) lparam; 3548 Widget p = new Widget(null); 3549 p._parent = parent; 3550 p.parentWindow = parent.parentWindow; 3551 p.hwnd = hwnd; 3552 p.implicitlyCreated = true; 3553 Widget.nativeMapping[p.hwnd] = p; 3554 p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 3555 return true; 3556 } 3557 3558 /++ 3559 Encapsulates the simpledisplay [ScreenPainter] for use on a [Widget], with [VisualTheme] and invalidated area awareness. 3560 +/ 3561 struct WidgetPainter { 3562 this(ScreenPainter screenPainter, Widget drawingUpon) { 3563 this.drawingUpon = drawingUpon; 3564 this.screenPainter = screenPainter; 3565 if(auto font = visualTheme.defaultFontCached) 3566 this.screenPainter.setFont(font); 3567 } 3568 3569 /++ 3570 EXPERIMENTAL. subject to change. 3571 3572 When you draw a cursor, you can draw this to notify your window of where it is, 3573 for IME systems to use. 3574 +/ 3575 void notifyCursorPosition(int x, int y, int width, int height) { 3576 if(auto a = drawingUpon.parentWindow) 3577 if(auto w = a.inputProxy) { 3578 w.setIMEPopupLocation(x + screenPainter.originX + width, y + screenPainter.originY + height); 3579 } 3580 } 3581 3582 3583 /// 3584 ScreenPainter screenPainter; 3585 /// Forward to the screen painter for other methods 3586 alias screenPainter this; 3587 3588 private Widget drawingUpon; 3589 3590 /++ 3591 This is the list of rectangles that actually need to be redrawn. 3592 3593 Not actually implemented yet. 3594 +/ 3595 Rectangle[] invalidatedRectangles; 3596 3597 private static BaseVisualTheme _visualTheme; 3598 3599 /++ 3600 Functions to access the visual theme and helpers to easily use it. 3601 3602 These are aware of the current widget's computed style out of the theme. 3603 +/ 3604 static @property BaseVisualTheme visualTheme() { 3605 if(_visualTheme is null) 3606 _visualTheme = new DefaultVisualTheme(); 3607 return _visualTheme; 3608 } 3609 3610 /// ditto 3611 static @property void visualTheme(BaseVisualTheme theme) { 3612 _visualTheme = theme; 3613 3614 // FIXME: notify all windows about the new theme 3615 } 3616 3617 /// ditto 3618 Color themeForeground() { 3619 return drawingUpon.getComputedStyle().foregroundColor(); 3620 } 3621 3622 /// ditto 3623 Color themeBackground() { 3624 return drawingUpon.getComputedStyle().background.color; 3625 } 3626 3627 int isDarkTheme() { 3628 return 0; // unspecified, yes, no as enum. FIXME 3629 } 3630 3631 /++ 3632 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. 3633 3634 It gives your draw delegate a [Rectangle] representing the coordinates inside your border and padding. 3635 3636 If you change teh clip rectangle, you should change it back before you return. 3637 3638 3639 The sequence it uses is: 3640 background 3641 content (delegated to you) 3642 border 3643 focused outline 3644 selected overlay 3645 3646 Example code: 3647 3648 --- 3649 void paint(WidgetPainter painter) { 3650 painter.drawThemed((bounds) { 3651 return bounds; // if the selection overlay should be contained, you can return it here. 3652 }); 3653 } 3654 --- 3655 +/ 3656 void drawThemed(scope Rectangle delegate(const Rectangle bounds) drawBody) { 3657 drawThemed((WidgetPainter painter, const Rectangle bounds) { 3658 return drawBody(bounds); 3659 }); 3660 } 3661 // this overload is actually mroe for setting the delegate to a virtual function 3662 void drawThemed(scope Rectangle delegate(WidgetPainter painter, const Rectangle bounds) drawBody) { 3663 Rectangle rect = Rectangle(0, 0, drawingUpon.width, drawingUpon.height); 3664 3665 auto cs = drawingUpon.getComputedStyle(); 3666 3667 auto bg = cs.background.color; 3668 3669 auto borderWidth = draw3dFrame(0, 0, drawingUpon.width, drawingUpon.height, this, cs.borderStyle, bg, cs.borderColor); 3670 3671 rect.left += borderWidth; 3672 rect.right -= borderWidth; 3673 rect.top += borderWidth; 3674 rect.bottom -= borderWidth; 3675 3676 auto insideBorderRect = rect; 3677 3678 rect.left += cs.paddingLeft; 3679 rect.right -= cs.paddingRight; 3680 rect.top += cs.paddingTop; 3681 rect.bottom -= cs.paddingBottom; 3682 3683 this.outlineColor = this.themeForeground; 3684 this.fillColor = bg; 3685 3686 auto widgetFont = cs.fontCached; 3687 if(widgetFont !is null) 3688 this.setFont(widgetFont); 3689 3690 rect = drawBody(this, rect); 3691 3692 if(widgetFont !is null) { 3693 if(auto vtFont = visualTheme.defaultFontCached) 3694 this.setFont(vtFont); 3695 else 3696 this.setFont(null); 3697 } 3698 3699 if(auto os = cs.outlineStyle()) { 3700 this.pen = Pen(cs.outlineColor(), 1, os == FrameStyle.dotted ? Pen.Style.Dotted : Pen.Style.Solid); 3701 this.fillColor = Color.transparent; 3702 this.drawRectangle(insideBorderRect); 3703 } 3704 } 3705 3706 /++ 3707 First, draw the background. 3708 Then draw your content. 3709 Next, draw the border. 3710 And the focused indicator. 3711 And the is-selected box. 3712 3713 If it is focused i can draw the outline too... 3714 3715 If selected i can even do the xor action but that's at the end. 3716 +/ 3717 void drawThemeBackground() { 3718 3719 } 3720 3721 void drawThemeBorder() { 3722 3723 } 3724 3725 // all this stuff is a dangerous experiment.... 3726 static class ScriptableVersion { 3727 ScreenPainterImplementation* p; 3728 int originX, originY; 3729 3730 @scriptable: 3731 void drawRectangle(int x, int y, int width, int height) { 3732 p.drawRectangle(x + originX, y + originY, width, height); 3733 } 3734 void drawLine(int x1, int y1, int x2, int y2) { 3735 p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY); 3736 } 3737 void drawText(int x, int y, string text) { 3738 p.drawText(x + originX, y + originY, 100000, 100000, text, 0); 3739 } 3740 void setOutlineColor(int r, int g, int b) { 3741 p.pen = Pen(Color(r,g,b), 1); 3742 } 3743 void setFillColor(int r, int g, int b) { 3744 p.fillColor = Color(r,g,b); 3745 } 3746 } 3747 3748 ScriptableVersion toArsdJsvar() { 3749 auto sv = new ScriptableVersion; 3750 sv.p = this.screenPainter.impl; 3751 sv.originX = this.screenPainter.originX; 3752 sv.originY = this.screenPainter.originY; 3753 return sv; 3754 } 3755 3756 static WidgetPainter fromJsVar(T)(T t) { 3757 return WidgetPainter.init; 3758 } 3759 // done.......... 3760 } 3761 3762 3763 struct Style { 3764 static struct helper(string m, T) { 3765 enum method = m; 3766 T v; 3767 3768 mixin template MethodOverride(typeof(this) v) { 3769 mixin("override typeof(v.v) "~v.method~"() { return v.v; }"); 3770 } 3771 } 3772 3773 static auto opDispatch(string method, T)(T value) { 3774 return helper!(method, T)(value); 3775 } 3776 } 3777 3778 /++ 3779 Implementation detail of the [ControlledBy] UDA. 3780 3781 History: 3782 Added Oct 28, 2020 3783 +/ 3784 struct ControlledBy_(T, Args...) { 3785 Args args; 3786 3787 static if(Args.length) 3788 this(Args args) { 3789 this.args = args; 3790 } 3791 3792 private T construct(Widget parent) { 3793 return new T(args, parent); 3794 } 3795 } 3796 3797 /++ 3798 User-defined attribute you can add to struct members contrlled by [addDataControllerWidget] or [dialog] to tell which widget you want created for them. 3799 3800 History: 3801 Added Oct 28, 2020 3802 +/ 3803 auto ControlledBy(T, Args...)(Args args) { 3804 return ControlledBy_!(T, Args)(args); 3805 } 3806 3807 struct ContainerMeta { 3808 string name; 3809 ContainerMeta[] children; 3810 Widget function(Widget parent) factory; 3811 3812 Widget instantiate(Widget parent) { 3813 auto n = factory(parent); 3814 n.name = name; 3815 foreach(child; children) 3816 child.instantiate(n); 3817 return n; 3818 } 3819 } 3820 3821 /++ 3822 This is a helper for [addDataControllerWidget]. You can use it as a UDA on the type. See 3823 http://dpldocs.info/this-week-in-d/Blog.Posted_2020_11_02.html for more information. 3824 3825 Please note that as of May 28, 2021, a dmd bug prevents this from compiling on module-level 3826 structures. It works fine on structs declared inside functions though. 3827 3828 See: https://issues.dlang.org/show_bug.cgi?id=21984 3829 +/ 3830 template Container(CArgs...) { 3831 static if(CArgs.length && is(CArgs[0] : Widget)) { 3832 private alias Super = CArgs[0]; 3833 private alias CArgs2 = CArgs[1 .. $]; 3834 } else { 3835 private alias Super = Layout; 3836 private alias CArgs2 = CArgs; 3837 } 3838 3839 class Container : Super { 3840 this(Widget parent) { super(parent); } 3841 3842 // just to partially support old gdc versions 3843 version(GNU) { 3844 static if(CArgs2.length >= 1) { enum tmp0 = CArgs2[0]; mixin typeof(tmp0).MethodOverride!(CArgs2[0]); } 3845 static if(CArgs2.length >= 2) { enum tmp1 = CArgs2[1]; mixin typeof(tmp1).MethodOverride!(CArgs2[1]); } 3846 static if(CArgs2.length >= 3) { enum tmp2 = CArgs2[2]; mixin typeof(tmp2).MethodOverride!(CArgs2[2]); } 3847 static if(CArgs2.length > 3) static assert(0, "only a few overrides like this supported on your compiler version at this time"); 3848 } else mixin(q{ 3849 static foreach(Arg; CArgs2) { 3850 mixin Arg.MethodOverride!(Arg); 3851 } 3852 }); 3853 3854 static ContainerMeta opCall(string name, ContainerMeta[] children...) { 3855 return ContainerMeta( 3856 name, 3857 children.dup, 3858 function (Widget parent) { return new typeof(this)(parent); } 3859 ); 3860 } 3861 3862 static ContainerMeta opCall(ContainerMeta[] children...) { 3863 return opCall(null, children); 3864 } 3865 } 3866 } 3867 3868 /++ 3869 The data controller widget is created by reflecting over the given 3870 data type. You can use [ControlledBy] as a UDA on a struct or 3871 just let it create things automatically. 3872 3873 Unlike [dialog], this uses real-time updating of the data and 3874 you add it to another window yourself. 3875 3876 --- 3877 struct Test { 3878 int x; 3879 int y; 3880 } 3881 3882 auto window = new Window(); 3883 auto dcw = new DataControllerWidget!Test(new Test, window); 3884 --- 3885 3886 The way it works is any public members are given a widget based 3887 on their data type, and public methods trigger an action button 3888 if no relevant parameters or a dialog action if it does have 3889 parameters, similar to the [menu] facility. 3890 3891 If you change data programmatically, without going through the 3892 DataControllerWidget methods, you will have to tell it something 3893 has changed and it needs to redraw. This is done with the `invalidate` 3894 method. 3895 3896 History: 3897 Added Oct 28, 2020 3898 +/ 3899 /// Group: generating_from_code 3900 class DataControllerWidget(T) : WidgetContainer { 3901 static if(is(T == class) || is(T == interface) || is(T : const E[], E)) 3902 private alias Tref = T; 3903 else 3904 private alias Tref = T*; 3905 3906 Tref datum; 3907 3908 /++ 3909 See_also: [addDataControllerWidget] 3910 +/ 3911 this(Tref datum, Widget parent) { 3912 this.datum = datum; 3913 3914 Widget cp = this; 3915 3916 super(parent); 3917 3918 foreach(attr; __traits(getAttributes, T)) 3919 static if(is(typeof(attr) == ContainerMeta)) { 3920 cp = attr.instantiate(this); 3921 } 3922 3923 auto def = this.getByName("default"); 3924 if(def !is null) 3925 cp = def; 3926 3927 Widget helper(string name) { 3928 auto maybe = this.getByName(name); 3929 if(maybe is null) 3930 return cp; 3931 return maybe; 3932 3933 } 3934 3935 foreach(member; __traits(allMembers, T)) 3936 static if(member != "this") // wtf https://issues.dlang.org/show_bug.cgi?id=22011 3937 static if(is(typeof(__traits(getMember, this.datum, member)))) 3938 static if(__traits(getProtection, __traits(getMember, this.datum, member)) == "public") { 3939 void delegate() update; 3940 3941 auto w = widgetFor!(__traits(getMember, T, member))(&__traits(getMember, this.datum, member), helper(member), update); 3942 3943 if(update) 3944 updaters ~= update; 3945 3946 static if(is(typeof(__traits(getMember, this.datum, member)) == function)) { 3947 w.addEventListener("triggered", delegate() { 3948 makeAutomaticHandler!(__traits(getMember, this.datum, member))(&__traits(getMember, this.datum, member))(); 3949 notifyDataUpdated(); 3950 }); 3951 } else static if(is(typeof(w.isChecked) == bool)) { 3952 w.addEventListener(EventType.change, (Event ev) { 3953 __traits(getMember, this.datum, member) = w.isChecked; 3954 }); 3955 } else static if(is(typeof(w.value) == string) || is(typeof(w.content) == string)) { 3956 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.stringValue); } ); 3957 } else static if(is(typeof(w.value) == int)) { 3958 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 3959 } else static if(is(typeof(w) == DropDownSelection)) { 3960 // special case for this to kinda support enums and such. coudl be better though 3961 w.addEventListener("change", (Event e) { genericSetValue(&__traits(getMember, this.datum, member), e.intValue); } ); 3962 } else { 3963 //static assert(0, "unsupported type " ~ typeof(__traits(getMember, this.datum, member)).stringof ~ " " ~ typeof(w).stringof); 3964 } 3965 } 3966 } 3967 3968 /++ 3969 If you modify the data in the structure directly, you need to call this to update the UI and propagate any change messages. 3970 3971 History: 3972 Added May 28, 2021 3973 +/ 3974 void notifyDataUpdated() { 3975 foreach(updater; updaters) 3976 updater(); 3977 3978 this.emit!(ChangeEvent!void)(delegate{}); 3979 } 3980 3981 private Widget[string] memberWidgets; 3982 private void delegate()[] updaters; 3983 3984 mixin Emits!(ChangeEvent!void); 3985 } 3986 3987 private int saturatedSum(int[] values...) { 3988 int sum; 3989 foreach(value; values) { 3990 if(value == int.max) 3991 return int.max; 3992 sum += value; 3993 } 3994 return sum; 3995 } 3996 3997 void genericSetValue(T, W)(T* where, W what) { 3998 import std.conv; 3999 *where = to!T(what); 4000 //*where = cast(T) stringToLong(what); 4001 } 4002 4003 /++ 4004 Creates a widget for the value `tt`, which is pointed to at runtime by `valptr`, with the given parent. 4005 4006 The `update` delegate can be called if you change `*valptr` to reflect those changes in the widget. 4007 4008 Note that this creates the widget but does not attach any event handlers to it. 4009 +/ 4010 private static auto widgetFor(alias tt, P)(P valptr, Widget parent, out void delegate() update) { 4011 4012 string displayName = __traits(identifier, tt).beautify; 4013 4014 static if(controlledByCount!tt == 1) { 4015 foreach(i, attr; __traits(getAttributes, tt)) { 4016 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) { 4017 auto w = attr.construct(parent); 4018 static if(__traits(compiles, w.setPosition(*valptr))) 4019 update = () { w.setPosition(*valptr); }; 4020 else static if(__traits(compiles, w.setValue(*valptr))) 4021 update = () { w.setValue(*valptr); }; 4022 4023 if(update) 4024 update(); 4025 return w; 4026 } 4027 } 4028 } else static if(controlledByCount!tt == 0) { 4029 static if(is(typeof(tt) == enum)) { 4030 // FIXME: update 4031 auto dds = new DropDownSelection(parent); 4032 foreach(idx, option; __traits(allMembers, typeof(tt))) { 4033 dds.addOption(option); 4034 if(__traits(getMember, typeof(tt), option) == *valptr) 4035 dds.setSelection(cast(int) idx); 4036 } 4037 return dds; 4038 } else static if(is(typeof(tt) == bool)) { 4039 auto box = new Checkbox(displayName, parent); 4040 update = () { box.isChecked = *valptr; }; 4041 update(); 4042 return box; 4043 } else static if(is(typeof(tt) : const long)) { 4044 auto le = new LabeledLineEdit(displayName, parent); 4045 update = () { le.content = toInternal!string(*valptr); }; 4046 update(); 4047 return le; 4048 } else static if(is(typeof(tt) : const double)) { 4049 auto le = new LabeledLineEdit(displayName, parent); 4050 import std.conv; 4051 update = () { le.content = to!string(*valptr); }; 4052 update(); 4053 return le; 4054 } else static if(is(typeof(tt) : const string)) { 4055 auto le = new LabeledLineEdit(displayName, parent); 4056 update = () { le.content = *valptr; }; 4057 update(); 4058 return le; 4059 } else static if(is(typeof(tt) == function)) { 4060 auto w = new Button(displayName, parent); 4061 return w; 4062 } else static if(is(typeof(tt) == class) || is(typeof(tt) == interface)) { 4063 return parent.addDataControllerWidget(tt); 4064 } else static assert(0, typeof(tt).stringof); 4065 } else static assert(0, "multiple controllers not yet supported"); 4066 } 4067 4068 private template controlledByCount(alias tt) { 4069 static int helper() { 4070 int count; 4071 foreach(i, attr; __traits(getAttributes, tt)) 4072 static if(is(typeof(attr) == ControlledBy_!(T, Args), T, Args...)) 4073 count++; 4074 return count; 4075 } 4076 4077 enum controlledByCount = helper; 4078 } 4079 4080 /++ 4081 Intended for UFCS action like `window.addDataControllerWidget(new MyObject());` 4082 4083 If you provide a `redrawOnChange` widget, it will automatically register a change event handler that calls that widget's redraw method. 4084 4085 History: 4086 The `redrawOnChange` parameter was added on May 28, 2021. 4087 +/ 4088 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T t, Widget redrawOnChange = null) if(is(T == class) || is(T == interface)) { 4089 auto dcw = new DataControllerWidget!T(t, parent); 4090 initializeDataControllerWidget(dcw, redrawOnChange); 4091 return dcw; 4092 } 4093 4094 /// ditto 4095 DataControllerWidget!T addDataControllerWidget(T)(Widget parent, T* t, Widget redrawOnChange = null) if(is(T == struct)) { 4096 auto dcw = new DataControllerWidget!T(t, parent); 4097 initializeDataControllerWidget(dcw, redrawOnChange); 4098 return dcw; 4099 } 4100 4101 private void initializeDataControllerWidget(Widget w, Widget redrawOnChange) { 4102 if(redrawOnChange !is null) 4103 w.addEventListener("change", delegate() { redrawOnChange.redraw(); }); 4104 } 4105 4106 /++ 4107 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. 4108 4109 History: 4110 Finalized on June 3, 2021 for the dub v10.0 release 4111 +/ 4112 struct StyleInformation { 4113 private Widget w; 4114 private BaseVisualTheme visualTheme; 4115 4116 private this(Widget w) { 4117 this.w = w; 4118 this.visualTheme = WidgetPainter.visualTheme; 4119 } 4120 4121 /++ 4122 Forwards to [Widget.Style] 4123 4124 Bugs: 4125 It is supposed to fall back to the [VisualTheme] if 4126 the style doesn't override the default, but that is 4127 not generally implemented. Many of them may end up 4128 being explicit overloads instead of the generic 4129 opDispatch fallback, like [font] is now. 4130 +/ 4131 public @property opDispatch(string name)() { 4132 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4133 w.useStyleProperties((scope Widget.Style props) { 4134 //visualTheme.useStyleProperties(w, (props) { 4135 prop = __traits(getMember, props, name); 4136 }); 4137 return prop; 4138 } 4139 4140 /++ 4141 Returns the cached font object associated with the widget, 4142 if overridden by the [Widget.Style|Style], or the [VisualTheme] if not. 4143 4144 History: 4145 Prior to March 21, 2022 (dub v10.7), `font` went through 4146 [opDispatch], which did not use the cache. You can now call it 4147 repeatedly without guilt. 4148 +/ 4149 public @property OperatingSystemFont font() { 4150 OperatingSystemFont prop; 4151 w.useStyleProperties((scope Widget.Style props) { 4152 prop = props.fontCached; 4153 }); 4154 if(prop is null) 4155 prop = visualTheme.defaultFontCached; 4156 return prop; 4157 } 4158 4159 @property { 4160 // Layout helpers. Currently just forwarding since I haven't made up my mind on a better way. 4161 /** */ int paddingLeft() { return w.paddingLeft(); } 4162 /** */ int paddingRight() { return w.paddingRight(); } 4163 /** */ int paddingTop() { return w.paddingTop(); } 4164 /** */ int paddingBottom() { return w.paddingBottom(); } 4165 4166 /** */ int marginLeft() { return w.marginLeft(); } 4167 /** */ int marginRight() { return w.marginRight(); } 4168 /** */ int marginTop() { return w.marginTop(); } 4169 /** */ int marginBottom() { return w.marginBottom(); } 4170 4171 /** */ int maxHeight() { return w.maxHeight(); } 4172 /** */ int minHeight() { return w.minHeight(); } 4173 4174 /** */ int maxWidth() { return w.maxWidth(); } 4175 /** */ int minWidth() { return w.minWidth(); } 4176 4177 /** */ int flexBasisWidth() { return w.flexBasisWidth(); } 4178 /** */ int flexBasisHeight() { return w.flexBasisHeight(); } 4179 4180 /** */ int heightStretchiness() { return w.heightStretchiness(); } 4181 /** */ int widthStretchiness() { return w.widthStretchiness(); } 4182 4183 /** */ int heightShrinkiness() { return w.heightShrinkiness(); } 4184 /** */ int widthShrinkiness() { return w.widthShrinkiness(); } 4185 4186 // Global helpers some of these are unstable. 4187 static: 4188 /** */ Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4189 /** */ Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4190 /** */ Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4191 /** */ Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4192 /** */ Color selectionForegroundColor() { return WidgetPainter.visualTheme.selectionForegroundColor(); } 4193 /** */ Color selectionBackgroundColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4194 4195 /** */ Color activeTabColor() { return lightAccentColor; } 4196 /** */ Color buttonColor() { return windowBackgroundColor; } 4197 /** */ Color depressedButtonColor() { return darkAccentColor; } 4198 /** */ Color hoveringColor() { return lightAccentColor; } 4199 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color activeListXorColor() { 4200 auto c = WidgetPainter.visualTheme.selectionColor(); 4201 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4202 } 4203 /** */ Color progressBarColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4204 /** */ Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionBackgroundColor(); } 4205 } 4206 4207 4208 4209 /+ 4210 4211 private static auto extractStyleProperty(string name)(Widget w) { 4212 typeof(__traits(getMember, Widget.Style.init, name)()) prop; 4213 w.useStyleProperties((props) { 4214 prop = __traits(getMember, props, name); 4215 }); 4216 return prop; 4217 } 4218 4219 // FIXME: clear this upon a X server disconnect 4220 private static OperatingSystemFont[string] fontCache; 4221 4222 T getProperty(T)(string name, lazy T default_) { 4223 if(visualTheme !is null) { 4224 auto str = visualTheme.getPropertyString(w, name); 4225 if(str is null) 4226 return default_; 4227 static if(is(T == Color)) 4228 return Color.fromString(str); 4229 else static if(is(T == Measurement)) 4230 return Measurement(cast(int) toInternal!int(str)); 4231 else static if(is(T == WidgetBackground)) 4232 return WidgetBackground.fromString(str); 4233 else static if(is(T == OperatingSystemFont)) { 4234 if(auto f = str in fontCache) 4235 return *f; 4236 else 4237 return fontCache[str] = new OperatingSystemFont(str); 4238 } else static if(is(T == FrameStyle)) { 4239 switch(str) { 4240 default: 4241 return FrameStyle.none; 4242 foreach(style; __traits(allMembers, FrameStyle)) 4243 case style: 4244 return __traits(getMember, FrameStyle, style); 4245 } 4246 } else static assert(0); 4247 } else 4248 return default_; 4249 } 4250 4251 static struct Measurement { 4252 int value; 4253 alias value this; 4254 } 4255 4256 @property: 4257 4258 int paddingLeft() { return getProperty("padding-left", Measurement(w.paddingLeft())); } 4259 int paddingRight() { return getProperty("padding-right", Measurement(w.paddingRight())); } 4260 int paddingTop() { return getProperty("padding-top", Measurement(w.paddingTop())); } 4261 int paddingBottom() { return getProperty("padding-bottom", Measurement(w.paddingBottom())); } 4262 4263 int marginLeft() { return getProperty("margin-left", Measurement(w.marginLeft())); } 4264 int marginRight() { return getProperty("margin-right", Measurement(w.marginRight())); } 4265 int marginTop() { return getProperty("margin-top", Measurement(w.marginTop())); } 4266 int marginBottom() { return getProperty("margin-bottom", Measurement(w.marginBottom())); } 4267 4268 int maxHeight() { return getProperty("max-height", Measurement(w.maxHeight())); } 4269 int minHeight() { return getProperty("min-height", Measurement(w.minHeight())); } 4270 4271 int maxWidth() { return getProperty("max-width", Measurement(w.maxWidth())); } 4272 int minWidth() { return getProperty("min-width", Measurement(w.minWidth())); } 4273 4274 4275 WidgetBackground background() { return getProperty("background", extractStyleProperty!"background"(w)); } 4276 Color foregroundColor() { return getProperty("foreground-color", extractStyleProperty!"foregroundColor"(w)); } 4277 4278 OperatingSystemFont font() { return getProperty("font", extractStyleProperty!"fontCached"(w)); } 4279 4280 FrameStyle borderStyle() { return getProperty("border-style", extractStyleProperty!"borderStyle"(w)); } 4281 Color borderColor() { return getProperty("border-color", extractStyleProperty!"borderColor"(w)); } 4282 4283 FrameStyle outlineStyle() { return getProperty("outline-style", extractStyleProperty!"outlineStyle"(w)); } 4284 Color outlineColor() { return getProperty("outline-color", extractStyleProperty!"outlineColor"(w)); } 4285 4286 4287 Color windowBackgroundColor() { return WidgetPainter.visualTheme.windowBackgroundColor(); } 4288 Color widgetBackgroundColor() { return WidgetPainter.visualTheme.widgetBackgroundColor(); } 4289 Color lightAccentColor() { return WidgetPainter.visualTheme.lightAccentColor(); } 4290 Color darkAccentColor() { return WidgetPainter.visualTheme.darkAccentColor(); } 4291 4292 Color activeTabColor() { return lightAccentColor; } 4293 Color buttonColor() { return windowBackgroundColor; } 4294 Color depressedButtonColor() { return darkAccentColor; } 4295 Color hoveringColor() { return Color(228, 228, 228); } 4296 Color activeListXorColor() { 4297 auto c = WidgetPainter.visualTheme.selectionColor(); 4298 return Color(c.r ^ 255, c.g ^ 255, c.b ^ 255, c.a); 4299 } 4300 Color progressBarColor() { return WidgetPainter.visualTheme.selectionColor(); } 4301 Color activeMenuItemColor() { return WidgetPainter.visualTheme.selectionColor(); } 4302 +/ 4303 } 4304 4305 4306 4307 // pragma(msg, __traits(classInstanceSize, Widget)); 4308 4309 /*private*/ template EventString(E) { 4310 static if(is(typeof(E.EventString))) 4311 enum EventString = E.EventString; 4312 else 4313 enum EventString = E.mangleof; // FIXME fqn? or something more user friendly 4314 } 4315 4316 /*private*/ template EventStringIdentifier(E) { 4317 string helper() { 4318 auto es = EventString!E; 4319 char[] id = new char[](es.length * 2); 4320 size_t idx; 4321 foreach(char ch; es) { 4322 id[idx++] = cast(char)('a' + (ch >> 4)); 4323 id[idx++] = cast(char)('a' + (ch & 0x0f)); 4324 } 4325 return cast(string) id; 4326 } 4327 4328 enum EventStringIdentifier = helper(); 4329 } 4330 4331 4332 template classStaticallyEmits(This, EventType) { 4333 static if(is(This Base == super)) 4334 static if(is(Base : Widget)) 4335 enum baseEmits = classStaticallyEmits!(Base, EventType); 4336 else 4337 enum baseEmits = false; 4338 else 4339 enum baseEmits = false; 4340 4341 enum thisEmits = is(typeof(__traits(getMember, This, "emits_" ~ EventStringIdentifier!EventType)) == EventType[0]); 4342 4343 enum classStaticallyEmits = thisEmits || baseEmits; 4344 } 4345 4346 /++ 4347 A helper to make widgets out of other native windows. 4348 4349 History: 4350 Factored out of OpenGlWidget on November 5, 2021 4351 +/ 4352 class NestedChildWindowWidget : Widget { 4353 SimpleWindow win; 4354 4355 /++ 4356 Used on X to send focus to the appropriate child window when requested by the window manager. 4357 4358 Normally returns its own nested window. Can also return another child or null to revert to the parent 4359 if you override it in a child class. 4360 4361 History: 4362 Added April 2, 2022 (dub v10.8) 4363 +/ 4364 SimpleWindow focusableWindow() { 4365 return win; 4366 } 4367 4368 /// 4369 // win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4370 this(SimpleWindow win, Widget parent) { 4371 this.parentWindow = parent.parentWindow; 4372 this.win = win; 4373 4374 super(parent); 4375 windowsetup(win); 4376 } 4377 4378 static protected SimpleWindow getParentWindow(Widget parent) { 4379 assert(parent !is null); 4380 SimpleWindow pwin = parent.parentWindow.win; 4381 4382 version(win32_widgets) { 4383 HWND phwnd; 4384 auto wtf = parent; 4385 while(wtf) { 4386 if(wtf.hwnd) { 4387 phwnd = wtf.hwnd; 4388 break; 4389 } 4390 wtf = wtf.parent; 4391 } 4392 // kinda a hack here just because the ctor below just needs a SimpleWindow wrapper.... 4393 if(phwnd) 4394 pwin = new SimpleWindow(phwnd); 4395 } 4396 4397 return pwin; 4398 } 4399 4400 /++ 4401 Called upon the nested window being destroyed. 4402 Remember the window has already been destroyed at 4403 this point, so don't use the native handle for anything. 4404 4405 History: 4406 Added April 3, 2022 (dub v10.8) 4407 +/ 4408 protected void dispose() { 4409 4410 } 4411 4412 protected void windowsetup(SimpleWindow w) { 4413 /* 4414 win.onFocusChange = (bool getting) { 4415 if(getting) 4416 this.focus(); 4417 }; 4418 */ 4419 4420 /+ 4421 win.onFocusChange = (bool getting) { 4422 if(getting) { 4423 this.parentWindow.focusedWidget = this; 4424 this.emit!FocusEvent(); 4425 this.emit!FocusInEvent(); 4426 } else { 4427 this.emit!BlurEvent(); 4428 this.emit!FocusOutEvent(); 4429 } 4430 }; 4431 +/ 4432 4433 win.onDestroyed = () { 4434 this.dispose(); 4435 }; 4436 4437 version(win32_widgets) { 4438 Widget.nativeMapping[win.hwnd] = this; 4439 this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc); 4440 } else { 4441 win.setEventHandlers( 4442 (MouseEvent e) { 4443 Widget p = this; 4444 while(p ! is parentWindow) { 4445 e.x += p.x; 4446 e.y += p.y; 4447 p = p.parent; 4448 } 4449 parentWindow.dispatchMouseEvent(e); 4450 }, 4451 (KeyEvent e) { 4452 //writefln("%s %x %s", cast(void*) win, cast(uint) e.key, e.key); 4453 parentWindow.dispatchKeyEvent(e); 4454 }, 4455 (dchar e) { 4456 parentWindow.dispatchCharEvent(e); 4457 }, 4458 ); 4459 } 4460 4461 } 4462 4463 override void showing(bool s, bool recalc) { 4464 auto cur = hidden; 4465 win.hidden = !s; 4466 if(cur != s && s) 4467 redraw(); 4468 } 4469 4470 /// OpenGL widgets cannot have child widgets. Do not call this. 4471 /* @disable */ final override void addChild(Widget, int) { 4472 throw new Error("cannot add children to OpenGL widgets"); 4473 } 4474 4475 /// When an opengl widget is laid out, it will adjust the glViewport for you automatically. 4476 /// Keep in mind that events like mouse coordinates are still relative to your size. 4477 override void registerMovement() { 4478 // writefln("%d %d %d %d", x,y,width,height); 4479 version(win32_widgets) 4480 auto pos = getChildPositionRelativeToParentHwnd(this); 4481 else 4482 auto pos = getChildPositionRelativeToParentOrigin(this); 4483 win.moveResize(pos[0], pos[1], width, height); 4484 4485 registerMovementAdditionalWork(); 4486 sendResizeEvent(); 4487 } 4488 4489 abstract void registerMovementAdditionalWork(); 4490 } 4491 4492 /++ 4493 Nests an opengl capable window inside this window as a widget. 4494 4495 You may also just want to create an additional [SimpleWindow] with 4496 [OpenGlOptions.yes] yourself. 4497 4498 An OpenGL widget cannot have child widgets. It will throw if you try. 4499 +/ 4500 static if(OpenGlEnabled) 4501 class OpenGlWidget : NestedChildWindowWidget { 4502 4503 override void registerMovementAdditionalWork() { 4504 win.setAsCurrentOpenGlContext(); 4505 } 4506 4507 /// 4508 this(Widget parent) { 4509 auto win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 4510 super(win, parent); 4511 } 4512 4513 override void paint(WidgetPainter painter) { 4514 win.setAsCurrentOpenGlContext(); 4515 glViewport(0, 0, this.width, this.height); 4516 win.redrawOpenGlSceneNow(); 4517 } 4518 4519 void redrawOpenGlScene(void delegate() dg) { 4520 win.redrawOpenGlScene = dg; 4521 } 4522 } 4523 4524 /++ 4525 This demo shows how to draw text in an opengl scene. 4526 +/ 4527 unittest { 4528 import arsd.minigui; 4529 import arsd.ttf; 4530 4531 void main() { 4532 auto window = new Window(); 4533 4534 auto widget = new OpenGlWidget(window); 4535 4536 // old means non-shader code so compatible with glBegin etc. 4537 // tbh I haven't implemented new one in font yet... 4538 // anyway, declaring here, will construct soon. 4539 OpenGlLimitedFont!(OpenGlFontGLVersion.old) glfont; 4540 4541 // this is a little bit awkward, calling some methods through 4542 // the underlying SimpleWindow `win` method, and you can't do this 4543 // on a nanovega widget due to conflicts so I should probably fix 4544 // the api to be a bit easier. But here it will work. 4545 // 4546 // Alternatively, you could load the font on the first draw, inside 4547 // the redrawOpenGlScene, and keep a flag so you don't do it every 4548 // time. That'd be a bit easier since the lib sets up the context 4549 // by then guaranteed. 4550 // 4551 // But still, I wanna show this. 4552 widget.win.visibleForTheFirstTime = delegate { 4553 // must set the opengl context 4554 widget.win.setAsCurrentOpenGlContext(); 4555 4556 // if you were doing a OpenGL 3+ shader, this 4557 // gets especially important to do in order. With 4558 // old-style opengl, I think you can even do it 4559 // in main(), but meh, let's show it more correctly. 4560 4561 // Anyway, now it is time to load the font from the 4562 // OS (you can alternatively load one from a .ttf file 4563 // you bundle with the application), then load the 4564 // font into texture for drawing. 4565 4566 auto osfont = new OperatingSystemFont("DejaVu Sans", 18); 4567 4568 assert(!osfont.isNull()); // make sure it actually loaded 4569 4570 // using typeof to avoid repeating the long name lol 4571 glfont = new typeof(glfont)( 4572 // get the raw data from the font for loading in here 4573 // since it doesn't use the OS function to draw the 4574 // text, we gotta treat it more as a file than as 4575 // a drawing api. 4576 osfont.getTtfBytes(), 4577 18, // need to respecify size since opengl world is different coordinate system 4578 4579 // these last two numbers are why it is called 4580 // "Limited" font. It only loads the characters 4581 // in the given range, since the texture atlas 4582 // it references is all a big image generated ahead 4583 // of time. You could maybe do the whole thing but 4584 // idk how much memory that is. 4585 // 4586 // But here, 0-128 represents the ASCII range, so 4587 // good enough for most English things, numeric labels, 4588 // etc. 4589 0, 4590 128 4591 ); 4592 }; 4593 4594 widget.redrawOpenGlScene = () { 4595 // now we can use the glfont's drawString function 4596 4597 // first some opengl setup. You can do this in one place 4598 // on window first visible too in many cases, just showing 4599 // here cuz it is easier for me. 4600 4601 // gonna need some alpha blending or it just looks awful 4602 glEnable(GL_BLEND); 4603 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 4604 glClearColor(0,0,0,0); 4605 glDepthFunc(GL_LEQUAL); 4606 4607 // Also need to enable 2d textures, since it draws the 4608 // font characters as images baked in 4609 glMatrixMode(GL_MODELVIEW); 4610 glLoadIdentity(); 4611 glDisable(GL_DEPTH_TEST); 4612 glEnable(GL_TEXTURE_2D); 4613 4614 // the orthographic matrix is best for 2d things like text 4615 // so let's set that up. This matrix makes the coordinates 4616 // in the opengl scene be one-to-one with the actual pixels 4617 // on screen. (Not necessarily best, you may wish to scale 4618 // things, but it does help keep fonts looking normal.) 4619 glMatrixMode(GL_PROJECTION); 4620 glLoadIdentity(); 4621 glOrtho(0, widget.width, widget.height, 0, 0, 1); 4622 4623 // you can do other glScale, glRotate, glTranslate, etc 4624 // to the matrix here of course if you want. 4625 4626 // note the x,y coordinates here are for the text baseline 4627 // NOT the upper-left corner. The baseline is like the line 4628 // in the notebook you write on. Most the letters are actually 4629 // above it, but some, like p and q, dip a bit below it. 4630 // 4631 // So if you're used to the upper left coordinate like the 4632 // rest of simpledisplay/minigui usually do, do the 4633 // y + glfont.ascent to bring it down a little. So this 4634 // example puts the string in the upper left of the window. 4635 glfont.drawString(0, 0 + glfont.ascent, "Hello!!", Color.green); 4636 4637 // re color btw: the function sets a solid color internally, 4638 // but you actually COULD do your own thing for rainbow effects 4639 // and the sort if you wanted too, by pulling its guts out. 4640 // Just view its source for an idea of how it actually draws: 4641 // http://arsd-official.dpldocs.info/source/arsd.ttf.d.html#L332 4642 4643 // it gets a bit complicated with the character positioning, 4644 // but the opengl parts are fairly simple: bind a texture, 4645 // set the color, draw a quad for each letter. 4646 4647 4648 // the last optional argument there btw is a bounding box 4649 // it will/ use to word wrap and return an object you can 4650 // use to implement scrolling or pagination; it tells how 4651 // much of the string didn't fit in the box. But for simple 4652 // labels we can just ignore that. 4653 4654 4655 // I'd suggest drawing text as the last step, after you 4656 // do your other drawing. You might use the push/pop matrix 4657 // stuff to keep your place. You, in theory, should be able 4658 // to do text in a 3d space but I've never actually tried 4659 // that.... 4660 }; 4661 4662 window.loop(); 4663 } 4664 } 4665 4666 version(custom_widgets) 4667 private alias ListWidgetBase = ScrollableWidget; 4668 else 4669 private alias ListWidgetBase = Widget; 4670 4671 /++ 4672 A list widget contains a list of strings that the user can examine and select. 4673 4674 4675 In the future, items in the list may be possible to be more than just strings. 4676 4677 See_Also: 4678 [TableView] 4679 +/ 4680 class ListWidget : ListWidgetBase { 4681 /// 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. 4682 mixin Emits!(ChangeEvent!void); 4683 4684 static struct Option { 4685 string label; 4686 bool selected; 4687 void* tag; 4688 } 4689 4690 /++ 4691 Sets the current selection to the `y`th item in the list. Will emit [ChangeEvent] when complete. 4692 +/ 4693 void setSelection(int y) { 4694 if(!multiSelect) 4695 foreach(ref opt; options) 4696 opt.selected = false; 4697 if(y >= 0 && y < options.length) 4698 options[y].selected = !options[y].selected; 4699 4700 this.emit!(ChangeEvent!void)(delegate {}); 4701 4702 version(custom_widgets) 4703 redraw(); 4704 } 4705 4706 /++ 4707 Gets the index of the selected item. In case of multi select, the index of the first selected item is returned. 4708 Returns -1 if nothing is selected. 4709 +/ 4710 int getSelection() 4711 { 4712 foreach(i, opt; options) { 4713 if (opt.selected) 4714 return cast(int) i; 4715 } 4716 return -1; 4717 } 4718 4719 version(custom_widgets) 4720 override void defaultEventHandler_click(ClickEvent event) { 4721 this.focus(); 4722 if(event.button == MouseButton.left) { 4723 auto y = (event.clientY - 4) / defaultLineHeight; 4724 if(y >= 0 && y < options.length) { 4725 setSelection(y); 4726 } 4727 } 4728 super.defaultEventHandler_click(event); 4729 } 4730 4731 this(Widget parent) { 4732 tabStop = false; 4733 super(parent); 4734 version(win32_widgets) 4735 createWin32Window(this, WC_LISTBOX, "", 4736 0|WS_CHILD|WS_VISIBLE|LBS_NOTIFY, 0); 4737 } 4738 4739 version(win32_widgets) 4740 override void handleWmCommand(ushort code, ushort id) { 4741 switch(code) { 4742 case LBN_SELCHANGE: 4743 auto sel = SendMessageW(hwnd, LB_GETCURSEL, 0, 0); 4744 setSelection(cast(int) sel); 4745 break; 4746 default: 4747 } 4748 } 4749 4750 4751 version(custom_widgets) 4752 override void paintFrameAndBackground(WidgetPainter painter) { 4753 draw3dFrame(this, painter, FrameStyle.sunk, painter.visualTheme.widgetBackgroundColor); 4754 } 4755 4756 version(custom_widgets) 4757 override void paint(WidgetPainter painter) { 4758 auto cs = getComputedStyle(); 4759 auto pos = Point(4, 4); 4760 foreach(idx, option; options) { 4761 painter.fillColor = painter.visualTheme.widgetBackgroundColor; 4762 painter.outlineColor = painter.visualTheme.widgetBackgroundColor; 4763 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4764 if(option.selected) { 4765 //painter.rasterOp = RasterOp.xor; 4766 painter.outlineColor = cs.selectionForegroundColor; 4767 painter.fillColor = cs.selectionBackgroundColor; 4768 painter.drawRectangle(pos, width - 8, defaultLineHeight); 4769 //painter.rasterOp = RasterOp.normal; 4770 } 4771 painter.outlineColor = option.selected ? cs.selectionForegroundColor : cs.foregroundColor; 4772 painter.drawText(pos, option.label); 4773 pos.y += defaultLineHeight; 4774 } 4775 } 4776 4777 static class Style : Widget.Style { 4778 override WidgetBackground background() { 4779 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 4780 } 4781 } 4782 mixin OverrideStyle!Style; 4783 //mixin Padding!q{2}; 4784 4785 void addOption(string text, void* tag = null) { 4786 options ~= Option(text, false, tag); 4787 version(win32_widgets) { 4788 WCharzBuffer buffer = WCharzBuffer(text); 4789 SendMessageW(hwnd, LB_ADDSTRING, 0, cast(LPARAM) buffer.ptr); 4790 } 4791 version(custom_widgets) { 4792 setContentSize(width, cast(int) (options.length * defaultLineHeight)); 4793 redraw(); 4794 } 4795 } 4796 4797 void clear() { 4798 options = null; 4799 version(win32_widgets) { 4800 while(SendMessageW(hwnd, LB_DELETESTRING, 0, 0) > 0) 4801 {} 4802 4803 } else version(custom_widgets) { 4804 scrollTo(Point(0, 0)); 4805 redraw(); 4806 } 4807 } 4808 4809 Option[] options; 4810 version(win32_widgets) 4811 enum multiSelect = false; /// not implemented yet 4812 else 4813 bool multiSelect; 4814 4815 override int heightStretchiness() { return 6; } 4816 } 4817 4818 4819 4820 /// For [ScrollableWidget], determines when to show the scroll bar to the user. 4821 enum ScrollBarShowPolicy { 4822 automatic, /// automatically show the scroll bar if it is necessary 4823 never, /// never show the scroll bar (scrolling must be done programmatically) 4824 always /// always show the scroll bar, even if it is disabled 4825 } 4826 4827 /++ 4828 A widget that tries (with, at best, limited success) to offer scrolling that is transparent to the inner. 4829 4830 It isn't very good and will very likely be removed. Try [ScrollMessageWidget] or [ScrollableContainerWidget] instead for new code. 4831 +/ 4832 // FIXME ScrollBarShowPolicy 4833 // FIXME: use the ScrollMessageWidget in here now that it exists 4834 class ScrollableWidget : Widget { 4835 // FIXME: make line size configurable 4836 // FIXME: add keyboard controls 4837 version(win32_widgets) { 4838 override int hookedWndProc(UINT msg, WPARAM wParam, LPARAM lParam) { 4839 if(msg == WM_VSCROLL || msg == WM_HSCROLL) { 4840 auto pos = HIWORD(wParam); 4841 auto m = LOWORD(wParam); 4842 4843 // FIXME: I can reintroduce the 4844 // scroll bars now by using this 4845 // in the top-level window handler 4846 // to forward comamnds 4847 auto scrollbarHwnd = lParam; 4848 switch(m) { 4849 case SB_BOTTOM: 4850 if(msg == WM_HSCROLL) 4851 horizontalScrollTo(contentWidth_); 4852 else 4853 verticalScrollTo(contentHeight_); 4854 break; 4855 case SB_TOP: 4856 if(msg == WM_HSCROLL) 4857 horizontalScrollTo(0); 4858 else 4859 verticalScrollTo(0); 4860 break; 4861 case SB_ENDSCROLL: 4862 // idk 4863 break; 4864 case SB_LINEDOWN: 4865 if(msg == WM_HSCROLL) 4866 horizontalScroll(scaleWithDpi(16)); 4867 else 4868 verticalScroll(scaleWithDpi(16)); 4869 break; 4870 case SB_LINEUP: 4871 if(msg == WM_HSCROLL) 4872 horizontalScroll(scaleWithDpi(-16)); 4873 else 4874 verticalScroll(scaleWithDpi(-16)); 4875 break; 4876 case SB_PAGEDOWN: 4877 if(msg == WM_HSCROLL) 4878 horizontalScroll(scaleWithDpi(100)); 4879 else 4880 verticalScroll(scaleWithDpi(100)); 4881 break; 4882 case SB_PAGEUP: 4883 if(msg == WM_HSCROLL) 4884 horizontalScroll(scaleWithDpi(-100)); 4885 else 4886 verticalScroll(scaleWithDpi(-100)); 4887 break; 4888 case SB_THUMBPOSITION: 4889 case SB_THUMBTRACK: 4890 if(msg == WM_HSCROLL) 4891 horizontalScrollTo(pos); 4892 else 4893 verticalScrollTo(pos); 4894 4895 if(m == SB_THUMBTRACK) { 4896 // the event loop doesn't seem to carry on with a requested redraw.. 4897 // so we request it to get our dirty bit set... 4898 redraw(); 4899 4900 // then we need to immediately actually redraw it too for instant feedback to user 4901 4902 SimpleWindow.processAllCustomEvents(); 4903 //if(parentWindow) 4904 //parentWindow.actualRedraw(); 4905 } 4906 break; 4907 default: 4908 } 4909 } 4910 return super.hookedWndProc(msg, wParam, lParam); 4911 } 4912 } 4913 /// 4914 this(Widget parent) { 4915 this.parentWindow = parent.parentWindow; 4916 4917 version(win32_widgets) { 4918 createWin32Window(this, Win32Class!"arsd_minigui_ScrollableWidget"w, "", 4919 0|WS_CHILD|WS_VISIBLE|WS_HSCROLL|WS_VSCROLL, 0); 4920 super(parent); 4921 } else version(custom_widgets) { 4922 outerContainer = new InternalScrollableContainerWidget(this, parent); 4923 super(outerContainer); 4924 } else static assert(0); 4925 } 4926 4927 version(custom_widgets) 4928 InternalScrollableContainerWidget outerContainer; 4929 4930 override void defaultEventHandler_click(ClickEvent event) { 4931 if(event.button == MouseButton.wheelUp) 4932 verticalScroll(scaleWithDpi(-16)); 4933 if(event.button == MouseButton.wheelDown) 4934 verticalScroll(scaleWithDpi(16)); 4935 super.defaultEventHandler_click(event); 4936 } 4937 4938 override void defaultEventHandler_keydown(KeyDownEvent event) { 4939 switch(event.key) { 4940 case Key.Left: 4941 horizontalScroll(scaleWithDpi(-16)); 4942 break; 4943 case Key.Right: 4944 horizontalScroll(scaleWithDpi(16)); 4945 break; 4946 case Key.Up: 4947 verticalScroll(scaleWithDpi(-16)); 4948 break; 4949 case Key.Down: 4950 verticalScroll(scaleWithDpi(16)); 4951 break; 4952 case Key.Home: 4953 verticalScrollTo(0); 4954 break; 4955 case Key.End: 4956 verticalScrollTo(contentHeight); 4957 break; 4958 case Key.PageUp: 4959 verticalScroll(scaleWithDpi(-160)); 4960 break; 4961 case Key.PageDown: 4962 verticalScroll(scaleWithDpi(160)); 4963 break; 4964 default: 4965 } 4966 super.defaultEventHandler_keydown(event); 4967 } 4968 4969 4970 version(win32_widgets) 4971 override void recomputeChildLayout() { 4972 super.recomputeChildLayout(); 4973 SCROLLINFO info; 4974 info.cbSize = info.sizeof; 4975 info.nPage = viewportHeight; 4976 info.fMask = SIF_PAGE | SIF_RANGE; 4977 info.nMin = 0; 4978 info.nMax = contentHeight_; 4979 SetScrollInfo(hwnd, SB_VERT, &info, true); 4980 4981 info.cbSize = info.sizeof; 4982 info.nPage = viewportWidth; 4983 info.fMask = SIF_PAGE | SIF_RANGE; 4984 info.nMin = 0; 4985 info.nMax = contentWidth_; 4986 SetScrollInfo(hwnd, SB_HORZ, &info, true); 4987 } 4988 4989 /* 4990 Scrolling 4991 ------------ 4992 4993 You are assigned a width and a height by the layout engine, which 4994 is your viewport box. However, you may draw more than that by setting 4995 a contentWidth and contentHeight. 4996 4997 If these can be contained by the viewport, no scrollbar is displayed. 4998 If they cannot fit though, it will automatically show scroll as necessary. 4999 5000 If contentWidth == 0, no horizontal scrolling is performed. If contentHeight 5001 is zero, no vertical scrolling is performed. 5002 5003 If scrolling is necessary, the lib will automatically work with the bars. 5004 When you redraw, the origin and clipping info in the painter is set so if 5005 you just draw everything, it will work, but you can be more efficient by checking 5006 the viewportWidth, viewportHeight, and scrollOrigin members. 5007 */ 5008 5009 /// 5010 final @property int viewportWidth() { 5011 return width - (showingVerticalScroll ? scaleWithDpi(16) : 0); 5012 } 5013 /// 5014 final @property int viewportHeight() { 5015 return height - (showingHorizontalScroll ? scaleWithDpi(16) : 0); 5016 } 5017 5018 // FIXME property 5019 Point scrollOrigin_; 5020 5021 /// 5022 final const(Point) scrollOrigin() { 5023 return scrollOrigin_; 5024 } 5025 5026 // the user sets these two 5027 private int contentWidth_ = 0; 5028 private int contentHeight_ = 0; 5029 5030 /// 5031 int contentWidth() { return contentWidth_; } 5032 /// 5033 int contentHeight() { return contentHeight_; } 5034 5035 /// 5036 void setContentSize(int width, int height) { 5037 contentWidth_ = width; 5038 contentHeight_ = height; 5039 5040 version(custom_widgets) { 5041 if(showingVerticalScroll || showingHorizontalScroll) { 5042 outerContainer.recomputeChildLayout(); 5043 } 5044 5045 if(showingVerticalScroll()) 5046 outerContainer.verticalScrollBar.redraw(); 5047 if(showingHorizontalScroll()) 5048 outerContainer.horizontalScrollBar.redraw(); 5049 } else version(win32_widgets) { 5050 recomputeChildLayout(); 5051 } else static assert(0); 5052 } 5053 5054 /// 5055 void verticalScroll(int delta) { 5056 verticalScrollTo(scrollOrigin.y + delta); 5057 } 5058 /// 5059 void verticalScrollTo(int pos) { 5060 scrollOrigin_.y = pos; 5061 if(pos == int.max || (scrollOrigin_.y + viewportHeight > contentHeight)) 5062 scrollOrigin_.y = contentHeight - viewportHeight; 5063 5064 if(scrollOrigin_.y < 0) 5065 scrollOrigin_.y = 0; 5066 5067 version(win32_widgets) { 5068 SCROLLINFO info; 5069 info.cbSize = info.sizeof; 5070 info.fMask = SIF_POS; 5071 info.nPos = scrollOrigin_.y; 5072 SetScrollInfo(hwnd, SB_VERT, &info, true); 5073 } else version(custom_widgets) { 5074 outerContainer.verticalScrollBar.setPosition(scrollOrigin_.y); 5075 } else static assert(0); 5076 5077 redraw(); 5078 } 5079 5080 /// 5081 void horizontalScroll(int delta) { 5082 horizontalScrollTo(scrollOrigin.x + delta); 5083 } 5084 /// 5085 void horizontalScrollTo(int pos) { 5086 scrollOrigin_.x = pos; 5087 if(pos == int.max || (scrollOrigin_.x + viewportWidth > contentWidth)) 5088 scrollOrigin_.x = contentWidth - viewportWidth; 5089 5090 if(scrollOrigin_.x < 0) 5091 scrollOrigin_.x = 0; 5092 5093 version(win32_widgets) { 5094 SCROLLINFO info; 5095 info.cbSize = info.sizeof; 5096 info.fMask = SIF_POS; 5097 info.nPos = scrollOrigin_.x; 5098 SetScrollInfo(hwnd, SB_HORZ, &info, true); 5099 } else version(custom_widgets) { 5100 outerContainer.horizontalScrollBar.setPosition(scrollOrigin_.x); 5101 } else static assert(0); 5102 5103 redraw(); 5104 } 5105 /// 5106 void scrollTo(Point p) { 5107 verticalScrollTo(p.y); 5108 horizontalScrollTo(p.x); 5109 } 5110 5111 /// 5112 void ensureVisibleInScroll(Point p) { 5113 auto rect = viewportRectangle(); 5114 if(rect.contains(p)) 5115 return; 5116 if(p.x < rect.left) 5117 horizontalScroll(p.x - rect.left); 5118 else if(p.x > rect.right) 5119 horizontalScroll(p.x - rect.right); 5120 5121 if(p.y < rect.top) 5122 verticalScroll(p.y - rect.top); 5123 else if(p.y > rect.bottom) 5124 verticalScroll(p.y - rect.bottom); 5125 } 5126 5127 /// 5128 void ensureVisibleInScroll(Rectangle rect) { 5129 ensureVisibleInScroll(rect.upperLeft); 5130 ensureVisibleInScroll(rect.lowerRight); 5131 } 5132 5133 /// 5134 Rectangle viewportRectangle() { 5135 return Rectangle(scrollOrigin, Size(viewportWidth, viewportHeight)); 5136 } 5137 5138 /// 5139 bool showingHorizontalScroll() { 5140 return contentWidth > width; 5141 } 5142 /// 5143 bool showingVerticalScroll() { 5144 return contentHeight > height; 5145 } 5146 5147 /// This is called before the ordinary paint delegate, 5148 /// giving you a chance to draw the window frame, etc, 5149 /// before the scroll clip takes effect 5150 void paintFrameAndBackground(WidgetPainter painter) { 5151 version(win32_widgets) { 5152 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 5153 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 5154 // since the pen is null, to fill the whole space, we need the +1 on both. 5155 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 5156 SelectObject(painter.impl.hdc, p); 5157 SelectObject(painter.impl.hdc, b); 5158 } 5159 5160 } 5161 5162 // make space for the scroll bar, and that's it. 5163 final override int paddingRight() { return scaleWithDpi(16); } 5164 final override int paddingBottom() { return scaleWithDpi(16); } 5165 5166 /* 5167 END SCROLLING 5168 */ 5169 5170 override WidgetPainter draw() { 5171 int x = this.x, y = this.y; 5172 auto parent = this.parent; 5173 while(parent) { 5174 x += parent.x; 5175 y += parent.y; 5176 parent = parent.parent; 5177 } 5178 5179 //version(win32_widgets) { 5180 //auto painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5181 //} else { 5182 auto painter = parentWindow.win.draw(true); 5183 //} 5184 painter.originX = x; 5185 painter.originY = y; 5186 5187 painter.originX = painter.originX - scrollOrigin.x; 5188 painter.originY = painter.originY - scrollOrigin.y; 5189 painter.setClipRectangle(scrollOrigin, viewportWidth(), viewportHeight()); 5190 5191 return WidgetPainter(painter, this); 5192 } 5193 5194 mixin ScrollableChildren; 5195 } 5196 5197 // you need to have a Point scrollOrigin in the class somewhere 5198 // and a paintFrameAndBackground 5199 private mixin template ScrollableChildren() { 5200 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5201 if(hidden) 5202 return; 5203 5204 //version(win32_widgets) 5205 //painter = simpleWindowWrappingHwnd ? simpleWindowWrappingHwnd.draw(true) : parentWindow.win.draw(true); 5206 5207 painter.originX = lox + x; 5208 painter.originY = loy + y; 5209 5210 bool actuallyPainted = false; 5211 5212 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width, height))); 5213 if(clip == Rectangle.init) 5214 return; 5215 5216 if(force || redrawRequested) { 5217 //painter.setClipRectangle(scrollOrigin, width, height); 5218 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5219 paintFrameAndBackground(painter); 5220 } 5221 5222 painter.originX = painter.originX - scrollOrigin.x; 5223 painter.originY = painter.originY - scrollOrigin.y; 5224 if(force || redrawRequested) { 5225 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY) + Point(2, 2) /* border */, clip.width - 4, clip.height - 4); 5226 //painter.setClipRectangle(scrollOrigin + Point(2, 2) /* border */, width - 4, height - 4); 5227 5228 //erase(painter); // we paintFrameAndBackground above so no need 5229 if(painter.visualTheme) 5230 painter.visualTheme.doPaint(this, painter); 5231 else 5232 paint(painter); 5233 5234 if(invalidate) { 5235 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5236 // children are contained inside this, so no need to do extra work 5237 invalidate = false; 5238 } 5239 5240 5241 actuallyPainted = true; 5242 redrawRequested = false; 5243 } 5244 foreach(child; children) { 5245 if(cast(FixedPosition) child) 5246 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5247 else 5248 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5249 } 5250 } 5251 } 5252 5253 private class InternalScrollableContainerInsideWidget : ContainerWidget { 5254 ScrollableContainerWidget scw; 5255 5256 this(ScrollableContainerWidget parent) { 5257 scw = parent; 5258 super(parent); 5259 } 5260 5261 version(custom_widgets) 5262 override protected void privatePaint(WidgetPainter painter, int lox, int loy, Rectangle containment, bool force, bool invalidate) { 5263 if(hidden) 5264 return; 5265 5266 bool actuallyPainted = false; 5267 5268 auto scrollOrigin = Point(scw.scrollX_, scw.scrollY_); 5269 5270 const clip = containment.intersectionOf(Rectangle(Point(lox + x, loy + y), Size(width + scw.scrollX_, height + scw.scrollY_))); 5271 if(clip == Rectangle.init) 5272 return; 5273 5274 painter.originX = lox + x - scrollOrigin.x; 5275 painter.originY = loy + y - scrollOrigin.y; 5276 if(force || redrawRequested) { 5277 painter.setClipRectangle(clip.upperLeft - Point(painter.originX, painter.originY), clip.width, clip.height); 5278 5279 erase(painter); 5280 if(painter.visualTheme) 5281 painter.visualTheme.doPaint(this, painter); 5282 else 5283 paint(painter); 5284 5285 if(invalidate) { 5286 painter.invalidateRect(Rectangle(Point(clip.upperLeft.x - painter.originX, clip.upperRight.y - painter.originY), Size(clip.width, clip.height))); 5287 // children are contained inside this, so no need to do extra work 5288 invalidate = false; 5289 } 5290 5291 actuallyPainted = true; 5292 redrawRequested = false; 5293 } 5294 foreach(child; children) { 5295 if(cast(FixedPosition) child) 5296 child.privatePaint(painter, painter.originX + scrollOrigin.x, painter.originY + scrollOrigin.y, clip, actuallyPainted, invalidate); 5297 else 5298 child.privatePaint(painter, painter.originX, painter.originY, clip, actuallyPainted, invalidate); 5299 } 5300 } 5301 5302 version(custom_widgets) 5303 override protected void addScrollPosition(ref int x, ref int y) { 5304 x += scw.scrollX_; 5305 y += scw.scrollY_; 5306 } 5307 } 5308 5309 /++ 5310 A widget meant to contain other widgets that may need to scroll. 5311 5312 Currently buggy. 5313 5314 History: 5315 Added July 1, 2021 (dub v10.2) 5316 5317 On January 3, 2022, I tried to use it in a few other cases 5318 and found it only worked well in the original test case. Since 5319 it still sucks, I think I'm going to rewrite it again. 5320 +/ 5321 class ScrollableContainerWidget : ContainerWidget { 5322 /// 5323 this(Widget parent) { 5324 super(parent); 5325 5326 container = new InternalScrollableContainerInsideWidget(this); 5327 hsb = new HorizontalScrollbar(this); 5328 vsb = new VerticalScrollbar(this); 5329 5330 tabStop = false; 5331 container.tabStop = false; 5332 magic = true; 5333 5334 5335 vsb.addEventListener("scrolltonextline", () { 5336 scrollBy(0, scaleWithDpi(16)); 5337 }); 5338 vsb.addEventListener("scrolltopreviousline", () { 5339 scrollBy(0,scaleWithDpi( -16)); 5340 }); 5341 vsb.addEventListener("scrolltonextpage", () { 5342 scrollBy(0, container.height); 5343 }); 5344 vsb.addEventListener("scrolltopreviouspage", () { 5345 scrollBy(0, -container.height); 5346 }); 5347 vsb.addEventListener((scope ScrollToPositionEvent spe) { 5348 scrollTo(scrollX_, spe.value); 5349 }); 5350 5351 this.addEventListener(delegate (scope ClickEvent e) { 5352 if(e.button == MouseButton.wheelUp) { 5353 if(!e.defaultPrevented) 5354 scrollBy(0, scaleWithDpi(-16)); 5355 e.stopPropagation(); 5356 } else if(e.button == MouseButton.wheelDown) { 5357 if(!e.defaultPrevented) 5358 scrollBy(0, scaleWithDpi(16)); 5359 e.stopPropagation(); 5360 } 5361 }); 5362 } 5363 5364 /+ 5365 override void defaultEventHandler_click(ClickEvent e) { 5366 } 5367 +/ 5368 5369 override void removeAllChildren() { 5370 container.removeAllChildren(); 5371 } 5372 5373 void scrollTo(int x, int y) { 5374 scrollBy(x - scrollX_, y - scrollY_); 5375 } 5376 5377 void scrollBy(int x, int y) { 5378 auto ox = scrollX_; 5379 auto oy = scrollY_; 5380 5381 auto nx = ox + x; 5382 auto ny = oy + y; 5383 5384 if(nx < 0) 5385 nx = 0; 5386 if(ny < 0) 5387 ny = 0; 5388 5389 auto maxX = hsb.max - container.width; 5390 if(maxX < 0) maxX = 0; 5391 auto maxY = vsb.max - container.height; 5392 if(maxY < 0) maxY = 0; 5393 5394 if(nx > maxX) 5395 nx = maxX; 5396 if(ny > maxY) 5397 ny = maxY; 5398 5399 auto dx = nx - ox; 5400 auto dy = ny - oy; 5401 5402 if(dx || dy) { 5403 version(win32_widgets) 5404 ScrollWindowEx(container.hwnd, -dx, -dy, null, null, null, null, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); 5405 else { 5406 redraw(); 5407 } 5408 5409 hsb.setPosition = nx; 5410 vsb.setPosition = ny; 5411 5412 scrollX_ = nx; 5413 scrollY_ = ny; 5414 } 5415 } 5416 5417 private int scrollX_; 5418 private int scrollY_; 5419 5420 void setTotalArea(int width, int height) { 5421 hsb.setMax(width); 5422 vsb.setMax(height); 5423 } 5424 5425 /// 5426 void setViewableArea(int width, int height) { 5427 hsb.setViewableArea(width); 5428 vsb.setViewableArea(height); 5429 } 5430 5431 private bool magic; 5432 override void addChild(Widget w, int position = int.max) { 5433 if(magic) 5434 container.addChild(w, position); 5435 else 5436 super.addChild(w, position); 5437 } 5438 5439 override void recomputeChildLayout() { 5440 if(hsb is null || vsb is null || container is null) return; 5441 5442 /+ 5443 writeln(x, " ", y , " ", width, " ", height); 5444 writeln(this.ContainerWidget.minWidth(), "x", this.ContainerWidget.minHeight()); 5445 +/ 5446 5447 registerMovement(); 5448 5449 hsb.height = scaleWithDpi(16); // FIXME? are tese 16s sane? 5450 hsb.x = 0; 5451 hsb.y = this.height - hsb.height; 5452 hsb.width = this.width - scaleWithDpi(16); 5453 hsb.recomputeChildLayout(); 5454 5455 vsb.width = scaleWithDpi(16); // FIXME? 5456 vsb.x = this.width - vsb.width; 5457 vsb.y = 0; 5458 vsb.height = this.height - scaleWithDpi(16); 5459 vsb.recomputeChildLayout(); 5460 5461 container.x = 0; 5462 container.y = 0; 5463 container.width = this.width - vsb.width; 5464 container.height = this.height - hsb.height; 5465 container.recomputeChildLayout(); 5466 5467 scrollX_ = 0; 5468 scrollY_ = 0; 5469 5470 hsb.setPosition(0); 5471 vsb.setPosition(0); 5472 5473 int mw, mh; 5474 Widget c = container; 5475 // FIXME: hack here to handle a layout inside... 5476 if(c.children.length == 1 && cast(Layout) c.children[0]) 5477 c = c.children[0]; 5478 foreach(child; c.children) { 5479 auto w = child.x + child.width; 5480 auto h = child.y + child.height; 5481 5482 if(w > mw) mw = w; 5483 if(h > mh) mh = h; 5484 } 5485 5486 setTotalArea(mw, mh); 5487 setViewableArea(width, height); 5488 } 5489 5490 override int minHeight() { return scaleWithDpi(64); } 5491 5492 HorizontalScrollbar hsb; 5493 VerticalScrollbar vsb; 5494 ContainerWidget container; 5495 } 5496 5497 5498 version(custom_widgets) 5499 private class InternalScrollableContainerWidget : Widget { 5500 5501 ScrollableWidget sw; 5502 5503 VerticalScrollbar verticalScrollBar; 5504 HorizontalScrollbar horizontalScrollBar; 5505 5506 this(ScrollableWidget sw, Widget parent) { 5507 this.sw = sw; 5508 5509 this.tabStop = false; 5510 5511 horizontalScrollBar = new HorizontalScrollbar(this); 5512 verticalScrollBar = new VerticalScrollbar(this); 5513 5514 horizontalScrollBar.showing_ = false; 5515 verticalScrollBar.showing_ = false; 5516 5517 horizontalScrollBar.addEventListener("scrolltonextline", { 5518 horizontalScrollBar.setPosition(horizontalScrollBar.position + 1); 5519 sw.horizontalScrollTo(horizontalScrollBar.position); 5520 }); 5521 horizontalScrollBar.addEventListener("scrolltopreviousline", { 5522 horizontalScrollBar.setPosition(horizontalScrollBar.position - 1); 5523 sw.horizontalScrollTo(horizontalScrollBar.position); 5524 }); 5525 verticalScrollBar.addEventListener("scrolltonextline", { 5526 verticalScrollBar.setPosition(verticalScrollBar.position + 1); 5527 sw.verticalScrollTo(verticalScrollBar.position); 5528 }); 5529 verticalScrollBar.addEventListener("scrolltopreviousline", { 5530 verticalScrollBar.setPosition(verticalScrollBar.position - 1); 5531 sw.verticalScrollTo(verticalScrollBar.position); 5532 }); 5533 horizontalScrollBar.addEventListener("scrolltonextpage", { 5534 horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_); 5535 sw.horizontalScrollTo(horizontalScrollBar.position); 5536 }); 5537 horizontalScrollBar.addEventListener("scrolltopreviouspage", { 5538 horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_); 5539 sw.horizontalScrollTo(horizontalScrollBar.position); 5540 }); 5541 verticalScrollBar.addEventListener("scrolltonextpage", { 5542 verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_); 5543 sw.verticalScrollTo(verticalScrollBar.position); 5544 }); 5545 verticalScrollBar.addEventListener("scrolltopreviouspage", { 5546 verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_); 5547 sw.verticalScrollTo(verticalScrollBar.position); 5548 }); 5549 horizontalScrollBar.addEventListener("scrolltoposition", (Event event) { 5550 horizontalScrollBar.setPosition(event.intValue); 5551 sw.horizontalScrollTo(horizontalScrollBar.position); 5552 }); 5553 verticalScrollBar.addEventListener("scrolltoposition", (Event event) { 5554 verticalScrollBar.setPosition(event.intValue); 5555 sw.verticalScrollTo(verticalScrollBar.position); 5556 }); 5557 horizontalScrollBar.addEventListener("scrolltrack", (Event event) { 5558 horizontalScrollBar.setPosition(event.intValue); 5559 sw.horizontalScrollTo(horizontalScrollBar.position); 5560 }); 5561 verticalScrollBar.addEventListener("scrolltrack", (Event event) { 5562 verticalScrollBar.setPosition(event.intValue); 5563 }); 5564 5565 super(parent); 5566 } 5567 5568 // this is supposed to be basically invisible... 5569 override int minWidth() { return sw.minWidth; } 5570 override int minHeight() { return sw.minHeight; } 5571 override int maxWidth() { return sw.maxWidth; } 5572 override int maxHeight() { return sw.maxHeight; } 5573 override int widthStretchiness() { return sw.widthStretchiness; } 5574 override int heightStretchiness() { return sw.heightStretchiness; } 5575 override int marginLeft() { return sw.marginLeft; } 5576 override int marginRight() { return sw.marginRight; } 5577 override int marginTop() { return sw.marginTop; } 5578 override int marginBottom() { return sw.marginBottom; } 5579 override int paddingLeft() { return sw.paddingLeft; } 5580 override int paddingRight() { return sw.paddingRight; } 5581 override int paddingTop() { return sw.paddingTop; } 5582 override int paddingBottom() { return sw.paddingBottom; } 5583 override void focus() { sw.focus(); } 5584 5585 5586 override void recomputeChildLayout() { 5587 // The stupid thing needs to calculate if a scroll bar is needed... 5588 recomputeChildLayoutHelper(); 5589 // then running it again will position things correctly if the bar is NOT needed 5590 recomputeChildLayoutHelper(); 5591 5592 // this sucks but meh it barely works 5593 } 5594 5595 private void recomputeChildLayoutHelper() { 5596 if(sw is null) return; 5597 5598 bool both = sw.showingVerticalScroll && sw.showingHorizontalScroll; 5599 if(horizontalScrollBar && verticalScrollBar) { 5600 horizontalScrollBar.width = this.width - (both ? verticalScrollBar.minWidth() : 0); 5601 horizontalScrollBar.height = horizontalScrollBar.minHeight(); 5602 horizontalScrollBar.x = 0; 5603 horizontalScrollBar.y = this.height - horizontalScrollBar.minHeight(); 5604 5605 verticalScrollBar.width = verticalScrollBar.minWidth(); 5606 verticalScrollBar.height = this.height - (both ? horizontalScrollBar.minHeight() : 0) - 2 - 2; 5607 verticalScrollBar.x = this.width - verticalScrollBar.minWidth(); 5608 verticalScrollBar.y = 0 + 2; 5609 5610 sw.x = 0; 5611 sw.y = 0; 5612 sw.width = this.width - (verticalScrollBar.showing ? verticalScrollBar.width : 0); 5613 sw.height = this.height - (horizontalScrollBar.showing ? horizontalScrollBar.height : 0); 5614 5615 if(sw.contentWidth_ <= this.width) 5616 sw.scrollOrigin_.x = 0; 5617 if(sw.contentHeight_ <= this.height) 5618 sw.scrollOrigin_.y = 0; 5619 5620 horizontalScrollBar.recomputeChildLayout(); 5621 verticalScrollBar.recomputeChildLayout(); 5622 sw.recomputeChildLayout(); 5623 } 5624 5625 if(sw.contentWidth_ <= this.width) 5626 sw.scrollOrigin_.x = 0; 5627 if(sw.contentHeight_ <= this.height) 5628 sw.scrollOrigin_.y = 0; 5629 5630 if(sw.showingHorizontalScroll()) 5631 horizontalScrollBar.showing(true, false); 5632 else 5633 horizontalScrollBar.showing(false, false); 5634 if(sw.showingVerticalScroll()) 5635 verticalScrollBar.showing(true, false); 5636 else 5637 verticalScrollBar.showing(false, false); 5638 5639 verticalScrollBar.setViewableArea(sw.viewportHeight()); 5640 verticalScrollBar.setMax(sw.contentHeight); 5641 verticalScrollBar.setPosition(sw.scrollOrigin.y); 5642 5643 horizontalScrollBar.setViewableArea(sw.viewportWidth()); 5644 horizontalScrollBar.setMax(sw.contentWidth); 5645 horizontalScrollBar.setPosition(sw.scrollOrigin.x); 5646 } 5647 } 5648 5649 /* 5650 class ScrollableClientWidget : Widget { 5651 this(Widget parent) { 5652 super(parent); 5653 } 5654 override void paint(WidgetPainter p) { 5655 parent.paint(p); 5656 } 5657 } 5658 */ 5659 5660 /++ 5661 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. 5662 +/ 5663 abstract class Slider : Widget { 5664 this(int min, int max, int step, Widget parent) { 5665 min_ = min; 5666 max_ = max; 5667 step_ = step; 5668 page_ = step; 5669 super(parent); 5670 } 5671 5672 private int min_; 5673 private int max_; 5674 private int step_; 5675 private int position_; 5676 private int page_; 5677 5678 // selection start and selection end 5679 // tics 5680 // tooltip? 5681 // some way to see and just type the value 5682 // win32 buddy controls are labels 5683 5684 /// 5685 void setMin(int a) { 5686 min_ = a; 5687 version(custom_widgets) 5688 redraw(); 5689 version(win32_widgets) 5690 SendMessage(hwnd, TBM_SETRANGEMIN, true, a); 5691 } 5692 /// 5693 int min() { 5694 return min_; 5695 } 5696 /// 5697 void setMax(int a) { 5698 max_ = a; 5699 version(custom_widgets) 5700 redraw(); 5701 version(win32_widgets) 5702 SendMessage(hwnd, TBM_SETRANGEMAX, true, a); 5703 } 5704 /// 5705 int max() { 5706 return max_; 5707 } 5708 /// 5709 void setPosition(int a) { 5710 if(a > max) 5711 a = max; 5712 if(a < min) 5713 a = min; 5714 position_ = a; 5715 version(custom_widgets) 5716 setPositionCustom(a); 5717 5718 version(win32_widgets) 5719 setPositionWindows(a); 5720 } 5721 version(win32_widgets) { 5722 protected abstract void setPositionWindows(int a); 5723 } 5724 5725 protected abstract int win32direction(); 5726 5727 /++ 5728 Alias for [position] for better compatibility with generic code. 5729 5730 History: 5731 Added October 5, 2021 5732 +/ 5733 @property int value() { 5734 return position; 5735 } 5736 5737 /// 5738 int position() { 5739 return position_; 5740 } 5741 /// 5742 void setStep(int a) { 5743 step_ = a; 5744 version(win32_widgets) 5745 SendMessage(hwnd, TBM_SETLINESIZE, 0, a); 5746 } 5747 /// 5748 int step() { 5749 return step_; 5750 } 5751 /// 5752 void setPageSize(int a) { 5753 page_ = a; 5754 version(win32_widgets) 5755 SendMessage(hwnd, TBM_SETPAGESIZE, 0, a); 5756 } 5757 /// 5758 int pageSize() { 5759 return page_; 5760 } 5761 5762 private void notify() { 5763 auto event = new ChangeEvent!int(this, &this.position); 5764 event.dispatch(); 5765 } 5766 5767 version(win32_widgets) 5768 void win32Setup(int style) { 5769 createWin32Window(this, TRACKBAR_CLASS, "", 5770 0|WS_CHILD|WS_VISIBLE|style|TBS_TOOLTIPS, 0); 5771 5772 // the trackbar sends the same messages as scroll, which 5773 // our other layer sends as these... just gonna translate 5774 // here 5775 this.addDirectEventListener("scrolltoposition", (Event event) { 5776 event.stopPropagation(); 5777 this.setPosition(this.win32direction > 0 ? event.intValue : max - event.intValue); 5778 notify(); 5779 }); 5780 this.addDirectEventListener("scrolltonextline", (Event event) { 5781 event.stopPropagation(); 5782 this.setPosition(this.position + this.step_ * this.win32direction); 5783 notify(); 5784 }); 5785 this.addDirectEventListener("scrolltopreviousline", (Event event) { 5786 event.stopPropagation(); 5787 this.setPosition(this.position - this.step_ * this.win32direction); 5788 notify(); 5789 }); 5790 this.addDirectEventListener("scrolltonextpage", (Event event) { 5791 event.stopPropagation(); 5792 this.setPosition(this.position + this.page_ * this.win32direction); 5793 notify(); 5794 }); 5795 this.addDirectEventListener("scrolltopreviouspage", (Event event) { 5796 event.stopPropagation(); 5797 this.setPosition(this.position - this.page_ * this.win32direction); 5798 notify(); 5799 }); 5800 5801 setMin(min_); 5802 setMax(max_); 5803 setStep(step_); 5804 setPageSize(page_); 5805 } 5806 5807 version(custom_widgets) { 5808 protected MouseTrackingWidget thumb; 5809 5810 protected abstract void setPositionCustom(int a); 5811 5812 override void defaultEventHandler_keydown(KeyDownEvent event) { 5813 switch(event.key) { 5814 case Key.Up: 5815 case Key.Right: 5816 setPosition(position() - step() * win32direction); 5817 changed(); 5818 break; 5819 case Key.Down: 5820 case Key.Left: 5821 setPosition(position() + step() * win32direction); 5822 changed(); 5823 break; 5824 case Key.Home: 5825 setPosition(win32direction > 0 ? min() : max()); 5826 changed(); 5827 break; 5828 case Key.End: 5829 setPosition(win32direction > 0 ? max() : min()); 5830 changed(); 5831 break; 5832 case Key.PageUp: 5833 setPosition(position() - pageSize() * win32direction); 5834 changed(); 5835 break; 5836 case Key.PageDown: 5837 setPosition(position() + pageSize() * win32direction); 5838 changed(); 5839 break; 5840 default: 5841 } 5842 super.defaultEventHandler_keydown(event); 5843 } 5844 5845 protected void changed() { 5846 auto ev = new ChangeEvent!int(this, &position); 5847 ev.dispatch(); 5848 } 5849 } 5850 } 5851 5852 /++ 5853 5854 +/ 5855 class VerticalSlider : Slider { 5856 this(int min, int max, int step, Widget parent) { 5857 version(custom_widgets) 5858 initialize(); 5859 5860 super(min, max, step, parent); 5861 5862 version(win32_widgets) 5863 win32Setup(TBS_VERT | 0x0200 /* TBS_REVERSED */); 5864 } 5865 5866 protected override int win32direction() { 5867 return -1; 5868 } 5869 5870 version(win32_widgets) 5871 protected override void setPositionWindows(int a) { 5872 // the windows thing makes the top 0 and i don't like that. 5873 SendMessage(hwnd, TBM_SETPOS, true, max - a); 5874 } 5875 5876 version(custom_widgets) 5877 private void initialize() { 5878 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, this); 5879 5880 thumb.tabStop = false; 5881 5882 thumb.thumbWidth = width; 5883 thumb.thumbHeight = scaleWithDpi(16); 5884 5885 thumb.addEventListener(EventType.change, () { 5886 auto sx = thumb.positionY * max() / (thumb.height - scaleWithDpi(16)); 5887 sx = max - sx; 5888 //informProgramThatUserChangedPosition(sx); 5889 5890 position_ = sx; 5891 5892 changed(); 5893 }); 5894 } 5895 5896 version(custom_widgets) 5897 override void recomputeChildLayout() { 5898 thumb.thumbWidth = this.width; 5899 super.recomputeChildLayout(); 5900 setPositionCustom(position_); 5901 } 5902 5903 version(custom_widgets) 5904 protected override void setPositionCustom(int a) { 5905 if(max()) 5906 thumb.positionY = (max - a) * (thumb.height - scaleWithDpi(16)) / max(); 5907 redraw(); 5908 } 5909 } 5910 5911 /++ 5912 5913 +/ 5914 class HorizontalSlider : Slider { 5915 this(int min, int max, int step, Widget parent) { 5916 version(custom_widgets) 5917 initialize(); 5918 5919 super(min, max, step, parent); 5920 5921 version(win32_widgets) 5922 win32Setup(TBS_HORZ); 5923 } 5924 5925 version(win32_widgets) 5926 protected override void setPositionWindows(int a) { 5927 SendMessage(hwnd, TBM_SETPOS, true, a); 5928 } 5929 5930 protected override int win32direction() { 5931 return 1; 5932 } 5933 5934 version(custom_widgets) 5935 private void initialize() { 5936 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, this); 5937 5938 thumb.tabStop = false; 5939 5940 thumb.thumbWidth = scaleWithDpi(16); 5941 thumb.thumbHeight = height; 5942 5943 thumb.addEventListener(EventType.change, () { 5944 auto sx = thumb.positionX * max() / (thumb.width - scaleWithDpi(16)); 5945 //informProgramThatUserChangedPosition(sx); 5946 5947 position_ = sx; 5948 5949 changed(); 5950 }); 5951 } 5952 5953 version(custom_widgets) 5954 override void recomputeChildLayout() { 5955 thumb.thumbHeight = this.height; 5956 super.recomputeChildLayout(); 5957 setPositionCustom(position_); 5958 } 5959 5960 version(custom_widgets) 5961 protected override void setPositionCustom(int a) { 5962 if(max()) 5963 thumb.positionX = a * (thumb.width - scaleWithDpi(16)) / max(); 5964 redraw(); 5965 } 5966 } 5967 5968 5969 /// 5970 abstract class ScrollbarBase : Widget { 5971 /// 5972 this(Widget parent) { 5973 super(parent); 5974 tabStop = false; 5975 step_ = scaleWithDpi(16); 5976 } 5977 5978 private int viewableArea_; 5979 private int max_; 5980 private int step_;// = 16; 5981 private int position_; 5982 5983 /// 5984 bool atEnd() { 5985 return position_ + viewableArea_ >= max_; 5986 } 5987 5988 /// 5989 bool atStart() { 5990 return position_ == 0; 5991 } 5992 5993 /// 5994 void setViewableArea(int a) { 5995 viewableArea_ = a; 5996 version(custom_widgets) 5997 redraw(); 5998 } 5999 /// 6000 void setMax(int a) { 6001 max_ = a; 6002 version(custom_widgets) 6003 redraw(); 6004 } 6005 /// 6006 int max() { 6007 return max_; 6008 } 6009 /// 6010 void setPosition(int a) { 6011 auto logicalMax = max_ - viewableArea_; 6012 if(a == int.max) 6013 a = logicalMax; 6014 6015 if(a > logicalMax) 6016 a = logicalMax; 6017 if(a < 0) 6018 a = 0; 6019 6020 position_ = a; 6021 6022 version(custom_widgets) 6023 redraw(); 6024 } 6025 /// 6026 int position() { 6027 return position_; 6028 } 6029 /// 6030 void setStep(int a) { 6031 step_ = a; 6032 } 6033 /// 6034 int step() { 6035 return step_; 6036 } 6037 6038 // FIXME: remove this.... maybe 6039 /+ 6040 protected void informProgramThatUserChangedPosition(int n) { 6041 position_ = n; 6042 auto evt = new Event(EventType.change, this); 6043 evt.intValue = n; 6044 evt.dispatch(); 6045 } 6046 +/ 6047 6048 version(custom_widgets) { 6049 enum MIN_THUMB_SIZE = 8; 6050 6051 abstract protected int getBarDim(); 6052 int thumbSize() { 6053 if(viewableArea_ >= max_ || max_ == 0) 6054 return getBarDim(); 6055 6056 int res = viewableArea_ * getBarDim() / max_; 6057 6058 if(res < scaleWithDpi(MIN_THUMB_SIZE)) 6059 res = scaleWithDpi(MIN_THUMB_SIZE); 6060 6061 return res; 6062 } 6063 6064 int thumbPosition() { 6065 /* 6066 viewableArea_ is the viewport height/width 6067 position_ is where we are 6068 */ 6069 //if(position_ + viewableArea_ >= max_) 6070 //return getBarDim - thumbSize; 6071 6072 auto maximumPossibleValue = getBarDim() - thumbSize; 6073 auto maximiumLogicalValue = max_ - viewableArea_; 6074 6075 auto p = (maximiumLogicalValue > 0) ? cast(int) (cast(long) position_ * maximumPossibleValue / maximiumLogicalValue) : 0; 6076 6077 return p; 6078 } 6079 } 6080 } 6081 6082 //public import mgt; 6083 6084 /++ 6085 A mouse tracking widget is one that follows the mouse when dragged inside it. 6086 6087 Concrete subclasses may include a scrollbar thumb and a volume control. 6088 +/ 6089 //version(custom_widgets) 6090 class MouseTrackingWidget : Widget { 6091 6092 /// 6093 int positionX() { return positionX_; } 6094 /// 6095 int positionY() { return positionY_; } 6096 6097 /// 6098 void positionX(int p) { positionX_ = p; } 6099 /// 6100 void positionY(int p) { positionY_ = p; } 6101 6102 private int positionX_; 6103 private int positionY_; 6104 6105 /// 6106 enum Orientation { 6107 horizontal, /// 6108 vertical, /// 6109 twoDimensional, /// 6110 } 6111 6112 private int thumbWidth_; 6113 private int thumbHeight_; 6114 6115 /// 6116 int thumbWidth() { return thumbWidth_; } 6117 /// 6118 int thumbHeight() { return thumbHeight_; } 6119 /// 6120 int thumbWidth(int a) { return thumbWidth_ = a; } 6121 /// 6122 int thumbHeight(int a) { return thumbHeight_ = a; } 6123 6124 private bool dragging; 6125 private bool hovering; 6126 private int startMouseX, startMouseY; 6127 6128 /// 6129 this(Orientation orientation, Widget parent) { 6130 super(parent); 6131 6132 //assert(parentWindow !is null); 6133 6134 addEventListener((MouseDownEvent event) { 6135 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6136 dragging = true; 6137 startMouseX = event.clientX - positionX; 6138 startMouseY = event.clientY - positionY; 6139 parentWindow.captureMouse(this); 6140 } else { 6141 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6142 positionX = event.clientX - thumbWidth / 2; 6143 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6144 positionY = event.clientY - thumbHeight / 2; 6145 6146 if(positionX + thumbWidth > this.width) 6147 positionX = this.width - thumbWidth; 6148 if(positionY + thumbHeight > this.height) 6149 positionY = this.height - thumbHeight; 6150 6151 if(positionX < 0) 6152 positionX = 0; 6153 if(positionY < 0) 6154 positionY = 0; 6155 6156 6157 // this.emit!(ChangeEvent!void)(); 6158 auto evt = new Event(EventType.change, this); 6159 evt.sendDirectly(); 6160 6161 redraw(); 6162 6163 } 6164 }); 6165 6166 addEventListener(EventType.mouseup, (Event event) { 6167 dragging = false; 6168 parentWindow.releaseMouseCapture(); 6169 }); 6170 6171 addEventListener(EventType.mouseout, (Event event) { 6172 if(!hovering) 6173 return; 6174 hovering = false; 6175 redraw(); 6176 }); 6177 6178 int lpx, lpy; 6179 6180 addEventListener((MouseMoveEvent event) { 6181 auto oh = hovering; 6182 if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) { 6183 hovering = true; 6184 } else { 6185 hovering = false; 6186 } 6187 if(!dragging) { 6188 if(hovering != oh) 6189 redraw(); 6190 return; 6191 } 6192 6193 if(orientation == Orientation.horizontal || orientation == Orientation.twoDimensional) 6194 positionX = event.clientX - startMouseX; // FIXME: click could be in the middle of it 6195 if(orientation == Orientation.vertical || orientation == Orientation.twoDimensional) 6196 positionY = event.clientY - startMouseY; 6197 6198 if(positionX + thumbWidth > this.width) 6199 positionX = this.width - thumbWidth; 6200 if(positionY + thumbHeight > this.height) 6201 positionY = this.height - thumbHeight; 6202 6203 if(positionX < 0) 6204 positionX = 0; 6205 if(positionY < 0) 6206 positionY = 0; 6207 6208 if(positionX != lpx || positionY != lpy) { 6209 lpx = positionX; 6210 lpy = positionY; 6211 6212 auto evt = new Event(EventType.change, this); 6213 evt.sendDirectly(); 6214 } 6215 6216 redraw(); 6217 }); 6218 } 6219 6220 version(custom_widgets) 6221 override void paint(WidgetPainter painter) { 6222 auto cs = getComputedStyle(); 6223 auto c = darken(cs.windowBackgroundColor, 0.2); 6224 painter.outlineColor = c; 6225 painter.fillColor = c; 6226 painter.drawRectangle(Point(0, 0), this.width, this.height); 6227 6228 auto color = hovering ? cs.hoveringColor : cs.windowBackgroundColor; 6229 draw3dFrame(positionX, positionY, thumbWidth, thumbHeight, painter, FrameStyle.risen, color); 6230 } 6231 } 6232 6233 //version(custom_widgets) 6234 //private 6235 class HorizontalScrollbar : ScrollbarBase { 6236 6237 version(custom_widgets) { 6238 private MouseTrackingWidget thumb; 6239 6240 override int getBarDim() { 6241 return thumb.width; 6242 } 6243 } 6244 6245 override void setViewableArea(int a) { 6246 super.setViewableArea(a); 6247 6248 version(win32_widgets) { 6249 SCROLLINFO info; 6250 info.cbSize = info.sizeof; 6251 info.nPage = a + 1; 6252 info.fMask = SIF_PAGE; 6253 SetScrollInfo(hwnd, SB_CTL, &info, true); 6254 } else version(custom_widgets) { 6255 thumb.positionX = thumbPosition; 6256 thumb.thumbWidth = thumbSize; 6257 thumb.redraw(); 6258 } else static assert(0); 6259 6260 } 6261 6262 override void setMax(int a) { 6263 super.setMax(a); 6264 version(win32_widgets) { 6265 SCROLLINFO info; 6266 info.cbSize = info.sizeof; 6267 info.nMin = 0; 6268 info.nMax = max; 6269 info.fMask = SIF_RANGE; 6270 SetScrollInfo(hwnd, SB_CTL, &info, true); 6271 } else version(custom_widgets) { 6272 thumb.positionX = thumbPosition; 6273 thumb.thumbWidth = thumbSize; 6274 thumb.redraw(); 6275 } 6276 } 6277 6278 override void setPosition(int a) { 6279 super.setPosition(a); 6280 version(win32_widgets) { 6281 SCROLLINFO info; 6282 info.cbSize = info.sizeof; 6283 info.fMask = SIF_POS; 6284 info.nPos = position; 6285 SetScrollInfo(hwnd, SB_CTL, &info, true); 6286 } else version(custom_widgets) { 6287 thumb.positionX = thumbPosition(); 6288 thumb.thumbWidth = thumbSize; 6289 thumb.redraw(); 6290 } else static assert(0); 6291 } 6292 6293 this(Widget parent) { 6294 super(parent); 6295 6296 version(win32_widgets) { 6297 createWin32Window(this, "Scrollbar"w, "", 6298 0|WS_CHILD|WS_VISIBLE|SBS_HORZ|SBS_BOTTOMALIGN, 0); 6299 } else version(custom_widgets) { 6300 auto vl = new HorizontalLayout(this); 6301 auto leftButton = new ArrowButton(ArrowDirection.left, vl); 6302 leftButton.setClickRepeat(scrollClickRepeatInterval); 6303 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.horizontal, vl); 6304 auto rightButton = new ArrowButton(ArrowDirection.right, vl); 6305 rightButton.setClickRepeat(scrollClickRepeatInterval); 6306 6307 leftButton.tabStop = false; 6308 rightButton.tabStop = false; 6309 thumb.tabStop = false; 6310 6311 leftButton.addEventListener(EventType.triggered, () { 6312 this.emitCommand!"scrolltopreviousline"(); 6313 //informProgramThatUserChangedPosition(position - step()); 6314 }); 6315 rightButton.addEventListener(EventType.triggered, () { 6316 this.emitCommand!"scrolltonextline"(); 6317 //informProgramThatUserChangedPosition(position + step()); 6318 }); 6319 6320 thumb.thumbWidth = this.minWidth; 6321 thumb.thumbHeight = scaleWithDpi(16); 6322 6323 thumb.addEventListener(EventType.change, () { 6324 auto maximumPossibleValue = thumb.width - thumb.thumbWidth; 6325 auto sx = maximumPossibleValue ? cast(int)(cast(long) thumb.positionX * (max()-viewableArea_) / maximumPossibleValue) : 0; 6326 6327 //informProgramThatUserChangedPosition(sx); 6328 6329 auto ev = new ScrollToPositionEvent(this, sx); 6330 ev.dispatch(); 6331 }); 6332 } 6333 } 6334 6335 override int minHeight() { return scaleWithDpi(16); } 6336 override int maxHeight() { return scaleWithDpi(16); } 6337 override int minWidth() { return scaleWithDpi(48); } 6338 } 6339 6340 class ScrollToPositionEvent : Event { 6341 enum EventString = "scrolltoposition"; 6342 6343 this(Widget target, int value) { 6344 this.value = value; 6345 super(EventString, target); 6346 } 6347 6348 immutable int value; 6349 6350 override @property int intValue() { 6351 return value; 6352 } 6353 } 6354 6355 //version(custom_widgets) 6356 //private 6357 class VerticalScrollbar : ScrollbarBase { 6358 6359 version(custom_widgets) { 6360 override int getBarDim() { 6361 return thumb.height; 6362 } 6363 6364 private MouseTrackingWidget thumb; 6365 } 6366 6367 override void setViewableArea(int a) { 6368 super.setViewableArea(a); 6369 6370 version(win32_widgets) { 6371 SCROLLINFO info; 6372 info.cbSize = info.sizeof; 6373 info.nPage = a + 1; 6374 info.fMask = SIF_PAGE; 6375 SetScrollInfo(hwnd, SB_CTL, &info, true); 6376 } else version(custom_widgets) { 6377 thumb.positionY = thumbPosition; 6378 thumb.thumbHeight = thumbSize; 6379 thumb.redraw(); 6380 } else static assert(0); 6381 6382 } 6383 6384 override void setMax(int a) { 6385 super.setMax(a); 6386 version(win32_widgets) { 6387 SCROLLINFO info; 6388 info.cbSize = info.sizeof; 6389 info.nMin = 0; 6390 info.nMax = max; 6391 info.fMask = SIF_RANGE; 6392 SetScrollInfo(hwnd, SB_CTL, &info, true); 6393 } else version(custom_widgets) { 6394 thumb.positionY = thumbPosition; 6395 thumb.thumbHeight = thumbSize; 6396 thumb.redraw(); 6397 } 6398 } 6399 6400 override void setPosition(int a) { 6401 super.setPosition(a); 6402 version(win32_widgets) { 6403 SCROLLINFO info; 6404 info.cbSize = info.sizeof; 6405 info.fMask = SIF_POS; 6406 info.nPos = position; 6407 SetScrollInfo(hwnd, SB_CTL, &info, true); 6408 } else version(custom_widgets) { 6409 thumb.positionY = thumbPosition; 6410 thumb.thumbHeight = thumbSize; 6411 thumb.redraw(); 6412 } else static assert(0); 6413 } 6414 6415 this(Widget parent) { 6416 super(parent); 6417 6418 version(win32_widgets) { 6419 createWin32Window(this, "Scrollbar"w, "", 6420 0|WS_CHILD|WS_VISIBLE|SBS_VERT|SBS_RIGHTALIGN, 0); 6421 } else version(custom_widgets) { 6422 auto vl = new VerticalLayout(this); 6423 auto upButton = new ArrowButton(ArrowDirection.up, vl); 6424 upButton.setClickRepeat(scrollClickRepeatInterval); 6425 thumb = new MouseTrackingWidget(MouseTrackingWidget.Orientation.vertical, vl); 6426 auto downButton = new ArrowButton(ArrowDirection.down, vl); 6427 downButton.setClickRepeat(scrollClickRepeatInterval); 6428 6429 upButton.addEventListener(EventType.triggered, () { 6430 this.emitCommand!"scrolltopreviousline"(); 6431 //informProgramThatUserChangedPosition(position - step()); 6432 }); 6433 downButton.addEventListener(EventType.triggered, () { 6434 this.emitCommand!"scrolltonextline"(); 6435 //informProgramThatUserChangedPosition(position + step()); 6436 }); 6437 6438 thumb.thumbWidth = this.minWidth; 6439 thumb.thumbHeight = scaleWithDpi(16); 6440 6441 thumb.addEventListener(EventType.change, () { 6442 auto maximumPossibleValue = thumb.height - thumb.thumbHeight; 6443 auto sy = maximumPossibleValue ? cast(int) (cast(long) thumb.positionY * (max()-viewableArea_) / maximumPossibleValue) : 0; 6444 6445 auto ev = new ScrollToPositionEvent(this, sy); 6446 ev.dispatch(); 6447 6448 //informProgramThatUserChangedPosition(sy); 6449 }); 6450 6451 upButton.tabStop = false; 6452 downButton.tabStop = false; 6453 thumb.tabStop = false; 6454 } 6455 } 6456 6457 override int minWidth() { return scaleWithDpi(16); } 6458 override int maxWidth() { return scaleWithDpi(16); } 6459 override int minHeight() { return scaleWithDpi(48); } 6460 } 6461 6462 6463 /++ 6464 EXPERIMENTAL 6465 6466 A widget specialized for being a container for other widgets. 6467 6468 History: 6469 Added May 29, 2021. Not stabilized at this time. 6470 +/ 6471 class WidgetContainer : Widget { 6472 this(Widget parent) { 6473 tabStop = false; 6474 super(parent); 6475 } 6476 6477 override int maxHeight() { 6478 if(this.children.length == 1) { 6479 return saturatedSum(this.children[0].maxHeight, this.children[0].marginTop, this.children[0].marginBottom); 6480 } else { 6481 return int.max; 6482 } 6483 } 6484 6485 override int maxWidth() { 6486 if(this.children.length == 1) { 6487 return saturatedSum(this.children[0].maxWidth, this.children[0].marginLeft, this.children[0].marginRight); 6488 } else { 6489 return int.max; 6490 } 6491 } 6492 6493 /+ 6494 6495 override int minHeight() { 6496 int largest = 0; 6497 int margins = 0; 6498 int lastMargin = 0; 6499 foreach(child; children) { 6500 auto mh = child.minHeight(); 6501 if(mh > largest) 6502 largest = mh; 6503 margins += mymax(lastMargin, child.marginTop()); 6504 lastMargin = child.marginBottom(); 6505 } 6506 return largest + margins; 6507 } 6508 6509 override int maxHeight() { 6510 int largest = 0; 6511 int margins = 0; 6512 int lastMargin = 0; 6513 foreach(child; children) { 6514 auto mh = child.maxHeight(); 6515 if(mh == int.max) 6516 return int.max; 6517 if(mh > largest) 6518 largest = mh; 6519 margins += mymax(lastMargin, child.marginTop()); 6520 lastMargin = child.marginBottom(); 6521 } 6522 return largest + margins; 6523 } 6524 6525 override int minWidth() { 6526 int min; 6527 foreach(child; children) { 6528 auto cm = child.minWidth; 6529 if(cm > min) 6530 min = cm; 6531 } 6532 return min + paddingLeft + paddingRight; 6533 } 6534 6535 override int minHeight() { 6536 int min; 6537 foreach(child; children) { 6538 auto cm = child.minHeight; 6539 if(cm > min) 6540 min = cm; 6541 } 6542 return min + paddingTop + paddingBottom; 6543 } 6544 6545 override int maxHeight() { 6546 int largest = 0; 6547 int margins = 0; 6548 int lastMargin = 0; 6549 foreach(child; children) { 6550 auto mh = child.maxHeight(); 6551 if(mh == int.max) 6552 return int.max; 6553 if(mh > largest) 6554 largest = mh; 6555 margins += mymax(lastMargin, child.marginTop()); 6556 lastMargin = child.marginBottom(); 6557 } 6558 return largest + margins; 6559 } 6560 6561 override int heightStretchiness() { 6562 int max; 6563 foreach(child; children) { 6564 auto c = child.heightStretchiness; 6565 if(c > max) 6566 max = c; 6567 } 6568 return max; 6569 } 6570 6571 override int marginTop() { 6572 if(this.children.length) 6573 return this.children[0].marginTop; 6574 return 0; 6575 } 6576 +/ 6577 } 6578 6579 /// 6580 abstract class Layout : Widget { 6581 this(Widget parent) { 6582 tabStop = false; 6583 super(parent); 6584 } 6585 } 6586 6587 /++ 6588 Makes all children minimum width and height, placing them down 6589 left to right, top to bottom. 6590 6591 Useful if you want to make a list of buttons that automatically 6592 wrap to a new line when necessary. 6593 +/ 6594 class InlineBlockLayout : Layout { 6595 /// 6596 this(Widget parent) { super(parent); } 6597 6598 override void recomputeChildLayout() { 6599 registerMovement(); 6600 6601 int x = this.paddingLeft, y = this.paddingTop; 6602 6603 int lineHeight; 6604 int previousMargin = 0; 6605 int previousMarginBottom = 0; 6606 6607 foreach(child; children) { 6608 if(child.hidden) 6609 continue; 6610 if(cast(FixedPosition) child) { 6611 child.recomputeChildLayout(); 6612 continue; 6613 } 6614 child.width = child.flexBasisWidth(); 6615 if(child.width == 0) 6616 child.width = child.minWidth(); 6617 if(child.width == 0) 6618 child.width = 32; 6619 6620 child.height = child.flexBasisHeight(); 6621 if(child.height == 0) 6622 child.height = child.minHeight(); 6623 if(child.height == 0) 6624 child.height = 32; 6625 6626 if(x + child.width + paddingRight > this.width) { 6627 x = this.paddingLeft; 6628 y += lineHeight; 6629 lineHeight = 0; 6630 previousMargin = 0; 6631 previousMarginBottom = 0; 6632 } 6633 6634 auto margin = child.marginLeft; 6635 if(previousMargin > margin) 6636 margin = previousMargin; 6637 6638 x += margin; 6639 6640 child.x = x; 6641 child.y = y; 6642 6643 int marginTopApplied; 6644 if(child.marginTop > previousMarginBottom) { 6645 child.y += child.marginTop; 6646 marginTopApplied = child.marginTop; 6647 } 6648 6649 x += child.width; 6650 previousMargin = child.marginRight; 6651 6652 if(child.marginBottom > previousMarginBottom) 6653 previousMarginBottom = child.marginBottom; 6654 6655 auto h = child.height + previousMarginBottom + marginTopApplied; 6656 if(h > lineHeight) 6657 lineHeight = h; 6658 6659 child.recomputeChildLayout(); 6660 } 6661 6662 } 6663 6664 override int minWidth() { 6665 int min; 6666 foreach(child; children) { 6667 auto cm = child.minWidth; 6668 if(cm > min) 6669 min = cm; 6670 } 6671 return min + paddingLeft + paddingRight; 6672 } 6673 6674 override int minHeight() { 6675 int min; 6676 foreach(child; children) { 6677 auto cm = child.minHeight; 6678 if(cm > min) 6679 min = cm; 6680 } 6681 return min + paddingTop + paddingBottom; 6682 } 6683 } 6684 6685 /++ 6686 A TabMessageWidget is a clickable row of tabs followed by a content area, very similar 6687 to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas 6688 the [TabWidget] will automatically change pages of child widgets. 6689 6690 This allows you to react to it however you see fit rather than having to 6691 be tied to just the new sets of child widgets. 6692 6693 It sends the message in the form of `this.emitCommand!"changetab"();`. 6694 6695 History: 6696 Added December 24, 2021 (dub v10.5) 6697 +/ 6698 class TabMessageWidget : Widget { 6699 6700 protected void tabIndexClicked(int item) { 6701 this.emitCommand!"changetab"(); 6702 } 6703 6704 /++ 6705 Adds the a new tab to the control with the given title. 6706 6707 Returns: 6708 The index of the newly added tab. You will need to know 6709 this index to refer to it later and to know which tab to 6710 change to when you get a changetab message. 6711 +/ 6712 int addTab(string title, int pos = int.max) { 6713 version(win32_widgets) { 6714 TCITEM item; 6715 item.mask = TCIF_TEXT; 6716 WCharzBuffer buf = WCharzBuffer(title); 6717 item.pszText = buf.ptr; 6718 return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); 6719 } else version(custom_widgets) { 6720 if(pos >= tabs.length) { 6721 tabs ~= title; 6722 redraw(); 6723 return cast(int) tabs.length - 1; 6724 } else if(pos <= 0) { 6725 tabs = title ~ tabs; 6726 redraw(); 6727 return 0; 6728 } else { 6729 tabs = tabs[0 .. pos] ~ title ~ title[pos .. $]; 6730 redraw(); 6731 return pos; 6732 } 6733 } 6734 } 6735 6736 override void addChild(Widget child, int pos = int.max) { 6737 if(container) 6738 container.addChild(child, pos); 6739 else 6740 super.addChild(child, pos); 6741 } 6742 6743 protected Widget makeContainer() { 6744 return new Widget(this); 6745 } 6746 6747 private Widget container; 6748 6749 override void recomputeChildLayout() { 6750 version(win32_widgets) { 6751 this.registerMovement(); 6752 6753 RECT rect; 6754 GetWindowRect(hwnd, &rect); 6755 6756 auto left = rect.left; 6757 auto top = rect.top; 6758 6759 TabCtrl_AdjustRect(hwnd, false, &rect); 6760 foreach(child; children) { 6761 if(!child.showing) continue; 6762 child.x = rect.left - left; 6763 child.y = rect.top - top; 6764 child.width = rect.right - rect.left; 6765 child.height = rect.bottom - rect.top; 6766 child.recomputeChildLayout(); 6767 } 6768 } else version(custom_widgets) { 6769 this.registerMovement(); 6770 foreach(child; children) { 6771 if(!child.showing) continue; 6772 child.x = 2; 6773 child.y = tabBarHeight + 2; // for the border 6774 child.width = width - 4; // for the border 6775 child.height = height - tabBarHeight - 2 - 2; // for the border 6776 child.recomputeChildLayout(); 6777 } 6778 } else static assert(0); 6779 } 6780 6781 version(custom_widgets) 6782 string[] tabs; 6783 6784 this(Widget parent) { 6785 super(parent); 6786 6787 tabStop = false; 6788 6789 version(win32_widgets) { 6790 createWin32Window(this, WC_TABCONTROL, "", 0); 6791 } else version(custom_widgets) { 6792 addEventListener((ClickEvent event) { 6793 if(event.target !is this && this.container !is null && event.target !is this.container) return; 6794 if(event.clientY < tabBarHeight) { 6795 auto t = (event.clientX / tabWidth); 6796 if(t >= 0 && t < tabs.length) { 6797 currentTab_ = t; 6798 tabIndexClicked(t); 6799 redraw(); 6800 } 6801 } 6802 }); 6803 } else static assert(0); 6804 6805 this.container = makeContainer(); 6806 } 6807 6808 override int marginTop() { return 4; } 6809 override int paddingBottom() { return 4; } 6810 6811 override int minHeight() { 6812 int max = 0; 6813 foreach(child; children) 6814 max = mymax(child.minHeight, max); 6815 6816 6817 version(win32_widgets) { 6818 RECT rect; 6819 rect.right = this.width; 6820 rect.bottom = max; 6821 TabCtrl_AdjustRect(hwnd, true, &rect); 6822 6823 max = rect.bottom; 6824 } else { 6825 max += defaultLineHeight + 4; 6826 } 6827 6828 6829 return max; 6830 } 6831 6832 version(win32_widgets) 6833 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 6834 switch(code) { 6835 case TCN_SELCHANGE: 6836 auto sel = TabCtrl_GetCurSel(hwnd); 6837 tabIndexClicked(sel); 6838 break; 6839 default: 6840 } 6841 return 0; 6842 } 6843 6844 version(custom_widgets) { 6845 private int currentTab_; 6846 private int tabBarHeight() { return defaultLineHeight; } 6847 int tabWidth = 80; 6848 } 6849 6850 version(win32_widgets) 6851 override void paint(WidgetPainter painter) {} 6852 6853 version(custom_widgets) 6854 override void paint(WidgetPainter painter) { 6855 auto cs = getComputedStyle(); 6856 6857 draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); 6858 6859 int posX = 0; 6860 foreach(idx, title; tabs) { 6861 auto isCurrent = idx == getCurrentTab(); 6862 6863 painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); 6864 6865 draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); 6866 painter.outlineColor = cs.foregroundColor; 6867 painter.drawText(Point(posX + 4, 2), title, Point(posX + tabWidth, tabBarHeight - 2), TextAlignment.VerticalCenter); 6868 6869 if(isCurrent) { 6870 painter.outlineColor = cs.windowBackgroundColor; 6871 painter.fillColor = Color.transparent; 6872 painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); 6873 painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); 6874 6875 painter.outlineColor = Color.white; 6876 painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); 6877 painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); 6878 painter.outlineColor = cs.activeTabColor; 6879 painter.drawPixel(Point(posX, tabBarHeight - 1)); 6880 } 6881 6882 posX += tabWidth - 2; 6883 } 6884 } 6885 6886 /// 6887 @scriptable 6888 void setCurrentTab(int item) { 6889 version(win32_widgets) 6890 TabCtrl_SetCurSel(hwnd, item); 6891 else version(custom_widgets) 6892 currentTab_ = item; 6893 else static assert(0); 6894 6895 tabIndexClicked(item); 6896 } 6897 6898 /// 6899 @scriptable 6900 int getCurrentTab() { 6901 version(win32_widgets) 6902 return TabCtrl_GetCurSel(hwnd); 6903 else version(custom_widgets) 6904 return currentTab_; // FIXME 6905 else static assert(0); 6906 } 6907 6908 /// 6909 @scriptable 6910 void removeTab(int item) { 6911 if(item && item == getCurrentTab()) 6912 setCurrentTab(item - 1); 6913 6914 version(win32_widgets) { 6915 TabCtrl_DeleteItem(hwnd, item); 6916 } 6917 6918 for(int a = item; a < children.length - 1; a++) 6919 this._children[a] = this._children[a + 1]; 6920 this._children = this._children[0 .. $-1]; 6921 } 6922 6923 } 6924 6925 6926 /++ 6927 A tab widget is a set of clickable tab buttons followed by a content area. 6928 6929 6930 Tabs can change existing content or can be new pages. 6931 6932 When the user picks a different tab, a `change` message is generated. 6933 +/ 6934 class TabWidget : TabMessageWidget { 6935 this(Widget parent) { 6936 super(parent); 6937 } 6938 6939 override protected Widget makeContainer() { 6940 return null; 6941 } 6942 6943 override void addChild(Widget child, int pos = int.max) { 6944 if(auto twp = cast(TabWidgetPage) child) { 6945 Widget.addChild(child, pos); 6946 if(pos == int.max) 6947 pos = cast(int) this.children.length - 1; 6948 6949 super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... 6950 6951 if(pos != getCurrentTab) { 6952 child.showing = false; 6953 } 6954 } else { 6955 assert(0, "Don't add children directly to a tab widget, instead add them to a page (see addPage)"); 6956 } 6957 } 6958 6959 // FIXME: add tab icons at some point, Windows supports them 6960 /++ 6961 Adds a page and its associated tab with the given label to the widget. 6962 6963 Returns: 6964 The added page object, to which you can add other widgets. 6965 +/ 6966 @scriptable 6967 TabWidgetPage addPage(string title) { 6968 return new TabWidgetPage(title, this); 6969 } 6970 6971 /++ 6972 Gets the page at the given tab index, or `null` if the index is bad. 6973 6974 History: 6975 Added December 24, 2021. 6976 +/ 6977 TabWidgetPage getPage(int index) { 6978 if(index < this.children.length) 6979 return null; 6980 return cast(TabWidgetPage) this.children[index]; 6981 } 6982 6983 /++ 6984 While you can still use the addTab from the parent class, 6985 *strongly* recommend you use [addPage] insteaad. 6986 6987 History: 6988 Added December 24, 2021 to fulful the interface 6989 requirement that came from adding [TabMessageWidget]. 6990 6991 You should not use it though since the [addPage] function 6992 is much easier to use here. 6993 +/ 6994 override int addTab(string title, int pos = int.max) { 6995 auto p = addPage(title); 6996 foreach(idx, child; this.children) 6997 if(child is p) 6998 return cast(int) idx; 6999 return -1; 7000 } 7001 7002 protected override void tabIndexClicked(int item) { 7003 foreach(idx, child; children) { 7004 child.showing(false, false); // batch the recalculates for the end 7005 } 7006 7007 foreach(idx, child; children) { 7008 if(idx == item) { 7009 child.showing(true, false); 7010 if(parentWindow) { 7011 auto f = parentWindow.getFirstFocusable(child); 7012 if(f) 7013 f.focus(); 7014 } 7015 recomputeChildLayout(); 7016 } 7017 } 7018 7019 version(win32_widgets) { 7020 InvalidateRect(hwnd, null, true); 7021 } else version(custom_widgets) { 7022 this.redraw(); 7023 } 7024 } 7025 7026 } 7027 7028 /++ 7029 A page widget is basically a tab widget with hidden tabs. It is also sometimes called a "StackWidget". 7030 7031 You add [TabWidgetPage]s to it. 7032 +/ 7033 class PageWidget : Widget { 7034 this(Widget parent) { 7035 super(parent); 7036 } 7037 7038 override int minHeight() { 7039 int max = 0; 7040 foreach(child; children) 7041 max = mymax(child.minHeight, max); 7042 7043 return max; 7044 } 7045 7046 7047 override void addChild(Widget child, int pos = int.max) { 7048 if(auto twp = cast(TabWidgetPage) child) { 7049 super.addChild(child, pos); 7050 if(pos == int.max) 7051 pos = cast(int) this.children.length - 1; 7052 7053 if(pos != getCurrentTab) { 7054 child.showing = false; 7055 } 7056 } else { 7057 assert(0, "Don't add children directly to a page widget, instead add them to a page (see addPage)"); 7058 } 7059 } 7060 7061 override void recomputeChildLayout() { 7062 this.registerMovement(); 7063 foreach(child; children) { 7064 child.x = 0; 7065 child.y = 0; 7066 child.width = width; 7067 child.height = height; 7068 child.recomputeChildLayout(); 7069 } 7070 } 7071 7072 private int currentTab_; 7073 7074 /// 7075 @scriptable 7076 void setCurrentTab(int item) { 7077 currentTab_ = item; 7078 7079 showOnly(item); 7080 } 7081 7082 /// 7083 @scriptable 7084 int getCurrentTab() { 7085 return currentTab_; 7086 } 7087 7088 /// 7089 @scriptable 7090 void removeTab(int item) { 7091 if(item && item == getCurrentTab()) 7092 setCurrentTab(item - 1); 7093 7094 for(int a = item; a < children.length - 1; a++) 7095 this._children[a] = this._children[a + 1]; 7096 this._children = this._children[0 .. $-1]; 7097 } 7098 7099 /// 7100 @scriptable 7101 TabWidgetPage addPage(string title) { 7102 return new TabWidgetPage(title, this); 7103 } 7104 7105 private void showOnly(int item) { 7106 foreach(idx, child; children) 7107 if(idx == item) { 7108 child.show(); 7109 child.recomputeChildLayout(); 7110 } else { 7111 child.hide(); 7112 } 7113 } 7114 7115 } 7116 7117 /++ 7118 7119 +/ 7120 class TabWidgetPage : Widget { 7121 string title; 7122 this(string title, Widget parent) { 7123 this.title = title; 7124 this.tabStop = false; 7125 super(parent); 7126 7127 ///* 7128 version(win32_widgets) { 7129 createWin32Window(this, Win32Class!"arsd_minigui_TabWidgetPage"w, "", 0); 7130 } 7131 //*/ 7132 } 7133 7134 override int minHeight() { 7135 int sum = 0; 7136 foreach(child; children) 7137 sum += child.minHeight(); 7138 return sum; 7139 } 7140 } 7141 7142 version(none) 7143 /++ 7144 A collapsable sidebar is a container that shows if its assigned width is greater than its minimum and otherwise shows as a button. 7145 7146 I think I need to modify the layout algorithms to support this. 7147 +/ 7148 class CollapsableSidebar : Widget { 7149 7150 } 7151 7152 /// Stacks the widgets vertically, taking all the available width for each child. 7153 class VerticalLayout : Layout { 7154 // most of this is intentionally blank - widget's default is vertical layout right now 7155 /// 7156 this(Widget parent) { super(parent); } 7157 7158 /++ 7159 Sets a max width for the layout so you don't have to subclass. The max width 7160 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7161 7162 History: 7163 Added November 29, 2021 (dub v10.5) 7164 +/ 7165 this(int maxWidth, Widget parent) { 7166 this.mw = maxWidth; 7167 super(parent); 7168 } 7169 7170 private int mw = int.max; 7171 7172 override int maxWidth() { return scaleWithDpi(mw); } 7173 } 7174 7175 /// Stacks the widgets horizontally, taking all the available height for each child. 7176 class HorizontalLayout : Layout { 7177 /// 7178 this(Widget parent) { super(parent); } 7179 7180 /++ 7181 Sets a max height for the layout so you don't have to subclass. The max height 7182 is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled. 7183 7184 History: 7185 Added November 29, 2021 (dub v10.5) 7186 +/ 7187 this(int maxHeight, Widget parent) { 7188 this.mh = maxHeight; 7189 super(parent); 7190 } 7191 7192 private int mh = 0; 7193 7194 7195 7196 override void recomputeChildLayout() { 7197 .recomputeChildLayout!"width"(this); 7198 } 7199 7200 override int minHeight() { 7201 int largest = 0; 7202 int margins = 0; 7203 int lastMargin = 0; 7204 foreach(child; children) { 7205 auto mh = child.minHeight(); 7206 if(mh > largest) 7207 largest = mh; 7208 margins += mymax(lastMargin, child.marginTop()); 7209 lastMargin = child.marginBottom(); 7210 } 7211 return largest + margins; 7212 } 7213 7214 override int maxHeight() { 7215 if(mh != 0) 7216 return mymax(minHeight, scaleWithDpi(mh)); 7217 7218 int largest = 0; 7219 int margins = 0; 7220 int lastMargin = 0; 7221 foreach(child; children) { 7222 auto mh = child.maxHeight(); 7223 if(mh == int.max) 7224 return int.max; 7225 if(mh > largest) 7226 largest = mh; 7227 margins += mymax(lastMargin, child.marginTop()); 7228 lastMargin = child.marginBottom(); 7229 } 7230 return largest + margins; 7231 } 7232 7233 override int heightStretchiness() { 7234 int max; 7235 foreach(child; children) { 7236 auto c = child.heightStretchiness; 7237 if(c > max) 7238 max = c; 7239 } 7240 return max; 7241 } 7242 7243 } 7244 7245 version(win32_widgets) 7246 private 7247 extern(Windows) 7248 LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { 7249 Widget* pwin = hwnd in Widget.nativeMapping; 7250 if(pwin is null) 7251 return DefWindowProc(hwnd, message, wparam, lparam); 7252 SimpleWindow win = pwin.simpleWindowWrappingHwnd; 7253 if(win is null) 7254 return DefWindowProc(hwnd, message, wparam, lparam); 7255 7256 switch(message) { 7257 case WM_SIZE: 7258 auto width = LOWORD(lparam); 7259 auto height = HIWORD(lparam); 7260 7261 auto hdc = GetDC(hwnd); 7262 auto hdcBmp = CreateCompatibleDC(hdc); 7263 7264 // FIXME: could this be more efficient? it never relinquishes a large bitmap 7265 if(width > win.bmpWidth || height > win.bmpHeight) { 7266 auto oldBuffer = win.buffer; 7267 win.buffer = CreateCompatibleBitmap(hdc, width, height); 7268 7269 if(oldBuffer) 7270 DeleteObject(oldBuffer); 7271 7272 win.bmpWidth = width; 7273 win.bmpHeight = height; 7274 } 7275 7276 // just always erase it upon resizing so minigui can draw over with a clean slate 7277 auto oldBmp = SelectObject(hdcBmp, win.buffer); 7278 7279 auto brush = GetSysColorBrush(COLOR_3DFACE); 7280 RECT r; 7281 r.left = 0; 7282 r.top = 0; 7283 r.right = width; 7284 r.bottom = height; 7285 FillRect(hdcBmp, &r, brush); 7286 7287 SelectObject(hdcBmp, oldBmp); 7288 DeleteDC(hdcBmp); 7289 ReleaseDC(hwnd, hdc); 7290 break; 7291 case WM_PAINT: 7292 if(win.buffer is null) 7293 goto default; 7294 7295 BITMAP bm; 7296 PAINTSTRUCT ps; 7297 7298 HDC hdc = BeginPaint(hwnd, &ps); 7299 7300 HDC hdcMem = CreateCompatibleDC(hdc); 7301 HBITMAP hbmOld = SelectObject(hdcMem, win.buffer); 7302 7303 GetObject(win.buffer, bm.sizeof, &bm); 7304 7305 BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); 7306 7307 SelectObject(hdcMem, hbmOld); 7308 DeleteDC(hdcMem); 7309 EndPaint(hwnd, &ps); 7310 break; 7311 default: 7312 return DefWindowProc(hwnd, message, wparam, lparam); 7313 } 7314 7315 return 0; 7316 } 7317 7318 private wstring Win32Class(wstring name)() { 7319 static bool classRegistered; 7320 if(!classRegistered) { 7321 HINSTANCE hInstance = cast(HINSTANCE) GetModuleHandle(null); 7322 WNDCLASSEX wc; 7323 wc.cbSize = wc.sizeof; 7324 wc.hInstance = hInstance; 7325 wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH); 7326 wc.lpfnWndProc = &DoubleBufferWndProc; 7327 wc.lpszClassName = name.ptr; 7328 if(!RegisterClassExW(&wc)) 7329 throw new Exception("RegisterClass ");// ~ to!string(GetLastError())); 7330 classRegistered = true; 7331 } 7332 7333 return name; 7334 } 7335 7336 /+ 7337 version(win32_widgets) 7338 extern(Windows) 7339 private 7340 LRESULT CustomDrawWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow { 7341 switch(iMessage) { 7342 case WM_PAINT: 7343 if(auto te = hWnd in Widget.nativeMapping) { 7344 try { 7345 //te.redraw(); 7346 writeln(te, " drawing"); 7347 } catch(Exception) {} 7348 } 7349 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7350 default: 7351 return DefWindowProc(hWnd, iMessage, wParam, lParam); 7352 } 7353 } 7354 +/ 7355 7356 7357 /++ 7358 A widget specifically designed to hold other widgets. 7359 7360 History: 7361 Added July 1, 2021 7362 +/ 7363 class ContainerWidget : Widget { 7364 this(Widget parent) { 7365 super(parent); 7366 this.tabStop = false; 7367 7368 version(win32_widgets) { 7369 createWin32Window(this, Win32Class!"arsd_minigui_ContainerWidget"w, "", 0); 7370 } 7371 } 7372 } 7373 7374 /++ 7375 A widget that takes your widget, puts scroll bars around it, and sends 7376 messages to it when the user scrolls. Unlike [ScrollableWidget], it makes 7377 no effort to automatically scroll or clip its child widgets - it just sends 7378 the messages. 7379 7380 7381 A ScrollMessageWidget notifies you with a [ScrollEvent] that it has changed. 7382 The scroll coordinates are all given in a unit you interpret as you wish. One 7383 of these units is moved on each press of the arrow buttons and represents the 7384 smallest amount the user can scroll. The intention is for this to be one line, 7385 one item in a list, one row in a table, etc. Whatever makes sense for your widget 7386 in each direction that the user might be interested in. 7387 7388 You can set a "page size" with the [step] property. (Yes, I regret the name...) 7389 This is the amount it jumps when the user pressed page up and page down, or clicks 7390 in the exposed part of the scroll bar. 7391 7392 You should add child content to the ScrollMessageWidget. However, it is important to 7393 note that the coordinates are always independent of the scroll position! It is YOUR 7394 responsibility to do any necessary transforms, clipping, etc., while drawing the 7395 content and interpreting mouse events if they are supposed to change with the scroll. 7396 This is in contrast to the (likely to be deprecated) [ScrollableWidget], which tries 7397 to maintain the illusion that there's an infinite space. The [ScrollMessageWidget] gives 7398 you more control (which can be considerably more efficient and adapted to your actual data) 7399 at the expense of you also needing to be aware of its reality. 7400 7401 Please note that it does NOT react to mouse wheel events or various keyboard events as of 7402 version 10.3. Maybe this will change in the future.... but for now you must call 7403 [addDefaultKeyboardListeners] and/or [addDefaultWheelListeners] or set something up yourself. 7404 +/ 7405 class ScrollMessageWidget : Widget { 7406 this(Widget parent) { 7407 super(parent); 7408 7409 container = new Widget(this); 7410 hsb = new HorizontalScrollbar(this); 7411 vsb = new VerticalScrollbar(this); 7412 7413 hsb.addEventListener("scrolltonextline", { 7414 hsb.setPosition(hsb.position + movementPerButtonClickH_); 7415 notify(); 7416 }); 7417 hsb.addEventListener("scrolltopreviousline", { 7418 hsb.setPosition(hsb.position - movementPerButtonClickH_); 7419 notify(); 7420 }); 7421 vsb.addEventListener("scrolltonextline", { 7422 vsb.setPosition(vsb.position + movementPerButtonClickV_); 7423 notify(); 7424 }); 7425 vsb.addEventListener("scrolltopreviousline", { 7426 vsb.setPosition(vsb.position - movementPerButtonClickV_); 7427 notify(); 7428 }); 7429 hsb.addEventListener("scrolltonextpage", { 7430 hsb.setPosition(hsb.position + hsb.step_); 7431 notify(); 7432 }); 7433 hsb.addEventListener("scrolltopreviouspage", { 7434 hsb.setPosition(hsb.position - hsb.step_); 7435 notify(); 7436 }); 7437 vsb.addEventListener("scrolltonextpage", { 7438 vsb.setPosition(vsb.position + vsb.step_); 7439 notify(); 7440 }); 7441 vsb.addEventListener("scrolltopreviouspage", { 7442 vsb.setPosition(vsb.position - vsb.step_); 7443 notify(); 7444 }); 7445 hsb.addEventListener("scrolltoposition", (Event event) { 7446 hsb.setPosition(event.intValue); 7447 notify(); 7448 }); 7449 vsb.addEventListener("scrolltoposition", (Event event) { 7450 vsb.setPosition(event.intValue); 7451 notify(); 7452 }); 7453 7454 7455 tabStop = false; 7456 container.tabStop = false; 7457 magic = true; 7458 } 7459 7460 private int movementPerButtonClickH_ = 1; 7461 private int movementPerButtonClickV_ = 1; 7462 public void movementPerButtonClick(int h, int v) { 7463 movementPerButtonClickH_ = h; 7464 movementPerButtonClickV_ = v; 7465 } 7466 7467 /++ 7468 Add default event listeners for keyboard and mouse wheel scrolling shortcuts. 7469 7470 7471 The defaults for [addDefaultWheelListeners] are: 7472 7473 $(LIST 7474 * Mouse wheel scrolls vertically 7475 * Alt key + mouse wheel scrolls horiontally 7476 * Shift + mouse wheel scrolls faster. 7477 * Any mouse click or wheel event will focus the inner widget if it has `tabStop = true` 7478 ) 7479 7480 The defaults for [addDefaultKeyboardListeners] are: 7481 7482 $(LIST 7483 * Arrow keys scroll by the given amounts 7484 * Shift+arrow keys scroll by the given amounts times the given shiftMultiplier 7485 * Page up and down scroll by the vertical viewable area 7486 * Home and end scroll to the start and end of the verticle viewable area. 7487 * Alt + page up / page down / home / end will horizonally scroll instead of vertical. 7488 ) 7489 7490 My recommendation is to change the scroll amounts if you are scrolling by pixels, but otherwise keep them at one line. 7491 7492 Params: 7493 horizontalArrowScrollAmount = 7494 verticalArrowScrollAmount = 7495 verticalWheelScrollAmount = how much should be scrolled vertically on each tick of the mouse wheel 7496 horizontalWheelScrollAmount = how much should be scrolled horizontally when alt is held on each tick of the mouse wheel 7497 shiftMultiplier = multiplies the scroll amount by this when shift is held 7498 +/ 7499 void addDefaultKeyboardListeners(int verticalArrowScrollAmount = 1, int horizontalArrowScrollAmount = 1, int shiftMultiplier = 3) { 7500 auto _this = this; 7501 7502 container.addEventListener((scope KeyDownEvent ke) { 7503 switch(ke.key) { 7504 case Key.Left: 7505 _this.scrollLeft(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7506 break; 7507 case Key.Right: 7508 _this.scrollRight(horizontalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7509 break; 7510 case Key.Up: 7511 _this.scrollUp(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7512 break; 7513 case Key.Down: 7514 _this.scrollDown(verticalArrowScrollAmount * (ke.shiftKey ? shiftMultiplier : 1)); 7515 break; 7516 case Key.PageUp: 7517 if(ke.altKey) 7518 _this.scrollLeft(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7519 else 7520 _this.scrollUp(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7521 break; 7522 case Key.PageDown: 7523 if(ke.altKey) 7524 _this.scrollRight(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7525 else 7526 _this.scrollDown(_this.vsb.viewableArea_ * (ke.shiftKey ? shiftMultiplier : 1)); 7527 break; 7528 case Key.Home: 7529 if(ke.altKey) 7530 _this.scrollLeft(short.max * 16); 7531 else 7532 _this.scrollUp(short.max * 16); 7533 break; 7534 case Key.End: 7535 if(ke.altKey) 7536 _this.scrollRight(short.max * 16); 7537 else 7538 _this.scrollDown(short.max * 16); 7539 break; 7540 7541 default: 7542 // ignore, not for us. 7543 } 7544 7545 }); 7546 } 7547 7548 /// ditto 7549 void addDefaultWheelListeners(int verticalWheelScrollAmount = 1, int horizontalWheelScrollAmount = 1, int shiftMultiplier = 3) { 7550 auto _this = this; 7551 container.addEventListener((scope ClickEvent ce) { 7552 7553 if(ce.target && ce.target.tabStop) 7554 ce.target.focus(); 7555 7556 // ctrl is reserved for the application 7557 if(ce.ctrlKey) 7558 return; 7559 7560 if(horizontalWheelScrollAmount == 0 && ce.altKey) 7561 return; 7562 7563 if(shiftMultiplier == 0 && ce.shiftKey) 7564 return; 7565 7566 if(ce.button == MouseButton.wheelDown) { 7567 if(ce.altKey) 7568 _this.scrollRight(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7569 else 7570 _this.scrollDown(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7571 } else if(ce.button == MouseButton.wheelUp) { 7572 if(ce.altKey) 7573 _this.scrollLeft(horizontalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7574 else 7575 _this.scrollUp(verticalWheelScrollAmount * (ce.shiftKey ? shiftMultiplier : 1)); 7576 } 7577 }); 7578 } 7579 7580 /++ 7581 Scrolls the given amount. 7582 7583 History: 7584 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. 7585 +/ 7586 void scrollUp(int amount = 1) { 7587 vsb.setPosition(vsb.position - amount); 7588 notify(); 7589 } 7590 /// ditto 7591 void scrollDown(int amount = 1) { 7592 vsb.setPosition(vsb.position + amount); 7593 notify(); 7594 } 7595 /// ditto 7596 void scrollLeft(int amount = 1) { 7597 hsb.setPosition(hsb.position - amount); 7598 notify(); 7599 } 7600 /// ditto 7601 void scrollRight(int amount = 1) { 7602 hsb.setPosition(hsb.position + amount); 7603 notify(); 7604 } 7605 7606 /// 7607 VerticalScrollbar verticalScrollBar() { return vsb; } 7608 /// 7609 HorizontalScrollbar horizontalScrollBar() { return hsb; } 7610 7611 void notify() { 7612 static bool insideNotify; 7613 7614 if(insideNotify) 7615 return; // avoid the recursive call, even if it isn't strictly correct 7616 7617 insideNotify = true; 7618 scope(exit) insideNotify = false; 7619 7620 this.emit!ScrollEvent(); 7621 } 7622 7623 mixin Emits!ScrollEvent; 7624 7625 /// 7626 Point position() { 7627 return Point(hsb.position, vsb.position); 7628 } 7629 7630 /// 7631 void setPosition(int x, int y) { 7632 hsb.setPosition(x); 7633 vsb.setPosition(y); 7634 } 7635 7636 /// 7637 void setPageSize(int unitsX, int unitsY) { 7638 hsb.setStep(unitsX); 7639 vsb.setStep(unitsY); 7640 } 7641 7642 /// Always call this BEFORE setViewableArea 7643 void setTotalArea(int width, int height) { 7644 hsb.setMax(width); 7645 vsb.setMax(height); 7646 } 7647 7648 /++ 7649 Always set the viewable area AFTER setitng the total area if you are going to change both. 7650 NEVER call this from inside a scroll event. This includes through recomputeChildLayout. 7651 If you need to do that, use [queueRecomputeChildLayout]. 7652 +/ 7653 void setViewableArea(int width, int height) { 7654 7655 // actually there IS A need to dothis cuz the max might have changed since then 7656 //if(width == hsb.viewableArea_ && height == vsb.viewableArea_) 7657 //return; // no need to do what is already done 7658 hsb.setViewableArea(width); 7659 vsb.setViewableArea(height); 7660 7661 bool needsNotify = false; 7662 7663 // FIXME: if at any point the rhs is outside the scrollbar, we need 7664 // to reset to 0. but it should remember the old position in case the 7665 // window resizes again, so it can kinda return ot where it was. 7666 // 7667 // so there's an inner position and a exposed position. the exposed one is always in bounds and thus may be (0,0) 7668 if(width >= hsb.max) { 7669 // there's plenty of room to display it all so we need to reset to zero 7670 // FIXME: adjust so it matches the note above 7671 hsb.setPosition(0); 7672 needsNotify = true; 7673 } 7674 if(height >= vsb.max) { 7675 // there's plenty of room to display it all so we need to reset to zero 7676 // FIXME: adjust so it matches the note above 7677 vsb.setPosition(0); 7678 needsNotify = true; 7679 } 7680 if(needsNotify) 7681 notify(); 7682 } 7683 7684 private bool magic; 7685 override void addChild(Widget w, int position = int.max) { 7686 if(magic) 7687 container.addChild(w, position); 7688 else 7689 super.addChild(w, position); 7690 } 7691 7692 override void recomputeChildLayout() { 7693 if(hsb is null || vsb is null || container is null) return; 7694 7695 registerMovement(); 7696 7697 enum BUTTON_SIZE = 16; 7698 7699 hsb.height = scaleWithDpi(BUTTON_SIZE); // FIXME? are tese 16s sane? 7700 hsb.x = 0; 7701 hsb.y = this.height - hsb.height; 7702 7703 vsb.width = scaleWithDpi(BUTTON_SIZE); // FIXME? 7704 vsb.x = this.width - vsb.width; 7705 vsb.y = 0; 7706 7707 auto vsb_width = vsb.showing ? vsb.width : 0; 7708 auto hsb_height = hsb.showing ? hsb.height : 0; 7709 7710 hsb.width = this.width - vsb_width; 7711 vsb.height = this.height - hsb_height; 7712 7713 hsb.recomputeChildLayout(); 7714 vsb.recomputeChildLayout(); 7715 7716 if(this.header is null) { 7717 container.x = 0; 7718 container.y = 0; 7719 container.width = this.width - vsb_width; 7720 container.height = this.height - hsb_height; 7721 container.recomputeChildLayout(); 7722 } else { 7723 header.x = 0; 7724 header.y = 0; 7725 header.width = this.width - vsb_width; 7726 header.height = scaleWithDpi(BUTTON_SIZE); // size of the button 7727 header.recomputeChildLayout(); 7728 7729 container.x = 0; 7730 container.y = scaleWithDpi(BUTTON_SIZE); 7731 container.width = this.width - vsb_width; 7732 container.height = this.height - hsb_height - scaleWithDpi(BUTTON_SIZE); 7733 container.recomputeChildLayout(); 7734 } 7735 } 7736 7737 private HorizontalScrollbar hsb; 7738 private VerticalScrollbar vsb; 7739 Widget container; 7740 private Widget header; 7741 7742 /++ 7743 Adds a fixed-size "header" widget. This will be positioned to align with the scroll up button. 7744 7745 History: 7746 Added September 27, 2021 (dub v10.3) 7747 +/ 7748 Widget getHeader() { 7749 if(this.header is null) { 7750 magic = false; 7751 scope(exit) magic = true; 7752 this.header = new Widget(this); 7753 recomputeChildLayout(); 7754 } 7755 return this.header; 7756 } 7757 7758 /++ 7759 Makes an effort to ensure as much of `rect` is visible as possible, scrolling if necessary. 7760 7761 History: 7762 Added January 3, 2023 (dub v11.0) 7763 +/ 7764 void scrollIntoView(Rectangle rect) { 7765 Rectangle viewRectangle = Rectangle(position, Size(hsb.viewableArea_, vsb.viewableArea_)); 7766 7767 // import std.stdio;writeln(viewRectangle, "\n", rect, " ", viewRectangle.contains(rect.lowerRight - Point(1, 1))); 7768 7769 // the lower right is exclusive normally 7770 auto test = rect.lowerRight; 7771 if(test.x > 0) test.x--; 7772 if(test.y > 0) test.y--; 7773 7774 if(!viewRectangle.contains(test) || !viewRectangle.contains(rect.upperLeft)) { 7775 // try to scroll only one dimension at a time if we can 7776 if(!viewRectangle.contains(Point(test.x, position.y)) || !viewRectangle.contains(Point(rect.upperLeft.x, position.y))) 7777 setPosition(rect.upperLeft.x, position.y); 7778 if(!viewRectangle.contains(Point(position.x, test.y)) || !viewRectangle.contains(Point(position.x, rect.upperLeft.y))) 7779 setPosition(position.x, rect.upperLeft.y); 7780 } 7781 7782 } 7783 7784 override int minHeight() { 7785 int min = container ? container.minHeight : 0; 7786 if(header !is null) 7787 min += header.minHeight; 7788 if(horizontalScrollBar.showing) 7789 min += horizontalScrollBar.minHeight; 7790 return min; 7791 } 7792 7793 override int maxHeight() { 7794 int max = container ? container.maxHeight : int.max; 7795 if(max == int.max) 7796 return max; 7797 if(horizontalScrollBar.showing) 7798 max += horizontalScrollBar.minHeight; 7799 return max; 7800 } 7801 } 7802 7803 /++ 7804 $(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") 7805 $(IMG //arsdnet.net/minigui-screenshots/linux/ScrollMessageWidget.png, Same thing, but in the default Linux theme.) 7806 +/ 7807 version(minigui_screenshots) 7808 @Screenshot("ScrollMessageWidget") 7809 unittest { 7810 auto window = new Window("ScrollMessageWidget"); 7811 7812 auto smw = new ScrollMessageWidget(window); 7813 smw.addDefaultKeyboardListeners(); 7814 smw.addDefaultWheelListeners(); 7815 7816 window.loop(); 7817 } 7818 7819 /++ 7820 Bypasses automatic layout for its children, using manual positioning and sizing only. 7821 While you need to manually position them, you must ensure they are inside the StaticLayout's 7822 bounding box to avoid undefined behavior. 7823 7824 You should almost never use this. 7825 +/ 7826 class StaticLayout : Layout { 7827 /// 7828 this(Widget parent) { super(parent); } 7829 override void recomputeChildLayout() { 7830 registerMovement(); 7831 foreach(child; children) 7832 child.recomputeChildLayout(); 7833 } 7834 } 7835 7836 /++ 7837 Bypasses automatic positioning when being laid out. It is your responsibility to make 7838 room for this widget in the parent layout. 7839 7840 Its children are laid out normally, unless there is exactly one, in which case it takes 7841 on the full size of the `StaticPosition` object (if you plan to put stuff on the edge, you 7842 can do that with `padding`). 7843 +/ 7844 class StaticPosition : Layout { 7845 /// 7846 this(Widget parent) { super(parent); } 7847 7848 override void recomputeChildLayout() { 7849 registerMovement(); 7850 if(this.children.length == 1) { 7851 auto child = children[0]; 7852 child.x = 0; 7853 child.y = 0; 7854 child.width = this.width; 7855 child.height = this.height; 7856 child.recomputeChildLayout(); 7857 } else 7858 foreach(child; children) 7859 child.recomputeChildLayout(); 7860 } 7861 7862 alias width = typeof(super).width; 7863 alias height = typeof(super).height; 7864 7865 @property int width(int w) @nogc pure @safe nothrow { 7866 return this._width = w; 7867 } 7868 7869 @property int height(int w) @nogc pure @safe nothrow { 7870 return this._height = w; 7871 } 7872 7873 } 7874 7875 /++ 7876 FixedPosition is like [StaticPosition], but its coordinates 7877 are always relative to the viewport, meaning they do not scroll with 7878 the parent content. 7879 +/ 7880 class FixedPosition : StaticPosition { 7881 /// 7882 this(Widget parent) { super(parent); } 7883 } 7884 7885 version(win32_widgets) 7886 int processWmCommand(HWND parentWindow, HWND handle, ushort cmd, ushort idm) { 7887 if(true) { 7888 // cmd == 0 = menu, cmd == 1 = accelerator 7889 if(auto item = idm in Action.mapping) { 7890 foreach(handler; (*item).triggered) 7891 handler(); 7892 /* 7893 auto event = new Event("triggered", *item); 7894 event.button = idm; 7895 event.dispatch(); 7896 */ 7897 return 0; 7898 } 7899 } 7900 if(handle) 7901 if(auto widgetp = handle in Widget.nativeMapping) { 7902 (*widgetp).handleWmCommand(cmd, idm); 7903 return 0; 7904 } 7905 return 1; 7906 } 7907 7908 7909 /// 7910 class Window : Widget { 7911 int mouseCaptureCount = 0; 7912 Widget mouseCapturedBy; 7913 void captureMouse(Widget byWhom) { 7914 assert(mouseCapturedBy is null || byWhom is mouseCapturedBy); 7915 mouseCaptureCount++; 7916 mouseCapturedBy = byWhom; 7917 win.grabInput(); 7918 } 7919 void releaseMouseCapture() { 7920 mouseCaptureCount--; 7921 mouseCapturedBy = null; 7922 win.releaseInputGrab(); 7923 } 7924 7925 /++ 7926 Sets the window icon which is often seen in title bars and taskbars. 7927 7928 History: 7929 Added April 5, 2022 (dub v10.8) 7930 +/ 7931 @property void icon(MemoryImage icon) { 7932 if(win && icon) 7933 win.icon = icon; 7934 } 7935 7936 /// 7937 @scriptable 7938 @property bool focused() { 7939 return win.focused; 7940 } 7941 7942 static class Style : Widget.Style { 7943 override WidgetBackground background() { 7944 version(custom_widgets) 7945 return WidgetBackground(WidgetPainter.visualTheme.windowBackgroundColor); 7946 else version(win32_widgets) 7947 return WidgetBackground(Color.transparent); 7948 else static assert(0); 7949 } 7950 } 7951 mixin OverrideStyle!Style; 7952 7953 /++ 7954 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. 7955 +/ 7956 static int lineHeight() { 7957 OperatingSystemFont font; 7958 if(auto vt = WidgetPainter.visualTheme) { 7959 font = vt.defaultFontCached(); 7960 } 7961 7962 if(font is null) { 7963 static int defaultHeightCache; 7964 if(defaultHeightCache == 0) { 7965 font = new OperatingSystemFont; 7966 font.loadDefault; 7967 defaultHeightCache = font.height();// * 5 / 4; 7968 } 7969 return defaultHeightCache; 7970 } 7971 7972 return font.height();// * 5 / 4; 7973 } 7974 7975 Widget focusedWidget; 7976 7977 private SimpleWindow win_; 7978 7979 @property { 7980 /++ 7981 Provides access to the underlying [SimpleWindow]. Note that changing properties on this window may disconnect minigui's event dispatchers. 7982 7983 History: 7984 Prior to June 21, 2021, it was a public (but undocumented) member. Now it a semi-protected property. 7985 +/ 7986 public SimpleWindow win() { 7987 return win_; 7988 } 7989 /// 7990 protected void win(SimpleWindow w) { 7991 win_ = w; 7992 } 7993 } 7994 7995 /// YOU ALMOST CERTAINLY SHOULD NOT USE THIS. This is really only for special purposes like pseudowindows or popup windows doing their own thing. 7996 this(Widget p) { 7997 tabStop = false; 7998 super(p); 7999 } 8000 8001 private void actualRedraw() { 8002 if(recomputeChildLayoutRequired) 8003 recomputeChildLayoutEntry(); 8004 if(!showing) return; 8005 8006 assert(parentWindow !is null); 8007 8008 auto w = drawableWindow; 8009 if(w is null) 8010 w = parentWindow.win; 8011 8012 if(w.closed()) 8013 return; 8014 8015 auto ugh = this.parent; 8016 int lox, loy; 8017 while(ugh) { 8018 lox += ugh.x; 8019 loy += ugh.y; 8020 ugh = ugh.parent; 8021 } 8022 auto painter = w.draw(true); 8023 privatePaint(WidgetPainter(painter, this), lox, loy, Rectangle(0, 0, int.max, int.max), false, willDraw()); 8024 // RedrawWindow(hwnd, null, null, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN); 8025 } 8026 8027 8028 private bool skipNextChar = false; 8029 8030 /++ 8031 Creates a window from an existing [SimpleWindow]. This constructor attaches various event handlers to the SimpleWindow object which may overwrite your existing handlers. 8032 8033 This constructor is intended primarily for internal use and may be changed to `protected` later. 8034 +/ 8035 this(SimpleWindow win) { 8036 8037 static if(UsingSimpledisplayX11) { 8038 win.discardAdditionalConnectionState = &discardXConnectionState; 8039 win.recreateAdditionalConnectionState = &recreateXConnectionState; 8040 } 8041 8042 tabStop = false; 8043 super(null); 8044 this.win = win; 8045 8046 win.addEventListener((Widget.RedrawEvent) { 8047 if(win.eventQueued!RecomputeEvent) { 8048 // writeln("skipping"); 8049 return; // let the recompute event do the actual redraw 8050 } 8051 this.actualRedraw(); 8052 }); 8053 8054 win.addEventListener((Widget.RecomputeEvent) { 8055 recomputeChildLayoutEntry(); 8056 if(win.eventQueued!RedrawEvent) 8057 return; // let the queued one do it 8058 else { 8059 // writeln("drawing"); 8060 this.actualRedraw(); // if not queued, it needs to be done now anyway 8061 } 8062 }); 8063 8064 this.width = win.width; 8065 this.height = win.height; 8066 this.parentWindow = this; 8067 8068 win.closeQuery = () { 8069 if(this.emit!ClosingEvent()) 8070 win.close(); 8071 }; 8072 win.onClosing = () { 8073 this.emit!ClosedEvent(); 8074 }; 8075 8076 win.windowResized = (int w, int h) { 8077 this.width = w; 8078 this.height = h; 8079 recomputeChildLayout(); 8080 // this causes a HUGE performance problem for no apparent benefit, hence the commenting 8081 //version(win32_widgets) 8082 //InvalidateRect(hwnd, null, true); 8083 redraw(); 8084 }; 8085 8086 win.onFocusChange = (bool getting) { 8087 if(this.focusedWidget) { 8088 if(getting) { 8089 this.focusedWidget.emit!FocusEvent(); 8090 this.focusedWidget.emit!FocusInEvent(); 8091 } else { 8092 this.focusedWidget.emit!BlurEvent(); 8093 this.focusedWidget.emit!FocusOutEvent(); 8094 } 8095 } 8096 8097 if(getting) { 8098 this.emit!FocusEvent(); 8099 this.emit!FocusInEvent(); 8100 } else { 8101 this.emit!BlurEvent(); 8102 this.emit!FocusOutEvent(); 8103 } 8104 }; 8105 8106 win.onDpiChanged = { 8107 this.queueRecomputeChildLayout(); 8108 auto event = new DpiChangedEvent(this); 8109 event.sendDirectly(); 8110 8111 privateDpiChanged(); 8112 }; 8113 8114 win.setEventHandlers( 8115 (MouseEvent e) { 8116 dispatchMouseEvent(e); 8117 }, 8118 (KeyEvent e) { 8119 //writefln("%x %s", cast(uint) e.key, e.key); 8120 dispatchKeyEvent(e); 8121 }, 8122 (dchar e) { 8123 if(e == 13) e = 10; // hack? 8124 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8125 dispatchCharEvent(e); 8126 }, 8127 ); 8128 8129 addEventListener("char", (Widget, Event ev) { 8130 if(skipNextChar) { 8131 ev.preventDefault(); 8132 skipNextChar = false; 8133 } 8134 }); 8135 8136 version(win32_widgets) 8137 win.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, out int mustReturn) { 8138 if(hwnd !is this.win.impl.hwnd) 8139 return 1; // we don't care... pass it on 8140 auto ret = WindowProcedureHelper(this, hwnd, msg, wParam, lParam, mustReturn); 8141 if(mustReturn) 8142 return ret; 8143 return 1; // pass it on 8144 }; 8145 8146 if(Window.newWindowCreated) 8147 Window.newWindowCreated(this); 8148 } 8149 8150 version(custom_widgets) 8151 override void defaultEventHandler_click(ClickEvent event) { 8152 if(event.target && event.target.tabStop) 8153 event.target.focus(); 8154 } 8155 8156 private static void delegate(Window) newWindowCreated; 8157 8158 version(win32_widgets) 8159 override void paint(WidgetPainter painter) { 8160 /* 8161 RECT rect; 8162 rect.right = this.width; 8163 rect.bottom = this.height; 8164 DrawThemeBackground(theme, painter.impl.hdc, 4, 1, &rect, null); 8165 */ 8166 // 3dface is used as window backgrounds by Windows too, so that's why I'm using it here 8167 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 8168 auto p = SelectObject(painter.impl.hdc, GetStockObject(NULL_PEN)); 8169 // since the pen is null, to fill the whole space, we need the +1 on both. 8170 gdi.Rectangle(painter.impl.hdc, 0, 0, this.width + 1, this.height + 1); 8171 SelectObject(painter.impl.hdc, p); 8172 SelectObject(painter.impl.hdc, b); 8173 } 8174 version(custom_widgets) 8175 override void paint(WidgetPainter painter) { 8176 auto cs = getComputedStyle(); 8177 painter.fillColor = cs.windowBackgroundColor; 8178 painter.outlineColor = cs.windowBackgroundColor; 8179 painter.drawRectangle(Point(0, 0), this.width, this.height); 8180 } 8181 8182 8183 override void defaultEventHandler_keydown(KeyDownEvent event) { 8184 Widget _this = event.target; 8185 8186 if(event.key == Key.Tab) { 8187 /* Window tab ordering is a recursive thingy with each group */ 8188 8189 // FIXME inefficient 8190 Widget[] helper(Widget p) { 8191 if(p.hidden) 8192 return null; 8193 Widget[] childOrdering; 8194 8195 auto children = p.children.dup; 8196 8197 while(true) { 8198 // UIs should be generally small, so gonna brute force it a little 8199 // note that it must be a stable sort here; if all are index 0, it should be in order of declaration 8200 8201 Widget smallestTab; 8202 foreach(ref c; children) { 8203 if(c is null) continue; 8204 if(smallestTab is null || c.tabOrder < smallestTab.tabOrder) { 8205 smallestTab = c; 8206 c = null; 8207 } 8208 } 8209 if(smallestTab !is null) { 8210 if(smallestTab.tabStop && !smallestTab.hidden) 8211 childOrdering ~= smallestTab; 8212 if(!smallestTab.hidden) 8213 childOrdering ~= helper(smallestTab); 8214 } else 8215 break; 8216 8217 } 8218 8219 return childOrdering; 8220 } 8221 8222 Widget[] tabOrdering = helper(this); 8223 8224 Widget recipient; 8225 8226 if(tabOrdering.length) { 8227 bool seenThis = false; 8228 Widget previous; 8229 foreach(idx, child; tabOrdering) { 8230 if(child is focusedWidget) { 8231 8232 if(event.shiftKey) { 8233 if(idx == 0) 8234 recipient = tabOrdering[$-1]; 8235 else 8236 recipient = tabOrdering[idx - 1]; 8237 break; 8238 } 8239 8240 seenThis = true; 8241 if(idx + 1 == tabOrdering.length) { 8242 // we're at the end, either move to the next group 8243 // or start back over 8244 recipient = tabOrdering[0]; 8245 } 8246 continue; 8247 } 8248 if(seenThis) { 8249 recipient = child; 8250 break; 8251 } 8252 previous = child; 8253 } 8254 } 8255 8256 if(recipient !is null) { 8257 // writeln(typeid(recipient)); 8258 recipient.focus(); 8259 8260 skipNextChar = true; 8261 } 8262 } 8263 8264 debug if(event.key == Key.F12) { 8265 if(devTools) { 8266 devTools.close(); 8267 devTools = null; 8268 } else { 8269 devTools = new DevToolWindow(this); 8270 devTools.show(); 8271 } 8272 } 8273 } 8274 8275 debug DevToolWindow devTools; 8276 8277 8278 /++ 8279 Creates a window. Please note windows are created in a hidden state, so you must call [show] or [loop] to get it to display. 8280 8281 History: 8282 Prior to May 12, 2021, the default title was "D Application" (simpledisplay.d's default). After that, the default is `Runtime.args[0]` instead. 8283 8284 The width and height arguments were added to the overload that takes `string` first on June 21, 2021. 8285 +/ 8286 this(int width = 500, int height = 500, string title = null) { 8287 if(title is null) { 8288 import core.runtime; 8289 if(Runtime.args.length) 8290 title = Runtime.args[0]; 8291 } 8292 win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus); 8293 8294 static if(UsingSimpledisplayX11) { 8295 ///+ 8296 // for input proxy 8297 auto display = XDisplayConnection.get; 8298 auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); 8299 XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask | EventMask.FocusChangeMask); 8300 XMapWindow(display, inputProxy); 8301 // writefln("input proxy: 0x%0x", inputProxy); 8302 this.inputProxy = new SimpleWindow(inputProxy); 8303 8304 XEvent lastEvent; 8305 this.inputProxy.handleNativeEvent = (XEvent ev) { 8306 lastEvent = ev; 8307 return 1; 8308 }; 8309 this.inputProxy.setEventHandlers( 8310 (MouseEvent e) { 8311 dispatchMouseEvent(e); 8312 }, 8313 (KeyEvent e) { 8314 //writefln("%x %s", cast(uint) e.key, e.key); 8315 if(dispatchKeyEvent(e)) { 8316 // FIXME: i should trap error 8317 if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { 8318 auto thing = nw.focusableWindow(); 8319 if(thing && thing.window) { 8320 lastEvent.xkey.window = thing.window; 8321 // writeln("sending event ", lastEvent.xkey); 8322 trapXErrors( { 8323 XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); 8324 }); 8325 } 8326 } 8327 } 8328 }, 8329 (dchar e) { 8330 if(e == 13) e = 10; // hack? 8331 if(e == 127) return; // linux sends this, windows doesn't. we don't want it. 8332 dispatchCharEvent(e); 8333 }, 8334 ); 8335 8336 this.inputProxy.populateXic(); 8337 // done 8338 //+/ 8339 } 8340 8341 8342 8343 win.setRequestedInputFocus = &this.setRequestedInputFocus; 8344 8345 this(win); 8346 } 8347 8348 SimpleWindow inputProxy; 8349 8350 private SimpleWindow setRequestedInputFocus() { 8351 return inputProxy; 8352 } 8353 8354 /// ditto 8355 this(string title, int width = 500, int height = 500) { 8356 this(width, height, title); 8357 } 8358 8359 /// 8360 @property string title() { return parentWindow.win.title; } 8361 /// 8362 @property void title(string title) { parentWindow.win.title = title; } 8363 8364 /// 8365 @scriptable 8366 void close() { 8367 win.close(); 8368 // I synchronize here upon window closing to ensure all child windows 8369 // get updated too before the event loop. This avoids some random X errors. 8370 static if(UsingSimpledisplayX11) { 8371 runInGuiThread( { 8372 XSync(XDisplayConnection.get, false); 8373 }); 8374 } 8375 } 8376 8377 bool dispatchKeyEvent(KeyEvent ev) { 8378 auto wid = focusedWidget; 8379 if(wid is null) 8380 wid = this; 8381 KeyEventBase event = ev.pressed ? new KeyDownEvent(wid) : new KeyUpEvent(wid); 8382 event.originalKeyEvent = ev; 8383 event.key = ev.key; 8384 event.state = ev.modifierState; 8385 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8386 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8387 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8388 event.dispatch(); 8389 8390 return !event.propagationStopped; 8391 } 8392 8393 // returns true if propagation should continue into nested things.... prolly not a great thing to do. 8394 bool dispatchCharEvent(dchar ch) { 8395 if(focusedWidget) { 8396 auto event = new CharEvent(focusedWidget, ch); 8397 event.dispatch(); 8398 return !event.propagationStopped; 8399 } 8400 return true; 8401 } 8402 8403 Widget mouseLastOver; 8404 Widget mouseLastDownOn; 8405 bool lastWasDoubleClick; 8406 bool dispatchMouseEvent(MouseEvent ev) { 8407 auto eleR = widgetAtPoint(this, ev.x, ev.y); 8408 auto ele = eleR.widget; 8409 8410 auto captureEle = ele; 8411 8412 if(mouseCapturedBy !is null) { 8413 if(ele !is mouseCapturedBy && !mouseCapturedBy.isAParentOf(ele)) 8414 captureEle = mouseCapturedBy; 8415 } 8416 8417 // a hack to get it relative to the widget. 8418 eleR.x = ev.x; 8419 eleR.y = ev.y; 8420 auto pain = captureEle; 8421 while(pain) { 8422 eleR.x -= pain.x; 8423 eleR.y -= pain.y; 8424 pain.addScrollPosition(eleR.x, eleR.y); 8425 pain = pain.parent; 8426 } 8427 8428 void populateMouseEventBase(MouseEventBase event) { 8429 event.button = ev.button; 8430 event.buttonLinear = ev.buttonLinear; 8431 event.state = ev.modifierState; 8432 event.clientX = eleR.x; 8433 event.clientY = eleR.y; 8434 8435 event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false; 8436 event.altKey = (ev.modifierState & ModifierState.alt) ? true : false; 8437 event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; 8438 } 8439 8440 if(ev.type == MouseEventType.buttonPressed) { 8441 { 8442 auto event = new MouseDownEvent(captureEle); 8443 populateMouseEventBase(event); 8444 event.dispatch(); 8445 } 8446 8447 if(ev.button != MouseButton.wheelDown && ev.button != MouseButton.wheelUp && mouseLastDownOn is ele && ev.doubleClick) { 8448 auto event = new DoubleClickEvent(captureEle); 8449 populateMouseEventBase(event); 8450 event.dispatch(); 8451 lastWasDoubleClick = ev.doubleClick; 8452 } else { 8453 lastWasDoubleClick = false; 8454 } 8455 8456 mouseLastDownOn = ele; 8457 } else if(ev.type == MouseEventType.buttonReleased) { 8458 { 8459 auto event = new MouseUpEvent(captureEle); 8460 populateMouseEventBase(event); 8461 event.dispatch(); 8462 } 8463 if(!lastWasDoubleClick && mouseLastDownOn is ele) { 8464 auto event = new ClickEvent(captureEle); 8465 populateMouseEventBase(event); 8466 event.dispatch(); 8467 } 8468 } else if(ev.type == MouseEventType.motion) { 8469 // motion 8470 { 8471 auto event = new MouseMoveEvent(captureEle); 8472 populateMouseEventBase(event); // fills in button which is meaningless but meh 8473 event.dispatch(); 8474 } 8475 8476 if(mouseLastOver !is ele) { 8477 if(ele !is null) { 8478 if(!isAParentOf(ele, mouseLastOver)) { 8479 ele.setDynamicState(DynamicState.hover, true); 8480 auto event = new MouseEnterEvent(ele); 8481 event.relatedTarget = mouseLastOver; 8482 event.sendDirectly(); 8483 8484 ele.useStyleProperties((scope Widget.Style s) { 8485 ele.parentWindow.win.cursor = s.cursor; 8486 }); 8487 } 8488 } 8489 8490 if(mouseLastOver !is null) { 8491 if(!isAParentOf(mouseLastOver, ele)) { 8492 mouseLastOver.setDynamicState(DynamicState.hover, false); 8493 auto event = new MouseLeaveEvent(mouseLastOver); 8494 event.relatedTarget = ele; 8495 event.sendDirectly(); 8496 } 8497 } 8498 8499 if(ele !is null) { 8500 auto event = new MouseOverEvent(ele); 8501 event.relatedTarget = mouseLastOver; 8502 event.dispatch(); 8503 } 8504 8505 if(mouseLastOver !is null) { 8506 auto event = new MouseOutEvent(mouseLastOver); 8507 event.relatedTarget = ele; 8508 event.dispatch(); 8509 } 8510 8511 mouseLastOver = ele; 8512 } 8513 } 8514 8515 return true; // FIXME: the event default prevented? 8516 } 8517 8518 /++ 8519 Shows the window and runs the application event loop. 8520 8521 Blocks until this window is closed. 8522 8523 Bugs: 8524 8525 $(PITFALL 8526 You should always have one event loop live for your application. 8527 If you make two windows in sequence, the second call to loop (or 8528 simpledisplay's [SimpleWindow.eventLoop], upon which this is built) 8529 might fail: 8530 8531 --- 8532 // don't do this! 8533 auto window = new Window(); 8534 window.loop(); 8535 8536 // or new Window or new MainWindow, all the same 8537 auto window2 = new SimpleWindow(); 8538 window2.eventLoop(0); // problematic! might crash 8539 --- 8540 8541 simpledisplay's current implementation assumes that final cleanup is 8542 done when the event loop refcount reaches zero. So after the first 8543 eventLoop returns, when there isn't already another one active, it assumes 8544 the program will exit soon and cleans up. 8545 8546 This is arguably a bug that it doesn't reinitialize, and I'll probably change 8547 it eventually, but in the mean time, there's an easy solution: 8548 8549 --- 8550 // do this 8551 EventLoop mainEventLoop = EventLoop.get; // just add this line 8552 8553 auto window = new Window(); 8554 window.loop(); 8555 8556 // or any other type of Window etc. 8557 auto window2 = new Window(); 8558 window2.loop(); // perfectly fine since mainEventLoop still alive 8559 --- 8560 8561 By adding a top-level reference to the event loop, it ensures the final cleanup 8562 is not performed until it goes out of scope too, letting the individual window loops 8563 work without trouble despite the bug. 8564 ) 8565 8566 History: 8567 The [BlockingMode] parameter was added on December 8, 2021. 8568 The default behavior is to block until the application quits 8569 (so all windows have been closed), unless another minigui or 8570 simpledisplay event loop is already running, in which case it 8571 will block until this window closes specifically. 8572 +/ 8573 @scriptable 8574 void loop(BlockingMode bm = BlockingMode.automatic) { 8575 if(win.closed) 8576 return; // otherwise show will throw 8577 show(); 8578 win.eventLoopWithBlockingMode(bm, 0); 8579 } 8580 8581 private bool firstShow = true; 8582 8583 @scriptable 8584 override void show() { 8585 bool rd = false; 8586 if(firstShow) { 8587 firstShow = false; 8588 recomputeChildLayout(); 8589 auto f = getFirstFocusable(this); // FIXME: autofocus? 8590 if(f) 8591 f.focus(); 8592 redraw(); 8593 } 8594 win.show(); 8595 super.show(); 8596 } 8597 @scriptable 8598 override void hide() { 8599 win.hide(); 8600 super.hide(); 8601 } 8602 8603 static Widget getFirstFocusable(Widget start) { 8604 if(start is null) 8605 return null; 8606 8607 foreach(widget; &start.focusableWidgets) { 8608 return widget; 8609 } 8610 8611 return null; 8612 } 8613 8614 static Widget getLastFocusable(Widget start) { 8615 if(start is null) 8616 return null; 8617 8618 Widget last; 8619 foreach(widget; &start.focusableWidgets) { 8620 last = widget; 8621 } 8622 8623 return last; 8624 } 8625 8626 8627 mixin Emits!ClosingEvent; 8628 mixin Emits!ClosedEvent; 8629 } 8630 8631 /++ 8632 History: 8633 Added January 12, 2022 8634 +/ 8635 class DpiChangedEvent : Event { 8636 enum EventString = "dpichanged"; 8637 8638 this(Widget target) { 8639 super(EventString, target); 8640 } 8641 } 8642 8643 debug private class DevToolWindow : Window { 8644 Window p; 8645 8646 TextEdit parentList; 8647 TextEdit logWindow; 8648 TextLabel clickX, clickY; 8649 8650 this(Window p) { 8651 this.p = p; 8652 super(400, 300, "Developer Toolbox"); 8653 8654 logWindow = new TextEdit(this); 8655 parentList = new TextEdit(this); 8656 8657 auto hl = new HorizontalLayout(this); 8658 clickX = new TextLabel("", TextAlignment.Right, hl); 8659 clickY = new TextLabel("", TextAlignment.Right, hl); 8660 8661 parentListeners ~= p.addEventListener("*", (Event ev) { 8662 log(typeid(ev.source).name, " emitted ", typeid(ev).name); 8663 }); 8664 8665 parentListeners ~= p.addEventListener((ClickEvent ev) { 8666 auto s = ev.srcElement; 8667 8668 string list; 8669 8670 void addInfo(Widget s) { 8671 list ~= s.toString(); 8672 list ~= "\n\tminHeight: " ~ toInternal!string(s.minHeight); 8673 list ~= "\n\tmaxHeight: " ~ toInternal!string(s.maxHeight); 8674 list ~= "\n\theightStretchiness: " ~ toInternal!string(s.heightStretchiness); 8675 list ~= "\n\theight: " ~ toInternal!string(s.height); 8676 list ~= "\n\tmarginTop: " ~ toInternal!string(s.marginTop); 8677 list ~= "\n\tmarginBottom: " ~ toInternal!string(s.marginBottom); 8678 } 8679 8680 addInfo(s); 8681 8682 s = s.parent; 8683 while(s) { 8684 list ~= "\n"; 8685 addInfo(s); 8686 s = s.parent; 8687 } 8688 parentList.content = list; 8689 8690 clickX.label = toInternal!string(ev.clientX); 8691 clickY.label = toInternal!string(ev.clientY); 8692 }); 8693 } 8694 8695 EventListener[] parentListeners; 8696 8697 override void close() { 8698 assert(p !is null); 8699 foreach(p; parentListeners) 8700 p.disconnect(); 8701 parentListeners = null; 8702 p.devTools = null; 8703 p = null; 8704 super.close(); 8705 } 8706 8707 override void defaultEventHandler_keydown(KeyDownEvent ev) { 8708 if(ev.key == Key.F12) { 8709 this.close(); 8710 if(p) 8711 p.devTools = null; 8712 } else { 8713 super.defaultEventHandler_keydown(ev); 8714 } 8715 } 8716 8717 void log(T...)(T t) { 8718 string str; 8719 import std.conv; 8720 foreach(i; t) 8721 str ~= to!string(i); 8722 str ~= "\n"; 8723 logWindow.addText(str); 8724 8725 //version(custom_widgets) 8726 //logWindow.ensureVisibleInScroll(logWindow.textLayout.caretBoundingBox()); 8727 } 8728 } 8729 8730 /++ 8731 A dialog is a transient window that intends to get information from 8732 the user before being dismissed. 8733 +/ 8734 abstract class Dialog : Window { 8735 /// 8736 this(int width, int height, string title = null) { 8737 super(width, height, title); 8738 } 8739 8740 /// 8741 abstract void OK(); 8742 8743 /// 8744 void Cancel() { 8745 this.close(); 8746 } 8747 } 8748 8749 /++ 8750 A custom widget similar to the HTML5 <details> tag. 8751 +/ 8752 version(none) 8753 class DetailsView : Widget { 8754 8755 } 8756 8757 // FIXME: maybe i should expose the other list views Windows offers too 8758 8759 /++ 8760 A TableView is a widget made to display a table of data strings. 8761 8762 8763 Future_Directions: 8764 Each item should be able to take an icon too and maybe I'll allow more of the view modes Windows offers. 8765 8766 I will add a selection changed event at some point, as well as item clicked events. 8767 History: 8768 Added September 24, 2021. Initial api stabilized in dub v10.4, but it isn't completely feature complete yet. 8769 See_Also: 8770 [ListWidget] which displays a list of strings without additional columns. 8771 +/ 8772 class TableView : Widget { 8773 /++ 8774 8775 +/ 8776 this(Widget parent) { 8777 super(parent); 8778 8779 version(win32_widgets) { 8780 createWin32Window(this, WC_LISTVIEW, "", LVS_REPORT | LVS_OWNERDATA);//| LVS_OWNERDRAWFIXED); 8781 } else version(custom_widgets) { 8782 auto smw = new ScrollMessageWidget(this); 8783 smw.addDefaultKeyboardListeners(); 8784 smw.addDefaultWheelListeners(1, scaleWithDpi(16)); 8785 tvwi = new TableViewWidgetInner(this, smw); 8786 } 8787 } 8788 8789 // FIXME: auto-size columns on double click of header thing like in Windows 8790 // it need only make the currently displayed things fit well. 8791 8792 8793 private ColumnInfo[] columns; 8794 private int itemCount; 8795 8796 version(custom_widgets) private { 8797 TableViewWidgetInner tvwi; 8798 } 8799 8800 /// Passed to [setColumnInfo] 8801 static struct ColumnInfo { 8802 const(char)[] name; /// the name displayed in the header 8803 /++ 8804 The default width, in pixels. As a special case, you can set this to -1 8805 if you want the system to try to automatically size the width to fit visible 8806 content. If it can't, it will try to pick a sensible default size. 8807 8808 Any other negative value is not allowed and may lead to unpredictable results. 8809 8810 History: 8811 The -1 behavior was specified on December 3, 2021. It actually worked before 8812 anyway on Win32 but now it is a formal feature with partial Linux support. 8813 8814 Bugs: 8815 It doesn't actually attempt to calculate a best-fit width on Linux as of 8816 December 3, 2021. I do plan to fix this in the future, but Windows is the 8817 priority right now. At least it doesn't break things when you use it now. 8818 +/ 8819 int width; 8820 8821 /++ 8822 Alignment of the text in the cell. Applies to the header as well as all data in this 8823 column. 8824 8825 Bugs: 8826 On Windows, the first column ignores this member and is always left aligned. 8827 You can work around this by inserting a dummy first column with width = 0 8828 then putting your actual data in the second column, which does respect the 8829 alignment. 8830 8831 This is a quirk of the operating system's implementation going back a very 8832 long time and is unlikely to ever be fixed. 8833 +/ 8834 TextAlignment alignment; 8835 8836 /++ 8837 After all the pixel widths have been assigned, any left over 8838 space is divided up among all columns and distributed to according 8839 to the widthPercent field. 8840 8841 8842 For example, if you have two fields, both with width 50 and one with 8843 widthPercent of 25 and the other with widthPercent of 75, and the 8844 container is 200 pixels wide, first both get their width of 50. 8845 then the 100 remaining pixels are split up, so the one gets a total 8846 of 75 pixels and the other gets a total of 125. 8847 8848 This is automatically applied as the window is resized. 8849 8850 If there is not enough space - that is, when a horizontal scrollbar 8851 needs to appear - there are 0 pixels divided up, and thus everyone 8852 gets 0. This can cause a column to shrink out of proportion when 8853 passing the scroll threshold. 8854 8855 It is important to still set a fixed width (that is, to populate the 8856 `width` field) even if you use the percents because that will be the 8857 default minimum in the event of a scroll bar appearing. 8858 8859 The percents total in the column can never exceed 100 or be less than 0. 8860 Doing this will trigger an assert error. 8861 8862 Implementation note: 8863 8864 Please note that percentages are only recalculated 1) upon original 8865 construction and 2) upon resizing the control. If the user adjusts the 8866 width of a column, the percentage items will not be updated. 8867 8868 On the other hand, if the user adjusts the width of a percentage column 8869 then resizes the window, it is recalculated, meaning their hand adjustment 8870 is discarded. This specific behavior may change in the future as it is 8871 arguably a bug, but I'm not certain yet. 8872 8873 History: 8874 Added November 10, 2021 (dub v10.4) 8875 +/ 8876 int widthPercent; 8877 8878 8879 private int calculatedWidth; 8880 } 8881 /++ 8882 Sets the number of columns along with information about the headers. 8883 8884 Please note: on Windows, the first column ignores your alignment preference 8885 and is always left aligned. 8886 +/ 8887 void setColumnInfo(ColumnInfo[] columns...) { 8888 8889 foreach(ref c; columns) { 8890 c.name = c.name.idup; 8891 } 8892 this.columns = columns.dup; 8893 8894 updateCalculatedWidth(false); 8895 8896 version(custom_widgets) { 8897 tvwi.header.updateHeaders(); 8898 tvwi.updateScrolls(); 8899 } else version(win32_widgets) 8900 foreach(i, column; this.columns) { 8901 LVCOLUMN lvColumn; 8902 lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; 8903 lvColumn.cx = column.width == -1 ? -1 : column.calculatedWidth; 8904 8905 auto bfr = WCharzBuffer(column.name); 8906 lvColumn.pszText = bfr.ptr; 8907 8908 if(column.alignment & TextAlignment.Center) 8909 lvColumn.fmt = LVCFMT_CENTER; 8910 else if(column.alignment & TextAlignment.Right) 8911 lvColumn.fmt = LVCFMT_RIGHT; 8912 else 8913 lvColumn.fmt = LVCFMT_LEFT; 8914 8915 if(SendMessage(hwnd, LVM_INSERTCOLUMN, cast(WPARAM) i, cast(LPARAM) &lvColumn) == -1) 8916 throw new WindowsApiException("Insert Column Fail", GetLastError()); 8917 } 8918 } 8919 8920 private int getActualSetSize(size_t i, bool askWindows) { 8921 version(win32_widgets) 8922 if(askWindows) 8923 return cast(int) SendMessage(hwnd, LVM_GETCOLUMNWIDTH, cast(WPARAM) i, 0); 8924 auto w = columns[i].width; 8925 if(w == -1) 8926 return 50; // idk, just give it some space so the percents aren't COMPLETELY off FIXME 8927 return w; 8928 } 8929 8930 private void updateCalculatedWidth(bool informWindows) { 8931 int padding; 8932 version(win32_widgets) 8933 padding = 4; 8934 int remaining = this.width; 8935 foreach(i, column; columns) 8936 remaining -= this.getActualSetSize(i, informWindows && column.widthPercent == 0) + padding; 8937 remaining -= padding; 8938 if(remaining < 0) 8939 remaining = 0; 8940 8941 int percentTotal; 8942 foreach(i, ref column; columns) { 8943 percentTotal += column.widthPercent; 8944 8945 auto c = this.getActualSetSize(i, informWindows && column.widthPercent == 0) + (remaining * column.widthPercent) / 100; 8946 8947 column.calculatedWidth = c; 8948 8949 version(win32_widgets) 8950 if(informWindows) 8951 SendMessage(hwnd, LVM_SETCOLUMNWIDTH, i, c); // LVSCW_AUTOSIZE or LVSCW_AUTOSIZE_USEHEADER are amazing omg 8952 } 8953 8954 assert(percentTotal >= 0, "The total percents in your column definitions were negative. They must add up to something between 0 and 100."); 8955 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)."); 8956 8957 8958 } 8959 8960 override void registerMovement() { 8961 super.registerMovement(); 8962 8963 updateCalculatedWidth(true); 8964 } 8965 8966 /++ 8967 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. 8968 +/ 8969 void setItemCount(int count) { 8970 this.itemCount = count; 8971 version(custom_widgets) { 8972 tvwi.updateScrolls(); 8973 redraw(); 8974 } else version(win32_widgets) { 8975 SendMessage(hwnd, LVM_SETITEMCOUNT, count, 0); 8976 } 8977 } 8978 8979 /++ 8980 Clears all items; 8981 +/ 8982 void clear() { 8983 this.itemCount = 0; 8984 this.columns = null; 8985 version(custom_widgets) { 8986 tvwi.header.updateHeaders(); 8987 tvwi.updateScrolls(); 8988 redraw(); 8989 } else version(win32_widgets) { 8990 SendMessage(hwnd, LVM_DELETEALLITEMS, 0, 0); 8991 } 8992 } 8993 8994 /+ 8995 version(win32_widgets) 8996 override int handleWmDrawItem(DRAWITEMSTRUCT* dis) 8997 auto itemId = dis.itemID; 8998 auto hdc = dis.hDC; 8999 auto rect = dis.rcItem; 9000 switch(dis.itemAction) { 9001 case ODA_DRAWENTIRE: 9002 9003 // FIXME: do other items 9004 // FIXME: do the focus rectangle i guess 9005 // FIXME: alignment 9006 // FIXME: column width 9007 // FIXME: padding left 9008 // FIXME: check dpi scaling 9009 // FIXME: don't owner draw unless it is necessary. 9010 9011 auto padding = GetSystemMetrics(SM_CXEDGE); // FIXME: for dpi 9012 RECT itemRect; 9013 itemRect.top = 1; // subitem idx, 1-based 9014 itemRect.left = LVIR_BOUNDS; 9015 9016 SendMessage(hwnd, LVM_GETSUBITEMRECT, itemId, cast(LPARAM) &itemRect); 9017 itemRect.left += padding; 9018 9019 getData(itemId, 0, (in char[] data) { 9020 auto wdata = WCharzBuffer(data); 9021 DrawTextW(hdc, wdata.ptr, wdata.length, &itemRect, DT_RIGHT| DT_END_ELLIPSIS); 9022 9023 }); 9024 goto case; 9025 case ODA_FOCUS: 9026 if(dis.itemState & ODS_FOCUS) 9027 DrawFocusRect(hdc, &rect); 9028 break; 9029 case ODA_SELECT: 9030 // itemState & ODS_SELECTED 9031 break; 9032 default: 9033 } 9034 return 1; 9035 } 9036 +/ 9037 9038 version(win32_widgets) { 9039 CellStyle last; 9040 COLORREF defaultColor; 9041 COLORREF defaultBackground; 9042 } 9043 9044 version(win32_widgets) 9045 override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 9046 switch(code) { 9047 case NM_CUSTOMDRAW: 9048 auto s = cast(NMLVCUSTOMDRAW*) hdr; 9049 switch(s.nmcd.dwDrawStage) { 9050 case CDDS_PREPAINT: 9051 if(getCellStyle is null) 9052 return 0; 9053 9054 mustReturn = true; 9055 return CDRF_NOTIFYITEMDRAW; 9056 case CDDS_ITEMPREPAINT: 9057 mustReturn = true; 9058 return CDRF_NOTIFYSUBITEMDRAW; 9059 case CDDS_ITEMPREPAINT | CDDS_SUBITEM: 9060 mustReturn = true; 9061 9062 if(getCellStyle is null) // this SHOULD never happen... 9063 return 0; 9064 9065 if(s.iSubItem == 0) { 9066 // Windows resets it per row so we'll use item 0 as a chance 9067 // to capture these for later 9068 defaultColor = s.clrText; 9069 defaultBackground = s.clrTextBk; 9070 } 9071 9072 auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem); 9073 // if no special style and no reset needed... 9074 if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init)) 9075 return 0; // allow default processing to continue 9076 9077 last = style; 9078 9079 // might still need to reset or use the preference. 9080 9081 if(style.flags & CellStyle.Flags.textColorSet) 9082 s.clrText = style.textColor.asWindowsColorRef; 9083 else 9084 s.clrText = defaultColor; // reset in case it was set from last iteration not a fan 9085 if(style.flags & CellStyle.Flags.backgroundColorSet) 9086 s.clrTextBk = style.backgroundColor.asWindowsColorRef; 9087 else 9088 s.clrTextBk = defaultBackground; // need to reset it... not a fan of this 9089 9090 return CDRF_NEWFONT; 9091 default: 9092 return 0; 9093 9094 } 9095 case NM_RETURN: // no need since i subclass keydown 9096 break; 9097 case LVN_COLUMNCLICK: 9098 auto info = cast(LPNMLISTVIEW) hdr; 9099 this.emit!HeaderClickedEvent(info.iSubItem); 9100 break; 9101 case NM_CLICK: 9102 case NM_DBLCLK: 9103 case NM_RCLICK: 9104 case NM_RDBLCLK: 9105 // the item/subitem is set here and that can be a useful notification 9106 // even beyond the normal click notification 9107 break; 9108 case LVN_GETDISPINFO: 9109 LV_DISPINFO* info = cast(LV_DISPINFO*) hdr; 9110 if(info.item.mask & LVIF_TEXT) { 9111 if(getData) { 9112 getData(info.item.iItem, info.item.iSubItem, (in char[] dataReceived) { 9113 auto bfr = WCharzBuffer(dataReceived); 9114 auto len = info.item.cchTextMax; 9115 if(bfr.length < len) 9116 len = cast(typeof(len)) bfr.length; 9117 info.item.pszText[0 .. len] = bfr.ptr[0 .. len]; 9118 info.item.pszText[len] = 0; 9119 }); 9120 } else { 9121 info.item.pszText[0] = 0; 9122 } 9123 //info.item.iItem 9124 //if(info.item.iSubItem) 9125 } 9126 break; 9127 default: 9128 } 9129 return 0; 9130 } 9131 9132 override bool encapsulatedChildren() { 9133 return true; 9134 } 9135 9136 /++ 9137 Informs the control that content has changed. 9138 9139 History: 9140 Added November 10, 2021 (dub v10.4) 9141 +/ 9142 void update() { 9143 version(custom_widgets) 9144 redraw(); 9145 else { 9146 SendMessage(hwnd, LVM_REDRAWITEMS, 0, SendMessage(hwnd, LVM_GETITEMCOUNT, 0, 0)); 9147 UpdateWindow(hwnd); 9148 } 9149 9150 9151 } 9152 9153 /++ 9154 Called by the system to request the text content of an individual cell. You 9155 should pass the text into the provided `sink` delegate. This function will be 9156 called for each visible cell as-needed when drawing. 9157 +/ 9158 void delegate(int row, int column, scope void delegate(in char[]) sink) getData; 9159 9160 /++ 9161 Available per-cell style customization options. Use one of the constructors 9162 provided to set the values conveniently, or default construct it and set individual 9163 values yourself. Just remember to set the `flags` so your values are actually used. 9164 If the flag isn't set, the field is ignored and the system default is used instead. 9165 9166 This is returned by the [getCellStyle] delegate. 9167 9168 Examples: 9169 --- 9170 // assumes you have a variables called `my_data` which is an array of arrays of numbers 9171 auto table = new TableView(window); 9172 // snip: you would set up columns here 9173 9174 // this is how you provide data to the table view class 9175 table.getData = delegate(int row, int column, scope void delegate(in char[]) sink) { 9176 import std.conv; 9177 sink(to!string(my_data[row][column])); 9178 }; 9179 9180 // and this is how you customize the colors 9181 table.getCellStyle = delegate(int row, int column) { 9182 return (my_data[row][column] < 0) ? 9183 TableView.CellStyle(Color.red); // make negative numbers red 9184 : TableView.CellStyle.init; // leave the rest alone 9185 }; 9186 // snip: you would call table.setItemCount here then continue with the rest of your window setup work 9187 --- 9188 9189 History: 9190 Added November 27, 2021 (dub v10.4) 9191 +/ 9192 struct CellStyle { 9193 /// 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. 9194 this(Color textColor) { 9195 this.textColor = textColor; 9196 this.flags |= Flags.textColorSet; 9197 } 9198 /// Sets a custom text and background color. 9199 this(Color textColor, Color backgroundColor) { 9200 this.textColor = textColor; 9201 this.backgroundColor = backgroundColor; 9202 this.flags |= Flags.textColorSet | Flags.backgroundColorSet; 9203 } 9204 9205 Color textColor; 9206 Color backgroundColor; 9207 int flags; /// bitmask of [Flags] 9208 /// available options to combine into [flags] 9209 enum Flags { 9210 textColorSet = 1 << 0, 9211 backgroundColorSet = 1 << 1, 9212 } 9213 } 9214 /++ 9215 Companion delegate to [getData] that allows you to custom style each 9216 cell of the table. 9217 9218 Returns: 9219 A [CellStyle] structure that describes the desired style for the 9220 given cell. `return CellStyle.init` if you want the default style. 9221 9222 History: 9223 Added November 27, 2021 (dub v10.4) 9224 +/ 9225 CellStyle delegate(int row, int column) getCellStyle; 9226 9227 // i want to be able to do things like draw little colored things to show red for negative numbers 9228 // or background color indicators or even in-cell charts 9229 // void delegate(int row, int column, WidgetPainter painter, int width, int height, in char[] text) drawCell; 9230 9231 /++ 9232 When the user clicks on a header, this event is emitted. It has a meber to identify which header (by index) was clicked. 9233 +/ 9234 mixin Emits!HeaderClickedEvent; 9235 } 9236 9237 /++ 9238 This is emitted by the [TableView] when a user clicks on a column header. 9239 9240 Its member `columnIndex` has the zero-based index of the column that was clicked. 9241 9242 The default behavior of this event is to do nothing, so `preventDefault` has no effect. 9243 9244 History: 9245 Added November 27, 2021 (dub v10.4) 9246 +/ 9247 class HeaderClickedEvent : Event { 9248 enum EventString = "HeaderClicked"; 9249 this(Widget target, int columnIndex) { 9250 this.columnIndex = columnIndex; 9251 super(EventString, target); 9252 } 9253 9254 /// The index of the column 9255 int columnIndex; 9256 9257 /// 9258 override @property int intValue() { 9259 return columnIndex; 9260 } 9261 } 9262 9263 version(custom_widgets) 9264 private class TableViewWidgetInner : Widget { 9265 9266 // wrap this thing in a ScrollMessageWidget 9267 9268 TableView tvw; 9269 ScrollMessageWidget smw; 9270 HeaderWidget header; 9271 9272 this(TableView tvw, ScrollMessageWidget smw) { 9273 this.tvw = tvw; 9274 this.smw = smw; 9275 super(smw); 9276 9277 this.tabStop = true; 9278 9279 header = new HeaderWidget(this, smw.getHeader()); 9280 9281 smw.addEventListener("scroll", () { 9282 this.redraw(); 9283 header.redraw(); 9284 }); 9285 9286 9287 // I need headers outside the scroll area but rendered on the same line as the up arrow 9288 // FIXME: add a fixed header to the SMW 9289 } 9290 9291 enum padding = 3; 9292 9293 void updateScrolls() { 9294 int w; 9295 foreach(idx, column; tvw.columns) { 9296 if(column.width == 0) continue; 9297 w += tvw.getActualSetSize(idx, false);// + padding; 9298 } 9299 smw.setTotalArea(w, tvw.itemCount); 9300 columnsWidth = w; 9301 } 9302 9303 private int columnsWidth; 9304 9305 private int lh() { return scaleWithDpi(16); } // FIXME lineHeight 9306 9307 override void registerMovement() { 9308 super.registerMovement(); 9309 // FIXME: actual column width. it might need to be done per-pixel instead of per-colum 9310 smw.setViewableArea(this.width, this.height / lh); 9311 } 9312 9313 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 9314 int x; 9315 int y; 9316 9317 int row = smw.position.y; 9318 9319 foreach(lol; 0 .. this.height / lh) { 9320 if(row >= tvw.itemCount) 9321 break; 9322 x = 0; 9323 foreach(columnNumber, column; tvw.columns) { 9324 auto x2 = x + column.calculatedWidth; 9325 auto smwx = smw.position.x; 9326 9327 if(x2 > smwx /* if right side of it is visible at all */ || (x >= smwx && x < smwx + this.width) /* left side is visible at all*/) { 9328 auto startX = x; 9329 auto endX = x + column.calculatedWidth; 9330 switch (column.alignment & (TextAlignment.Left | TextAlignment.Center | TextAlignment.Right)) { 9331 case TextAlignment.Left: startX += padding; break; 9332 case TextAlignment.Center: startX += padding; endX -= padding; break; 9333 case TextAlignment.Right: endX -= padding; break; 9334 default: /* broken */ break; 9335 } 9336 if(column.width != 0) // no point drawing an invisible column 9337 tvw.getData(row, cast(int) columnNumber, (in char[] info) { 9338 // auto clip = painter.setClipRectangle( 9339 9340 void dotext(WidgetPainter painter) { 9341 painter.drawText(Point(startX - smw.position.x, y), info, Point(endX - smw.position.x, y + lh), column.alignment); 9342 } 9343 9344 if(tvw.getCellStyle !is null) { 9345 auto style = tvw.getCellStyle(row, cast(int) columnNumber); 9346 9347 if(style.flags & TableView.CellStyle.Flags.backgroundColorSet) { 9348 auto tempPainter = painter; 9349 tempPainter.fillColor = style.backgroundColor; 9350 tempPainter.outlineColor = style.backgroundColor; 9351 9352 tempPainter.drawRectangle(Point(startX - smw.position.x, y), 9353 Point(endX - smw.position.x, y + lh)); 9354 } 9355 auto tempPainter = painter; 9356 if(style.flags & TableView.CellStyle.Flags.textColorSet) 9357 tempPainter.outlineColor = style.textColor; 9358 9359 dotext(tempPainter); 9360 } else { 9361 dotext(painter); 9362 } 9363 }); 9364 } 9365 9366 x += column.calculatedWidth; 9367 } 9368 row++; 9369 y += lh; 9370 } 9371 return bounds; 9372 } 9373 9374 static class Style : Widget.Style { 9375 override WidgetBackground background() { 9376 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 9377 } 9378 } 9379 mixin OverrideStyle!Style; 9380 9381 private static class HeaderWidget : Widget { 9382 this(TableViewWidgetInner tvw, Widget parent) { 9383 super(parent); 9384 this.tvw = tvw; 9385 9386 this.remainder = new Button("", this); 9387 9388 this.addEventListener((scope ClickEvent ev) { 9389 int header = -1; 9390 foreach(idx, child; this.children[1 .. $]) { 9391 if(child is ev.target) { 9392 header = cast(int) idx; 9393 break; 9394 } 9395 } 9396 9397 if(header != -1) { 9398 auto hce = new HeaderClickedEvent(tvw.tvw, header); 9399 hce.dispatch(); 9400 } 9401 9402 }); 9403 } 9404 9405 void updateHeaders() { 9406 foreach(child; children[1 .. $]) 9407 child.removeWidget(); 9408 9409 foreach(column; tvw.tvw.columns) { 9410 // the cast is ok because I dup it above, just the type is never changed. 9411 // all this is private so it should never get messed up. 9412 new Button(ImageLabel(cast(string) column.name, column.alignment), this); 9413 } 9414 } 9415 9416 Button remainder; 9417 TableViewWidgetInner tvw; 9418 9419 override void recomputeChildLayout() { 9420 registerMovement(); 9421 int pos; 9422 foreach(idx, child; children[1 .. $]) { 9423 if(idx >= tvw.tvw.columns.length) 9424 continue; 9425 child.x = pos; 9426 child.y = 0; 9427 child.width = tvw.tvw.columns[idx].calculatedWidth; 9428 child.height = scaleWithDpi(16);// this.height; 9429 pos += child.width; 9430 9431 child.recomputeChildLayout(); 9432 } 9433 9434 if(remainder is null) 9435 return; 9436 9437 remainder.x = pos; 9438 remainder.y = 0; 9439 if(pos < this.width) 9440 remainder.width = this.width - pos;// + 4; 9441 else 9442 remainder.width = 0; 9443 remainder.height = scaleWithDpi(16); 9444 9445 remainder.recomputeChildLayout(); 9446 } 9447 9448 // for the scrollable children mixin 9449 Point scrollOrigin() { 9450 return Point(tvw.smw.position.x, 0); 9451 } 9452 void paintFrameAndBackground(WidgetPainter painter) { } 9453 9454 mixin ScrollableChildren; 9455 } 9456 } 9457 9458 /+ 9459 9460 // given struct / array / number / string / etc, make it viewable and editable 9461 class DataViewerWidget : Widget { 9462 9463 } 9464 +/ 9465 9466 /++ 9467 A line edit box with an associated label. 9468 9469 History: 9470 On May 17, 2021, the default internal layout was changed from horizontal to vertical. 9471 9472 ``` 9473 Old: ________ 9474 9475 New: 9476 ____________ 9477 ``` 9478 9479 To restore the old behavior, use `new LabeledLineEdit("label", TextAlignment.Right, parent);` 9480 9481 You can also use `new LabeledLineEdit("label", TextAlignment.Left, parent);` if you want a 9482 horizontal label but left aligned. You may also consider a [GridLayout]. 9483 +/ 9484 alias LabeledLineEdit = Labeled!LineEdit; 9485 9486 /++ 9487 History: 9488 Added May 19, 2021 9489 +/ 9490 class Labeled(T) : Widget { 9491 /// 9492 this(string label, Widget parent) { 9493 super(parent); 9494 initialize!VerticalLayout(label, TextAlignment.Left, parent); 9495 } 9496 9497 /++ 9498 History: 9499 The alignment parameter was added May 17, 2021 9500 +/ 9501 this(string label, TextAlignment alignment, Widget parent) { 9502 super(parent); 9503 initialize!HorizontalLayout(label, alignment, parent); 9504 } 9505 9506 private void initialize(L)(string label, TextAlignment alignment, Widget parent) { 9507 tabStop = false; 9508 horizontal = is(L == HorizontalLayout); 9509 auto hl = new L(this); 9510 this.label = new TextLabel(label, alignment, hl); 9511 this.lineEdit = new T(hl); 9512 9513 this.label.labelFor = this.lineEdit; 9514 } 9515 9516 private bool horizontal; 9517 9518 TextLabel label; /// 9519 T lineEdit; /// 9520 9521 override int flexBasisWidth() { return 250; } 9522 9523 override int minHeight() { 9524 return this.children[0].minHeight; 9525 } 9526 override int maxHeight() { return minHeight(); } 9527 override int marginTop() { return 4; } 9528 override int marginBottom() { return 4; } 9529 9530 // FIXME: i should prolly call it value as well as content tbh 9531 9532 /// 9533 @property string content() { 9534 return lineEdit.content; 9535 } 9536 /// 9537 @property void content(string c) { 9538 return lineEdit.content(c); 9539 } 9540 9541 /// 9542 void selectAll() { 9543 lineEdit.selectAll(); 9544 } 9545 9546 override void focus() { 9547 lineEdit.focus(); 9548 } 9549 } 9550 9551 /++ 9552 A labeled password edit. 9553 9554 History: 9555 Added as a class on January 25, 2021, changed into an alias of the new [Labeled] template on May 19, 2021 9556 9557 The default parameters for the constructors were also removed on May 19, 2021 9558 +/ 9559 alias LabeledPasswordEdit = Labeled!PasswordEdit; 9560 9561 private string toMenuLabel(string s) { 9562 string n; 9563 n.reserve(s.length); 9564 foreach(c; s) 9565 if(c == '_') 9566 n ~= ' '; 9567 else 9568 n ~= c; 9569 return n; 9570 } 9571 9572 private void autoExceptionHandler(Exception e) { 9573 messageBox(e.msg); 9574 } 9575 9576 private void delegate() makeAutomaticHandler(alias fn, T)(T t) { 9577 static if(is(T : void delegate())) { 9578 return () { 9579 try 9580 t(); 9581 catch(Exception e) 9582 autoExceptionHandler(e); 9583 }; 9584 } else static if(is(typeof(fn) Params == __parameters)) { 9585 static if(Params.length == 1 && is(Params[0] == FileName!(member, filters, type), alias member, string[] filters, FileDialogType type)) { 9586 return () { 9587 void onOK(string s) { 9588 member = s; 9589 try 9590 t(Params[0](s)); 9591 catch(Exception e) 9592 autoExceptionHandler(e); 9593 } 9594 9595 if( 9596 (type == FileDialogType.Automatic && (__traits(identifier, fn).startsWith("Save") || __traits(identifier, fn).startsWith("Export"))) 9597 || type == FileDialogType.Save) 9598 { 9599 getSaveFileName(&onOK, member, filters, null); 9600 } else 9601 getOpenFileName(&onOK, member, filters, null); 9602 }; 9603 } else { 9604 struct S { 9605 static if(!__traits(compiles, mixin(`{ static foreach(i; 1..4) {} }`))) { 9606 pragma(msg, "warning: automatic handler of params not yet implemented on your compiler"); 9607 } else mixin(q{ 9608 static foreach(idx, ignore; Params) { 9609 mixin("Params[idx] " ~ __traits(identifier, Params[idx .. idx + 1]) ~ ";"); 9610 } 9611 }); 9612 } 9613 return () { 9614 dialog((S s) { 9615 try { 9616 static if(is(typeof(t) Ret == return)) { 9617 static if(is(Ret == void)) { 9618 t(s.tupleof); 9619 } else { 9620 auto ret = t(s.tupleof); 9621 import std.conv; 9622 messageBox(to!string(ret), "Returned Value"); 9623 } 9624 } 9625 } catch(Exception e) 9626 autoExceptionHandler(e); 9627 }, null, __traits(identifier, fn)); 9628 }; 9629 } 9630 } 9631 } 9632 9633 private template hasAnyRelevantAnnotations(a...) { 9634 bool helper() { 9635 bool any; 9636 foreach(attr; a) { 9637 static if(is(typeof(attr) == .menu)) 9638 any = true; 9639 else static if(is(typeof(attr) == .toolbar)) 9640 any = true; 9641 else static if(is(attr == .separator)) 9642 any = true; 9643 else static if(is(typeof(attr) == .accelerator)) 9644 any = true; 9645 else static if(is(typeof(attr) == .hotkey)) 9646 any = true; 9647 else static if(is(typeof(attr) == .icon)) 9648 any = true; 9649 else static if(is(typeof(attr) == .label)) 9650 any = true; 9651 else static if(is(typeof(attr) == .tip)) 9652 any = true; 9653 } 9654 return any; 9655 } 9656 9657 enum bool hasAnyRelevantAnnotations = helper(); 9658 } 9659 9660 /++ 9661 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. 9662 +/ 9663 class MainWindow : Window { 9664 /// 9665 this(string title = null, int initialWidth = 500, int initialHeight = 500) { 9666 super(initialWidth, initialHeight, title); 9667 9668 _clientArea = new ClientAreaWidget(); 9669 _clientArea.x = 0; 9670 _clientArea.y = 0; 9671 _clientArea.width = this.width; 9672 _clientArea.height = this.height; 9673 _clientArea.tabStop = false; 9674 9675 super.addChild(_clientArea); 9676 9677 statusBar = new StatusBar(this); 9678 } 9679 9680 /++ 9681 Adds a menu and toolbar from annotated functions. 9682 9683 --- 9684 struct Commands { 9685 @menu("File") { 9686 void New() {} 9687 void Open() {} 9688 void Save() {} 9689 @separator 9690 void Exit() @accelerator("Alt+F4") @hotkey('x') { 9691 window.close(); 9692 } 9693 } 9694 9695 @menu("Edit") { 9696 void Undo() { 9697 undo(); 9698 } 9699 @separator 9700 void Cut() {} 9701 void Copy() {} 9702 void Paste() {} 9703 } 9704 9705 @menu("Help") { 9706 void About() {} 9707 } 9708 } 9709 9710 Commands commands; 9711 9712 window.setMenuAndToolbarFromAnnotatedCode(commands); 9713 --- 9714 9715 Note that you can call this function multiple times and it will add the items in order to the given items. 9716 9717 +/ 9718 void setMenuAndToolbarFromAnnotatedCode(T)(ref T t) if(!is(T == class) && !is(T == interface)) { 9719 setMenuAndToolbarFromAnnotatedCode_internal(t); 9720 } 9721 void setMenuAndToolbarFromAnnotatedCode(T)(T t) if(is(T == class) || is(T == interface)) { 9722 setMenuAndToolbarFromAnnotatedCode_internal(t); 9723 } 9724 void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) { 9725 Action[] toolbarActions; 9726 auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar; 9727 Menu[string] mcs; 9728 9729 foreach(menu; menuBar.subMenus) { 9730 mcs[menu.label] = menu; 9731 } 9732 9733 foreach(memberName; __traits(derivedMembers, T)) { 9734 static if(memberName != "this") 9735 static if(hasAnyRelevantAnnotations!(__traits(getAttributes, __traits(getMember, T, memberName)))) { 9736 .menu menu; 9737 .toolbar toolbar; 9738 bool separator; 9739 .accelerator accelerator; 9740 .hotkey hotkey; 9741 .icon icon; 9742 string label; 9743 string tip; 9744 foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) { 9745 static if(is(typeof(attr) == .menu)) 9746 menu = attr; 9747 else static if(is(typeof(attr) == .toolbar)) 9748 toolbar = attr; 9749 else static if(is(attr == .separator)) 9750 separator = true; 9751 else static if(is(typeof(attr) == .accelerator)) 9752 accelerator = attr; 9753 else static if(is(typeof(attr) == .hotkey)) 9754 hotkey = attr; 9755 else static if(is(typeof(attr) == .icon)) 9756 icon = attr; 9757 else static if(is(typeof(attr) == .label)) 9758 label = attr.label; 9759 else static if(is(typeof(attr) == .tip)) 9760 tip = attr.tip; 9761 } 9762 9763 if(menu !is .menu.init || toolbar !is .toolbar.init) { 9764 ushort correctIcon = icon.id; // FIXME 9765 if(label.length == 0) 9766 label = memberName.toMenuLabel; 9767 9768 auto handler = makeAutomaticHandler!(__traits(getMember, T, memberName))(&__traits(getMember, t, memberName)); 9769 9770 auto action = new Action(label, correctIcon, handler); 9771 9772 if(accelerator.keyString.length) { 9773 auto ke = KeyEvent.parse(accelerator.keyString); 9774 action.accelerator = ke; 9775 accelerators[ke.toStr] = handler; 9776 } 9777 9778 if(toolbar !is .toolbar.init) 9779 toolbarActions ~= action; 9780 if(menu !is .menu.init) { 9781 Menu mc; 9782 if(menu.name in mcs) { 9783 mc = mcs[menu.name]; 9784 } else { 9785 mc = new Menu(menu.name, this); 9786 menuBar.addItem(mc); 9787 mcs[menu.name] = mc; 9788 } 9789 9790 if(separator) 9791 mc.addSeparator(); 9792 mc.addItem(new MenuItem(action)); 9793 } 9794 } 9795 } 9796 } 9797 9798 this.menuBar = menuBar; 9799 9800 if(toolbarActions.length) { 9801 auto tb = new ToolBar(toolbarActions, this); 9802 } 9803 } 9804 9805 void delegate()[string] accelerators; 9806 9807 override void defaultEventHandler_keydown(KeyDownEvent event) { 9808 auto str = event.originalKeyEvent.toStr; 9809 if(auto acl = str in accelerators) 9810 (*acl)(); 9811 super.defaultEventHandler_keydown(event); 9812 } 9813 9814 override void defaultEventHandler_mouseover(MouseOverEvent event) { 9815 super.defaultEventHandler_mouseover(event); 9816 if(this.statusBar !is null && event.target.statusTip.length) 9817 this.statusBar.parts[0].content = event.target.statusTip; 9818 else if(this.statusBar !is null && this.statusTip.length) 9819 this.statusBar.parts[0].content = this.statusTip; // ~ " " ~ event.target.toString(); 9820 } 9821 9822 override void addChild(Widget c, int position = int.max) { 9823 if(auto tb = cast(ToolBar) c) 9824 version(win32_widgets) 9825 super.addChild(c, 0); 9826 else version(custom_widgets) 9827 super.addChild(c, menuBar ? 1 : 0); 9828 else static assert(0); 9829 else 9830 clientArea.addChild(c, position); 9831 } 9832 9833 ToolBar _toolBar; 9834 /// 9835 ToolBar toolBar() { return _toolBar; } 9836 /// 9837 ToolBar toolBar(ToolBar t) { 9838 _toolBar = t; 9839 foreach(child; this.children) 9840 if(child is t) 9841 return t; 9842 version(win32_widgets) 9843 super.addChild(t, 0); 9844 else version(custom_widgets) 9845 super.addChild(t, menuBar ? 1 : 0); 9846 else static assert(0); 9847 return t; 9848 } 9849 9850 MenuBar _menu; 9851 /// 9852 MenuBar menuBar() { return _menu; } 9853 /// 9854 MenuBar menuBar(MenuBar m) { 9855 if(m is _menu) { 9856 version(custom_widgets) 9857 recomputeChildLayout(); 9858 return m; 9859 } 9860 9861 if(_menu !is null) { 9862 // make sure it is sanely removed 9863 // FIXME 9864 } 9865 9866 _menu = m; 9867 9868 version(win32_widgets) { 9869 SetMenu(parentWindow.win.impl.hwnd, m.handle); 9870 } else version(custom_widgets) { 9871 super.addChild(m, 0); 9872 9873 // clientArea.y = menu.height; 9874 // clientArea.height = this.height - menu.height; 9875 9876 recomputeChildLayout(); 9877 } else static assert(false); 9878 9879 return _menu; 9880 } 9881 private Widget _clientArea; 9882 /// 9883 @property Widget clientArea() { return _clientArea; } 9884 protected @property void clientArea(Widget wid) { 9885 _clientArea = wid; 9886 } 9887 9888 private StatusBar _statusBar; 9889 /++ 9890 Returns the window's [StatusBar]. Be warned it may be `null`. 9891 +/ 9892 @property StatusBar statusBar() { return _statusBar; } 9893 /// ditto 9894 @property void statusBar(StatusBar bar) { 9895 if(_statusBar !is null) 9896 _statusBar.removeWidget(); 9897 _statusBar = bar; 9898 if(bar !is null) 9899 super.addChild(_statusBar); 9900 } 9901 } 9902 9903 /+ 9904 This is really an implementation detail of [MainWindow] 9905 +/ 9906 private class ClientAreaWidget : Widget { 9907 this() { 9908 this.tabStop = false; 9909 super(null); 9910 //sa = new ScrollableWidget(this); 9911 } 9912 /* 9913 ScrollableWidget sa; 9914 override void addChild(Widget w, int position) { 9915 if(sa is null) 9916 super.addChild(w, position); 9917 else { 9918 sa.addChild(w, position); 9919 sa.setContentSize(this.minWidth + 1, this.minHeight); 9920 writeln(sa.contentWidth, "x", sa.contentHeight); 9921 } 9922 } 9923 */ 9924 } 9925 9926 /** 9927 Toolbars are lists of buttons (typically icons) that appear under the menu. 9928 Each button ought to correspond to a menu item, represented by [Action] objects. 9929 */ 9930 class ToolBar : Widget { 9931 version(win32_widgets) { 9932 private int idealHeight; 9933 override int minHeight() { return idealHeight; } 9934 override int maxHeight() { return idealHeight; } 9935 } else version(custom_widgets) { 9936 override int minHeight() { return toolbarIconSize; }// defaultLineHeight * 3/2; } 9937 override int maxHeight() { return toolbarIconSize; } //defaultLineHeight * 3/2; } 9938 } else static assert(false); 9939 override int heightStretchiness() { return 0; } 9940 9941 version(win32_widgets) { 9942 HIMAGELIST imageListSmall; 9943 HIMAGELIST imageListLarge; 9944 } 9945 9946 this(Widget parent) { 9947 this(null, parent); 9948 } 9949 9950 version(win32_widgets) 9951 void changeIconSize(bool useLarge) { 9952 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) (useLarge ? imageListLarge : imageListSmall)); 9953 9954 /+ 9955 SIZE size; 9956 import core.sys.windows.commctrl; 9957 SendMessageW(hwnd, TB_GETMAXSIZE, 0, cast(LPARAM) &size); 9958 idealHeight = size.cy + 4; // the plus 4 is a hack 9959 +/ 9960 9961 idealHeight = useLarge ? 34 : 26; 9962 9963 if(parent) { 9964 parent.recomputeChildLayout(); 9965 parent.redraw(); 9966 } 9967 9968 SendMessageW(hwnd, TB_SETBUTTONSIZE, 0, (idealHeight-4) << 16 | (idealHeight-4)); 9969 SendMessageW(hwnd, TB_AUTOSIZE, 0, 0); 9970 } 9971 9972 /// 9973 this(Action[] actions, Widget parent) { 9974 super(parent); 9975 9976 tabStop = false; 9977 9978 version(win32_widgets) { 9979 // so i like how the flat thing looks on windows, but not on wine 9980 // and eh, with windows visual styles enabled it looks cool anyway soooo gonna 9981 // leave it commented 9982 createWin32Window(this, "ToolbarWindow32"w, "", TBSTYLE_LIST|/*TBSTYLE_FLAT|*/TBSTYLE_TOOLTIPS); 9983 9984 SendMessageW(hwnd, TB_SETEXTENDEDSTYLE, 0, 8/*TBSTYLE_EX_MIXEDBUTTONS*/); 9985 9986 imageListSmall = ImageList_Create( 9987 // width, height 9988 16, 16, 9989 ILC_COLOR16 | ILC_MASK, 9990 16 /*numberOfButtons*/, 0); 9991 9992 imageListLarge = ImageList_Create( 9993 // width, height 9994 24, 24, 9995 ILC_COLOR16 | ILC_MASK, 9996 16 /*numberOfButtons*/, 0); 9997 9998 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListSmall); 9999 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_SMALL_COLOR, cast(LPARAM) HINST_COMMCTRL); 10000 10001 SendMessageW(hwnd, TB_SETIMAGELIST, cast(WPARAM) 0, cast(LPARAM) imageListLarge); 10002 SendMessageW(hwnd, TB_LOADIMAGES, cast(WPARAM) IDB_STD_LARGE_COLOR, cast(LPARAM) HINST_COMMCTRL); 10003 10004 SendMessageW(hwnd, TB_SETMAXTEXTROWS, 0, 0); 10005 10006 TBBUTTON[] buttons; 10007 10008 // FIXME: I_IMAGENONE is if here is no icon 10009 foreach(action; actions) 10010 buttons ~= TBBUTTON( 10011 MAKELONG(cast(ushort)(action.iconId ? (action.iconId - 1) : -2 /* I_IMAGENONE */), 0), 10012 action.id, 10013 TBSTATE_ENABLED, // state 10014 0, // style 10015 0, // reserved array, just zero it out 10016 0, // dwData 10017 cast(size_t) toWstringzInternal(action.label) // INT_PTR 10018 ); 10019 10020 SendMessageW(hwnd, TB_BUTTONSTRUCTSIZE, cast(WPARAM)TBBUTTON.sizeof, 0); 10021 SendMessageW(hwnd, TB_ADDBUTTONSW, cast(WPARAM) buttons.length, cast(LPARAM)buttons.ptr); 10022 10023 /* 10024 RECT rect; 10025 GetWindowRect(hwnd, &rect); 10026 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 10027 */ 10028 10029 dpiChanged(); // to load the things calling changeIconSize the first time 10030 10031 assert(idealHeight); 10032 } else version(custom_widgets) { 10033 foreach(action; actions) 10034 new ToolButton(action, this); 10035 } else static assert(false); 10036 } 10037 10038 override void recomputeChildLayout() { 10039 .recomputeChildLayout!"width"(this); 10040 } 10041 10042 10043 version(win32_widgets) 10044 override protected void dpiChanged() { 10045 auto sz = scaleWithDpi(16); 10046 if(sz >= 20) 10047 changeIconSize(true); 10048 else 10049 changeIconSize(false); 10050 } 10051 } 10052 10053 enum toolbarIconSize = 24; 10054 10055 /// 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. 10056 class ToolButton : Button { 10057 /// 10058 this(string label, Widget parent) { 10059 super(label, parent); 10060 tabStop = false; 10061 } 10062 /// 10063 this(Action action, Widget parent) { 10064 super(action.label, parent); 10065 tabStop = false; 10066 this.action = action; 10067 } 10068 10069 version(custom_widgets) 10070 override void defaultEventHandler_click(ClickEvent event) { 10071 foreach(handler; action.triggered) 10072 handler(); 10073 } 10074 10075 Action action; 10076 10077 override int maxWidth() { return toolbarIconSize; } 10078 override int minWidth() { return toolbarIconSize; } 10079 override int maxHeight() { return toolbarIconSize; } 10080 override int minHeight() { return toolbarIconSize; } 10081 10082 version(custom_widgets) 10083 override void paint(WidgetPainter painter) { 10084 painter.drawThemed(delegate Rectangle (const Rectangle bounds) { 10085 painter.outlineColor = Color.black; 10086 10087 // I want to get from 16 to 24. that's * 3 / 2 10088 static assert(toolbarIconSize >= 16); 10089 enum multiplier = toolbarIconSize / 8; 10090 enum divisor = 2 + ((toolbarIconSize % 8) ? 1 : 0); 10091 switch(action.iconId) { 10092 case GenericIcons.New: 10093 painter.fillColor = Color.white; 10094 painter.drawPolygon( 10095 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor, Point(12, 13) * multiplier / divisor, Point(12, 6) * multiplier / divisor, 10096 Point(8, 2) * multiplier / divisor, Point(8, 6) * multiplier / divisor, Point(12, 6) * multiplier / divisor, Point(8, 2) * multiplier / divisor, 10097 Point(3, 2) * multiplier / divisor, Point(3, 13) * multiplier / divisor 10098 ); 10099 break; 10100 case GenericIcons.Save: 10101 painter.fillColor = Color.white; 10102 painter.outlineColor = Color.black; 10103 painter.drawRectangle(Point(2, 2) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10104 10105 // the label 10106 painter.drawRectangle(Point(4, 8) * multiplier / divisor, Point(11, 13) * multiplier / divisor); 10107 10108 // the slider 10109 painter.fillColor = Color.black; 10110 painter.outlineColor = Color.black; 10111 painter.drawRectangle(Point(4, 3) * multiplier / divisor, Point(10, 6) * multiplier / divisor); 10112 10113 painter.fillColor = Color.white; 10114 painter.outlineColor = Color.white; 10115 // the disc window 10116 painter.drawRectangle(Point(5, 3) * multiplier / divisor, Point(6, 5) * multiplier / divisor); 10117 break; 10118 case GenericIcons.Open: 10119 painter.fillColor = Color.white; 10120 painter.drawPolygon( 10121 Point(4, 4) * multiplier / divisor, Point(4, 12) * multiplier / divisor, Point(13, 12) * multiplier / divisor, Point(13, 3) * multiplier / divisor, 10122 Point(9, 3) * multiplier / divisor, Point(9, 4) * multiplier / divisor, Point(4, 4) * multiplier / divisor); 10123 painter.drawPolygon( 10124 Point(2, 6) * multiplier / divisor, Point(11, 6) * multiplier / divisor, 10125 Point(12, 12) * multiplier / divisor, Point(4, 12) * multiplier / divisor, 10126 Point(2, 6) * multiplier / divisor); 10127 //painter.drawLine(Point(9, 6) * multiplier / divisor, Point(13, 7) * multiplier / divisor); 10128 break; 10129 case GenericIcons.Copy: 10130 painter.fillColor = Color.white; 10131 painter.drawRectangle(Point(3, 2) * multiplier / divisor, Point(9, 10) * multiplier / divisor); 10132 painter.drawRectangle(Point(6, 5) * multiplier / divisor, Point(12, 13) * multiplier / divisor); 10133 break; 10134 case GenericIcons.Cut: 10135 painter.fillColor = Color.transparent; 10136 painter.outlineColor = getComputedStyle.foregroundColor(); 10137 painter.drawLine(Point(3, 2) * multiplier / divisor, Point(10, 9) * multiplier / divisor); 10138 painter.drawLine(Point(4, 9) * multiplier / divisor, Point(11, 2) * multiplier / divisor); 10139 painter.drawRectangle(Point(3, 9) * multiplier / divisor, Point(5, 13) * multiplier / divisor); 10140 painter.drawRectangle(Point(9, 9) * multiplier / divisor, Point(11, 12) * multiplier / divisor); 10141 break; 10142 case GenericIcons.Paste: 10143 painter.fillColor = Color.white; 10144 painter.drawRectangle(Point(2, 3) * multiplier / divisor, Point(11, 11) * multiplier / divisor); 10145 painter.drawRectangle(Point(6, 8) * multiplier / divisor, Point(13, 13) * multiplier / divisor); 10146 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(4, 5) * multiplier / divisor); 10147 painter.drawLine(Point(6, 2) * multiplier / divisor, Point(9, 5) * multiplier / divisor); 10148 painter.fillColor = Color.black; 10149 painter.drawRectangle(Point(4, 5) * multiplier / divisor, Point(9, 6) * multiplier / divisor); 10150 break; 10151 case GenericIcons.Help: 10152 painter.outlineColor = getComputedStyle.foregroundColor(); 10153 painter.drawText(Point(0, 0), "?", Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10154 break; 10155 case GenericIcons.Undo: 10156 painter.fillColor = Color.transparent; 10157 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10158 painter.outlineColor = Color.black; 10159 painter.fillColor = Color.black; 10160 painter.drawPolygon( 10161 Point(4, 4) * multiplier / divisor, 10162 Point(8, 2) * multiplier / divisor, 10163 Point(8, 6) * multiplier / divisor, 10164 Point(4, 4) * multiplier / divisor, 10165 ); 10166 break; 10167 case GenericIcons.Redo: 10168 painter.fillColor = Color.transparent; 10169 painter.drawArc(Point(3, 4) * multiplier / divisor, 9 * multiplier / divisor, 9 * multiplier / divisor, 0, 360 * 64); 10170 painter.outlineColor = Color.black; 10171 painter.fillColor = Color.black; 10172 painter.drawPolygon( 10173 Point(10, 4) * multiplier / divisor, 10174 Point(6, 2) * multiplier / divisor, 10175 Point(6, 6) * multiplier / divisor, 10176 Point(10, 4) * multiplier / divisor, 10177 ); 10178 break; 10179 default: 10180 painter.drawText(Point(0, 0), action.label, Point(width, height), TextAlignment.Center | TextAlignment.VerticalCenter); 10181 } 10182 return bounds; 10183 }); 10184 } 10185 10186 } 10187 10188 10189 /// 10190 class MenuBar : Widget { 10191 MenuItem[] items; 10192 Menu[] subMenus; 10193 10194 version(win32_widgets) { 10195 HMENU handle; 10196 /// 10197 this(Widget parent = null) { 10198 super(parent); 10199 10200 handle = CreateMenu(); 10201 tabStop = false; 10202 } 10203 } else version(custom_widgets) { 10204 /// 10205 this(Widget parent = null) { 10206 tabStop = false; // these are selected some other way 10207 super(parent); 10208 } 10209 10210 mixin Padding!q{2}; 10211 } else static assert(false); 10212 10213 version(custom_widgets) 10214 override void paint(WidgetPainter painter) { 10215 draw3dFrame(this, painter, FrameStyle.risen, getComputedStyle().background.color); 10216 } 10217 10218 /// 10219 MenuItem addItem(MenuItem item) { 10220 this.addChild(item); 10221 items ~= item; 10222 version(win32_widgets) { 10223 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10224 } 10225 return item; 10226 } 10227 10228 10229 /// 10230 Menu addItem(Menu item) { 10231 10232 subMenus ~= item; 10233 10234 auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane 10235 10236 addChild(mbItem); 10237 items ~= mbItem; 10238 10239 version(win32_widgets) { 10240 AppendMenuW(handle, MF_STRING | MF_POPUP, cast(UINT) item.handle, toWstringzInternal(item.label)); 10241 } else version(custom_widgets) { 10242 mbItem.defaultEventHandlers["mousedown"] = (Widget e, Event ev) { 10243 item.popup(mbItem); 10244 }; 10245 } else static assert(false); 10246 10247 return item; 10248 } 10249 10250 override void recomputeChildLayout() { 10251 .recomputeChildLayout!"width"(this); 10252 } 10253 10254 override int maxHeight() { return defaultLineHeight + 4; } 10255 override int minHeight() { return defaultLineHeight + 4; } 10256 } 10257 10258 10259 /** 10260 Status bars appear at the bottom of a MainWindow. 10261 They are made out of Parts, with a width and content. 10262 10263 They can have multiple parts or be in simple mode. FIXME: implement simple mode. 10264 10265 10266 sb.parts[0].content = "Status bar text!"; 10267 */ 10268 class StatusBar : Widget { 10269 private Part[] partsArray; 10270 /// 10271 struct Parts { 10272 @disable this(); 10273 this(StatusBar owner) { this.owner = owner; } 10274 //@disable this(this); 10275 /// 10276 @property int length() { return cast(int) owner.partsArray.length; } 10277 private StatusBar owner; 10278 private this(StatusBar owner, Part[] parts) { 10279 this.owner.partsArray = parts; 10280 this.owner = owner; 10281 } 10282 /// 10283 Part opIndex(int p) { 10284 if(owner.partsArray.length == 0) 10285 this ~= new StatusBar.Part(300); 10286 return owner.partsArray[p]; 10287 } 10288 10289 /// 10290 Part opOpAssign(string op : "~" )(Part p) { 10291 assert(owner.partsArray.length < 255); 10292 p.owner = this.owner; 10293 p.idx = cast(int) owner.partsArray.length; 10294 owner.partsArray ~= p; 10295 version(win32_widgets) { 10296 int[256] pos; 10297 int cpos = 0; 10298 foreach(idx, part; owner.partsArray) { 10299 if(part.width) 10300 cpos += part.width; 10301 else 10302 cpos += 100; 10303 10304 if(idx + 1 == owner.partsArray.length) 10305 pos[idx] = -1; 10306 else 10307 pos[idx] = cpos; 10308 } 10309 SendMessageW(owner.hwnd, WM_USER + 4 /*SB_SETPARTS*/, owner.partsArray.length, cast(size_t) pos.ptr); 10310 } else version(custom_widgets) { 10311 owner.redraw(); 10312 } else static assert(false); 10313 10314 return p; 10315 } 10316 } 10317 10318 private Parts _parts; 10319 /// 10320 final @property Parts parts() { 10321 return _parts; 10322 } 10323 10324 /// 10325 static class Part { 10326 int width; 10327 StatusBar owner; 10328 10329 /// 10330 this(int w = 100) { width = w; } 10331 10332 private int idx; 10333 private string _content; 10334 /// 10335 @property string content() { return _content; } 10336 /// 10337 @property void content(string s) { 10338 version(win32_widgets) { 10339 _content = s; 10340 WCharzBuffer bfr = WCharzBuffer(s); 10341 SendMessageW(owner.hwnd, SB_SETTEXT, idx, cast(LPARAM) bfr.ptr); 10342 } else version(custom_widgets) { 10343 if(_content != s) { 10344 _content = s; 10345 owner.redraw(); 10346 } 10347 } else static assert(false); 10348 } 10349 } 10350 string simpleModeContent; 10351 bool inSimpleMode; 10352 10353 10354 /// 10355 this(Widget parent) { 10356 super(null); // FIXME 10357 _parts = Parts(this); 10358 tabStop = false; 10359 version(win32_widgets) { 10360 parentWindow = parent.parentWindow; 10361 createWin32Window(this, "msctls_statusbar32"w, "", 0); 10362 10363 RECT rect; 10364 GetWindowRect(hwnd, &rect); 10365 idealHeight = rect.bottom - rect.top; 10366 assert(idealHeight); 10367 } else version(custom_widgets) { 10368 } else static assert(false); 10369 } 10370 10371 version(win32_widgets) 10372 override protected void dpiChanged() { 10373 RECT rect; 10374 GetWindowRect(hwnd, &rect); 10375 idealHeight = rect.bottom - rect.top; 10376 assert(idealHeight); 10377 } 10378 10379 version(custom_widgets) 10380 override void paint(WidgetPainter painter) { 10381 auto cs = getComputedStyle(); 10382 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10383 int cpos = 0; 10384 int remainingLength = this.width; 10385 foreach(idx, part; this.partsArray) { 10386 auto partWidth = part.width ? part.width : ((idx + 1 == this.partsArray.length) ? remainingLength : 100); 10387 painter.setClipRectangle(Point(cpos, 0), partWidth, height); 10388 draw3dFrame(cpos, 0, partWidth, height, painter, FrameStyle.sunk, cs.background.color); 10389 painter.setClipRectangle(Point(cpos + 2, 2), partWidth - 4, height - 4); 10390 10391 painter.outlineColor = cs.foregroundColor(); 10392 painter.fillColor = cs.foregroundColor(); 10393 10394 painter.drawText(Point(cpos + 4, 0), part.content, Point(width, height), TextAlignment.VerticalCenter); 10395 cpos += partWidth; 10396 remainingLength -= partWidth; 10397 } 10398 } 10399 10400 10401 version(win32_widgets) { 10402 private int idealHeight; 10403 override int maxHeight() { return idealHeight; } 10404 override int minHeight() { return idealHeight; } 10405 } else version(custom_widgets) { 10406 override int maxHeight() { return defaultLineHeight + 4; } 10407 override int minHeight() { return defaultLineHeight + 4; } 10408 } else static assert(false); 10409 } 10410 10411 /// Displays an in-progress indicator without known values 10412 version(none) 10413 class IndefiniteProgressBar : Widget { 10414 version(win32_widgets) 10415 this(Widget parent) { 10416 super(parent); 10417 createWin32Window(this, "msctls_progress32"w, "", 8 /* PBS_MARQUEE */); 10418 tabStop = false; 10419 } 10420 override int minHeight() { return 10; } 10421 } 10422 10423 /// A progress bar with a known endpoint and completion amount 10424 class ProgressBar : Widget { 10425 /++ 10426 History: 10427 Added March 16, 2022 (dub v10.7) 10428 +/ 10429 this(int min, int max, Widget parent) { 10430 this(parent); 10431 setRange(cast(ushort) min, cast(ushort) max); // FIXME 10432 } 10433 this(Widget parent) { 10434 version(win32_widgets) { 10435 super(parent); 10436 createWin32Window(this, "msctls_progress32"w, "", 0); 10437 tabStop = false; 10438 } else version(custom_widgets) { 10439 super(parent); 10440 max = 100; 10441 step = 10; 10442 tabStop = false; 10443 } else static assert(0); 10444 } 10445 10446 version(custom_widgets) 10447 override void paint(WidgetPainter painter) { 10448 auto cs = getComputedStyle(); 10449 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10450 painter.fillColor = cs.progressBarColor; 10451 painter.drawRectangle(Point(0, 0), width * current / max, height); 10452 } 10453 10454 10455 version(custom_widgets) { 10456 int current; 10457 int max; 10458 int step; 10459 } 10460 10461 /// 10462 void advanceOneStep() { 10463 version(win32_widgets) 10464 SendMessageW(hwnd, PBM_STEPIT, 0, 0); 10465 else version(custom_widgets) 10466 addToPosition(step); 10467 else static assert(false); 10468 } 10469 10470 /// 10471 void setStepIncrement(int increment) { 10472 version(win32_widgets) 10473 SendMessageW(hwnd, PBM_SETSTEP, increment, 0); 10474 else version(custom_widgets) 10475 step = increment; 10476 else static assert(false); 10477 } 10478 10479 /// 10480 void addToPosition(int amount) { 10481 version(win32_widgets) 10482 SendMessageW(hwnd, PBM_DELTAPOS, amount, 0); 10483 else version(custom_widgets) 10484 setPosition(current + amount); 10485 else static assert(false); 10486 } 10487 10488 /// 10489 void setPosition(int pos) { 10490 version(win32_widgets) 10491 SendMessageW(hwnd, PBM_SETPOS, pos, 0); 10492 else version(custom_widgets) { 10493 current = pos; 10494 if(current > max) 10495 current = max; 10496 redraw(); 10497 } 10498 else static assert(false); 10499 } 10500 10501 /// 10502 void setRange(ushort min, ushort max) { 10503 version(win32_widgets) 10504 SendMessageW(hwnd, PBM_SETRANGE, 0, MAKELONG(min, max)); 10505 else version(custom_widgets) { 10506 this.max = max; 10507 } 10508 else static assert(false); 10509 } 10510 10511 override int minHeight() { return 10; } 10512 } 10513 10514 version(custom_widgets) 10515 private void extractWindowsStyleLabel(scope const char[] label, out string thisLabel, out dchar thisAccelerator) { 10516 thisLabel.reserve(label.length); 10517 bool justSawAmpersand; 10518 foreach(ch; label) { 10519 if(justSawAmpersand) { 10520 justSawAmpersand = false; 10521 if(ch == '&') { 10522 goto plain; 10523 } 10524 thisAccelerator = ch; 10525 } else { 10526 if(ch == '&') { 10527 justSawAmpersand = true; 10528 continue; 10529 } 10530 plain: 10531 thisLabel ~= ch; 10532 } 10533 } 10534 } 10535 10536 /++ 10537 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. 10538 10539 10540 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 10541 10542 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 10543 10544 History: 10545 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. 10546 +/ 10547 class Fieldset : Widget { 10548 // FIXME: on Windows,it doesn't draw the background on the label 10549 // on X, it doesn't fix the clipping rectangle for it 10550 version(win32_widgets) 10551 override int paddingTop() { return defaultLineHeight; } 10552 else version(custom_widgets) 10553 override int paddingTop() { return defaultLineHeight + 2; } 10554 else static assert(false); 10555 override int paddingBottom() { return 6; } 10556 override int paddingLeft() { return 6; } 10557 override int paddingRight() { return 6; } 10558 10559 override int marginLeft() { return 6; } 10560 override int marginRight() { return 6; } 10561 override int marginTop() { return 2; } 10562 override int marginBottom() { return 2; } 10563 10564 string legend; 10565 10566 version(custom_widgets) private dchar accelerator; 10567 10568 this(string legend, Widget parent) { 10569 version(win32_widgets) { 10570 super(parent); 10571 this.legend = legend; 10572 createWin32Window(this, "button"w, legend, BS_GROUPBOX); 10573 tabStop = false; 10574 } else version(custom_widgets) { 10575 super(parent); 10576 tabStop = false; 10577 10578 legend.extractWindowsStyleLabel(this.legend, this.accelerator); 10579 } else static assert(0); 10580 } 10581 10582 version(custom_widgets) 10583 override void paint(WidgetPainter painter) { 10584 painter.fillColor = Color.transparent; 10585 auto cs = getComputedStyle(); 10586 painter.pen = Pen(cs.foregroundColor, 1); 10587 painter.drawRectangle(Point(0, defaultLineHeight / 2), width, height - defaultLineHeight / 2); 10588 10589 auto tx = painter.textSize(legend); 10590 painter.outlineColor = Color.transparent; 10591 10592 static if(UsingSimpledisplayX11) { 10593 painter.fillColor = getComputedStyle().windowBackgroundColor; 10594 painter.drawRectangle(Point(8, 0), tx.width, tx.height); 10595 } else version(Windows) { 10596 auto b = SelectObject(painter.impl.hdc, GetSysColorBrush(COLOR_3DFACE)); 10597 painter.drawRectangle(Point(8, -tx.height/2), tx.width, tx.height); 10598 SelectObject(painter.impl.hdc, b); 10599 } else static assert(0); 10600 painter.outlineColor = cs.foregroundColor; 10601 painter.drawText(Point(8, 0), legend); 10602 } 10603 10604 override int maxHeight() { 10605 auto m = paddingTop() + paddingBottom(); 10606 foreach(child; children) { 10607 auto mh = child.maxHeight(); 10608 if(mh == int.max) 10609 return int.max; 10610 m += mh; 10611 m += child.marginBottom(); 10612 m += child.marginTop(); 10613 } 10614 m += 6; 10615 if(m < minHeight) 10616 return minHeight; 10617 return m; 10618 } 10619 10620 override int minHeight() { 10621 auto m = paddingTop() + paddingBottom(); 10622 foreach(child; children) { 10623 m += child.minHeight(); 10624 m += child.marginBottom(); 10625 m += child.marginTop(); 10626 } 10627 return m + 6; 10628 } 10629 } 10630 10631 /++ 10632 $(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") 10633 $(IMG //arsdnet.net/minigui-screenshots/linux/Fieldset.png, Same thing, but in the default Linux theme.) 10634 +/ 10635 version(minigui_screenshots) 10636 @Screenshot("Fieldset") 10637 unittest { 10638 auto window = new Window(200, 100); 10639 auto set = new Fieldset("Baby will", window); 10640 auto option1 = new Radiobox("Eat", set); 10641 auto option2 = new Radiobox("Cry", set); 10642 auto option3 = new Radiobox("Sleep", set); 10643 window.loop(); 10644 } 10645 10646 /// Draws a line 10647 class HorizontalRule : Widget { 10648 mixin Margin!q{ 2 }; 10649 override int minHeight() { return 2; } 10650 override int maxHeight() { return 2; } 10651 10652 /// 10653 this(Widget parent) { 10654 super(parent); 10655 } 10656 10657 override void paint(WidgetPainter painter) { 10658 auto cs = getComputedStyle(); 10659 painter.outlineColor = cs.darkAccentColor; 10660 painter.drawLine(Point(0, 0), Point(width, 0)); 10661 painter.outlineColor = cs.lightAccentColor; 10662 painter.drawLine(Point(0, 1), Point(width, 1)); 10663 } 10664 } 10665 10666 version(minigui_screenshots) 10667 @Screenshot("HorizontalRule") 10668 /++ 10669 $(IMG //arsdnet.net/minigui-screenshots/linux/HorizontalRule.png, Same thing, but in the default Linux theme.) 10670 10671 +/ 10672 unittest { 10673 auto window = new Window(200, 100); 10674 auto above = new TextLabel("Above the line", TextAlignment.Left, window); 10675 new HorizontalRule(window); 10676 auto below = new TextLabel("Below the line", TextAlignment.Left, window); 10677 window.loop(); 10678 } 10679 10680 /// ditto 10681 class VerticalRule : Widget { 10682 mixin Margin!q{ 2 }; 10683 override int minWidth() { return 2; } 10684 override int maxWidth() { return 2; } 10685 10686 /// 10687 this(Widget parent) { 10688 super(parent); 10689 } 10690 10691 override void paint(WidgetPainter painter) { 10692 auto cs = getComputedStyle(); 10693 painter.outlineColor = cs.darkAccentColor; 10694 painter.drawLine(Point(0, 0), Point(0, height)); 10695 painter.outlineColor = cs.lightAccentColor; 10696 painter.drawLine(Point(1, 0), Point(1, height)); 10697 } 10698 } 10699 10700 10701 /// 10702 class Menu : Window { 10703 void remove() { 10704 foreach(i, child; parentWindow.children) 10705 if(child is this) { 10706 parentWindow._children = parentWindow._children[0 .. i] ~ parentWindow._children[i + 1 .. $]; 10707 break; 10708 } 10709 parentWindow.redraw(); 10710 10711 parentWindow.releaseMouseCapture(); 10712 } 10713 10714 /// 10715 void addSeparator() { 10716 version(win32_widgets) 10717 AppendMenu(handle, MF_SEPARATOR, 0, null); 10718 else version(custom_widgets) 10719 auto hr = new HorizontalRule(this); 10720 else static assert(0); 10721 } 10722 10723 override int paddingTop() { return 4; } 10724 override int paddingBottom() { return 4; } 10725 override int paddingLeft() { return 2; } 10726 override int paddingRight() { return 2; } 10727 10728 version(win32_widgets) {} 10729 else version(custom_widgets) { 10730 SimpleWindow dropDown; 10731 Widget menuParent; 10732 void popup(Widget parent, int offsetX = 0, int offsetY = int.min) { 10733 this.menuParent = parent; 10734 10735 int w = 150; 10736 int h = paddingTop + paddingBottom; 10737 if(this.children.length) { 10738 // hacking it to get the ideal height out of recomputeChildLayout 10739 this.width = w; 10740 this.height = h; 10741 this.recomputeChildLayout(); 10742 h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom; 10743 h += paddingBottom; 10744 10745 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 10746 } 10747 10748 if(offsetY == int.min) 10749 offsetY = parent.defaultLineHeight; 10750 10751 auto coord = parent.globalCoordinates(); 10752 dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h); 10753 this.x = 0; 10754 this.y = 0; 10755 this.width = dropDown.width; 10756 this.height = dropDown.height; 10757 this.drawableWindow = dropDown; 10758 this.recomputeChildLayout(); 10759 10760 static if(UsingSimpledisplayX11) 10761 XSync(XDisplayConnection.get, 0); 10762 10763 dropDown.visibilityChanged = (bool visible) { 10764 if(visible) { 10765 this.redraw(); 10766 dropDown.grabInput(); 10767 } else { 10768 dropDown.releaseInputGrab(); 10769 } 10770 }; 10771 10772 dropDown.show(); 10773 10774 clickListener = this.addEventListener((scope ClickEvent ev) { 10775 unpopup(); 10776 // need to unlock asap just in case other user handlers block... 10777 static if(UsingSimpledisplayX11) 10778 flushGui(); 10779 }, true /* again for asap action */); 10780 } 10781 10782 EventListener clickListener; 10783 } 10784 else static assert(false); 10785 10786 version(custom_widgets) 10787 void unpopup() { 10788 mouseLastOver = mouseLastDownOn = null; 10789 dropDown.hide(); 10790 if(!menuParent.parentWindow.win.closed) { 10791 if(auto maw = cast(MouseActivatedWidget) menuParent) { 10792 maw.setDynamicState(DynamicState.depressed, false); 10793 maw.setDynamicState(DynamicState.hover, false); 10794 maw.redraw(); 10795 } 10796 // menuParent.parentWindow.win.focus(); 10797 } 10798 clickListener.disconnect(); 10799 } 10800 10801 MenuItem[] items; 10802 10803 /// 10804 MenuItem addItem(MenuItem item) { 10805 addChild(item); 10806 items ~= item; 10807 version(win32_widgets) { 10808 AppendMenuW(handle, MF_STRING, item.action is null ? 9000 : item.action.id, toWstringzInternal(item.label)); 10809 } 10810 return item; 10811 } 10812 10813 string label; 10814 10815 version(win32_widgets) { 10816 HMENU handle; 10817 /// 10818 this(string label, Widget parent) { 10819 // not actually passing the parent since it effs up the drawing 10820 super(cast(Widget) null);// parent); 10821 this.label = label; 10822 handle = CreatePopupMenu(); 10823 } 10824 } else version(custom_widgets) { 10825 /// 10826 this(string label, Widget parent) { 10827 10828 if(dropDown) { 10829 dropDown.close(); 10830 } 10831 dropDown = new SimpleWindow( 10832 150, 4, 10833 null, OpenGlOptions.no, Resizability.fixedSize, WindowTypes.dropdownMenu, WindowFlags.dontAutoShow, parent ? parent.parentWindow.win : null); 10834 10835 this.label = label; 10836 10837 super(dropDown); 10838 } 10839 } else static assert(false); 10840 10841 override int maxHeight() { return defaultLineHeight; } 10842 override int minHeight() { return defaultLineHeight; } 10843 10844 version(custom_widgets) 10845 override void paint(WidgetPainter painter) { 10846 this.draw3dFrame(painter, FrameStyle.risen, getComputedStyle.background.color); 10847 } 10848 } 10849 10850 /++ 10851 A MenuItem belongs to a [Menu] - use [Menu.addItem] to add one - and calls an [Action] when it is clicked. 10852 +/ 10853 class MenuItem : MouseActivatedWidget { 10854 Menu submenu; 10855 10856 Action action; 10857 string label; 10858 10859 override int paddingLeft() { return 4; } 10860 10861 override int maxHeight() { return defaultLineHeight + 4; } 10862 override int minHeight() { return defaultLineHeight + 4; } 10863 override int minWidth() { return defaultTextWidth(label) + 8 + scaleWithDpi(12); } 10864 override int maxWidth() { 10865 if(cast(MenuBar) parent) { 10866 return minWidth(); 10867 } 10868 return int.max; 10869 } 10870 /// This should ONLY be used if there is no associated action, for example, if the menu item is just a submenu. 10871 this(string lbl, Widget parent = null) { 10872 super(parent); 10873 //label = lbl; // FIXME 10874 foreach(char ch; lbl) // FIXME 10875 if(ch != '&') // FIXME 10876 label ~= ch; // FIXME 10877 tabStop = false; // these are selected some other way 10878 } 10879 10880 /// 10881 this(Action action, Widget parent = null) { 10882 assert(action !is null); 10883 this(action.label, parent); 10884 this.action = action; 10885 tabStop = false; // these are selected some other way 10886 } 10887 10888 version(custom_widgets) 10889 override void paint(WidgetPainter painter) { 10890 auto cs = getComputedStyle(); 10891 if(dynamicState & DynamicState.depressed) 10892 this.draw3dFrame(painter, FrameStyle.sunk, cs.background.color); 10893 if(dynamicState & DynamicState.hover) 10894 painter.outlineColor = cs.activeMenuItemColor; 10895 else 10896 painter.outlineColor = cs.foregroundColor; 10897 painter.fillColor = Color.transparent; 10898 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 10899 if(action && action.accelerator !is KeyEvent.init) { 10900 painter.drawText(scaleWithDpi(Point(cast(MenuBar) this.parent ? 4 : 20, 0)), action.accelerator.toStr(), Point(width - 4, height), TextAlignment.Right | TextAlignment.VerticalCenter); 10901 10902 } 10903 } 10904 10905 static class Style : Widget.Style { 10906 override bool variesWithState(ulong dynamicStateFlags) { 10907 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 10908 } 10909 } 10910 mixin OverrideStyle!Style; 10911 10912 override void defaultEventHandler_triggered(Event event) { 10913 if(action) 10914 foreach(handler; action.triggered) 10915 handler(); 10916 10917 if(auto pmenu = cast(Menu) this.parent) 10918 pmenu.remove(); 10919 10920 super.defaultEventHandler_triggered(event); 10921 } 10922 } 10923 10924 version(win32_widgets) 10925 /// A "mouse activiated widget" is really just an abstract variant of button. 10926 class MouseActivatedWidget : Widget { 10927 @property bool isChecked() { 10928 assert(hwnd); 10929 return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; 10930 10931 } 10932 @property void isChecked(bool state) { 10933 assert(hwnd); 10934 SendMessageW(hwnd, BM_SETCHECK, state ? BST_CHECKED : BST_UNCHECKED, 0); 10935 10936 } 10937 10938 override void handleWmCommand(ushort cmd, ushort id) { 10939 if(cmd == 0) { 10940 auto event = new Event(EventType.triggered, this); 10941 event.dispatch(); 10942 } 10943 } 10944 10945 this(Widget parent) { 10946 super(parent); 10947 } 10948 } 10949 else version(custom_widgets) 10950 /// ditto 10951 class MouseActivatedWidget : Widget { 10952 @property bool isChecked() { return isChecked_; } 10953 @property bool isChecked(bool b) { return isChecked_ = b; } 10954 10955 private bool isChecked_; 10956 10957 this(Widget parent) { 10958 super(parent); 10959 10960 addEventListener((MouseDownEvent ev) { 10961 if(ev.button == MouseButton.left) { 10962 setDynamicState(DynamicState.depressed, true); 10963 setDynamicState(DynamicState.hover, true); 10964 redraw(); 10965 } 10966 }); 10967 10968 addEventListener((MouseUpEvent ev) { 10969 if(ev.button == MouseButton.left) { 10970 setDynamicState(DynamicState.depressed, false); 10971 setDynamicState(DynamicState.hover, false); 10972 redraw(); 10973 } 10974 }); 10975 10976 addEventListener((MouseMoveEvent mme) { 10977 if(!(mme.state & ModifierState.leftButtonDown)) { 10978 if(dynamicState_ & DynamicState.depressed) { 10979 setDynamicState(DynamicState.depressed, false); 10980 redraw(); 10981 } 10982 } 10983 }); 10984 } 10985 10986 override void defaultEventHandler_focus(Event ev) { 10987 super.defaultEventHandler_focus(ev); 10988 this.redraw(); 10989 } 10990 override void defaultEventHandler_blur(Event ev) { 10991 super.defaultEventHandler_blur(ev); 10992 setDynamicState(DynamicState.depressed, false); 10993 this.redraw(); 10994 } 10995 override void defaultEventHandler_keydown(KeyDownEvent ev) { 10996 super.defaultEventHandler_keydown(ev); 10997 if(ev.key == Key.Space || ev.key == Key.Enter || ev.key == Key.PadEnter) { 10998 setDynamicState(DynamicState.depressed, true); 10999 setDynamicState(DynamicState.hover, true); 11000 this.redraw(); 11001 } 11002 } 11003 override void defaultEventHandler_keyup(KeyUpEvent ev) { 11004 super.defaultEventHandler_keyup(ev); 11005 if(!(dynamicState & DynamicState.depressed)) 11006 return; 11007 setDynamicState(DynamicState.depressed, false); 11008 setDynamicState(DynamicState.hover, false); 11009 this.redraw(); 11010 11011 auto event = new Event(EventType.triggered, this); 11012 event.sendDirectly(); 11013 } 11014 override void defaultEventHandler_click(ClickEvent ev) { 11015 super.defaultEventHandler_click(ev); 11016 if(ev.button == MouseButton.left) { 11017 auto event = new Event(EventType.triggered, this); 11018 event.sendDirectly(); 11019 } 11020 } 11021 11022 } 11023 else static assert(false); 11024 11025 /* 11026 /++ 11027 Like the tablet thing, it would have a label, a description, and a switch slider thingy. 11028 11029 Basically the same as a checkbox. 11030 +/ 11031 class OnOffSwitch : MouseActivatedWidget { 11032 11033 } 11034 */ 11035 11036 /++ 11037 History: 11038 Added June 15, 2021 (dub v10.1) 11039 +/ 11040 struct ImageLabel { 11041 /++ 11042 Defines a label+image combo used by some widgets. 11043 11044 If you provide just a text label, that is all the widget will try to 11045 display. Or just an image will display just that. If you provide both, 11046 it may display both text and image side by side or display the image 11047 and offer text on an input event depending on the widget. 11048 11049 History: 11050 The `alignment` parameter was added on September 27, 2021 11051 +/ 11052 this(string label, TextAlignment alignment = TextAlignment.Center) { 11053 this.label = label; 11054 this.displayFlags = DisplayFlags.displayText; 11055 this.alignment = alignment; 11056 } 11057 11058 /// ditto 11059 this(string label, MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11060 this.label = label; 11061 this.image = image; 11062 this.displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11063 this.alignment = alignment; 11064 } 11065 11066 /// ditto 11067 this(MemoryImage image, TextAlignment alignment = TextAlignment.Center) { 11068 this.image = image; 11069 this.displayFlags = DisplayFlags.displayImage; 11070 this.alignment = alignment; 11071 } 11072 11073 /// ditto 11074 this(string label, MemoryImage image, int displayFlags, TextAlignment alignment = TextAlignment.Center) { 11075 this.label = label; 11076 this.image = image; 11077 this.alignment = alignment; 11078 this.displayFlags = displayFlags; 11079 } 11080 11081 string label; 11082 MemoryImage image; 11083 11084 enum DisplayFlags { 11085 displayText = 1 << 0, 11086 displayImage = 1 << 1, 11087 } 11088 11089 int displayFlags = DisplayFlags.displayText | DisplayFlags.displayImage; 11090 11091 TextAlignment alignment; 11092 } 11093 11094 /++ 11095 A basic checked or not checked box with an attached label. 11096 11097 11098 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 11099 11100 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11101 11102 History: 11103 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. 11104 +/ 11105 class Checkbox : MouseActivatedWidget { 11106 version(win32_widgets) { 11107 override int maxHeight() { return scaleWithDpi(16); } 11108 override int minHeight() { return scaleWithDpi(16); } 11109 } else version(custom_widgets) { 11110 override int maxHeight() { return defaultLineHeight; } 11111 override int minHeight() { return defaultLineHeight; } 11112 } else static assert(0); 11113 11114 override int marginLeft() { return 4; } 11115 11116 override int flexBasisWidth() { return 24 + cast(int) label.length * 7; } 11117 11118 /++ 11119 Just an alias because I keep typing checked out of web habit. 11120 11121 History: 11122 Added May 31, 2021 11123 +/ 11124 alias checked = isChecked; 11125 11126 private string label; 11127 private dchar accelerator; 11128 11129 /++ 11130 +/ 11131 this(string label, Widget parent) { 11132 this(ImageLabel(label), Appearance.checkbox, parent); 11133 } 11134 11135 /// ditto 11136 this(string label, Appearance appearance, Widget parent) { 11137 this(ImageLabel(label), appearance, parent); 11138 } 11139 11140 /++ 11141 Changes the look and may change the ideal size of the widget without changing its behavior. The precise look is platform-specific. 11142 11143 History: 11144 Added June 29, 2021 (dub v10.2) 11145 +/ 11146 enum Appearance { 11147 checkbox, /// a normal checkbox 11148 pushbutton, /// a button that is showed as pushed when checked and up when unchecked. Similar to the bold button in a toolbar in Wordpad. 11149 //sliderswitch, 11150 } 11151 private Appearance appearance; 11152 11153 /// ditto 11154 private this(ImageLabel label, Appearance appearance, Widget parent) { 11155 super(parent); 11156 version(win32_widgets) { 11157 this.label = label.label; 11158 11159 uint extraStyle; 11160 final switch(appearance) { 11161 case Appearance.checkbox: 11162 break; 11163 case Appearance.pushbutton: 11164 extraStyle |= BS_PUSHLIKE; 11165 break; 11166 } 11167 11168 createWin32Window(this, "button"w, label.label, BS_CHECKBOX | extraStyle); 11169 } else version(custom_widgets) { 11170 label.label.extractWindowsStyleLabel(this.label, this.accelerator); 11171 } else static assert(0); 11172 } 11173 11174 version(custom_widgets) 11175 override void paint(WidgetPainter painter) { 11176 auto cs = getComputedStyle(); 11177 if(isFocused()) { 11178 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11179 painter.fillColor = cs.windowBackgroundColor; 11180 painter.drawRectangle(Point(0, 0), width, height); 11181 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11182 } else { 11183 painter.pen = Pen(cs.windowBackgroundColor, 1, Pen.Style.Solid); 11184 painter.fillColor = cs.windowBackgroundColor; 11185 painter.drawRectangle(Point(0, 0), width, height); 11186 } 11187 11188 11189 enum buttonSize = 16; 11190 11191 painter.outlineColor = Color.black; 11192 painter.fillColor = Color.white; 11193 painter.drawRectangle(scaleWithDpi(Point(2, 2)), scaleWithDpi(buttonSize - 2), scaleWithDpi(buttonSize - 2)); 11194 11195 if(isChecked) { 11196 painter.pen = Pen(Color.black, 2); 11197 // I'm using height so the checkbox is square 11198 enum padding = 5; 11199 painter.drawLine(scaleWithDpi(Point(padding, padding)), scaleWithDpi(Point(buttonSize - (padding-2), buttonSize - (padding-2)))); 11200 painter.drawLine(scaleWithDpi(Point(buttonSize-(padding-2), padding)), scaleWithDpi(Point(padding, buttonSize - (padding-2)))); 11201 11202 painter.pen = Pen(Color.black, 1); 11203 } 11204 11205 if(label !is null) { 11206 painter.outlineColor = cs.foregroundColor(); 11207 painter.fillColor = cs.foregroundColor(); 11208 11209 // FIXME: should prolly just align the baseline or something 11210 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 2)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11211 } 11212 } 11213 11214 override void defaultEventHandler_triggered(Event ev) { 11215 isChecked = !isChecked; 11216 11217 this.emit!(ChangeEvent!bool)(&isChecked); 11218 11219 redraw(); 11220 } 11221 11222 /// Emits a change event with the checked state 11223 mixin Emits!(ChangeEvent!bool); 11224 } 11225 11226 /// Adds empty space to a layout. 11227 class VerticalSpacer : Widget { 11228 /// 11229 this(Widget parent) { 11230 super(parent); 11231 } 11232 } 11233 11234 /// ditto 11235 class HorizontalSpacer : Widget { 11236 /// 11237 this(Widget parent) { 11238 super(parent); 11239 this.tabStop = false; 11240 } 11241 } 11242 11243 11244 /++ 11245 Creates a radio button with an associated label. These are usually put inside a [Fieldset]. 11246 11247 11248 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 11249 11250 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11251 11252 History: 11253 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. 11254 +/ 11255 class Radiobox : MouseActivatedWidget { 11256 11257 version(win32_widgets) { 11258 override int maxHeight() { return scaleWithDpi(16); } 11259 override int minHeight() { return scaleWithDpi(16); } 11260 } else version(custom_widgets) { 11261 override int maxHeight() { return defaultLineHeight; } 11262 override int minHeight() { return defaultLineHeight; } 11263 } else static assert(0); 11264 11265 override int marginLeft() { return 4; } 11266 11267 // FIXME: make a label getter 11268 private string label; 11269 private dchar accelerator; 11270 11271 version(win32_widgets) 11272 this(string label, Widget parent) { 11273 super(parent); 11274 this.label = label; 11275 createWin32Window(this, "button"w, label, BS_AUTORADIOBUTTON); 11276 } 11277 else version(custom_widgets) 11278 this(string label, Widget parent) { 11279 super(parent); 11280 label.extractWindowsStyleLabel(this.label, this.accelerator); 11281 height = 16; 11282 width = height + 4 + cast(int) label.length * 16; 11283 } 11284 else static assert(false); 11285 11286 version(custom_widgets) 11287 override void paint(WidgetPainter painter) { 11288 auto cs = getComputedStyle(); 11289 if(isFocused) { 11290 painter.fillColor = cs.windowBackgroundColor; 11291 painter.pen = Pen(Color.black, 1, Pen.Style.Dotted); 11292 } else { 11293 painter.fillColor = cs.windowBackgroundColor; 11294 painter.outlineColor = cs.windowBackgroundColor; 11295 } 11296 painter.drawRectangle(Point(0, 0), width, height); 11297 11298 painter.pen = Pen(Color.black, 1, Pen.Style.Solid); 11299 11300 enum buttonSize = 16; 11301 11302 painter.outlineColor = Color.black; 11303 painter.fillColor = Color.white; 11304 painter.drawEllipse(scaleWithDpi(Point(2, 2)), scaleWithDpi(Point(buttonSize - 2, buttonSize - 2))); 11305 if(isChecked) { 11306 painter.outlineColor = Color.black; 11307 painter.fillColor = Color.black; 11308 // I'm using height so the checkbox is square 11309 painter.drawEllipse(scaleWithDpi(Point(5, 5)), scaleWithDpi(Point(buttonSize - 5, buttonSize - 5))); 11310 } 11311 11312 painter.outlineColor = cs.foregroundColor(); 11313 painter.fillColor = cs.foregroundColor(); 11314 11315 painter.drawText(scaleWithDpi(Point(buttonSize + 4, 0)), label, Point(width, height), TextAlignment.Left | TextAlignment.VerticalCenter); 11316 } 11317 11318 11319 override void defaultEventHandler_triggered(Event ev) { 11320 isChecked = true; 11321 11322 if(this.parent) { 11323 foreach(child; this.parent.children) { 11324 if(child is this) continue; 11325 if(auto rb = cast(Radiobox) child) { 11326 rb.isChecked = false; 11327 rb.emit!(ChangeEvent!bool)(&rb.isChecked); 11328 rb.redraw(); 11329 } 11330 } 11331 } 11332 11333 this.emit!(ChangeEvent!bool)(&this.isChecked); 11334 11335 redraw(); 11336 } 11337 11338 /// 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. 11339 mixin Emits!(ChangeEvent!bool); 11340 } 11341 11342 11343 /++ 11344 Creates a push button with unbounded size. When it is clicked, it emits a `triggered` event. 11345 11346 11347 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 11348 11349 Use double-ampersand, "First && Second", to be displayed as a single one, "First & Second". 11350 11351 History: 11352 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. 11353 +/ 11354 class Button : MouseActivatedWidget { 11355 override int heightStretchiness() { return 3; } 11356 override int widthStretchiness() { return 3; } 11357 11358 /++ 11359 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. 11360 11361 History: 11362 Added July 2, 2021 11363 +/ 11364 public bool triggersOnMultiClick; 11365 11366 private string label_; 11367 private TextAlignment alignment; 11368 private dchar accelerator; 11369 11370 /// 11371 string label() { return label_; } 11372 /// 11373 void label(string l) { 11374 label_ = l; 11375 version(win32_widgets) { 11376 WCharzBuffer bfr = WCharzBuffer(l); 11377 SetWindowTextW(hwnd, bfr.ptr); 11378 } else version(custom_widgets) { 11379 redraw(); 11380 } 11381 } 11382 11383 override void defaultEventHandler_dblclick(DoubleClickEvent ev) { 11384 super.defaultEventHandler_dblclick(ev); 11385 if(triggersOnMultiClick) { 11386 if(ev.button == MouseButton.left) { 11387 auto event = new Event(EventType.triggered, this); 11388 event.sendDirectly(); 11389 } 11390 } 11391 } 11392 11393 private Sprite sprite; 11394 private int displayFlags; 11395 11396 /++ 11397 Creates a push button with the given label, which may be an image or some text. 11398 11399 Bugs: 11400 If the image is bigger than the button, it may not be displayed in the right position on Linux. 11401 11402 History: 11403 The [ImageLabel] overload was added on June 21, 2021 (dub v10.1). 11404 11405 The button with label and image will respect requests to show both on Windows as 11406 of March 28, 2022 iff you provide a manifest file to opt into common controls v6. 11407 +/ 11408 this(ImageLabel label, Widget parent) { 11409 version(win32_widgets) { 11410 // FIXME: use ideal button size instead 11411 width = 50; 11412 height = 30; 11413 super(parent); 11414 11415 // BS_BITMAP is set when we want image only, so checking for exactly that combination 11416 enum imgFlags = ImageLabel.DisplayFlags.displayImage | ImageLabel.DisplayFlags.displayText; 11417 auto extraStyle = ((label.displayFlags & imgFlags) == ImageLabel.DisplayFlags.displayImage) ? BS_BITMAP : 0; 11418 11419 // the transparent thing can mess up borders in other cases, so only going to keep it for bitmap things where it might matter 11420 createWin32Window(this, "button"w, label.label, BS_PUSHBUTTON | extraStyle, extraStyle == BS_BITMAP ? WS_EX_TRANSPARENT : 0 ); 11421 11422 if(label.image) { 11423 sprite = Sprite.fromMemoryImage(parentWindow.win, label.image, true); 11424 11425 SendMessageW(hwnd, BM_SETIMAGE, IMAGE_BITMAP, cast(LPARAM) sprite.nativeHandle); 11426 } 11427 11428 this.label = label.label; 11429 } else version(custom_widgets) { 11430 width = 50; 11431 height = 30; 11432 super(parent); 11433 11434 label.label.extractWindowsStyleLabel(this.label_, this.accelerator); 11435 11436 if(label.image) { 11437 this.sprite = Sprite.fromMemoryImage(parentWindow.win, label.image); 11438 this.displayFlags = label.displayFlags; 11439 } 11440 11441 this.alignment = label.alignment; 11442 } 11443 } 11444 11445 /// 11446 this(string label, Widget parent) { 11447 this(ImageLabel(label), parent); 11448 } 11449 11450 override int minHeight() { return defaultLineHeight + 4; } 11451 11452 static class Style : Widget.Style { 11453 override WidgetBackground background() { 11454 auto cs = widget.getComputedStyle(); // FIXME: this is potentially recursive 11455 11456 auto pressed = DynamicState.depressed | DynamicState.hover; 11457 if((widget.dynamicState & pressed) == pressed) { 11458 return WidgetBackground(cs.depressedButtonColor()); 11459 } else if(widget.dynamicState & DynamicState.hover) { 11460 return WidgetBackground(cs.hoveringColor()); 11461 } else { 11462 return WidgetBackground(cs.buttonColor()); 11463 } 11464 } 11465 11466 override FrameStyle borderStyle() { 11467 auto pressed = DynamicState.depressed | DynamicState.hover; 11468 if((widget.dynamicState & pressed) == pressed) { 11469 return FrameStyle.sunk; 11470 } else { 11471 return FrameStyle.risen; 11472 } 11473 11474 } 11475 11476 override bool variesWithState(ulong dynamicStateFlags) { 11477 return super.variesWithState(dynamicStateFlags) || (dynamicStateFlags & (DynamicState.depressed | DynamicState.hover)); 11478 } 11479 } 11480 mixin OverrideStyle!Style; 11481 11482 version(custom_widgets) 11483 override void paint(WidgetPainter painter) { 11484 painter.drawThemed(delegate Rectangle(const Rectangle bounds) { 11485 if(sprite) { 11486 sprite.drawAt( 11487 painter, 11488 bounds.upperLeft + Point((bounds.width - sprite.width) / 2, (bounds.height - sprite.height) / 2), 11489 Point(0, 0) 11490 ); 11491 } else { 11492 painter.drawText(bounds.upperLeft, label, bounds.lowerRight, alignment | TextAlignment.VerticalCenter); 11493 } 11494 return bounds; 11495 }); 11496 } 11497 11498 override int flexBasisWidth() { 11499 version(win32_widgets) { 11500 SIZE size; 11501 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11502 if(size.cx == 0) 11503 goto fallback; 11504 return size.cx + scaleWithDpi(16); 11505 } 11506 fallback: 11507 return scaleWithDpi(cast(int) label.length * 8 + 16); 11508 } 11509 11510 override int flexBasisHeight() { 11511 version(win32_widgets) { 11512 SIZE size; 11513 SendMessage(hwnd, BCM_GETIDEALSIZE, 0, cast(LPARAM) &size); 11514 if(size.cy == 0) 11515 goto fallback; 11516 return size.cy + scaleWithDpi(6); 11517 } 11518 fallback: 11519 return defaultLineHeight + 4; 11520 } 11521 } 11522 11523 /++ 11524 A button with a consistent size, suitable for user commands like OK and CANCEL. 11525 +/ 11526 class CommandButton : Button { 11527 this(string label, Widget parent) { 11528 super(label, parent); 11529 } 11530 11531 // FIXME: I think I can simply make this 0 stretchiness instead of max now that the flex basis is there 11532 11533 override int maxHeight() { 11534 return defaultLineHeight + 4; 11535 } 11536 11537 override int maxWidth() { 11538 return defaultLineHeight * 4; 11539 } 11540 11541 override int marginLeft() { return 12; } 11542 override int marginRight() { return 12; } 11543 override int marginTop() { return 12; } 11544 override int marginBottom() { return 12; } 11545 } 11546 11547 /// 11548 enum ArrowDirection { 11549 left, /// 11550 right, /// 11551 up, /// 11552 down /// 11553 } 11554 11555 /// 11556 version(custom_widgets) 11557 class ArrowButton : Button { 11558 /// 11559 this(ArrowDirection direction, Widget parent) { 11560 super("", parent); 11561 this.direction = direction; 11562 triggersOnMultiClick = true; 11563 } 11564 11565 private ArrowDirection direction; 11566 11567 override int minHeight() { return scaleWithDpi(16); } 11568 override int maxHeight() { return scaleWithDpi(16); } 11569 override int minWidth() { return scaleWithDpi(16); } 11570 override int maxWidth() { return scaleWithDpi(16); } 11571 11572 override void paint(WidgetPainter painter) { 11573 super.paint(painter); 11574 11575 auto cs = getComputedStyle(); 11576 11577 painter.outlineColor = cs.foregroundColor; 11578 painter.fillColor = cs.foregroundColor; 11579 11580 auto offset = Point((this.width - scaleWithDpi(16)) / 2, (this.height - scaleWithDpi(16)) / 2); 11581 11582 final switch(direction) { 11583 case ArrowDirection.up: 11584 painter.drawPolygon( 11585 scaleWithDpi(Point(2, 10) + offset), 11586 scaleWithDpi(Point(7, 5) + offset), 11587 scaleWithDpi(Point(12, 10) + offset), 11588 scaleWithDpi(Point(2, 10) + offset) 11589 ); 11590 break; 11591 case ArrowDirection.down: 11592 painter.drawPolygon( 11593 scaleWithDpi(Point(2, 6) + offset), 11594 scaleWithDpi(Point(7, 11) + offset), 11595 scaleWithDpi(Point(12, 6) + offset), 11596 scaleWithDpi(Point(2, 6) + offset) 11597 ); 11598 break; 11599 case ArrowDirection.left: 11600 painter.drawPolygon( 11601 scaleWithDpi(Point(10, 2) + offset), 11602 scaleWithDpi(Point(5, 7) + offset), 11603 scaleWithDpi(Point(10, 12) + offset), 11604 scaleWithDpi(Point(10, 2) + offset) 11605 ); 11606 break; 11607 case ArrowDirection.right: 11608 painter.drawPolygon( 11609 scaleWithDpi(Point(6, 2) + offset), 11610 scaleWithDpi(Point(11, 7) + offset), 11611 scaleWithDpi(Point(6, 12) + offset), 11612 scaleWithDpi(Point(6, 2) + offset) 11613 ); 11614 break; 11615 } 11616 } 11617 } 11618 11619 private 11620 int[2] getChildPositionRelativeToParentOrigin(Widget c) nothrow { 11621 int x, y; 11622 Widget par = c; 11623 while(par) { 11624 x += par.x; 11625 y += par.y; 11626 par = par.parent; 11627 } 11628 return [x, y]; 11629 } 11630 11631 version(win32_widgets) 11632 private 11633 int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow { 11634 // MapWindowPoints? 11635 int x, y; 11636 Widget par = c; 11637 while(par) { 11638 x += par.x; 11639 y += par.y; 11640 par = par.parent; 11641 if(par !is null && par.useNativeDrawing()) 11642 break; 11643 } 11644 return [x, y]; 11645 } 11646 11647 /// 11648 class ImageBox : Widget { 11649 private MemoryImage image_; 11650 11651 override int widthStretchiness() { return 1; } 11652 override int heightStretchiness() { return 1; } 11653 override int widthShrinkiness() { return 1; } 11654 override int heightShrinkiness() { return 1; } 11655 11656 override int flexBasisHeight() { 11657 return image_.height; 11658 } 11659 11660 override int flexBasisWidth() { 11661 return image_.width; 11662 } 11663 11664 /// 11665 public void setImage(MemoryImage image){ 11666 this.image_ = image; 11667 if(this.parentWindow && this.parentWindow.win) { 11668 if(sprite) 11669 sprite.dispose(); 11670 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11671 } 11672 redraw(); 11673 } 11674 11675 /// How to fit the image in the box if they aren't an exact match in size? 11676 enum HowToFit { 11677 center, /// centers the image, cropping around all the edges as needed 11678 crop, /// always draws the image in the upper left, cropping the lower right if needed 11679 // stretch, /// not implemented 11680 } 11681 11682 private Sprite sprite; 11683 private HowToFit howToFit_; 11684 11685 private Color backgroundColor_; 11686 11687 /// 11688 this(MemoryImage image, HowToFit howToFit, Color backgroundColor, Widget parent) { 11689 this.image_ = image; 11690 this.tabStop = false; 11691 this.howToFit_ = howToFit; 11692 this.backgroundColor_ = backgroundColor; 11693 super(parent); 11694 updateSprite(); 11695 } 11696 11697 /// ditto 11698 this(MemoryImage image, HowToFit howToFit, Widget parent) { 11699 this(image, howToFit, Color.transparent, parent); 11700 } 11701 11702 private void updateSprite() { 11703 if(sprite is null && this.parentWindow && this.parentWindow.win) { 11704 sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); 11705 } 11706 } 11707 11708 override void paint(WidgetPainter painter) { 11709 updateSprite(); 11710 if(backgroundColor_.a) { 11711 painter.fillColor = backgroundColor_; 11712 painter.drawRectangle(Point(0, 0), width, height); 11713 } 11714 if(howToFit_ == HowToFit.crop) 11715 sprite.drawAt(painter, Point(0, 0)); 11716 else if(howToFit_ == HowToFit.center) { 11717 sprite.drawAt(painter, Point((width - image_.width) / 2, (height - image_.height) / 2)); 11718 } 11719 } 11720 } 11721 11722 /// 11723 class TextLabel : Widget { 11724 override int maxHeight() { return defaultLineHeight; } 11725 override int minHeight() { return defaultLineHeight; } 11726 override int minWidth() { return 32; } 11727 11728 override int flexBasisHeight() { return minHeight(); } 11729 override int flexBasisWidth() { return defaultTextWidth(label); } 11730 11731 string label_; 11732 11733 /++ 11734 Indicates which other control this label is here for. Similar to HTML `for` attribute. 11735 11736 In practice this means a click on the label will focus the `labelFor`. In future versions 11737 it will also set screen reader hints but that is not yet implemented. 11738 11739 History: 11740 Added October 3, 2021 (dub v10.4) 11741 +/ 11742 Widget labelFor; 11743 11744 /// 11745 @scriptable 11746 string label() { return label_; } 11747 11748 /// 11749 @scriptable 11750 void label(string l) { 11751 label_ = l; 11752 version(win32_widgets) { 11753 WCharzBuffer bfr = WCharzBuffer(l); 11754 SetWindowTextW(hwnd, bfr.ptr); 11755 } else version(custom_widgets) 11756 redraw(); 11757 } 11758 11759 /// 11760 this(string label, TextAlignment alignment, Widget parent) { 11761 this.label_ = label; 11762 this.alignment = alignment; 11763 this.tabStop = false; 11764 super(parent); 11765 11766 version(win32_widgets) 11767 createWin32Window(this, "static"w, label, (alignment & TextAlignment.Center) ? SS_CENTER : 0, (alignment & TextAlignment.Right) ? WS_EX_RIGHT : WS_EX_LEFT); 11768 } 11769 11770 override void defaultEventHandler_click(scope ClickEvent ce) { 11771 if(this.labelFor !is null) 11772 this.labelFor.focus(); 11773 } 11774 11775 /++ 11776 WARNING: this currently sets TextAlignment.Right as the default. That will change in a future version. 11777 For future-proofing of your code, if you rely on TextAlignment.Right, you MUST specify that explicitly. 11778 +/ 11779 this(string label, Widget parent) { 11780 this(label, TextAlignment.Right, parent); 11781 } 11782 11783 11784 TextAlignment alignment; 11785 11786 version(custom_widgets) 11787 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 11788 painter.outlineColor = getComputedStyle().foregroundColor; 11789 painter.drawText(Point(0, 0), this.label, Point(width, height), alignment); 11790 return bounds; 11791 } 11792 11793 } 11794 11795 version(custom_widgets) 11796 private struct etc { 11797 mixin ExperimentalTextComponent; 11798 } 11799 11800 version(win32_widgets) 11801 alias EditableTextWidgetParent = Widget; /// 11802 else version(custom_widgets) { 11803 version(trash_text) { 11804 alias EditableTextWidgetParent = ScrollableWidget; /// 11805 } else { 11806 alias EditableTextWidgetParent = Widget; 11807 version=use_new_text_system; 11808 import arsd.textlayouter; 11809 } 11810 } else static assert(0); 11811 11812 version(use_new_text_system) 11813 class TextDisplayHelper : Widget { 11814 protected TextLayouter l; 11815 protected ScrollMessageWidget smw; 11816 11817 private const(TextLayouter.State)*[] undoStack; 11818 private const(TextLayouter.State)*[] redoStack; 11819 11820 bool readonly; 11821 bool caretNavigation; // scroll lock can flip this 11822 bool singleLine; 11823 bool acceptsTabInput; 11824 11825 private Menu ctx; 11826 override Menu contextMenu(int x, int y) { 11827 if(ctx is null) { 11828 ctx = new Menu("Actions", this); 11829 ctx.addItem(new MenuItem(new Action("&Undo", GenericIcons.Undo, &undo))); 11830 ctx.addItem(new MenuItem(new Action("&Redo", GenericIcons.Redo, &redo))); 11831 ctx.addSeparator(); 11832 ctx.addItem(new MenuItem(new Action("Cu&t", GenericIcons.Cut, &cut))); 11833 ctx.addItem(new MenuItem(new Action("&Copy", GenericIcons.Copy, ©))); 11834 ctx.addItem(new MenuItem(new Action("&Paste", GenericIcons.Paste, &paste))); 11835 ctx.addItem(new MenuItem(new Action("&Delete", 0, &deleteContentOfSelection))); 11836 ctx.addSeparator(); 11837 ctx.addItem(new MenuItem(new Action("Select &All", 0, &selectAll))); 11838 } 11839 return ctx; 11840 } 11841 11842 override void defaultEventHandler_blur(Event ev) { 11843 super.defaultEventHandler_blur(ev); 11844 if(l.wasMutated()) { 11845 auto evt = new ChangeEvent!string(this, &this.content); 11846 evt.dispatch(); 11847 l.clearWasMutatedFlag(); 11848 } 11849 } 11850 11851 private string content() { 11852 return l.getTextString(); 11853 } 11854 11855 void undo() { 11856 if(undoStack.length) { 11857 auto state = undoStack[$-1]; 11858 undoStack = undoStack[0 .. $-1]; 11859 undoStack.assumeSafeAppend(); 11860 redoStack ~= l.saveState(); 11861 l.restoreState(state); 11862 adjustScrollbarSizes(); 11863 scrollForCaret(); 11864 redraw(); 11865 stateCheckpoint = true; 11866 } 11867 } 11868 11869 void redo() { 11870 if(redoStack.length) { 11871 doStateCheckpoint(); 11872 auto state = redoStack[$-1]; 11873 redoStack = redoStack[0 .. $-1]; 11874 redoStack.assumeSafeAppend(); 11875 l.restoreState(state); 11876 adjustScrollbarSizes(); 11877 scrollForCaret(); 11878 redraw(); 11879 stateCheckpoint = true; 11880 } 11881 } 11882 11883 void cut() { 11884 with(l.selection()) { 11885 if(!isEmpty()) { 11886 setClipboardText(parentWindow.win, getContentString()); 11887 doStateCheckpoint(); 11888 replaceContent(""); 11889 adjustScrollbarSizes(); 11890 scrollForCaret(); 11891 this.redraw(); 11892 } 11893 } 11894 11895 } 11896 11897 void copy() { 11898 with(l.selection()) { 11899 if(!isEmpty()) { 11900 setClipboardText(parentWindow.win, getContentString()); 11901 this.redraw(); 11902 } 11903 } 11904 } 11905 11906 void paste() { 11907 getClipboardText(parentWindow.win, (txt) { 11908 doStateCheckpoint(); 11909 l.selection.replaceContent(txt); 11910 adjustScrollbarSizes(); 11911 scrollForCaret(); 11912 this.redraw(); 11913 }); 11914 } 11915 11916 void deleteContentOfSelection() { 11917 doStateCheckpoint(); 11918 l.selection.replaceContent(""); 11919 l.selection.setUserXCoordinate(); 11920 adjustScrollbarSizes(); 11921 scrollForCaret(); 11922 redraw(); 11923 } 11924 11925 void selectAll() { 11926 with(l.selection) { 11927 moveToStartOfDocument(); 11928 setAnchor(); 11929 moveToEndOfDocument(); 11930 setFocus(); 11931 } 11932 redraw(); 11933 } 11934 11935 protected bool stateCheckpoint = true; 11936 11937 protected void doStateCheckpoint() { 11938 if(stateCheckpoint) { 11939 undoStack ~= l.saveState(); 11940 stateCheckpoint = false; 11941 } 11942 } 11943 11944 protected void adjustScrollbarSizes() { 11945 // FIXME: will want a content area helper function instead of doing all these subtractions myself 11946 auto borderWidth = 2; 11947 this.smw.setTotalArea(l.width, l.height); 11948 this.smw.setViewableArea( 11949 this.width - this.paddingLeft - this.paddingRight - borderWidth * 2, 11950 this.height - this.paddingTop - this.paddingBottom - borderWidth * 2); 11951 } 11952 11953 protected void scrollForCaret() { 11954 // writeln(l.width, "x", l.height); writeln(this.width - this.paddingLeft - this.paddingRight, " ", this.height - this.paddingTop - this.paddingBottom); 11955 smw.scrollIntoView(l.selection.focusBoundingBox()); 11956 } 11957 11958 // FIXME: this should be a theme changed event listener instead 11959 private BaseVisualTheme currentTheme; 11960 override void recomputeChildLayout() { 11961 if(currentTheme is null) 11962 currentTheme = WidgetPainter.visualTheme; 11963 if(WidgetPainter.visualTheme !is currentTheme) { 11964 currentTheme = WidgetPainter.visualTheme; 11965 auto ds = this.l.defaultStyle; 11966 if(auto ms = cast(MyTextStyle) ds) { 11967 auto cs = getComputedStyle(); 11968 auto font = cs.font(); 11969 if(font !is null) 11970 ms.font_ = font; 11971 else { 11972 auto osc = new OperatingSystemFont(); 11973 osc.loadDefault; 11974 ms.font_ = osc; 11975 } 11976 } 11977 } 11978 super.recomputeChildLayout(); 11979 } 11980 11981 private Point adjustForSingleLine(Point p) { 11982 if(singleLine) 11983 return Point(p.x, this.height / 2); 11984 else 11985 return p; 11986 } 11987 11988 private bool wordWrapEnabled_; 11989 11990 this(TextLayouter l, ScrollMessageWidget parent) { 11991 this.smw = parent; 11992 11993 smw.addDefaultWheelListeners(16, 16, 8); 11994 smw.movementPerButtonClick(16, 16); 11995 11996 this.defaultPadding = Rectangle(2, 2, 2, 2); 11997 11998 this.l = l; 11999 super(parent); 12000 12001 smw.addEventListener((scope ScrollEvent se) { 12002 this.redraw(); 12003 }); 12004 12005 bool mouseDown; 12006 12007 this.addEventListener((scope ResizeEvent re) { 12008 // FIXME: I should add a method to give this client area width thing 12009 if(wordWrapEnabled_) 12010 this.l.wordWrapWidth = this.width - this.paddingLeft - this.paddingRight; 12011 12012 adjustScrollbarSizes(); 12013 scrollForCaret(); 12014 12015 this.redraw(); 12016 }); 12017 12018 this.addEventListener((scope KeyDownEvent kde) { 12019 switch(kde.key) { 12020 case Key.Up, Key.Down, Key.Left, Key.Right: 12021 case Key.Home, Key.End: 12022 stateCheckpoint = true; 12023 bool setPosition = false; 12024 switch(kde.key) { 12025 case Key.Up: l.selection.moveUp(); break; 12026 case Key.Down: l.selection.moveDown(); break; 12027 case Key.Left: l.selection.moveLeft(); setPosition = true; break; 12028 case Key.Right: l.selection.moveRight(); setPosition = true; break; 12029 case Key.Home: l.selection.moveToStartOfLine(); setPosition = true; break; 12030 case Key.End: l.selection.moveToEndOfLine(); setPosition = true; break; 12031 default: assert(0); 12032 } 12033 12034 if(kde.shiftKey) 12035 l.selection.setFocus(); 12036 else 12037 l.selection.setAnchor(); 12038 if(setPosition) 12039 l.selection.setUserXCoordinate(); 12040 scrollForCaret(); 12041 redraw(); 12042 break; 12043 case Key.PageUp, Key.PageDown: 12044 // FIXME 12045 scrollForCaret(); 12046 break; 12047 case Key.Delete: 12048 if(l.selection.isEmpty()) { 12049 l.selection.setAnchor(); 12050 l.selection.moveRight(); 12051 l.selection.setFocus(); 12052 } 12053 deleteContentOfSelection(); 12054 adjustScrollbarSizes(); 12055 scrollForCaret(); 12056 break; 12057 case Key.Insert: 12058 break; 12059 case Key.A: 12060 if(kde.ctrlKey) 12061 selectAll(); 12062 break; 12063 case Key.F: 12064 // find 12065 break; 12066 case Key.Z: 12067 if(kde.ctrlKey) 12068 undo(); 12069 break; 12070 case Key.R: 12071 if(kde.ctrlKey) 12072 redo(); 12073 break; 12074 case Key.X: 12075 if(kde.ctrlKey) 12076 cut(); 12077 break; 12078 case Key.C: 12079 if(kde.ctrlKey) 12080 copy(); 12081 break; 12082 case Key.V: 12083 if(kde.ctrlKey) 12084 paste(); 12085 break; 12086 case Key.F1: 12087 with(l.selection()) { 12088 moveToStartOfLine(); 12089 setAnchor(); 12090 moveToEndOfLine(); 12091 moveToIncludeAdjacentEndOfLineMarker(); 12092 setFocus(); 12093 replaceContent(""); 12094 } 12095 12096 redraw(); 12097 break; 12098 /* 12099 case Key.F2: 12100 l.selection().changeStyle((old) => l.registerStyle(new MyTextStyle( 12101 //(cast(MyTextStyle) old).font, 12102 font2, 12103 Color.red))); 12104 redraw(); 12105 break; 12106 */ 12107 case Key.Tab: 12108 // we process the char event, so don't want to change focus on it 12109 if(acceptsTabInput) 12110 kde.preventDefault(); 12111 break; 12112 default: 12113 } 12114 }); 12115 12116 Point downAt; 12117 12118 static if(UsingSimpledisplayX11) 12119 this.addEventListener((scope ClickEvent ce) { 12120 if(ce.button == MouseButton.middle) { 12121 parentWindow.win.getPrimarySelection((txt) { 12122 l.selection.replaceContent(txt); 12123 redraw(); 12124 }); 12125 } 12126 }); 12127 12128 this.addEventListener((scope MouseDownEvent ce) { 12129 if(ce.button == MouseButton.left) { 12130 downAt = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12131 l.selection.moveTo(adjustForSingleLine(smw.position + downAt)); 12132 l.selection.setAnchor(); 12133 mouseDown = true; 12134 parentWindow.captureMouse(this); 12135 this.redraw(); 12136 } else if(ce.button == MouseButton.right) { 12137 this.showContextMenu(ce.clientX, ce.clientY); 12138 } 12139 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12140 }); 12141 12142 Timer autoscrollTimer; 12143 int autoscrollDirection; 12144 int autoscrollAmount; 12145 12146 void autoscroll() { 12147 switch(autoscrollDirection) { 12148 case 0: smw.scrollUp(autoscrollAmount); break; 12149 case 1: smw.scrollDown(autoscrollAmount); break; 12150 case 2: smw.scrollLeft(autoscrollAmount); break; 12151 case 3: smw.scrollRight(autoscrollAmount); break; 12152 default: assert(0); 12153 } 12154 12155 this.redraw(); 12156 } 12157 12158 void setAutoscrollTimer(int direction, int amount) { 12159 if(autoscrollTimer is null) { 12160 autoscrollTimer = new Timer(1000 / 60, &autoscroll); 12161 } 12162 12163 autoscrollDirection = direction; 12164 autoscrollAmount = amount; 12165 } 12166 12167 void stopAutoscrollTimer() { 12168 if(autoscrollTimer !is null) { 12169 autoscrollTimer.dispose(); 12170 autoscrollTimer = null; 12171 } 12172 autoscrollAmount = 0; 12173 autoscrollDirection = 0; 12174 } 12175 12176 this.addEventListener((scope MouseMoveEvent ce) { 12177 if(mouseDown) { 12178 auto movedTo = Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop); 12179 12180 // FIXME: when scrolling i actually do want a timer. 12181 // i also want a zone near the sides of the window where i can auto scroll 12182 12183 auto scrollMultiplier = scaleWithDpi(16); 12184 auto scrollDivisor = scaleWithDpi(16); // if you go more than 64px up it will scroll faster 12185 12186 if(!singleLine && movedTo.y < 4) { 12187 setAutoscrollTimer(0, scrollMultiplier * -(movedTo.y-4) / scrollDivisor); 12188 } else 12189 if(!singleLine && (movedTo.y + 6) > this.height) { 12190 setAutoscrollTimer(1, scrollMultiplier * (movedTo.y + 6 - this.height) / scrollDivisor); 12191 } else 12192 if(movedTo.x < 4) { 12193 setAutoscrollTimer(2, scrollMultiplier * -(movedTo.x-4) / scrollDivisor); 12194 } else 12195 if((movedTo.x + 6) > this.width) { 12196 setAutoscrollTimer(3, scrollMultiplier * (movedTo.x + 6 - this.width) / scrollDivisor); 12197 } else 12198 stopAutoscrollTimer(); 12199 12200 l.selection.moveTo(adjustForSingleLine(smw.position + movedTo)); 12201 l.selection.setFocus(); 12202 this.redraw(); 12203 } 12204 }); 12205 12206 this.addEventListener((scope MouseUpEvent ce) { 12207 // FIXME: assert primary selection 12208 if(mouseDown && ce.button == MouseButton.left) { 12209 stateCheckpoint = true; 12210 //l.selection.moveTo(adjustForSingleLine(smw.position + Point(ce.clientX - this.paddingLeft, ce.clientY - this.paddingTop))); 12211 //l.selection.setFocus(); 12212 mouseDown = false; 12213 parentWindow.releaseMouseCapture(); 12214 stopAutoscrollTimer(); 12215 this.redraw(); 12216 } 12217 //writeln(ce.clientX, ", ", ce.clientY, " = ", l.offsetOfClick(Point(ce.clientX, ce.clientY))); 12218 }); 12219 12220 this.addEventListener((scope CharEvent ce) { 12221 if(readonly) 12222 return; 12223 if(ce.character < 32 && ce.character != '\t' && ce.character != '\n' && ce.character != '\b') 12224 return; // skip the ctrl+x characters we don't care about as plain text 12225 12226 if(singleLine && ce.character == '\n') 12227 return; 12228 if(!acceptsTabInput && ce.character == '\t') 12229 return; 12230 12231 doStateCheckpoint(); 12232 12233 char[4] buffer; 12234 import std.utf; // FIXME: i should remove this. compile time not significant but the logs get spammed with phobos' import web 12235 auto stride = encode(buffer, ce.character); 12236 l.selection.replaceContent(buffer[0 .. stride]); 12237 l.selection.setUserXCoordinate(); 12238 adjustScrollbarSizes(); 12239 scrollForCaret(); 12240 redraw(); 12241 }); 12242 } 12243 12244 static class Style : Widget.Style { 12245 override WidgetBackground background() { 12246 return WidgetBackground(WidgetPainter.visualTheme.widgetBackgroundColor); 12247 } 12248 12249 override Color foregroundColor() { 12250 return WidgetPainter.visualTheme.foregroundColor; 12251 } 12252 12253 override FrameStyle borderStyle() { 12254 return FrameStyle.sunk; 12255 } 12256 12257 override MouseCursor cursor() { 12258 return GenericCursor.Text; 12259 } 12260 } 12261 mixin OverrideStyle!Style; 12262 12263 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, Window.lineHeight))).height; } 12264 override int maxHeight() { 12265 if(singleLine) 12266 return minHeight; 12267 else 12268 return super.maxHeight(); 12269 } 12270 12271 void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 12272 painter.drawText(upperLeft, text); 12273 } 12274 12275 override Rectangle paintContent(WidgetPainter painter, const Rectangle bounds) { 12276 //painter.setFont(font); 12277 12278 auto cs = getComputedStyle(); 12279 auto defaultColor = cs.foregroundColor; 12280 12281 auto old = painter.setClipRectangle(bounds); 12282 scope(exit) painter.setClipRectangle(old); 12283 12284 l.getDrawableText(delegate bool(txt, style, info, carets...) { 12285 //writeln("Segment: ", txt); 12286 assert(style !is null); 12287 12288 auto myStyle = cast(MyTextStyle) style; 12289 assert(myStyle !is null); 12290 12291 painter.setFont(myStyle.font); 12292 // defaultColor = myStyle.color; // FIXME: so wrong 12293 12294 if(info.selections && info.boundingBox.width > 0) { 12295 auto color = this.isFocused ? cs.selectionBackgroundColor : Color(128, 128, 128); // FIXME don't hardcode 12296 painter.fillColor = color; 12297 painter.outlineColor = color; 12298 painter.drawRectangle(Rectangle(info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, info.boundingBox.size)); 12299 painter.outlineColor = cs.selectionForegroundColor; 12300 //painter.fillColor = Color.white; 12301 } else { 12302 painter.outlineColor = defaultColor; 12303 } 12304 12305 if(this.isFocused) 12306 foreach(idx, caret; carets) { 12307 if(idx == 0) 12308 painter.notifyCursorPosition(caret.boundingBox.left - smw.position.x + bounds.left, caret.boundingBox.top - smw.position.y + bounds.top, caret.boundingBox.width, caret.boundingBox.height); 12309 painter.drawLine( 12310 caret.boundingBox.upperLeft + bounds.upperLeft - smw.position(), 12311 bounds.upperLeft + Point(caret.boundingBox.left, caret.boundingBox.bottom) - smw.position() 12312 ); 12313 } 12314 12315 if(txt.stripInternal.length) 12316 drawTextSegment(painter, info.boundingBox.upperLeft - smw.position() + bounds.upperLeft, txt.stripRightInternal); 12317 12318 if(info.boundingBox.upperLeft.y - smw.position().y > this.height) 12319 return false; 12320 else { 12321 return true; 12322 } 12323 }, Rectangle(smw.position(), bounds.size)); 12324 12325 /+ 12326 int place = 0; 12327 int y = 75; 12328 foreach(width; widths) { 12329 painter.fillColor = Color.red; 12330 painter.drawRectangle(Point(place, y), Size(width, 75)); 12331 //y += 15; 12332 place += width; 12333 } 12334 +/ 12335 12336 return bounds; 12337 } 12338 12339 static class MyTextStyle : TextStyle { 12340 OperatingSystemFont font_; 12341 this(OperatingSystemFont font, bool passwordMode = false) { 12342 this.font_ = font; 12343 } 12344 12345 override OperatingSystemFont font() { 12346 return font_; 12347 } 12348 } 12349 } 12350 12351 /+ 12352 version(use_new_text_system) 12353 class TextWidget : Widget { 12354 TextLayouter l; 12355 ScrollMessageWidget smw; 12356 TextDisplayHelper helper; 12357 this(TextLayouter l, Widget parent) { 12358 this.l = l; 12359 super(parent); 12360 12361 smw = new ScrollMessageWidget(this); 12362 //smw.horizontalScrollBar.hide; 12363 //smw.verticalScrollBar.hide; 12364 smw.addDefaultWheelListeners(16, 16, 8); 12365 smw.movementPerButtonClick(16, 16); 12366 helper = new TextDisplayHelper(l, smw); 12367 12368 // no need to do this here since there's gonna be a resize 12369 // event immediately before any drawing 12370 // smw.setTotalArea(l.width, l.height); 12371 smw.setViewableArea( 12372 this.width - this.paddingLeft - this.paddingRight, 12373 this.height - this.paddingTop - this.paddingBottom); 12374 12375 /+ 12376 writeln(l.width, "x", l.height); 12377 +/ 12378 } 12379 } 12380 +/ 12381 12382 12383 12384 12385 /+ 12386 This awful thing has to be rewritten. And it needs to takecare of parentWindow.inputProxy.setIMEPopupLocation too 12387 +/ 12388 12389 /// Contains the implementation of text editing 12390 abstract class EditableTextWidget : EditableTextWidgetParent { 12391 this(Widget parent) { 12392 super(parent); 12393 12394 version(custom_widgets) 12395 setupCustomTextEditing(); 12396 } 12397 12398 private bool wordWrapEnabled_; 12399 void wordWrapEnabled(bool enabled) { 12400 version(win32_widgets) { 12401 SendMessageW(hwnd, EM_FMTLINES, enabled ? 1 : 0, 0); 12402 } else version(custom_widgets) { 12403 wordWrapEnabled_ = enabled; 12404 version(use_new_text_system) 12405 textLayout.wordWrapWidth = enabled ? this.width : 0; // FIXME 12406 } else static assert(false); 12407 } 12408 12409 override int minWidth() { return scaleWithDpi(16); } 12410 override int widthStretchiness() { return 7; } 12411 12412 version(use_new_text_system) 12413 override int maxHeight() { return tdh.maxHeight; } 12414 12415 version(use_new_text_system) 12416 override void focus() { if(tdh) tdh.focus(); else super.focus(); } 12417 12418 void selectAll() { 12419 version(win32_widgets) 12420 SendMessage(hwnd, EM_SETSEL, 0, -1); 12421 else version(custom_widgets) { 12422 version(use_new_text_system) 12423 tdh.selectAll(); 12424 else 12425 textLayout.selectAll(); 12426 redraw(); 12427 } 12428 } 12429 12430 version(use_new_text_system) 12431 TextDisplayHelper tdh; 12432 12433 @property string content() { 12434 version(win32_widgets) { 12435 wchar[4096] bufferstack; 12436 wchar[] buffer; 12437 auto len = GetWindowTextLength(hwnd); 12438 if(len < bufferstack.length) 12439 buffer = bufferstack[0 .. len + 1]; 12440 else 12441 buffer = new wchar[](len + 1); 12442 12443 auto l = GetWindowTextW(hwnd, buffer.ptr, cast(int) buffer.length); 12444 if(l >= 0) 12445 return makeUtf8StringFromWindowsString(buffer[0 .. l]); 12446 else 12447 return null; 12448 } else version(custom_widgets) { 12449 version(use_new_text_system) { 12450 return textLayout.getTextString(); 12451 } else 12452 return textLayout.getPlainText(); 12453 } else static assert(false); 12454 } 12455 @property void content(string s) { 12456 version(win32_widgets) { 12457 WCharzBuffer bfr = WCharzBuffer(s, WindowsStringConversionFlags.convertNewLines); 12458 SetWindowTextW(hwnd, bfr.ptr); 12459 } else version(custom_widgets) { 12460 version(use_new_text_system) { 12461 selectAll(); 12462 textLayout.selection.replaceContent(s); 12463 12464 tdh.adjustScrollbarSizes(); 12465 // these don't seem to help 12466 // tdh.smw.setPosition(0, 0); 12467 // tdh.scrollForCaret(); 12468 12469 redraw(); 12470 } else { 12471 textLayout.clear(); 12472 textLayout.addText(s); 12473 12474 { 12475 // FIXME: it should be able to get this info easier 12476 auto painter = draw(); 12477 textLayout.redoLayout(painter); 12478 } 12479 auto cbb = textLayout.contentBoundingBox(); 12480 setContentSize(cbb.width, cbb.height); 12481 /* 12482 textLayout.addText(ForegroundColor.red, s); 12483 textLayout.addText(ForegroundColor.blue, TextFormat.underline, "http://dpldocs.info/"); 12484 textLayout.addText(" is the best!"); 12485 */ 12486 redraw(); 12487 } 12488 } 12489 else static assert(false); 12490 } 12491 12492 void addText(string txt) { 12493 version(custom_widgets) { 12494 version(use_new_text_system) { 12495 textLayout.appendText(txt); 12496 tdh.adjustScrollbarSizes(); 12497 redraw(); 12498 } else { 12499 textLayout.addText(txt); 12500 12501 { 12502 // FIXME: it should be able to get this info easier 12503 auto painter = draw(); 12504 textLayout.redoLayout(painter); 12505 } 12506 auto cbb = textLayout.contentBoundingBox(); 12507 setContentSize(cbb.width, cbb.height); 12508 } 12509 } else version(win32_widgets) { 12510 // get the current selection 12511 DWORD StartPos, EndPos; 12512 SendMessageW( hwnd, EM_GETSEL, cast(WPARAM)(&StartPos), cast(LPARAM)(&EndPos) ); 12513 12514 // move the caret to the end of the text 12515 int outLength = GetWindowTextLengthW(hwnd); 12516 SendMessageW( hwnd, EM_SETSEL, outLength, outLength ); 12517 12518 // insert the text at the new caret position 12519 WCharzBuffer bfr = WCharzBuffer(txt, WindowsStringConversionFlags.convertNewLines); 12520 SendMessageW( hwnd, EM_REPLACESEL, TRUE, cast(LPARAM) bfr.ptr ); 12521 12522 // restore the previous selection 12523 SendMessageW( hwnd, EM_SETSEL, StartPos, EndPos ); 12524 } else static assert(0); 12525 } 12526 12527 version(custom_widgets) 12528 version(trash_text) 12529 override void paintFrameAndBackground(WidgetPainter painter) { 12530 this.draw3dFrame(painter, FrameStyle.sunk, Color.white); 12531 } 12532 12533 version(use_new_text_system) 12534 TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 12535 return new TextDisplayHelper(textLayout, smw); 12536 } 12537 12538 version(use_new_text_system) 12539 TextStyle defaultTextStyle() { 12540 auto cs = getComputedStyle(); 12541 auto font = cs.font; 12542 if(font is null) { 12543 font = new OperatingSystemFont; 12544 font.loadDefault(); 12545 } 12546 return new TextDisplayHelper.MyTextStyle(font); 12547 } 12548 12549 version(win32_widgets) { /* will do it with Windows calls in the classes */ } 12550 else version(custom_widgets) { 12551 // FIXME 12552 version(use_new_text_system) { 12553 TextLayouter textLayout; 12554 12555 void setupCustomTextEditing() { 12556 textLayout = new TextLayouter(defaultTextStyle()); 12557 auto smw = new ScrollMessageWidget(this); 12558 if(!showingHorizontalScroll) 12559 smw.horizontalScrollBar.hide(); 12560 if(!showingVerticalScroll) 12561 smw.verticalScrollBar.hide(); 12562 this.tabStop = false; 12563 smw.tabStop = false; 12564 tdh = textDisplayHelperFactory(textLayout, smw); 12565 } 12566 12567 } else { 12568 12569 static if(SimpledisplayTimerAvailable) 12570 Timer caretTimer; 12571 etc.TextLayout textLayout; 12572 12573 void setupCustomTextEditing() { 12574 textLayout = new etc.TextLayout(Rectangle(4, 2, width - 8, height - 4)); 12575 textLayout.selectionXorColor = getComputedStyle().activeListXorColor; 12576 } 12577 12578 override void paint(WidgetPainter painter) { 12579 if(parentWindow.win.closed) return; 12580 12581 textLayout.boundingBox = Rectangle(4, 2, width - 8, height - 4); 12582 12583 /* 12584 painter.outlineColor = Color.white; 12585 painter.fillColor = Color.white; 12586 painter.drawRectangle(Point(4, 4), contentWidth, contentHeight); 12587 */ 12588 12589 painter.outlineColor = Color.black; 12590 // painter.drawText(Point(4, 4), content, Point(width - 4, height - 4)); 12591 12592 textLayout.caretShowingOnScreen = false; 12593 12594 textLayout.drawInto(painter, !parentWindow.win.closed && isFocused()); 12595 } 12596 } 12597 12598 static class Style : Widget.Style { 12599 override FrameStyle borderStyle() { 12600 return FrameStyle.sunk; 12601 } 12602 override MouseCursor cursor() { 12603 return GenericCursor.Text; 12604 } 12605 } 12606 mixin OverrideStyle!Style; 12607 } 12608 else static assert(false); 12609 12610 version(trash_text) 12611 version(custom_widgets) 12612 override void defaultEventHandler_mousedown(MouseDownEvent ev) { 12613 super.defaultEventHandler_mousedown(ev); 12614 if(parentWindow.win.closed) return; 12615 if(ev.button == MouseButton.left) { 12616 if(textLayout.selectNone()) 12617 redraw(); 12618 textLayout.moveCaretToPixelCoordinates(ev.clientX, ev.clientY); 12619 this.focus(); 12620 //this.parentWindow.win.grabInput(); 12621 } else if(ev.button == MouseButton.middle) { 12622 static if(UsingSimpledisplayX11) { 12623 getPrimarySelection(parentWindow.win, (in char[] txt) { 12624 textLayout.insert(txt); 12625 redraw(); 12626 12627 auto cbb = textLayout.contentBoundingBox(); 12628 setContentSize(cbb.width, cbb.height); 12629 }); 12630 } 12631 } 12632 } 12633 12634 version(trash_text) 12635 version(custom_widgets) 12636 override void defaultEventHandler_mouseup(MouseUpEvent ev) { 12637 //this.parentWindow.win.releaseInputGrab(); 12638 super.defaultEventHandler_mouseup(ev); 12639 } 12640 12641 version(trash_text) 12642 version(custom_widgets) 12643 override void defaultEventHandler_mousemove(MouseMoveEvent ev) { 12644 super.defaultEventHandler_mousemove(ev); 12645 if(ev.state & ModifierState.leftButtonDown) { 12646 textLayout.selectToPixelCoordinates(ev.clientX, ev.clientY); 12647 redraw(); 12648 } 12649 } 12650 12651 version(trash_text) 12652 version(custom_widgets) 12653 override void defaultEventHandler_focus(Event ev) { 12654 super.defaultEventHandler_focus(ev); 12655 if(parentWindow.win.closed) return; 12656 auto painter = this.draw(); 12657 textLayout.drawCaret(painter); 12658 12659 static if(SimpledisplayTimerAvailable) 12660 if(caretTimer) { 12661 caretTimer.destroy(); 12662 caretTimer = null; 12663 } 12664 12665 bool blinkingCaret = true; 12666 static if(UsingSimpledisplayX11) 12667 if(!Image.impl.xshmAvailable) 12668 blinkingCaret = false; // if on a remote connection, don't waste bandwidth on an expendable blink 12669 12670 if(blinkingCaret) 12671 static if(SimpledisplayTimerAvailable) 12672 caretTimer = new Timer(500, { 12673 if(parentWindow.win.closed) { 12674 caretTimer.destroy(); 12675 return; 12676 } 12677 if(isFocused()) { 12678 auto painter = this.draw(); 12679 textLayout.drawCaret(painter); 12680 } else if(textLayout.caretShowingOnScreen) { 12681 auto painter = this.draw(); 12682 textLayout.eraseCaret(painter); 12683 } 12684 }); 12685 } 12686 12687 version(trash_text) { 12688 private string lastContentBlur; 12689 12690 override void defaultEventHandler_blur(Event ev) { 12691 super.defaultEventHandler_blur(ev); 12692 if(parentWindow.win.closed) return; 12693 version(custom_widgets) { 12694 auto painter = this.draw(); 12695 textLayout.eraseCaret(painter); 12696 static if(SimpledisplayTimerAvailable) 12697 if(caretTimer) { 12698 caretTimer.destroy(); 12699 caretTimer = null; 12700 } 12701 } 12702 12703 if(this.content != lastContentBlur) { 12704 auto evt = new ChangeEvent!string(this, &this.content); 12705 evt.dispatch(); 12706 lastContentBlur = this.content; 12707 } 12708 } 12709 } 12710 12711 version(win32_widgets) { 12712 private string lastContentBlur; 12713 12714 override void defaultEventHandler_blur(Event ev) { 12715 super.defaultEventHandler_blur(ev); 12716 12717 if(this.content != lastContentBlur) { 12718 auto evt = new ChangeEvent!string(this, &this.content); 12719 evt.dispatch(); 12720 lastContentBlur = this.content; 12721 } 12722 } 12723 } 12724 12725 12726 version(trash_text) 12727 version(custom_widgets) 12728 override void defaultEventHandler_char(CharEvent ev) { 12729 super.defaultEventHandler_char(ev); 12730 textLayout.insert(ev.character); 12731 redraw(); 12732 12733 // FIXME: too inefficient 12734 auto cbb = textLayout.contentBoundingBox(); 12735 setContentSize(cbb.width, cbb.height); 12736 } 12737 version(trash_text) 12738 version(custom_widgets) 12739 override void defaultEventHandler_keydown(KeyDownEvent ev) { 12740 //super.defaultEventHandler_keydown(ev); 12741 switch(ev.key) { 12742 case Key.Delete: 12743 textLayout.delete_(); 12744 redraw(); 12745 break; 12746 case Key.Left: 12747 textLayout.moveLeft(); 12748 redraw(); 12749 break; 12750 case Key.Right: 12751 textLayout.moveRight(); 12752 redraw(); 12753 break; 12754 case Key.Up: 12755 textLayout.moveUp(); 12756 redraw(); 12757 break; 12758 case Key.Down: 12759 textLayout.moveDown(); 12760 redraw(); 12761 break; 12762 case Key.Home: 12763 textLayout.moveHome(); 12764 redraw(); 12765 break; 12766 case Key.End: 12767 textLayout.moveEnd(); 12768 redraw(); 12769 break; 12770 case Key.PageUp: 12771 foreach(i; 0 .. 32) 12772 textLayout.moveUp(); 12773 redraw(); 12774 break; 12775 case Key.PageDown: 12776 foreach(i; 0 .. 32) 12777 textLayout.moveDown(); 12778 redraw(); 12779 break; 12780 12781 default: 12782 {} // intentionally blank, let "char" handle it 12783 } 12784 /* 12785 if(ev.key == Key.Backspace) { 12786 textLayout.backspace(); 12787 redraw(); 12788 } 12789 */ 12790 ensureVisibleInScroll(textLayout.caretBoundingBox()); 12791 } 12792 12793 version(use_new_text_system) { 12794 bool showingVerticalScroll() { return true; } 12795 bool showingHorizontalScroll() { return true; } 12796 } 12797 } 12798 12799 /// 12800 class LineEdit : EditableTextWidget { 12801 // FIXME: hack 12802 version(custom_widgets) { 12803 override bool showingVerticalScroll() { return false; } 12804 override bool showingHorizontalScroll() { return false; } 12805 } 12806 12807 override int flexBasisWidth() { return 250; } 12808 12809 /// 12810 this(Widget parent) { 12811 super(parent); 12812 version(win32_widgets) { 12813 createWin32Window(this, "edit"w, "", 12814 0, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 12815 } else version(custom_widgets) { 12816 version(trash_text) { 12817 setupCustomTextEditing(); 12818 addEventListener(delegate(CharEvent ev) { 12819 if(ev.character == '\n') 12820 ev.preventDefault(); 12821 }); 12822 } 12823 } else static assert(false); 12824 } 12825 12826 version(use_new_text_system) 12827 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 12828 auto tdh = new TextDisplayHelper(textLayout, smw); 12829 tdh.singleLine = true; 12830 return tdh; 12831 } 12832 12833 version(win32_widgets) { 12834 mixin Padding!q{2}; 12835 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 12836 override int maxHeight() { return minHeight; } 12837 } 12838 12839 /+ 12840 @property void passwordMode(bool p) { 12841 SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) | ES_PASSWORD); 12842 } 12843 +/ 12844 } 12845 12846 /++ 12847 A [LineEdit] that displays `*` in place of the actual characters. 12848 12849 Alas, Windows requires the window to be created differently to use this style, 12850 so it had to be a new class instead of a toggle on and off on an existing object. 12851 12852 FIXME: this is not yet implemented on Linux, it will work the same as a TextEdit there for now. 12853 12854 History: 12855 Added January 24, 2021 12856 +/ 12857 class PasswordEdit : EditableTextWidget { 12858 version(custom_widgets) { 12859 override bool showingVerticalScroll() { return false; } 12860 override bool showingHorizontalScroll() { return false; } 12861 } 12862 12863 override int flexBasisWidth() { return 250; } 12864 12865 version(use_new_text_system) 12866 override TextStyle defaultTextStyle() { 12867 auto cs = getComputedStyle(); 12868 12869 auto osf = new class OperatingSystemFont { 12870 this() { 12871 super(cs.font); 12872 } 12873 override int stringWidth(scope const(char)[] text, SimpleWindow window = null) { 12874 int count = 0; 12875 foreach(dchar ch; text) 12876 count++; 12877 return count * super.stringWidth("*", window); 12878 } 12879 }; 12880 12881 return new TextDisplayHelper.MyTextStyle(osf); 12882 } 12883 12884 version(use_new_text_system) 12885 override TextDisplayHelper textDisplayHelperFactory(TextLayouter textLayout, ScrollMessageWidget smw) { 12886 static class TDH : TextDisplayHelper { 12887 this(TextLayouter textLayout, ScrollMessageWidget smw) { 12888 singleLine = true; 12889 super(textLayout, smw); 12890 } 12891 12892 override void drawTextSegment(WidgetPainter painter, Point upperLeft, scope const(char)[] text) { 12893 char[256] buffer = void; 12894 int bufferLength = 0; 12895 foreach(dchar ch; text) 12896 buffer[bufferLength++] = '*'; 12897 painter.drawText(upperLeft, buffer[0..bufferLength]); 12898 } 12899 } 12900 12901 return new TDH(textLayout, smw); 12902 } 12903 12904 /// 12905 this(Widget parent) { 12906 super(parent); 12907 version(win32_widgets) { 12908 createWin32Window(this, "edit"w, "", 12909 ES_PASSWORD, WS_EX_CLIENTEDGE);//|WS_HSCROLL|ES_AUTOHSCROLL); 12910 } else version(custom_widgets) { 12911 version(trash_text) 12912 setupCustomTextEditing(); 12913 addEventListener(delegate(CharEvent ev) { 12914 if(ev.character == '\n') 12915 ev.preventDefault(); 12916 }); 12917 } else static assert(false); 12918 } 12919 version(win32_widgets) { 12920 mixin Padding!q{2}; 12921 override int minHeight() { return borderBoxForContentBox(Rectangle(Point(0, 0), Size(0, defaultLineHeight))).height; } 12922 override int maxHeight() { return minHeight; } 12923 } 12924 } 12925 12926 12927 /// 12928 class TextEdit : EditableTextWidget { 12929 /// 12930 this(Widget parent) { 12931 super(parent); 12932 version(win32_widgets) { 12933 createWin32Window(this, "edit"w, "", 12934 0|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_WANTRETURN|ES_AUTOHSCROLL|ES_AUTOVSCROLL, WS_EX_CLIENTEDGE); 12935 } else version(custom_widgets) { 12936 version(trash_text) 12937 setupCustomTextEditing(); 12938 } else static assert(false); 12939 } 12940 override int maxHeight() { return int.max; } 12941 override int heightStretchiness() { return 7; } 12942 12943 override int flexBasisWidth() { return 250; } 12944 override int flexBasisHeight() { return 25; } 12945 } 12946 12947 12948 /++ 12949 12950 +/ 12951 version(none) 12952 class RichTextDisplay : Widget { 12953 @property void content(string c) {} 12954 void appendContent(string c) {} 12955 } 12956 12957 /// 12958 class MessageBox : Window { 12959 private string message; 12960 MessageBoxButton buttonPressed = MessageBoxButton.None; 12961 /// 12962 this(string message, string[] buttons = ["OK"], MessageBoxButton[] buttonIds = [MessageBoxButton.OK]) { 12963 super(300, 100); 12964 12965 assert(buttons.length); 12966 assert(buttons.length == buttonIds.length); 12967 12968 this.message = message; 12969 12970 int buttonsWidth = cast(int) buttons.length * 50 + (cast(int) buttons.length - 1) * 16; 12971 buttonsWidth = scaleWithDpi(buttonsWidth); 12972 12973 int x = this.width / 2 - buttonsWidth / 2; 12974 12975 foreach(idx, buttonText; buttons) { 12976 auto button = new Button(buttonText, this); 12977 button.x = x; 12978 button.y = height - (button.height + 10); 12979 button.addEventListener(EventType.triggered, ((size_t idx) { return () { 12980 this.buttonPressed = buttonIds[idx]; 12981 win.close(); 12982 }; })(idx)); 12983 12984 button.registerMovement(); 12985 x += button.width; 12986 x += scaleWithDpi(16); 12987 if(idx == 0) 12988 button.focus(); 12989 } 12990 12991 win.show(); 12992 redraw(); 12993 } 12994 12995 override void paint(WidgetPainter painter) { 12996 super.paint(painter); 12997 12998 auto cs = getComputedStyle(); 12999 13000 painter.outlineColor = cs.foregroundColor(); 13001 painter.fillColor = cs.foregroundColor(); 13002 13003 painter.drawText(Point(0, 0), message, Point(width, height / 2), TextAlignment.Center | TextAlignment.VerticalCenter); 13004 } 13005 13006 // this one is all fixed position 13007 override void recomputeChildLayout() {} 13008 } 13009 13010 /// 13011 enum MessageBoxStyle { 13012 OK, /// 13013 OKCancel, /// 13014 RetryCancel, /// 13015 YesNo, /// 13016 YesNoCancel, /// 13017 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. 13018 } 13019 13020 /// 13021 enum MessageBoxIcon { 13022 None, /// 13023 Info, /// 13024 Warning, /// 13025 Error /// 13026 } 13027 13028 /// Identifies the button the user pressed on a message box. 13029 enum MessageBoxButton { 13030 None, /// The user closed the message box without clicking any of the buttons. 13031 OK, /// 13032 Cancel, /// 13033 Retry, /// 13034 Yes, /// 13035 No, /// 13036 Continue /// 13037 } 13038 13039 13040 /++ 13041 Displays a modal message box, blocking until the user dismisses it. 13042 13043 Returns: the button pressed. 13044 +/ 13045 MessageBoxButton messageBox(string title, string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13046 version(win32_widgets) { 13047 WCharzBuffer t = WCharzBuffer(title); 13048 WCharzBuffer m = WCharzBuffer(message); 13049 UINT type; 13050 with(MessageBoxStyle) 13051 final switch(style) { 13052 case OK: type |= MB_OK; break; 13053 case OKCancel: type |= MB_OKCANCEL; break; 13054 case RetryCancel: type |= MB_RETRYCANCEL; break; 13055 case YesNo: type |= MB_YESNO; break; 13056 case YesNoCancel: type |= MB_YESNOCANCEL; break; 13057 case RetryCancelContinue: type |= MB_CANCELTRYCONTINUE; break; 13058 } 13059 with(MessageBoxIcon) 13060 final switch(icon) { 13061 case None: break; 13062 case Info: type |= MB_ICONINFORMATION; break; 13063 case Warning: type |= MB_ICONWARNING; break; 13064 case Error: type |= MB_ICONERROR; break; 13065 } 13066 switch(MessageBoxW(null, m.ptr, t.ptr, type)) { 13067 case IDOK: return MessageBoxButton.OK; 13068 case IDCANCEL: return MessageBoxButton.Cancel; 13069 case IDTRYAGAIN, IDRETRY: return MessageBoxButton.Retry; 13070 case IDYES: return MessageBoxButton.Yes; 13071 case IDNO: return MessageBoxButton.No; 13072 case IDCONTINUE: return MessageBoxButton.Continue; 13073 default: return MessageBoxButton.None; 13074 } 13075 } else { 13076 string[] buttons; 13077 MessageBoxButton[] buttonIds; 13078 with(MessageBoxStyle) 13079 final switch(style) { 13080 case OK: 13081 buttons = ["OK"]; 13082 buttonIds = [MessageBoxButton.OK]; 13083 break; 13084 case OKCancel: 13085 buttons = ["OK", "Cancel"]; 13086 buttonIds = [MessageBoxButton.OK, MessageBoxButton.Cancel]; 13087 break; 13088 case RetryCancel: 13089 buttons = ["Retry", "Cancel"]; 13090 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel]; 13091 break; 13092 case YesNo: 13093 buttons = ["Yes", "No"]; 13094 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No]; 13095 break; 13096 case YesNoCancel: 13097 buttons = ["Yes", "No", "Cancel"]; 13098 buttonIds = [MessageBoxButton.Yes, MessageBoxButton.No, MessageBoxButton.Cancel]; 13099 break; 13100 case RetryCancelContinue: 13101 buttons = ["Try Again", "Cancel", "Continue"]; 13102 buttonIds = [MessageBoxButton.Retry, MessageBoxButton.Cancel, MessageBoxButton.Continue]; 13103 break; 13104 } 13105 auto mb = new MessageBox(message, buttons, buttonIds); 13106 EventLoop el = EventLoop.get; 13107 el.run(() { return !mb.win.closed; }); 13108 return mb.buttonPressed; 13109 } 13110 } 13111 13112 /// ditto 13113 int messageBox(string message, MessageBoxStyle style = MessageBoxStyle.OK, MessageBoxIcon icon = MessageBoxIcon.None) { 13114 return messageBox(null, message, style, icon); 13115 } 13116 13117 13118 13119 /// 13120 alias void delegate(Widget handlerAttachedTo, Event event) EventHandler; 13121 13122 /++ 13123 This is an opaque type you can use to disconnect an event handler when you're no longer interested. 13124 13125 History: 13126 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. 13127 +/ 13128 struct EventListener { 13129 private Widget widget; 13130 private string event; 13131 private EventHandler handler; 13132 private bool useCapture; 13133 13134 /// 13135 void disconnect() { 13136 widget.removeEventListener(this); 13137 } 13138 } 13139 13140 /++ 13141 The purpose of this enum was to give a compile-time checked version of various standard event strings. 13142 13143 Now, I recommend you use a statically typed event object instead. 13144 13145 See_Also: [Event] 13146 +/ 13147 enum EventType : string { 13148 click = "click", /// 13149 13150 mouseenter = "mouseenter", /// 13151 mouseleave = "mouseleave", /// 13152 mousein = "mousein", /// 13153 mouseout = "mouseout", /// 13154 mouseup = "mouseup", /// 13155 mousedown = "mousedown", /// 13156 mousemove = "mousemove", /// 13157 13158 keydown = "keydown", /// 13159 keyup = "keyup", /// 13160 char_ = "char", /// 13161 13162 focus = "focus", /// 13163 blur = "blur", /// 13164 13165 triggered = "triggered", /// 13166 13167 change = "change", /// 13168 } 13169 13170 /++ 13171 Represents an event that is currently being processed. 13172 13173 13174 Minigui's event model is based on the web browser. An event has a name, a target, 13175 and an associated data object. It starts from the window and works its way down through 13176 the target through all intermediate [Widget]s, triggering capture phase handlers as it goes, 13177 then goes back up again all the way back to the window, triggering bubble phase handlers. At 13178 the end, if [Event.preventDefault] has not been called, it calls the target widget's default 13179 handlers for the event (please note that default handlers will be called even if [Event.stopPropagation] 13180 was called; that just stops it from calling other handlers in the widget tree, but the default happens 13181 whenever propagation is done, not only if it gets to the end of the chain). 13182 13183 This model has several nice points: 13184 13185 $(LIST 13186 * It is easy to delegate dynamic handlers to a parent. You can have a parent container 13187 with event handlers set, then add/remove children as much as you want without needing 13188 to manage the event handlers on them - the parent alone can manage everything. 13189 13190 * It is easy to create new custom events in your application. 13191 13192 * It is familiar to many web developers. 13193 ) 13194 13195 There's a few downsides though: 13196 13197 $(LIST 13198 * There's not a lot of type safety. 13199 13200 * You don't get a static list of what events a widget can emit. 13201 13202 * Tracing where an event got cancelled along the chain can get difficult; the downside of 13203 the central delegation benefit is it can be lead to debugging of action at a distance. 13204 ) 13205 13206 In May 2021, I started to adjust this model to minigui takes better advantage of D over Javascript 13207 while keeping the benefits - and most compatibility with - the existing model. The main idea is 13208 to simply use a D object type which provides a static interface as well as a built-in event name. 13209 Then, a new static interface allows you to see what an event can emit and attach handlers to it 13210 similarly to C#, which just forwards to the JS style api. They're fully compatible so you can still 13211 delegate to a parent and use custom events as well as using the runtime dynamic access, in addition 13212 to having a little more help from the D compiler and documentation generator. 13213 13214 Your code would change like this: 13215 13216 --- 13217 // old 13218 widget.addEventListener("keydown", (Event ev) { ... }, /* optional arg */ useCapture ); 13219 13220 // new 13221 widget.addEventListener((KeyDownEvent ev) { ... }, /* optional arg */ useCapture ); 13222 --- 13223 13224 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. 13225 13226 All you have to do is replace the string with a specific Event subclass. It will figure out the event string from the class. 13227 13228 Alternatively, you can cast the Event yourself to the appropriate subclass, but it is easier to let the library do it for you! 13229 13230 Thus the family of functions are: 13231 13232 [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. 13233 13234 [Widget.addDirectEventListener] is addEventListener, but only calls the handler if target == this. Useful for something you can't afford to delegate. 13235 13236 [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. 13237 13238 Let's implement a custom widget that can emit a ChangeEvent describing its `checked` property: 13239 13240 --- 13241 class MyCheckbox : Widget { 13242 /// This gives a chance to document it and generates a convenience function to send it and attach handlers. 13243 /// It is NOT actually required but should be used whenever possible. 13244 mixin Emits!(ChangeEvent!bool); 13245 13246 this(Widget parent) { 13247 super(parent); 13248 setDefaultEventHandler((ClickEvent) { checked = !checked; }); 13249 } 13250 13251 private bool _checked; 13252 @property bool checked() { return _checked; } 13253 @property void checked(bool set) { 13254 _checked = set; 13255 emit!(ChangeEvent!bool)(&checked); 13256 } 13257 } 13258 --- 13259 13260 ## Creating Your Own Events 13261 13262 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. 13263 13264 --- 13265 class MyEvent : Event { 13266 this(Widget target) { super(EventString, target); } 13267 mixin Register; // adds EventString and other reflection information 13268 } 13269 --- 13270 13271 Then declare that it is sent with the [Emits] mixin, so you can use [Widget.emit] to dispatch it. 13272 13273 History: 13274 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. 13275 13276 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. 13277 +/ 13278 /+ 13279 13280 ## General Conventions 13281 13282 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. 13283 13284 13285 ## Qt-style signals and slots 13286 13287 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. 13288 13289 The intention is for events to be used when 13290 13291 --- 13292 class Demo : Widget { 13293 this() { 13294 myPropertyChanged = Signal!int(this); 13295 } 13296 @property myProperty(int v) { 13297 myPropertyChanged.emit(v); 13298 } 13299 13300 Signal!int myPropertyChanged; // i need to get `this` off it and inspect the name... 13301 // but it can just genuinely not care about `this` since that's not really passed. 13302 } 13303 13304 class Foo : Widget { 13305 // the slot uda is not necessary, but it helps the script and ui builder find it. 13306 @slot void setValue(int v) { ... } 13307 } 13308 13309 demo.myPropertyChanged.connect(&foo.setValue); 13310 --- 13311 13312 The Signal type has a disabled default constructor, meaning your widget constructor must pass `this` to it in its constructor. 13313 13314 Some events may also wish to implement the Signal interface. These use particular arguments to call a method automatically. 13315 13316 class StringChangeEvent : ChangeEvent, Signal!string { 13317 mixin SignalImpl 13318 } 13319 13320 +/ 13321 class Event : ReflectableProperties { 13322 /// Creates an event without populating any members and without sending it. See [dispatch] 13323 this(string eventName, Widget emittedBy) { 13324 this.eventName = eventName; 13325 this.srcElement = emittedBy; 13326 } 13327 13328 13329 /// Implementations for the [ReflectableProperties] interface/ 13330 void getPropertiesList(scope void delegate(string name) sink) const {} 13331 /// ditto 13332 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { } 13333 /// ditto 13334 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson) { 13335 return SetPropertyResult.notPermitted; 13336 } 13337 13338 13339 /+ 13340 /++ 13341 This is an internal implementation detail of [Register] and is subject to be changed or removed at any time without notice. 13342 13343 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. 13344 +/ 13345 protected final void sinkJsonString(string memberName, scope const(char)[] value, scope void delegate(string name, scope const(char)[] value) finalSink) { 13346 if(value.length == 0) { 13347 finalSink(memberName, `""`); 13348 return; 13349 } 13350 13351 char[1024] bufferBacking; 13352 char[] buffer = bufferBacking; 13353 int bufferPosition; 13354 13355 void sink(char ch) { 13356 if(bufferPosition >= buffer.length) 13357 buffer.length = buffer.length + 1024; 13358 buffer[bufferPosition++] = ch; 13359 } 13360 13361 sink('"'); 13362 13363 foreach(ch; value) { 13364 switch(ch) { 13365 case '\\': 13366 sink('\\'); sink('\\'); 13367 break; 13368 case '"': 13369 sink('\\'); sink('"'); 13370 break; 13371 case '\n': 13372 sink('\\'); sink('n'); 13373 break; 13374 case '\r': 13375 sink('\\'); sink('r'); 13376 break; 13377 case '\t': 13378 sink('\\'); sink('t'); 13379 break; 13380 default: 13381 sink(ch); 13382 } 13383 } 13384 13385 sink('"'); 13386 13387 finalSink(memberName, buffer[0 .. bufferPosition]); 13388 } 13389 +/ 13390 13391 /+ 13392 enum EventInitiator { 13393 system, 13394 minigui, 13395 user 13396 } 13397 13398 immutable EventInitiator; initiatedBy; 13399 +/ 13400 13401 /++ 13402 Events should generally follow the propagation model, but there's some exceptions 13403 to that rule. If so, they should override this to return false. In that case, only 13404 bubbling event handlers on the target itself and capturing event handlers on the containing 13405 window will be called. (That is, [dispatch] will call [sendDirectly] instead of doing the normal 13406 capture -> target -> bubble process.) 13407 13408 History: 13409 Added May 12, 2021 13410 +/ 13411 bool propagates() const pure nothrow @nogc @safe { 13412 return true; 13413 } 13414 13415 /++ 13416 hints as to whether preventDefault will actually do anything. not entirely reliable. 13417 13418 History: 13419 Added May 14, 2021 13420 +/ 13421 bool cancelable() const pure nothrow @nogc @safe { 13422 return true; 13423 } 13424 13425 /++ 13426 You can mix this into child class to register some boilerplate. It includes the `EventString` 13427 member, a constructor, and implementations of the dynamic get data interfaces. 13428 13429 If you fail to do this, your event will probably not have full compatibility but it might still work for you. 13430 13431 13432 You can override the default EventString by simply providing your own in the form of 13433 `enum string EventString = "some.name";` The default is the name of your class and its parent entity 13434 which provides some namespace protection against conflicts in other libraries while still being fairly 13435 easy to use. 13436 13437 If you provide your own constructor, it will override the default constructor provided here. A constructor 13438 must call `super(EventString, passed_widget_target)` at some point. The `passed_widget_target` must be the 13439 first argument to your constructor. 13440 13441 History: 13442 Added May 13, 2021. 13443 +/ 13444 protected static mixin template Register() { 13445 public enum string EventString = __traits(identifier, __traits(parent, typeof(this))) ~ "." ~ __traits(identifier, typeof(this)); 13446 this(Widget target) { super(EventString, target); } 13447 13448 mixin ReflectableProperties.RegisterGetters; 13449 } 13450 13451 /++ 13452 This is the widget that emitted the event. 13453 13454 13455 The aliased names come from Javascript for ease of web developers to transition in, but they're all synonyms. 13456 13457 History: 13458 The `source` name was added on May 14, 2021. It is a little weird that `source` and `target` are synonyms, 13459 but that's a side effect of it doing both capture and bubble handlers and people are used to it from the web 13460 so I don't intend to remove these aliases. 13461 +/ 13462 Widget source; 13463 /// ditto 13464 alias source target; 13465 /// ditto 13466 alias source srcElement; 13467 13468 Widget relatedTarget; /// Note: likely to be deprecated at some point. 13469 13470 /// Prevents the default event handler (if there is one) from being called 13471 void preventDefault() { 13472 lastDefaultPrevented = true; 13473 defaultPrevented = true; 13474 } 13475 13476 /// Stops the event propagation immediately. 13477 void stopPropagation() { 13478 propagationStopped = true; 13479 } 13480 13481 private bool defaultPrevented; 13482 private bool propagationStopped; 13483 private string eventName; 13484 13485 private bool isBubbling; 13486 13487 /// 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. 13488 protected void adjustScrolling() { } 13489 /// ditto 13490 protected void adjustClientCoordinates(int deltaX, int deltaY) { } 13491 13492 /++ 13493 this sends it only to the target. If you want propagation, use dispatch() instead. 13494 13495 This should be made private!!! 13496 13497 +/ 13498 void sendDirectly() { 13499 if(srcElement is null) 13500 return; 13501 13502 // 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. 13503 13504 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 13505 //target.parentWindow.devTools.log("Event ", eventName, " dispatched directly to ", srcElement); 13506 13507 adjustScrolling(); 13508 13509 if(auto e = target.parentWindow) { 13510 if(auto handlers = "*" in e.capturingEventHandlers) 13511 foreach(handler; *handlers) 13512 if(handler) handler(e, this); 13513 if(auto handlers = eventName in e.capturingEventHandlers) 13514 foreach(handler; *handlers) 13515 if(handler) handler(e, this); 13516 } 13517 13518 auto e = srcElement; 13519 13520 if(auto handlers = eventName in e.bubblingEventHandlers) 13521 foreach(handler; *handlers) 13522 if(handler) handler(e, this); 13523 13524 if(auto handlers = "*" in e.bubblingEventHandlers) 13525 foreach(handler; *handlers) 13526 if(handler) handler(e, this); 13527 13528 // there's never a default for a catch-all event 13529 if(!defaultPrevented) 13530 if(eventName in e.defaultEventHandlers) 13531 e.defaultEventHandlers[eventName](e, this); 13532 } 13533 13534 /// this dispatches the element using the capture -> target -> bubble process 13535 void dispatch() { 13536 if(srcElement is null) 13537 return; 13538 13539 if(!propagates) { 13540 sendDirectly; 13541 return; 13542 } 13543 13544 //debug if(eventName != "mousemove" && target !is null && target.parentWindow && target.parentWindow.devTools) 13545 //target.parentWindow.devTools.log("Event ", eventName, " dispatched to ", srcElement); 13546 13547 adjustScrolling(); 13548 // first capture, then bubble 13549 13550 Widget[] chain; 13551 Widget curr = srcElement; 13552 while(curr) { 13553 auto l = curr; 13554 chain ~= l; 13555 curr = curr.parent; 13556 } 13557 13558 isBubbling = false; 13559 13560 foreach_reverse(e; chain) { 13561 if(auto handlers = "*" in e.capturingEventHandlers) 13562 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13563 13564 if(propagationStopped) 13565 break; 13566 13567 if(auto handlers = eventName in e.capturingEventHandlers) 13568 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13569 13570 // the default on capture should really be to always do nothing 13571 13572 //if(!defaultPrevented) 13573 // if(eventName in e.defaultEventHandlers) 13574 // e.defaultEventHandlers[eventName](e.element, this); 13575 13576 if(propagationStopped) 13577 break; 13578 } 13579 13580 int adjustX; 13581 int adjustY; 13582 13583 isBubbling = true; 13584 if(!propagationStopped) 13585 foreach(e; chain) { 13586 if(auto handlers = eventName in e.bubblingEventHandlers) 13587 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13588 13589 if(propagationStopped) 13590 break; 13591 13592 if(auto handlers = "*" in e.bubblingEventHandlers) 13593 foreach(handler; *handlers) if(handler !is null) handler(e, this); 13594 13595 if(propagationStopped) 13596 break; 13597 13598 if(e.encapsulatedChildren()) { 13599 adjustClientCoordinates(adjustX, adjustY); 13600 target = e; 13601 } else { 13602 adjustX += e.x; 13603 adjustY += e.y; 13604 } 13605 } 13606 13607 if(!defaultPrevented) 13608 foreach(e; chain) { 13609 if(eventName in e.defaultEventHandlers) 13610 e.defaultEventHandlers[eventName](e, this); 13611 } 13612 } 13613 13614 13615 /* old compatibility things */ 13616 deprecated("Use some subclass of KeyEventBase instead of plain Event in your handler going forward. WARNING these may crash on non-key events!") 13617 final @property { 13618 Key key() { return (cast(KeyEventBase) this).key; } 13619 KeyEvent originalKeyEvent() { return (cast(KeyEventBase) this).originalKeyEvent; } 13620 13621 bool ctrlKey() { return (cast(KeyEventBase) this).ctrlKey; } 13622 bool altKey() { return (cast(KeyEventBase) this).altKey; } 13623 bool shiftKey() { return (cast(KeyEventBase) this).shiftKey; } 13624 } 13625 13626 deprecated("Use some subclass of MouseEventBase instead of Event in your handler going forward. WARNING these may crash on non-mouse events!") 13627 final @property { 13628 int clientX() { return (cast(MouseEventBase) this).clientX; } 13629 int clientY() { return (cast(MouseEventBase) this).clientY; } 13630 13631 int viewportX() { return (cast(MouseEventBase) this).viewportX; } 13632 int viewportY() { return (cast(MouseEventBase) this).viewportY; } 13633 13634 int button() { return (cast(MouseEventBase) this).button; } 13635 int buttonLinear() { return (cast(MouseEventBase) this).buttonLinear; } 13636 } 13637 13638 deprecated("Use either a KeyEventBase or a MouseEventBase instead of Event in your handler going forward") 13639 final @property { 13640 int state() { 13641 if(auto meb = cast(MouseEventBase) this) 13642 return meb.state; 13643 if(auto keb = cast(KeyEventBase) this) 13644 return keb.state; 13645 assert(0); 13646 } 13647 } 13648 13649 deprecated("Use a CharEvent instead of Event in your handler going forward") 13650 final @property { 13651 dchar character() { 13652 if(auto ce = cast(CharEvent) this) 13653 return ce.character; 13654 return dchar.init; 13655 } 13656 } 13657 13658 // for change events 13659 @property { 13660 /// 13661 int intValue() { return 0; } 13662 /// 13663 string stringValue() { return null; } 13664 } 13665 } 13666 13667 /++ 13668 This lets you statically verify you send the events you claim you send and gives you a hook to document them. 13669 13670 Please note that a widget may send events not listed as Emits. You can always construct and dispatch 13671 dynamic and custom events, but the static list helps ensure you get them right. 13672 13673 If this is declared, you can use [Widget.emit] to send the event. 13674 13675 All events work the same way though, following the capture->widget->bubble model described under [Event]. 13676 13677 History: 13678 Added May 4, 2021 13679 +/ 13680 mixin template Emits(EventType) { 13681 import arsd.minigui : EventString; 13682 static if(is(EventType : Event) && !is(EventType == Event)) 13683 mixin("private EventType[0] emits_" ~ EventStringIdentifier!EventType ~";"); 13684 else 13685 static assert(0, "You can only emit subclasses of Event"); 13686 } 13687 13688 /// ditto 13689 mixin template Emits(string eventString) { 13690 mixin("private Event[0] emits_" ~ eventString ~";"); 13691 } 13692 13693 /* 13694 class SignalEvent(string name) : Event { 13695 13696 } 13697 */ 13698 13699 /++ 13700 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". 13701 13702 13703 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. 13704 13705 History: 13706 Added on May 13, 2021. Prior to that, you'd most likely `addEventListener(EventType.triggered, ...)` to handle similar things. 13707 +/ 13708 class CommandEvent : Event { 13709 enum EventString = "command"; 13710 this(Widget source, string CommandString = EventString) { 13711 super(CommandString, source); 13712 } 13713 } 13714 13715 /++ 13716 A [CommandEvent] is typically actually an instance of these to hold the strongly-typed arguments. 13717 +/ 13718 class CommandEventWithArgs(Args...) : CommandEvent { 13719 this(Widget source, string CommandString, Args args) { super(source, CommandString); this.args = args; } 13720 Args args; 13721 } 13722 13723 /++ 13724 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. 13725 13726 See [CommandEvent] for more information. 13727 13728 Returns: 13729 The [EventListener] you can use to remove the handler. 13730 +/ 13731 EventListener consumesCommand(string CommandString, WidgetType, Args...)(WidgetType w, void delegate(Args) handler) { 13732 return w.addEventListener(CommandString, (Event ev) { 13733 if(ev.target is w) 13734 return; // it does not consume its own commands! 13735 if(auto cev = cast(CommandEventWithArgs!Args) ev) { 13736 handler(cev.args); 13737 ev.stopPropagation(); 13738 } 13739 }); 13740 } 13741 13742 /++ 13743 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. 13744 +/ 13745 void emitCommand(string CommandString, WidgetType, Args...)(WidgetType w, Args args) { 13746 auto event = new CommandEventWithArgs!Args(w, CommandString, args); 13747 event.dispatch(); 13748 } 13749 13750 class ResizeEvent : Event { 13751 enum EventString = "resize"; 13752 13753 this(Widget target) { super(EventString, target); } 13754 13755 override bool propagates() const { return false; } 13756 } 13757 13758 /++ 13759 ClosingEvent is fired when a user is attempting to close a window. You can `preventDefault` to cancel the close. 13760 13761 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. 13762 13763 History: 13764 Added June 21, 2021 (dub v10.1) 13765 +/ 13766 class ClosingEvent : Event { 13767 enum EventString = "closing"; 13768 13769 this(Widget target) { super(EventString, target); } 13770 13771 override bool propagates() const { return false; } 13772 override bool cancelable() const { return true; } 13773 } 13774 13775 /// ditto 13776 class ClosedEvent : Event { 13777 enum EventString = "closed"; 13778 13779 this(Widget target) { super(EventString, target); } 13780 13781 override bool propagates() const { return false; } 13782 override bool cancelable() const { return false; } 13783 } 13784 13785 /// 13786 class BlurEvent : Event { 13787 enum EventString = "blur"; 13788 13789 // FIXME: related target? 13790 this(Widget target) { super(EventString, target); } 13791 13792 override bool propagates() const { return false; } 13793 } 13794 13795 /// 13796 class FocusEvent : Event { 13797 enum EventString = "focus"; 13798 13799 // FIXME: related target? 13800 this(Widget target) { super(EventString, target); } 13801 13802 override bool propagates() const { return false; } 13803 } 13804 13805 /++ 13806 FocusInEvent is a FocusEvent that propagates, while FocusOutEvent is a BlurEvent that propagates. 13807 13808 History: 13809 Added July 3, 2021 13810 +/ 13811 class FocusInEvent : Event { 13812 enum EventString = "focusin"; 13813 13814 // FIXME: related target? 13815 this(Widget target) { super(EventString, target); } 13816 13817 override bool cancelable() const { return false; } 13818 } 13819 13820 /// ditto 13821 class FocusOutEvent : Event { 13822 enum EventString = "focusout"; 13823 13824 // FIXME: related target? 13825 this(Widget target) { super(EventString, target); } 13826 13827 override bool cancelable() const { return false; } 13828 } 13829 13830 /// 13831 class ScrollEvent : Event { 13832 enum EventString = "scroll"; 13833 this(Widget target) { super(EventString, target); } 13834 13835 override bool cancelable() const { return false; } 13836 } 13837 13838 /++ 13839 Indicates that a character has been typed by the user. Normally dispatched to the currently focused widget. 13840 13841 History: 13842 Added May 2, 2021. Previously, this was simply a "char" event and `character` as a member of the [Event] base class. 13843 +/ 13844 class CharEvent : Event { 13845 enum EventString = "char"; 13846 this(Widget target, dchar ch) { 13847 character = ch; 13848 super(EventString, target); 13849 } 13850 13851 immutable dchar character; 13852 } 13853 13854 /++ 13855 You should generally use a `ChangeEvent!Type` instead of this directly. See [ChangeEvent] for more information. 13856 +/ 13857 abstract class ChangeEventBase : Event { 13858 enum EventString = "change"; 13859 this(Widget target) { 13860 super(EventString, target); 13861 } 13862 13863 /+ 13864 // idk where or how exactly i want to do this. 13865 // i might come back to it later. 13866 13867 // If a widget itself broadcasts one of theses itself, it stops propagation going down 13868 // this way the source doesn't get too confused (think of a nested scroll widget) 13869 // 13870 // the idea is like the scroll bar emits a command event saying like "scroll left one line" 13871 // then you consume that command and change you scroll x position to whatever. then you do 13872 // some kind of change event that is broadcast back to the children and any horizontal scroll 13873 // listeners are now able to update, without having an explicit connection between them. 13874 void broadcastToChildren(string fieldName) { 13875 13876 } 13877 +/ 13878 } 13879 13880 /++ 13881 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. 13882 13883 13884 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). 13885 13886 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);` 13887 13888 Since it is emitted after the value has already changed, [preventDefault] is unlikely to do anything. 13889 13890 History: 13891 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. 13892 +/ 13893 class ChangeEvent(T) : ChangeEventBase { 13894 this(Widget target, T delegate() getNewValue) { 13895 assert(getNewValue !is null); 13896 this.getNewValue = getNewValue; 13897 super(target); 13898 } 13899 13900 private T delegate() getNewValue; 13901 13902 /++ 13903 Gets the new value that just changed. 13904 +/ 13905 @property T value() { 13906 return getNewValue(); 13907 } 13908 13909 /// compatibility method for old generic Events 13910 static if(is(immutable T == immutable int)) 13911 override int intValue() { return value; } 13912 /// ditto 13913 static if(is(immutable T == immutable string)) 13914 override string stringValue() { return value; } 13915 } 13916 13917 /++ 13918 Contains shared properties for [KeyDownEvent]s and [KeyUpEvent]s. 13919 13920 13921 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 13922 13923 History: 13924 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 13925 +/ 13926 abstract class KeyEventBase : Event { 13927 this(string name, Widget target) { 13928 super(name, target); 13929 } 13930 13931 // for key events 13932 Key key; /// 13933 13934 KeyEvent originalKeyEvent; 13935 13936 /++ 13937 Indicates the current state of the given keyboard modifier keys. 13938 13939 History: 13940 Added to events on April 15, 2020. 13941 +/ 13942 bool ctrlKey; 13943 13944 /// ditto 13945 bool altKey; 13946 13947 /// ditto 13948 bool shiftKey; 13949 13950 /++ 13951 The raw bitflags that are parsed out into [ctrlKey], [altKey], and [shiftKey]. 13952 13953 See [arsd.simpledisplay.ModifierState] for other possible flags. 13954 +/ 13955 int state; 13956 13957 mixin Register; 13958 } 13959 13960 /++ 13961 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]. 13962 13963 13964 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 13965 13966 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. 13967 13968 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. 13969 13970 See_Also: [KeyUpEvent], [CharEvent] 13971 13972 History: 13973 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keydown" event listeners. 13974 +/ 13975 class KeyDownEvent : KeyEventBase { 13976 enum EventString = "keydown"; 13977 this(Widget target) { super(EventString, target); } 13978 } 13979 13980 /++ 13981 Indicates that the user has released a key on the keyboard. For available properties, see [KeyEventBase]. 13982 13983 13984 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 13985 13986 See_Also: [KeyDownEvent], [CharEvent] 13987 13988 History: 13989 Added May 2, 2021. Previously, it was only seen as the base [Event] class on "keyup" event listeners. 13990 +/ 13991 class KeyUpEvent : KeyEventBase { 13992 enum EventString = "keyup"; 13993 this(Widget target) { super(EventString, target); } 13994 } 13995 13996 /++ 13997 Contains shared properties for various mouse events; 13998 13999 14000 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14001 14002 History: 14003 Added May 2, 2021. Previously, its properties were members of the [Event] base class. 14004 +/ 14005 abstract class MouseEventBase : Event { 14006 this(string name, Widget target) { 14007 super(name, target); 14008 } 14009 14010 // for mouse events 14011 int clientX; /// The mouse event location relative to the target widget 14012 int clientY; /// ditto 14013 14014 int viewportX; /// The mouse event location relative to the window origin 14015 int viewportY; /// ditto 14016 14017 int button; /// See: [MouseEvent.button] 14018 int buttonLinear; /// See: [MouseEvent.buttonLinear] 14019 14020 /++ 14021 Indicates the current state of the given keyboard modifier keys. 14022 14023 History: 14024 Added to mouse events on September 28, 2010. 14025 +/ 14026 bool ctrlKey; 14027 14028 /// ditto 14029 bool altKey; 14030 14031 /// ditto 14032 bool shiftKey; 14033 14034 14035 14036 int state; /// 14037 14038 /++ 14039 for consistent names with key event. 14040 14041 History: 14042 Added September 28, 2021 (dub v10.3) 14043 +/ 14044 alias modifierState = state; 14045 14046 /++ 14047 Mouse wheel movement sends down/up/click events just like other buttons clicking. This method is to help you filter that out. 14048 14049 History: 14050 Added May 15, 2021 14051 +/ 14052 bool isMouseWheel() { 14053 return button == MouseButton.wheelUp || button == MouseButton.wheelDown; 14054 } 14055 14056 // private 14057 override void adjustClientCoordinates(int deltaX, int deltaY) { 14058 clientX += deltaX; 14059 clientY += deltaY; 14060 } 14061 14062 override void adjustScrolling() { 14063 version(custom_widgets) { // TEMP 14064 viewportX = clientX; 14065 viewportY = clientY; 14066 if(auto se = cast(ScrollableWidget) srcElement) { 14067 clientX += se.scrollOrigin.x; 14068 clientY += se.scrollOrigin.y; 14069 } else if(auto se = cast(ScrollableContainerWidget) srcElement) { 14070 //clientX += se.scrollX_; 14071 //clientY += se.scrollY_; 14072 } 14073 } 14074 } 14075 14076 mixin Register; 14077 } 14078 14079 /++ 14080 Indicates that the user has worked with the mouse over your widget. For available properties, see [MouseEventBase]. 14081 14082 14083 $(WARNING 14084 Important: MouseDownEvent, MouseUpEvent, ClickEvent, and DoubleClickEvent are all sent for all mouse buttons and 14085 for wheel movement! You should check the [MouseEventBase.button|button] property in most your handlers to get correct 14086 behavior. 14087 ) 14088 14089 [MouseDownEvent] is sent when the user presses a mouse button. It is also sent on mouse wheel movement. 14090 14091 [MouseUpEvent] is sent when the user releases a mouse button. 14092 14093 [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.) 14094 14095 [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. 14096 14097 [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. 14098 14099 [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. 14100 14101 [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. 14102 14103 [MouseEnterEvent] is sent when the mouse enters the bounding box of a widget. 14104 14105 [MouseLeaveEvent] is sent when the mouse leaves the bounding box of a widget. 14106 14107 You can construct these yourself, but generally the system will send them to you and there's little need to emit your own. 14108 14109 Rationale: 14110 14111 If you only want to do drag, mousedown/up works just fine being consistently sent. 14112 14113 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). 14114 14115 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. 14116 14117 History: 14118 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. 14119 +/ 14120 class MouseUpEvent : MouseEventBase { 14121 enum EventString = "mouseup"; /// 14122 this(Widget target) { super(EventString, target); } 14123 } 14124 /// ditto 14125 class MouseDownEvent : MouseEventBase { 14126 enum EventString = "mousedown"; /// 14127 this(Widget target) { super(EventString, target); } 14128 } 14129 /// ditto 14130 class MouseMoveEvent : MouseEventBase { 14131 enum EventString = "mousemove"; /// 14132 this(Widget target) { super(EventString, target); } 14133 } 14134 /// ditto 14135 class ClickEvent : MouseEventBase { 14136 enum EventString = "click"; /// 14137 this(Widget target) { super(EventString, target); } 14138 } 14139 /// ditto 14140 class DoubleClickEvent : MouseEventBase { 14141 enum EventString = "dblclick"; /// 14142 this(Widget target) { super(EventString, target); } 14143 } 14144 /// ditto 14145 class MouseOverEvent : Event { 14146 enum EventString = "mouseover"; /// 14147 this(Widget target) { super(EventString, target); } 14148 } 14149 /// ditto 14150 class MouseOutEvent : Event { 14151 enum EventString = "mouseout"; /// 14152 this(Widget target) { super(EventString, target); } 14153 } 14154 /// ditto 14155 class MouseEnterEvent : Event { 14156 enum EventString = "mouseenter"; /// 14157 this(Widget target) { super(EventString, target); } 14158 14159 override bool propagates() const { return false; } 14160 } 14161 /// ditto 14162 class MouseLeaveEvent : Event { 14163 enum EventString = "mouseleave"; /// 14164 this(Widget target) { super(EventString, target); } 14165 14166 override bool propagates() const { return false; } 14167 } 14168 14169 private bool isAParentOf(Widget a, Widget b) { 14170 if(a is null || b is null) 14171 return false; 14172 14173 while(b !is null) { 14174 if(a is b) 14175 return true; 14176 b = b.parent; 14177 } 14178 14179 return false; 14180 } 14181 14182 private struct WidgetAtPointResponse { 14183 Widget widget; 14184 14185 // x, y relative to the widget in the response. 14186 int x; 14187 int y; 14188 } 14189 14190 private WidgetAtPointResponse widgetAtPoint(Widget starting, int x, int y) { 14191 assert(starting !is null); 14192 14193 starting.addScrollPosition(x, y); 14194 14195 auto child = starting.getChildAtPosition(x, y); 14196 while(child) { 14197 if(child.hidden) 14198 continue; 14199 starting = child; 14200 x -= child.x; 14201 y -= child.y; 14202 auto r = starting.widgetAtPoint(x, y);//starting.getChildAtPosition(x, y); 14203 child = r.widget; 14204 if(child is starting) 14205 break; 14206 } 14207 return WidgetAtPointResponse(starting, x, y); 14208 } 14209 14210 version(win32_widgets) { 14211 private: 14212 import core.sys.windows.commctrl; 14213 14214 pragma(lib, "comctl32"); 14215 shared static this() { 14216 // http://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx 14217 INITCOMMONCONTROLSEX ic; 14218 ic.dwSize = cast(DWORD) ic.sizeof; 14219 ic.dwICC = ICC_UPDOWN_CLASS | ICC_WIN95_CLASSES | ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_COOL_CLASSES | ICC_STANDARD_CLASSES | ICC_USEREX_CLASSES; 14220 if(!InitCommonControlsEx(&ic)) { 14221 //writeln("ICC failed"); 14222 } 14223 } 14224 14225 14226 // everything from here is just win32 headers copy pasta 14227 private: 14228 extern(Windows): 14229 14230 alias HANDLE HMENU; 14231 HMENU CreateMenu(); 14232 bool SetMenu(HWND, HMENU); 14233 HMENU CreatePopupMenu(); 14234 enum MF_POPUP = 0x10; 14235 enum MF_STRING = 0; 14236 14237 14238 BOOL InitCommonControlsEx(const INITCOMMONCONTROLSEX*); 14239 struct INITCOMMONCONTROLSEX { 14240 DWORD dwSize; 14241 DWORD dwICC; 14242 } 14243 enum HINST_COMMCTRL = cast(HINSTANCE) (-1); 14244 enum { 14245 IDB_STD_SMALL_COLOR, 14246 IDB_STD_LARGE_COLOR, 14247 IDB_VIEW_SMALL_COLOR = 4, 14248 IDB_VIEW_LARGE_COLOR = 5 14249 } 14250 enum { 14251 STD_CUT, 14252 STD_COPY, 14253 STD_PASTE, 14254 STD_UNDO, 14255 STD_REDOW, 14256 STD_DELETE, 14257 STD_FILENEW, 14258 STD_FILEOPEN, 14259 STD_FILESAVE, 14260 STD_PRINTPRE, 14261 STD_PROPERTIES, 14262 STD_HELP, 14263 STD_FIND, 14264 STD_REPLACE, 14265 STD_PRINT // = 14 14266 } 14267 14268 alias HANDLE HIMAGELIST; 14269 HIMAGELIST ImageList_Create(int, int, UINT, int, int); 14270 int ImageList_Add(HIMAGELIST, HBITMAP, HBITMAP); 14271 BOOL ImageList_Destroy(HIMAGELIST); 14272 14273 uint MAKELONG(ushort a, ushort b) { 14274 return cast(uint) ((b << 16) | a); 14275 } 14276 14277 14278 struct TBBUTTON { 14279 int iBitmap; 14280 int idCommand; 14281 BYTE fsState; 14282 BYTE fsStyle; 14283 version(Win64) 14284 BYTE[6] bReserved; 14285 else 14286 BYTE[2] bReserved; 14287 DWORD dwData; 14288 INT_PTR iString; 14289 } 14290 14291 enum { 14292 TB_ADDBUTTONSA = WM_USER + 20, 14293 TB_INSERTBUTTONA = WM_USER + 21, 14294 TB_GETIDEALSIZE = WM_USER + 99, 14295 } 14296 14297 struct SIZE { 14298 LONG cx; 14299 LONG cy; 14300 } 14301 14302 14303 enum { 14304 TBSTATE_CHECKED = 1, 14305 TBSTATE_PRESSED = 2, 14306 TBSTATE_ENABLED = 4, 14307 TBSTATE_HIDDEN = 8, 14308 TBSTATE_INDETERMINATE = 16, 14309 TBSTATE_WRAP = 32 14310 } 14311 14312 14313 14314 enum { 14315 ILC_COLOR = 0, 14316 ILC_COLOR4 = 4, 14317 ILC_COLOR8 = 8, 14318 ILC_COLOR16 = 16, 14319 ILC_COLOR24 = 24, 14320 ILC_COLOR32 = 32, 14321 ILC_COLORDDB = 254, 14322 ILC_MASK = 1, 14323 ILC_PALETTE = 2048 14324 } 14325 14326 14327 alias TBBUTTON* PTBBUTTON, LPTBBUTTON; 14328 14329 14330 enum { 14331 TB_ENABLEBUTTON = WM_USER + 1, 14332 TB_CHECKBUTTON, 14333 TB_PRESSBUTTON, 14334 TB_HIDEBUTTON, 14335 TB_INDETERMINATE, // = WM_USER + 5, 14336 TB_ISBUTTONENABLED = WM_USER + 9, 14337 TB_ISBUTTONCHECKED, 14338 TB_ISBUTTONPRESSED, 14339 TB_ISBUTTONHIDDEN, 14340 TB_ISBUTTONINDETERMINATE, // = WM_USER + 13, 14341 TB_SETSTATE = WM_USER + 17, 14342 TB_GETSTATE = WM_USER + 18, 14343 TB_ADDBITMAP = WM_USER + 19, 14344 TB_DELETEBUTTON = WM_USER + 22, 14345 TB_GETBUTTON, 14346 TB_BUTTONCOUNT, 14347 TB_COMMANDTOINDEX, 14348 TB_SAVERESTOREA, 14349 TB_CUSTOMIZE, 14350 TB_ADDSTRINGA, 14351 TB_GETITEMRECT, 14352 TB_BUTTONSTRUCTSIZE, 14353 TB_SETBUTTONSIZE, 14354 TB_SETBITMAPSIZE, 14355 TB_AUTOSIZE, // = WM_USER + 33, 14356 TB_GETTOOLTIPS = WM_USER + 35, 14357 TB_SETTOOLTIPS = WM_USER + 36, 14358 TB_SETPARENT = WM_USER + 37, 14359 TB_SETROWS = WM_USER + 39, 14360 TB_GETROWS, 14361 TB_GETBITMAPFLAGS, 14362 TB_SETCMDID, 14363 TB_CHANGEBITMAP, 14364 TB_GETBITMAP, 14365 TB_GETBUTTONTEXTA, 14366 TB_REPLACEBITMAP, // = WM_USER + 46, 14367 TB_GETBUTTONSIZE = WM_USER + 58, 14368 TB_SETBUTTONWIDTH = WM_USER + 59, 14369 TB_GETBUTTONTEXTW = WM_USER + 75, 14370 TB_SAVERESTOREW = WM_USER + 76, 14371 TB_ADDSTRINGW = WM_USER + 77, 14372 } 14373 14374 extern(Windows) 14375 BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); 14376 14377 alias extern(Windows) BOOL function (HWND, LPARAM) WNDENUMPROC; 14378 14379 14380 enum { 14381 TB_SETINDENT = WM_USER + 47, 14382 TB_SETIMAGELIST, 14383 TB_GETIMAGELIST, 14384 TB_LOADIMAGES, 14385 TB_GETRECT, 14386 TB_SETHOTIMAGELIST, 14387 TB_GETHOTIMAGELIST, 14388 TB_SETDISABLEDIMAGELIST, 14389 TB_GETDISABLEDIMAGELIST, 14390 TB_SETSTYLE, 14391 TB_GETSTYLE, 14392 //TB_GETBUTTONSIZE, 14393 //TB_SETBUTTONWIDTH, 14394 TB_SETMAXTEXTROWS, 14395 TB_GETTEXTROWS // = WM_USER + 61 14396 } 14397 14398 enum { 14399 CCM_FIRST = 0x2000, 14400 CCM_LAST = CCM_FIRST + 0x200, 14401 CCM_SETBKCOLOR = 8193, 14402 CCM_SETCOLORSCHEME = 8194, 14403 CCM_GETCOLORSCHEME = 8195, 14404 CCM_GETDROPTARGET = 8196, 14405 CCM_SETUNICODEFORMAT = 8197, 14406 CCM_GETUNICODEFORMAT = 8198, 14407 CCM_SETVERSION = 0x2007, 14408 CCM_GETVERSION = 0x2008, 14409 CCM_SETNOTIFYWINDOW = 0x2009 14410 } 14411 14412 14413 enum { 14414 PBM_SETRANGE = WM_USER + 1, 14415 PBM_SETPOS, 14416 PBM_DELTAPOS, 14417 PBM_SETSTEP, 14418 PBM_STEPIT, // = WM_USER + 5 14419 PBM_SETRANGE32 = 1030, 14420 PBM_GETRANGE, 14421 PBM_GETPOS, 14422 PBM_SETBARCOLOR, // = 1033 14423 PBM_SETBKCOLOR = CCM_SETBKCOLOR 14424 } 14425 14426 enum { 14427 PBS_SMOOTH = 1, 14428 PBS_VERTICAL = 4 14429 } 14430 14431 enum { 14432 ICC_LISTVIEW_CLASSES = 1, 14433 ICC_TREEVIEW_CLASSES = 2, 14434 ICC_BAR_CLASSES = 4, 14435 ICC_TAB_CLASSES = 8, 14436 ICC_UPDOWN_CLASS = 16, 14437 ICC_PROGRESS_CLASS = 32, 14438 ICC_HOTKEY_CLASS = 64, 14439 ICC_ANIMATE_CLASS = 128, 14440 ICC_WIN95_CLASSES = 255, 14441 ICC_DATE_CLASSES = 256, 14442 ICC_USEREX_CLASSES = 512, 14443 ICC_COOL_CLASSES = 1024, 14444 ICC_STANDARD_CLASSES = 0x00004000, 14445 } 14446 14447 enum WM_USER = 1024; 14448 } 14449 14450 version(win32_widgets) 14451 pragma(lib, "comdlg32"); 14452 14453 14454 /// 14455 enum GenericIcons : ushort { 14456 None, /// 14457 // these happen to match the win32 std icons numerically if you just subtract one from the value 14458 Cut, /// 14459 Copy, /// 14460 Paste, /// 14461 Undo, /// 14462 Redo, /// 14463 Delete, /// 14464 New, /// 14465 Open, /// 14466 Save, /// 14467 PrintPreview, /// 14468 Properties, /// 14469 Help, /// 14470 Find, /// 14471 Replace, /// 14472 Print, /// 14473 } 14474 14475 enum FileDialogType { 14476 Automatic, 14477 Open, 14478 Save 14479 } 14480 string previousFileReferenced; 14481 14482 /++ 14483 Used in automatic menu functions to indicate that the user should be able to browse for a file. 14484 14485 Params: 14486 storage = an alias to a `static string` variable that stores the last file referenced. It will 14487 use this to pre-fill the dialog with a suggestion. 14488 14489 Please note that it MUST be `static` or you will get compile errors. 14490 14491 filters = the filters param to [getFileName] 14492 14493 type = the type if dialog to show. If `FileDialogType.Automatic`, it the driver code will 14494 guess based on the function name. If it has the word "Save" or "Export" in it, it will show 14495 a save dialog box. Otherwise, it will show an open dialog box. 14496 +/ 14497 struct FileName(alias storage = previousFileReferenced, string[] filters = null, FileDialogType type = FileDialogType.Automatic) { 14498 string name; 14499 alias name this; 14500 } 14501 14502 /++ 14503 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. 14504 14505 History: 14506 onCancel was added November 6, 2021. 14507 14508 The dialog itself on Linux was modified on December 2, 2021 to include 14509 a directory picker in addition to the command line completion view. 14510 14511 The `initialDirectory` argument was added November 9, 2022 (dub v10.10) 14512 Future_directions: 14513 I want to add some kind of custom preview and maybe thumbnail thing in the future, 14514 at least on Linux, maybe on Windows too. 14515 +/ 14516 void getOpenFileName( 14517 void delegate(string) onOK, 14518 string prefilledName = null, 14519 string[] filters = null, 14520 void delegate() onCancel = null, 14521 string initialDirectory = null, 14522 ) 14523 { 14524 return getFileName(true, onOK, prefilledName, filters, onCancel, initialDirectory); 14525 } 14526 14527 /// ditto 14528 void getSaveFileName( 14529 void delegate(string) onOK, 14530 string prefilledName = null, 14531 string[] filters = null, 14532 void delegate() onCancel = null, 14533 string initialDirectory = null, 14534 ) 14535 { 14536 return getFileName(false, onOK, prefilledName, filters, onCancel, initialDirectory); 14537 } 14538 14539 void getFileName( 14540 bool openOrSave, 14541 void delegate(string) onOK, 14542 string prefilledName = null, 14543 string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\0*.png;*.jpg"] 14544 void delegate() onCancel = null, 14545 string initialDirectory = null, 14546 ) 14547 { 14548 14549 version(win32_widgets) { 14550 import core.sys.windows.commdlg; 14551 /* 14552 Ofn.lStructSize = sizeof(OPENFILENAME); 14553 Ofn.hwndOwner = hWnd; 14554 Ofn.lpstrFilter = szFilter; 14555 Ofn.lpstrFile= szFile; 14556 Ofn.nMaxFile = sizeof(szFile)/ sizeof(*szFile); 14557 Ofn.lpstrFileTitle = szFileTitle; 14558 Ofn.nMaxFileTitle = sizeof(szFileTitle); 14559 Ofn.lpstrInitialDir = (LPSTR)NULL; 14560 Ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; 14561 Ofn.lpstrTitle = szTitle; 14562 */ 14563 14564 14565 wchar[1024] file = 0; 14566 wchar[1024] filterBuffer = 0; 14567 makeWindowsString(prefilledName, file[]); 14568 OPENFILENAME ofn; 14569 ofn.lStructSize = ofn.sizeof; 14570 if(filters.length) { 14571 string filter; 14572 foreach(i, f; filters) { 14573 filter ~= f; 14574 filter ~= "\0"; 14575 } 14576 filter ~= "\0"; 14577 ofn.lpstrFilter = makeWindowsString(filter, filterBuffer[], 0 /* already terminated */).ptr; 14578 } 14579 ofn.lpstrFile = file.ptr; 14580 ofn.nMaxFile = file.length; 14581 14582 wchar[1024] initialDir = 0; 14583 if(initialDirectory !is null) { 14584 makeWindowsString(initialDirectory, initialDir[]); 14585 ofn.lpstrInitialDir = file.ptr; 14586 } 14587 14588 if(openOrSave ? GetOpenFileName(&ofn) : GetSaveFileName(&ofn)) 14589 { 14590 string okString = makeUtf8StringFromWindowsString(ofn.lpstrFile); 14591 if(okString.length && okString[$-1] == '\0') 14592 okString = okString[0..$-1]; 14593 onOK(okString); 14594 } else { 14595 if(onCancel) 14596 onCancel(); 14597 } 14598 } else version(custom_widgets) { 14599 if(filters.length == 0) 14600 filters = ["All Files\0*.*"]; 14601 auto picker = new FilePicker(prefilledName, filters, initialDirectory); 14602 picker.onOK = onOK; 14603 picker.onCancel = onCancel; 14604 picker.show(); 14605 } 14606 } 14607 14608 version(custom_widgets) 14609 private 14610 class FilePicker : Dialog { 14611 void delegate(string) onOK; 14612 void delegate() onCancel; 14613 LineEdit lineEdit; 14614 14615 // returns common prefix 14616 string loadFiles(string cwd, string[] filters...) { 14617 string[] files; 14618 string[] dirs; 14619 14620 string commonPrefix; 14621 14622 getFiles(cwd, (string name, bool isDirectory) { 14623 if(name == ".") 14624 return; // skip this as unnecessary 14625 if(isDirectory) 14626 dirs ~= name; 14627 else { 14628 foreach(filter; filters) 14629 if( 14630 filter.length <= 1 || 14631 filter == "*.*" || 14632 (filter[0] == '*' && name.endsWith(filter[1 .. $])) || 14633 (filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1])) 14634 ) 14635 { 14636 files ~= name; 14637 14638 if(filter.length > 0 && filter[$-1] == '*') { 14639 if(commonPrefix is null) { 14640 commonPrefix = name; 14641 } else { 14642 foreach(idx, char i; name) { 14643 if(idx >= commonPrefix.length || i != commonPrefix[idx]) { 14644 commonPrefix = commonPrefix[0 .. idx]; 14645 break; 14646 } 14647 } 14648 } 14649 } 14650 14651 break; 14652 } 14653 } 14654 }); 14655 14656 extern(C) static int comparator(scope const void* a, scope const void* b) { 14657 auto sa = *cast(string*) a; 14658 auto sb = *cast(string*) b; 14659 14660 for(int i = 0; i < sa.length; i++) { 14661 if(i == sb.length) 14662 return 1; 14663 return sa[i] - sb[i]; 14664 } 14665 14666 return 0; 14667 } 14668 14669 nonPhobosSort(files, &comparator); 14670 nonPhobosSort(dirs, &comparator); 14671 14672 listWidget.clear(); 14673 dirWidget.clear(); 14674 foreach(name; dirs) 14675 dirWidget.addOption(name); 14676 foreach(name; files) 14677 listWidget.addOption(name); 14678 14679 return commonPrefix; 14680 } 14681 14682 ListWidget listWidget; 14683 ListWidget dirWidget; 14684 14685 string currentDirectory; 14686 string[] processedFilters; 14687 14688 //string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"] 14689 this(string prefilledName, string[] filters, string initialDirectory, Window owner = null) { 14690 super(300, 200, "Choose File..."); // owner); 14691 14692 foreach(filter; filters) { 14693 while(filter.length && filter[0] != 0) { 14694 filter = filter[1 .. $]; 14695 } 14696 if(filter.length) 14697 filter = filter[1 .. $]; // trim off the 0 14698 14699 while(filter.length) { 14700 int idx = 0; 14701 while(idx < filter.length && filter[idx] != ';') { 14702 idx++; 14703 } 14704 14705 processedFilters ~= filter[0 .. idx]; 14706 if(idx < filter.length) 14707 idx++; // skip the ; 14708 filter = filter[idx .. $]; 14709 } 14710 } 14711 14712 currentDirectory = initialDirectory is null ? "." : initialDirectory; 14713 14714 { 14715 auto hl = new HorizontalLayout(this); 14716 dirWidget = new ListWidget(hl); 14717 listWidget = new ListWidget(hl); 14718 14719 // double click events normally trigger something else but 14720 // here user might be clicking kinda fast and we'd rather just 14721 // keep it 14722 dirWidget.addEventListener((scope DoubleClickEvent dev) { 14723 auto ce = new ChangeEvent!void(dirWidget, () {}); 14724 ce.dispatch(); 14725 }); 14726 14727 dirWidget.addEventListener((scope ChangeEvent!void sce) { 14728 string v; 14729 foreach(o; dirWidget.options) 14730 if(o.selected) { 14731 v = o.label; 14732 break; 14733 } 14734 if(v.length) { 14735 currentDirectory ~= "/" ~ v; 14736 loadFiles(currentDirectory, processedFilters); 14737 } 14738 }); 14739 14740 // double click here, on the other hand, selects the file 14741 // and moves on 14742 listWidget.addEventListener((scope DoubleClickEvent dev) { 14743 OK(); 14744 }); 14745 } 14746 14747 lineEdit = new LineEdit(this); 14748 lineEdit.focus(); 14749 lineEdit.addEventListener(delegate(CharEvent event) { 14750 if(event.character == '\t' || event.character == '\n') 14751 event.preventDefault(); 14752 }); 14753 14754 listWidget.addEventListener(EventType.change, () { 14755 foreach(o; listWidget.options) 14756 if(o.selected) 14757 lineEdit.content = o.label; 14758 }); 14759 14760 loadFiles(currentDirectory, processedFilters); 14761 14762 lineEdit.addEventListener((KeyDownEvent event) { 14763 if(event.key == Key.Tab) { 14764 14765 auto current = lineEdit.content; 14766 if(current.length >= 2 && current[0 ..2] == "./") 14767 current = current[2 .. $]; 14768 14769 auto commonPrefix = loadFiles(".", current ~ "*"); 14770 14771 if(commonPrefix.length) 14772 lineEdit.content = commonPrefix; 14773 14774 // FIXME: if that is a directory, add the slash? or even go inside? 14775 14776 event.preventDefault(); 14777 } 14778 }); 14779 14780 lineEdit.content = prefilledName; 14781 14782 auto hl = new HorizontalLayout(60, this); 14783 auto cancelButton = new Button("Cancel", hl); 14784 auto okButton = new Button("OK", hl); 14785 14786 cancelButton.addEventListener(EventType.triggered, &Cancel); 14787 okButton.addEventListener(EventType.triggered, &OK); 14788 14789 this.addEventListener((KeyDownEvent event) { 14790 if(event.key == Key.Enter || event.key == Key.PadEnter) { 14791 event.preventDefault(); 14792 OK(); 14793 } 14794 if(event.key == Key.Escape) 14795 Cancel(); 14796 }); 14797 14798 } 14799 14800 override void OK() { 14801 if(lineEdit.content.length) { 14802 string accepted; 14803 auto c = lineEdit.content; 14804 if(c.length && c[0] == '/') 14805 accepted = c; 14806 else 14807 accepted = currentDirectory ~ "/" ~ lineEdit.content; 14808 14809 if(isDir(accepted)) { 14810 // FIXME: would be kinda nice to support ~ and collapse these paths too 14811 // FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later. 14812 currentDirectory = accepted; 14813 loadFiles(currentDirectory, processedFilters); 14814 lineEdit.content = ""; 14815 return; 14816 } 14817 14818 if(onOK) 14819 onOK(accepted); 14820 } 14821 close(); 14822 } 14823 14824 override void Cancel() { 14825 if(onCancel) 14826 onCancel(); 14827 close(); 14828 } 14829 } 14830 14831 private bool isDir(string name) { 14832 version(Windows) { 14833 auto ws = WCharzBuffer(name); 14834 auto ret = GetFileAttributesW(ws.ptr); 14835 if(ret == INVALID_FILE_ATTRIBUTES) 14836 return false; 14837 return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0; 14838 } else version(Posix) { 14839 import core.sys.posix.sys.stat; 14840 stat_t buf; 14841 auto ret = stat((name ~ '\0').ptr, &buf); 14842 if(ret == -1) 14843 return false; // I could probably check more specific errors tbh 14844 return (buf.st_mode & S_IFMT) == S_IFDIR; 14845 } else return false; 14846 } 14847 14848 /* 14849 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes 14850 http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx 14851 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775943%28v=vs.85%29.aspx 14852 http://msdn.microsoft.com/en-us/library/windows/desktop/bb775951%28v=vs.85%29.aspx 14853 http://msdn.microsoft.com/en-us/library/windows/desktop/ms632680%28v=vs.85%29.aspx 14854 http://msdn.microsoft.com/en-us/library/windows/desktop/ms644996%28v=vs.85%29.aspx#message_box 14855 http://www.sbin.org/doc/Xlib/chapt_03.html 14856 14857 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760433%28v=vs.85%29.aspx 14858 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760446%28v=vs.85%29.aspx 14859 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760443%28v=vs.85%29.aspx 14860 http://msdn.microsoft.com/en-us/library/windows/desktop/bb760476%28v=vs.85%29.aspx 14861 */ 14862 14863 14864 // These are all for setMenuAndToolbarFromAnnotatedCode 14865 /// This item in the menu will be preceded by a separator line 14866 /// Group: generating_from_code 14867 struct separator {} 14868 deprecated("It was misspelled, use separator instead") alias seperator = separator; 14869 /// Program-wide keyboard shortcut to trigger the action 14870 /// Group: generating_from_code 14871 struct accelerator { string keyString; } 14872 /// tells which menu the action will be on 14873 /// Group: generating_from_code 14874 struct menu { string name; } 14875 /// Describes which toolbar section the action appears on 14876 /// Group: generating_from_code 14877 struct toolbar { string groupName; } 14878 /// 14879 /// Group: generating_from_code 14880 struct icon { ushort id; } 14881 /// 14882 /// Group: generating_from_code 14883 struct label { string label; } 14884 /// 14885 /// Group: generating_from_code 14886 struct hotkey { dchar ch; } 14887 /// 14888 /// Group: generating_from_code 14889 struct tip { string tip; } 14890 14891 14892 /++ 14893 Observes and allows inspection of an object via automatic gui 14894 +/ 14895 /// Group: generating_from_code 14896 ObjectInspectionWindow objectInspectionWindow(T)(T t) if(is(T == class)) { 14897 return new ObjectInspectionWindowImpl!(T)(t); 14898 } 14899 14900 class ObjectInspectionWindow : Window { 14901 this(int a, int b, string c) { 14902 super(a, b, c); 14903 } 14904 14905 abstract void readUpdatesFromObject(); 14906 } 14907 14908 class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow { 14909 T t; 14910 this(T t) { 14911 this.t = t; 14912 14913 super(300, 400, "ObjectInspectionWindow - " ~ T.stringof); 14914 14915 foreach(memberName; __traits(derivedMembers, T)) {{ 14916 alias member = I!(__traits(getMember, t, memberName))[0]; 14917 alias type = typeof(member); 14918 static if(is(type == int)) { 14919 auto le = new LabeledLineEdit(memberName ~ ": ", this); 14920 //le.addEventListener("char", (Event ev) { 14921 //if((ev.character < '0' || ev.character > '9') && ev.character != '-') 14922 //ev.preventDefault(); 14923 //}); 14924 le.addEventListener(EventType.change, (Event ev) { 14925 __traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue); 14926 }); 14927 14928 updateMemberDelegates[memberName] = () { 14929 le.content = toInternal!string(__traits(getMember, t, memberName)); 14930 }; 14931 } 14932 }} 14933 } 14934 14935 void delegate()[string] updateMemberDelegates; 14936 14937 override void readUpdatesFromObject() { 14938 foreach(k, v; updateMemberDelegates) 14939 v(); 14940 } 14941 } 14942 14943 /++ 14944 Creates a dialog based on a data structure. 14945 14946 --- 14947 dialog((YourStructure value) { 14948 // the user filled in the struct and clicked OK, 14949 // you can check the members now 14950 }); 14951 --- 14952 14953 Params: 14954 initialData = the initial value to show in the dialog. It will not modify this unless 14955 it is a class then it might, no promises. 14956 14957 History: 14958 The overload that lets you specify `initialData` was added on December 30, 2021 (dub v10.5) 14959 +/ 14960 /// Group: generating_from_code 14961 void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 14962 dialog(T.init, onOK, onCancel, title); 14963 } 14964 /// ditto 14965 void dialog(T)(T initialData, void delegate(T) onOK, void delegate() onCancel = null, string title = T.stringof) { 14966 auto dg = new AutomaticDialog!T(initialData, onOK, onCancel, title); 14967 dg.show(); 14968 } 14969 14970 private static template I(T...) { alias I = T; } 14971 14972 14973 private string beautify(string name, char space = ' ', bool allLowerCase = false) { 14974 if(name == "id") 14975 return allLowerCase ? name : "ID"; 14976 14977 char[160] buffer; 14978 int bufferIndex = 0; 14979 bool shouldCap = true; 14980 bool shouldSpace; 14981 bool lastWasCap; 14982 foreach(idx, char ch; name) { 14983 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 14984 14985 if((ch >= 'A' && ch <= 'Z') || ch == '_') { 14986 if(lastWasCap) { 14987 // two caps in a row, don't change. Prolly acronym. 14988 } else { 14989 if(idx) 14990 shouldSpace = true; // new word, add space 14991 } 14992 14993 lastWasCap = true; 14994 } else { 14995 lastWasCap = false; 14996 } 14997 14998 if(shouldSpace) { 14999 buffer[bufferIndex++] = space; 15000 if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important 15001 shouldSpace = false; 15002 } 15003 if(shouldCap) { 15004 if(ch >= 'a' && ch <= 'z') 15005 ch -= 32; 15006 shouldCap = false; 15007 } 15008 if(allLowerCase && ch >= 'A' && ch <= 'Z') 15009 ch += 32; 15010 buffer[bufferIndex++] = ch; 15011 } 15012 return buffer[0 .. bufferIndex].idup; 15013 } 15014 15015 /++ 15016 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. 15017 +/ 15018 class AutomaticDialog(T) : Dialog { 15019 T t; 15020 15021 void delegate(T) onOK; 15022 void delegate() onCancel; 15023 15024 override int paddingTop() { return defaultLineHeight; } 15025 override int paddingBottom() { return defaultLineHeight; } 15026 override int paddingRight() { return defaultLineHeight; } 15027 override int paddingLeft() { return defaultLineHeight; } 15028 15029 this(T initialData, void delegate(T) onOK, void delegate() onCancel, string title) { 15030 assert(onOK !is null); 15031 15032 t = initialData; 15033 15034 static if(is(T == class)) { 15035 if(t is null) 15036 t = new T(); 15037 } 15038 this.onOK = onOK; 15039 this.onCancel = onCancel; 15040 super(400, cast(int)(__traits(allMembers, T).length * 2) * (defaultLineHeight + 4 + 2) + Window.lineHeight + 56, title); 15041 15042 static if(is(T == class)) 15043 this.addDataControllerWidget(t); 15044 else 15045 this.addDataControllerWidget(&t); 15046 15047 auto hl = new HorizontalLayout(this); 15048 auto stretch = new HorizontalSpacer(hl); // to right align 15049 auto ok = new CommandButton("OK", hl); 15050 auto cancel = new CommandButton("Cancel", hl); 15051 ok.addEventListener(EventType.triggered, &OK); 15052 cancel.addEventListener(EventType.triggered, &Cancel); 15053 15054 this.addEventListener((KeyDownEvent ev) { 15055 if(ev.key == Key.Enter || ev.key == Key.PadEnter) { 15056 ok.focus(); 15057 OK(); 15058 ev.preventDefault(); 15059 } 15060 if(ev.key == Key.Escape) { 15061 Cancel(); 15062 ev.preventDefault(); 15063 } 15064 }); 15065 15066 this.addEventListener((scope ClosedEvent ce) { 15067 if(onCancel) 15068 onCancel(); 15069 }); 15070 15071 //this.children[0].focus(); 15072 } 15073 15074 override void OK() { 15075 onOK(t); 15076 close(); 15077 } 15078 15079 override void Cancel() { 15080 if(onCancel) 15081 onCancel(); 15082 close(); 15083 } 15084 } 15085 15086 private template baseClassCount(Class) { 15087 private int helper() { 15088 int count = 0; 15089 static if(is(Class bases == super)) { 15090 foreach(base; bases) 15091 static if(is(base == class)) 15092 count += 1 + baseClassCount!base; 15093 } 15094 return count; 15095 } 15096 15097 enum int baseClassCount = helper(); 15098 } 15099 15100 private long stringToLong(string s) { 15101 long ret; 15102 if(s.length == 0) 15103 return ret; 15104 bool negative = s[0] == '-'; 15105 if(negative) 15106 s = s[1 .. $]; 15107 foreach(ch; s) { 15108 if(ch >= '0' && ch <= '9') { 15109 ret *= 10; 15110 ret += ch - '0'; 15111 } 15112 } 15113 if(negative) 15114 ret = -ret; 15115 return ret; 15116 } 15117 15118 15119 interface ReflectableProperties { 15120 /++ 15121 Iterates the event's properties as strings. Note that keys may be repeated and a get property request may 15122 call your sink with `null`. It it does, it means the key either doesn't request or cannot be represented by 15123 json in the current implementation. 15124 15125 This is auto-implemented for you if you mixin [RegisterGetters] in your child classes and only have 15126 properties of type `bool`, `int`, `double`, or `string`. For other ones, you will need to do it yourself 15127 as of the June 2, 2021 release. 15128 15129 History: 15130 Added June 2, 2021. 15131 15132 See_Also: [getPropertyAsString], [setPropertyFromString] 15133 +/ 15134 void getPropertiesList(scope void delegate(string name) sink) const;// @nogc pure nothrow; 15135 /++ 15136 Requests a property to be delivered to you as a string, through your `sink` delegate. 15137 15138 If the `value` is null, it means the property could not be retreived. If `valueIsJson`, it should 15139 be interpreted as json, otherwise, it is just a plain string. 15140 15141 The sink should always be called exactly once for each call (it is basically a return value, but it might 15142 use a local buffer it maintains instead of allocating a return value). 15143 15144 History: 15145 Added June 2, 2021. 15146 15147 See_Also: [getPropertiesList], [setPropertyFromString] 15148 +/ 15149 void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink); 15150 /++ 15151 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. 15152 15153 History: 15154 Added June 2, 2021. 15155 15156 See_Also: [getPropertiesList], [getPropertyAsString], [SetPropertyResult] 15157 +/ 15158 SetPropertyResult setPropertyFromString(string name, scope const(char)[] str, bool strIsJson); 15159 15160 /// [setPropertyFromString] possible return values 15161 enum SetPropertyResult { 15162 success = 0, /// the property has been successfully set to the request value 15163 notPermitted = -1, /// the property exists but it cannot be changed at this time 15164 notImplemented = -2, /// the set function is not implemented for the given property (which may or may not exist) 15165 noSuchProperty = -3, /// there is no property by that name 15166 wrongFormat = -4, /// the string was given in the wrong format, e.g. passing "two" for an int value 15167 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) 15168 } 15169 15170 /++ 15171 You can mix this in to get an implementation in child classes. This does [setPropertyFromString]. 15172 15173 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15174 15175 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15176 rarely need to use these building blocks directly. 15177 +/ 15178 mixin template RegisterSetters() { 15179 override SetPropertyResult setPropertyFromString(string name, scope const(char)[] value, bool valueIsJson) { 15180 switch(name) { 15181 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15182 case memberName: 15183 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15184 if(value != "true" && value != "false") 15185 return SetPropertyResult.wrongFormat; 15186 __traits(getMember, this, memberName) = value == "true" ? true : false; 15187 return SetPropertyResult.success; 15188 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15189 import core.stdc.stdlib; 15190 char[128] zero = 0; 15191 if(buffer.length + 1 >= zero.length) 15192 return SetPropertyResult.wrongFormat; 15193 zero[0 .. buffer.length] = buffer[]; 15194 __traits(getMember, this, memberName) = strtol(buffer.ptr, null, 10); 15195 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15196 import core.stdc.stdlib; 15197 char[128] zero = 0; 15198 if(buffer.length + 1 >= zero.length) 15199 return SetPropertyResult.wrongFormat; 15200 zero[0 .. buffer.length] = buffer[]; 15201 __traits(getMember, this, memberName) = strtod(buffer.ptr, null, 10); 15202 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15203 __traits(getMember, this, memberName) = value.idup; 15204 } else { 15205 return SetPropertyResult.notImplemented; 15206 } 15207 15208 } 15209 default: 15210 return super.setPropertyFromString(name, value, valueIsJson); 15211 } 15212 } 15213 } 15214 15215 /++ 15216 You can mix this in to get an implementation in child classes. This does [getPropertyAsString] and [getPropertiesList]. 15217 15218 Your original base class, however, must implement its own methods. I recommend doing the initial ones by hand. 15219 15220 For [Widget] and [Event], the library provides [Widget.Register] and [Event.Register] that call these for you, so you should 15221 rarely need to use these building blocks directly. 15222 +/ 15223 mixin template RegisterGetters() { 15224 override void getPropertiesList(scope void delegate(string name) sink) const { 15225 super.getPropertiesList(sink); 15226 15227 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15228 sink(memberName); 15229 } 15230 } 15231 override void getPropertyAsString(string name, scope void delegate(string name, scope const(char)[] value, bool valueIsJson) sink) { 15232 switch(name) { 15233 foreach(memberName; __traits(derivedMembers, typeof(this))) { 15234 case memberName: 15235 static if(is(typeof(__traits(getMember, this, memberName)) : const bool)) { 15236 sink(name, __traits(getMember, this, memberName) ? "true" : "false", true); 15237 } else static if(is(typeof(__traits(getMember, this, memberName)) : const long)) { 15238 import core.stdc.stdio; 15239 char[32] buffer; 15240 auto len = snprintf(buffer.ptr, buffer.length, "%lld", cast(long) __traits(getMember, this, memberName)); 15241 sink(name, buffer[0 .. len], true); 15242 } else static if(is(typeof(__traits(getMember, this, memberName)) : const double)) { 15243 import core.stdc.stdio; 15244 char[32] buffer; 15245 auto len = snprintf(buffer.ptr, buffer.length, "%f", cast(double) __traits(getMember, this, memberName)); 15246 sink(name, buffer[0 .. len], true); 15247 } else static if(is(typeof(__traits(getMember, this, memberName)) : const string)) { 15248 sink(name, __traits(getMember, this, memberName), false); 15249 //sinkJsonString(memberName, __traits(getMember, this, memberName), sink); 15250 } else { 15251 sink(name, null, true); 15252 } 15253 15254 return; 15255 } 15256 default: 15257 return super.getPropertyAsString(name, sink); 15258 } 15259 } 15260 } 15261 } 15262 15263 private struct Stack(T) { 15264 this(int maxSize) { 15265 internalLength = 0; 15266 arr = initialBuffer[]; 15267 } 15268 15269 ///. 15270 void push(T t) { 15271 if(internalLength >= arr.length) { 15272 auto oldarr = arr; 15273 if(arr.length < 4096) 15274 arr = new T[arr.length * 2]; 15275 else 15276 arr = new T[arr.length + 4096]; 15277 arr[0 .. oldarr.length] = oldarr[]; 15278 } 15279 15280 arr[internalLength] = t; 15281 internalLength++; 15282 } 15283 15284 ///. 15285 T pop() { 15286 assert(internalLength); 15287 internalLength--; 15288 return arr[internalLength]; 15289 } 15290 15291 ///. 15292 T peek() { 15293 assert(internalLength); 15294 return arr[internalLength - 1]; 15295 } 15296 15297 ///. 15298 @property bool empty() { 15299 return internalLength ? false : true; 15300 } 15301 15302 ///. 15303 private T[] arr; 15304 private size_t internalLength; 15305 private T[64] initialBuffer; 15306 // 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), 15307 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 15308 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 15309 } 15310 15311 /// 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. 15312 private struct WidgetStream { 15313 15314 ///. 15315 @property Widget front() { 15316 return current.widget; 15317 } 15318 15319 /// Use Widget.tree instead. 15320 this(Widget start) { 15321 current.widget = start; 15322 current.childPosition = -1; 15323 isEmpty = false; 15324 stack = typeof(stack)(0); 15325 } 15326 15327 /* 15328 Handle it 15329 handle its children 15330 15331 */ 15332 15333 ///. 15334 void popFront() { 15335 more: 15336 if(isEmpty) return; 15337 15338 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 15339 15340 current.childPosition++; 15341 if(current.childPosition >= current.widget.children.length) { 15342 if(stack.empty()) 15343 isEmpty = true; 15344 else { 15345 current = stack.pop(); 15346 goto more; 15347 } 15348 } else { 15349 stack.push(current); 15350 current.widget = current.widget.children[current.childPosition]; 15351 current.childPosition = -1; 15352 } 15353 } 15354 15355 ///. 15356 @property bool empty() { 15357 return isEmpty; 15358 } 15359 15360 private: 15361 15362 struct Current { 15363 Widget widget; 15364 int childPosition; 15365 } 15366 15367 Current current; 15368 15369 Stack!(Current) stack; 15370 15371 bool isEmpty; 15372 } 15373 15374 15375 /+ 15376 15377 I could fix up the hierarchy kinda like this 15378 15379 class Widget { 15380 Widget[] children() { return null; } 15381 } 15382 interface WidgetContainer { 15383 Widget asWidget(); 15384 void addChild(Widget w); 15385 15386 // alias asWidget this; // but meh 15387 } 15388 15389 Widget can keep a (Widget parent) ctor, but it should prolly deprecate and tell people to instead change their ctors to take WidgetContainer instead. 15390 15391 class Layout : Widget, WidgetContainer {} 15392 15393 class Window : WidgetContainer {} 15394 15395 15396 All constructors that previously took Widgets should now take WidgetContainers instead 15397 15398 15399 15400 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". 15401 +/ 15402 15403 /+ 15404 LAYOUTS 2.0 15405 15406 can just be assigned as a function. assigning a new one will cause it to be immediately called. 15407 15408 they simply are responsible for the recomputeChildLayout. If this pointer is null, it uses the default virtual one. 15409 15410 recomputeChildLayout only really needs a property accessor proxy... just the layout info too. 15411 15412 and even Paint can just use computedStyle... 15413 15414 background color 15415 font 15416 border color and style 15417 15418 And actually the style proxy can offer some helper routines to draw these like the draw 3d box 15419 please note that many widgets and in some modes will completely ignore properties as they will. 15420 they are just hints you set, not promises. 15421 15422 15423 15424 15425 15426 So generally the existing virtual functions are just the default for the class. But individual objects 15427 or stylesheets can override this. The virtual ones count as tag-level specificity in css. 15428 +/ 15429 15430 /++ 15431 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. 15432 15433 History: 15434 Added May 24, 2021. 15435 +/ 15436 struct WidgetBackground { 15437 /++ 15438 A background with the given solid color. 15439 +/ 15440 this(Color color) { 15441 this.color = color; 15442 } 15443 15444 this(WidgetBackground bg) { 15445 this = bg; 15446 } 15447 15448 /++ 15449 Creates a widget from the string. 15450 15451 Currently, it only supports solid colors via [Color.fromString], but it will likely be expanded in the future to something more like css. 15452 +/ 15453 static WidgetBackground fromString(string s) { 15454 return WidgetBackground(Color.fromString(s)); 15455 } 15456 15457 /++ 15458 The background is not necessarily a solid color, but you can always specify a color as a fallback. 15459 15460 History: 15461 Made `public` on December 18, 2022 (dub v10.10). 15462 +/ 15463 Color color; 15464 } 15465 15466 /++ 15467 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!) 15468 15469 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. 15470 15471 You should not inherit from this directly, but instead use [VisualTheme]. 15472 15473 History: 15474 Added May 8, 2021 15475 +/ 15476 abstract class BaseVisualTheme { 15477 /// Don't implement this, instead use [VisualTheme] and implement `paint` methods on specific subclasses you want to override. 15478 abstract void doPaint(Widget widget, WidgetPainter painter); 15479 15480 /+ 15481 /// Don't implement this, instead use [VisualTheme] and implement `StyleOverride` aliases on specific subclasses you want to override. 15482 abstract void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg); 15483 +/ 15484 15485 /++ 15486 Returns the property as a string, or null if it was not overridden in the style definition. The idea here is something like css, 15487 where the interpretation of the string varies for each property and may include things like measurement units. 15488 +/ 15489 abstract string getPropertyString(Widget widget, string propertyName); 15490 15491 /++ 15492 Default background color of the window. Widgets also use this to simulate transparency. 15493 15494 Probably some shade of grey. 15495 +/ 15496 abstract Color windowBackgroundColor(); 15497 abstract Color widgetBackgroundColor(); 15498 abstract Color foregroundColor(); 15499 abstract Color lightAccentColor(); 15500 abstract Color darkAccentColor(); 15501 15502 /++ 15503 Colors used to indicate active selections in lists and text boxes, etc. 15504 +/ 15505 abstract Color selectionForegroundColor(); 15506 /// ditto 15507 abstract Color selectionBackgroundColor(); 15508 15509 deprecated("Use selectionForegroundColor and selectionBackgroundColor instead") Color selectionColor() { return selectionBackgroundColor(); } 15510 15511 abstract OperatingSystemFont defaultFont(); 15512 15513 private OperatingSystemFont defaultFontCache_; 15514 private bool defaultFontCachePopulated; 15515 private OperatingSystemFont defaultFontCached() { 15516 if(!defaultFontCachePopulated) { 15517 // FIXME: set this to false if X disconnect or if visual theme changes 15518 defaultFontCache_ = defaultFont(); 15519 defaultFontCachePopulated = true; 15520 } 15521 return defaultFontCache_; 15522 } 15523 } 15524 15525 /+ 15526 A widget should have: 15527 classList 15528 dataset 15529 attributes 15530 computedStyles 15531 state (persistent) 15532 dynamic state (focused, hover, etc) 15533 +/ 15534 15535 // visualTheme.computedStyle(this).paddingLeft 15536 15537 15538 /++ 15539 This is your entry point to create your own visual theme for custom widgets. 15540 +/ 15541 abstract class VisualTheme(CRTP) : BaseVisualTheme { 15542 override string getPropertyString(Widget widget, string propertyName) { 15543 return null; 15544 } 15545 15546 /+ 15547 mixin StyleOverride!Widget 15548 final override void useStyleProperties(Widget w, scope void delegate(scope Widget.Style props) dg) { 15549 w.useStyleProperties(dg); 15550 } 15551 +/ 15552 15553 final override void doPaint(Widget widget, WidgetPainter painter) { 15554 auto derived = cast(CRTP) cast(void*) this; 15555 15556 scope void delegate(Widget, WidgetPainter) bestMatch; 15557 int bestMatchScore; 15558 15559 static if(__traits(hasMember, CRTP, "paint")) 15560 foreach(overload; __traits(getOverloads, CRTP, "paint")) { 15561 static if(is(typeof(overload) Params == __parameters)) { 15562 static assert(Params.length == 2); 15563 static assert(is(Params[0] : Widget)); 15564 static assert(is(Params[1] == WidgetPainter)); 15565 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); 15566 15567 alias type = Params[0]; 15568 if(cast(type) widget) { 15569 auto score = baseClassCount!type; 15570 15571 if(score > bestMatchScore) { 15572 bestMatch = cast(typeof(bestMatch)) &__traits(child, derived, overload); 15573 bestMatchScore = score; 15574 } 15575 } 15576 } else static assert(0, "paint should be a method."); 15577 } 15578 15579 if(bestMatch) 15580 bestMatch(widget, painter); 15581 else 15582 widget.paint(painter); 15583 } 15584 15585 // 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 15586 override Color windowBackgroundColor() { return Color(212, 212, 212); } 15587 override Color widgetBackgroundColor() { return Color.white; } 15588 override Color foregroundColor() { return Color.black; } 15589 override Color darkAccentColor() { return Color(172, 172, 172); } 15590 override Color lightAccentColor() { return Color(223, 223, 223); } 15591 override Color selectionForegroundColor() { return Color.white; } 15592 override Color selectionBackgroundColor() { return Color(0, 0, 128); } 15593 override OperatingSystemFont defaultFont() { return null; } // will just use the default out of simpledisplay's xfontstr 15594 15595 private static struct Cached { 15596 // i prolly want to do this 15597 } 15598 } 15599 15600 final class DefaultVisualTheme : VisualTheme!DefaultVisualTheme { 15601 /+ 15602 OperatingSystemFont defaultFont() { return new OperatingSystemFont("Times New Roman", 8, FontWeight.medium); } 15603 Color windowBackgroundColor() { return Color(242, 242, 242); } 15604 Color darkAccentColor() { return windowBackgroundColor; } 15605 Color lightAccentColor() { return windowBackgroundColor; } 15606 +/ 15607 } 15608 15609 /++ 15610 Event fired when an [Observeable] variable changes. You will want to add an event listener referencing 15611 the field like `widget.addEventListener((scope StateChanged!(Whatever.field) ev) { });` 15612 15613 History: 15614 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 15615 +/ 15616 class StateChanged(alias field) : Event { 15617 enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change"; 15618 override bool cancelable() const { return false; } 15619 this(Widget target, typeof(field) newValue) { 15620 this.newValue = newValue; 15621 super(EventString, target); 15622 } 15623 15624 typeof(field) newValue; 15625 } 15626 15627 /++ 15628 Convenience function to add a `triggered` event listener. 15629 15630 Its implementation is simply `w.addEventListener("triggered", dg);` 15631 15632 History: 15633 Added November 27, 2021 (dub v10.4) 15634 +/ 15635 void addWhenTriggered(Widget w, void delegate() dg) { 15636 w.addEventListener("triggered", dg); 15637 } 15638 15639 /++ 15640 Observable varables can be added to widgets and when they are changed, it fires 15641 off a [StateChanged] event so you can react to it. 15642 15643 It is implemented as a getter and setter property, along with another helper you 15644 can use to subscribe whith is `name_changed`. You can also subscribe to the [StateChanged] 15645 event through the usual means. Just give the name of the variable. See [StateChanged] for an 15646 example. 15647 15648 History: 15649 Moved from minigui_addons.webview to main minigui on November 27, 2021 (dub v10.4) 15650 +/ 15651 mixin template Observable(T, string name) { 15652 private T backing; 15653 15654 mixin(q{ 15655 void } ~ name ~ q{_changed (void delegate(T) dg) { 15656 this.addEventListener((StateChanged!this_thing ev) { 15657 dg(ev.newValue); 15658 }); 15659 } 15660 15661 @property T } ~ name ~ q{ () { 15662 return backing; 15663 } 15664 15665 @property void } ~ name ~ q{ (T t) { 15666 backing = t; 15667 auto event = new StateChanged!this_thing(this, t); 15668 event.dispatch(); 15669 } 15670 }); 15671 15672 mixin("private alias this_thing = " ~ name ~ ";"); 15673 } 15674 15675 15676 private bool startsWith(string test, string thing) { 15677 if(test.length < thing.length) 15678 return false; 15679 return test[0 .. thing.length] == thing; 15680 } 15681 15682 private bool endsWith(string test, string thing) { 15683 if(test.length < thing.length) 15684 return false; 15685 return test[$ - thing.length .. $] == thing; 15686 } 15687 15688 // still do layout delegation 15689 // and... split off Window from Widget. 15690 15691 version(minigui_screenshots) 15692 struct Screenshot { 15693 string name; 15694 } 15695 15696 version(minigui_screenshots) 15697 static if(__VERSION__ > 2092) 15698 mixin(q{ 15699 shared static this() { 15700 import core.runtime; 15701 15702 static UnitTestResult screenshotMagic() { 15703 string name; 15704 15705 import arsd.png; 15706 15707 auto results = new Window(); 15708 auto button = new Button("do it", results); 15709 15710 Window.newWindowCreated = delegate(Window w) { 15711 Timer timer; 15712 timer = new Timer(250, { 15713 auto img = w.win.takeScreenshot(); 15714 timer.destroy(); 15715 15716 version(Windows) 15717 writePng("/var/www/htdocs/minigui-screenshots/windows/" ~ name ~ ".png", img); 15718 else 15719 writePng("/var/www/htdocs/minigui-screenshots/linux/" ~ name ~ ".png", img); 15720 15721 w.close(); 15722 }); 15723 }; 15724 15725 button.addWhenTriggered( { 15726 15727 foreach(test; __traits(getUnitTests, mixin(__MODULE__))) { 15728 name = null; 15729 static foreach(attr; __traits(getAttributes, test)) { 15730 static if(is(typeof(attr) == Screenshot)) 15731 name = attr.name; 15732 } 15733 if(name.length) { 15734 test(); 15735 } 15736 } 15737 15738 }); 15739 15740 results.loop(); 15741 15742 return UnitTestResult(0, 0, false, false); 15743 } 15744 15745 15746 Runtime.extendedModuleUnitTester = &screenshotMagic; 15747 } 15748 }); 15749 version(minigui_screenshots) { 15750 version(unittest) 15751 void main() {} 15752 else static assert(0, "dont forget the -unittest flag to dmd"); 15753 } 15754 15755 // FIXME: i called hotkey accelerator in some places. hotkey = key when menu is active like E&xit. accelerator = global shortcut. 15756 // FIXME: make multiple accelerators disambiguate based ona rgs 15757 // FIXME: MainWindow ctor should have same arg order as Window 15758 // FIXME: mainwindow ctor w/ client area size instead of total size. 15759 // 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. 15760 // FIXME: tri-state checkbox 15761 // FIXME: subordinate controls grouping...